Эта статья - глава из книги Андрея Попова «Современный PowerShell», вышедшей в издательстве «BHV» в марте этого года. В книге подробно описан язык PowerShell и работа с оболочкой Windows PowerShell в Windows Terminal, взаимодействие с файловой системой, структурированными данными и веб-ресурсами.
Собственно я больше знаком с первой книгой автора "Введение в PowerShell" которую он издал еще в 2009-м. С нее и началось мое вхождение в мир PowerShell. Поэтому книга «Современный PowerShell» мне будет не так интересна как первая, которую она повторяет на 80%. Но надо отдать должное автору, что его подача материала всегда отличается ясностью, практичностью и лаконичностью.
Поэтому сегодня мы вместе с Андреем напишем HTTP-запросы и пропарсим web-страницы через PowerShell.
В интернете есть множество сервисов, с которыми можно работать, обращаясь к их ресурсам по протоколу HTTP. Веб-разработчики постоянно работают с такими HTTP-запросами для доступа к функциям внешних API или для тестирования собственных приложений. PowerShell для обращения к вебу по HTTP предлагает два стандартных командлета: Invoke-WebRequest и Invoke-RestMethod.
КОМАНДЛЕТ INVOKE-WEBREQUEST
С помощью командлета Invoke-WebRequest можно направить веб-серверу HTTP-запрос и получить от него ответ.
Анализ HTML-страниц
Этот командлет хорошо подходит для анализа HTML-страниц. Еще он умеет сохранять страницы на локальном диске. В этом он похож на консольную утилиту wget и даже имеет такой псевдоним:
PS C:\Script> Get-Alias wget
CommandType Name
----------- ----
Alias wget -> Invoke-WebRequest
Обратимся с помощью Invoke-WebRequest к какой-нибудь простой странице, например Example Domain. По умолчанию Invoke-WebRequest выполняет HTTP-запрос с методом GET к ресурсу на веб-сервере, адрес ресурса указывается в качестве значения параметра -Uri. В результате возвращается объект типа HtmlWebResponseObject, в котором хранится информация об ответе сервера:
PS C:\Script> $web = Invoke-WebRequest -Uri https://example.com/index.html
PS C:\Script> $web | Get-Member
TypeName: Microsoft.PowerShell.Commands.HtmlWebResponseObject
. . .
PS C:\Script> $web
StatusCode : 200
StatusDescription : OK
Content : <!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; char
set=utf-8" />
<meta name="viewport" conten...
RawContent : HTTP/1.1 200 OK
Age: 497890
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Mon, 12 Jul 2021 16:05:14 GMT
Exp...
Forms : {}
Headers : {[Age, 497890], [Vary, Accept-Encoding], [X-Cache, HIT], [Content-Length, 1256]...}
Images : {}
InputFields : {}
Links : {@{innerHTML=More information...; innerText=More information
...; outerHTML=<A href="https://www.iana.org/domains/example
">More information...</A>; outerText=More information...; ta
gName=A; href=https://www.iana.org/domains/example}}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 1256
В поле StatusCode содержится код ответа от сервера (200 для нашего примера), в поле StatusDescription - текстовое описание этого ответа (OK).
Содержимое ответа от сервера и HTTP-заголовки
Содержимое ответа от сервера хранится в виде строки в поле Content. В нашем случае здесь будет записан HTML-код:
PS C:\Script> $web.Content
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
В поле RawContent записывается полный ответ от сервера с HTTP-заголовками в начале:
PS C:\Script> $web.RawContent
HTTP/1.1 200 OK
Age: 497890
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Mon, 12 Jul 2021 16:05:14 GMT
Expires: Mon, 19 Jul 2021 16:05:14 GMT
ETag: "3147526947+ident"
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (dcb/7F83)
<!doctype html>
<html>
<head>
<title>Example Domain</title>
. . .
Заголовки ответа тоже хранятся отдельно - в виде хеш-таблицы в свойстве headers:
PS C:\Script> $web.headers
Key Value
--- -----
Age 497890
Vary Accept-Encoding
X-Cache HIT
Content-Length 1256
Cache-Control max-age=604800
Content-Type text/html; charset=UTF-8
Date Mon, 12 Jul 2021 16:05:14 GMT
Expires Mon, 19 Jul 2021 16:05:14 GMT
ETag "3147526947+ident"
Last-Modified Thu, 17 Oct 2019 07:18:26 GMT
Server ECS (dcb/7F83)
Сохранение веб-ресурсов
Для сохранения ответа от сервера в виде локального файла надо при вызове Invoke-WebRequest использовать ключ -OutFile и указать путь к нужному файлу. Например, сохраним страницу https://example.com/index.html в файле page.html в текущем каталоге:
PS C:\Script> Invoke-WebRequest -Uri https://example.com/index.html -OutFile page.html
Проверим содержимое файла page.html и убедимся, что в нем записана HTML-разметка сохраненной страницы:
PS C:\Script> type .\page.html
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
Подобным образом можно сохранять не только HTML-файлы, но и ресурсы других типов (текстовые, графические или мультимедийные файлы и т. д.), к которым можно обратиться на сайте.
Поиск HTML-элементов на странице
В свойствах Forms (формы), Images (изображения), InputFields (поля ввода) и Links (ссылки) объекта HtmlWebResponseObject сохраняются массивы объектов, которые описывают соответствующие элементы HTML-разметки, полученной от сервера. Обрабатывая эти коллекции, можно получить информацию об интересующих нас элементах на загруженной странице.
Например, сохраним в переменной $web страницу результатов поиска слова PowerShell в Yandex:
PS C:\Script> $web = Invoke-WebRequest -Uri https://yandex.ru/search/?text=PowerShell
Выделим на этой странице все ссылки на сайт habr.com. Для этого нужно отфильтровать массив $web.Links, оставив в нем объекты, у которых значение свойства href соответствует маске *habr.com*:
PS C:\Script> $habr_links = $web.Links | Where-Object href -like '*habr.com*'
В $habr_links находятся два объекта PSCustomObject, содержащие различные HTML-атрибуты для ссылок:
PS C:\Script> $habr_links.count
2
PS C:\Script> $habr_links | Get-Member
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
class NoteProperty string class=Link Link_theme_normal OrganicTitl...
data-counter NoteProperty string data-counter=["b"]
data-log-node NoteProperty string data-log-node=bf7fw01-00
href NoteProperty string href=https://habr.com/ru/company/ruvds/b...
innerHTML NoteProperty string innerHTML=<div class="Favicon Favicon_si...
innerText NoteProperty string innerText=...
outerHTML NoteProperty string outerHTML=<a tabindex="0" class="Link Li...
outerText NoteProperty string outerText=...
tabindex NoteProperty string tabindex=0
tagName NoteProperty string tagName=A
target NoteProperty string target=_blank
Например, выведем значение атрибутов href и innerText для этих ссылок:
PS C:\Script> $habr_links | ForEach-Object {$_.href + " - " + $_.innerText }
https://habr.com/ru/company/ruvds/blog/487876/ -
Что такое Windows PowerShell и с чем его едят? / Хабр
https://habr.com/ru/company/ruvds/blog/487876/ - habr.com›ru/company/ruvds/blog/487876/
В свойстве ParsedHtml объекта HtmlWebResponseObject содержится объект типа mshtml.HTMLDocumentClass, который предоставляет доступ к DOM-дереву загруженной HTML-страницы.
PS C:\Users\andrv> $html = $web.ParsedHtml
PS C:\Users\andrv> Get-Member -InputObject $html
TypeName: mshtml.HTMLDocumentClass
. . .
INFO
Если в HTML-коде имеются сценарии JavaScript, то по умолчанию при построении DOM-дерева они будут выполнены. При необходимости выполнение сценариев можно отключить, указав параметр -UseBasicParsing.
В частности, используя методы getElementById(), getElementsByName() и getElementsByTagName(), можно получать объекты, соответствующие HTML-элементам с заданным идентификатором, именем или тегом соответственно.
Например, посмотрим, какой текст записан в первом заголовке второго уровня (HTML-тег <h2>):
PS C:\Users\andrv> $html.getElementsByTagName('h2')[0].innerText
Документация по PowerShell - PowerShell | Microsoft Docs
Найдем элемент с идентификатором search-result:
PS C:\Users\andrv> $html.getElementById('search-result')
className : serp-list serp-list_left_yes
id : search-result
tagName : UL
parentElement : System.__ComObject
style : System.__ComObject
. . .
Метод querySelector() позволяет найти HTML-элемент по определенному CSS-селектору. Например, обратимся к элементу с классом main__content:
PS C:\Users\andrv> $html.querySelector('.main__content')
className : main__content
id :
tagName : DIV
parentElement : System.__ComObject
style : System.__ComObject
. . .
Подробнее о свойствах, методах и событиях объекта mshtml.HTMLDocumentClass можно прочитать в документации на сайте Microsoft.
Выполнение POST-запросов
Командлет Invoke-WebRequest позволяет не только выполнять GET-запросы, но и вызывать другие методы, определенные в протоколе HTTP (DELETE, HEAD, MERGE, PATCH, POST, PUT, TRACE). Для этого нужный метод указывается в качестве значения параметра -Method.
Рассмотрим пример выполнения запроса с HTTP-методом POST, который часто используется для передачи данных из веб-форм или загрузки файлов на сервер. Обращаться мы будем к ресурсу http://httpbin.org/post, в результате сервер должен сообщить нам о полученных данных.
Передавать мы будем два параметра с именами name и lastName, которые поместим в хеш-таблицу $params:
PS C:\Users\andrv> $params = @{name='Andrey'; lastName='Popov'}
Выполним Invoke-WebRequest с методом POST, поместив передаваемые параметры в тело запроса (параметр -Body):
PS C:\Users\andrv> Invoke-WebRequest -Uri http://httpbin.org/post -Method POST -Body $params
StatusCode : 200
StatusDescription : OK
Content : {
"args": {},
"data": "",
"files": {},
"form": {
"lastName": "Popov",
"name": "Andrey"
},
"headers": {
"Content-Length": "26",
"Content-Type": "application/x-www-form..
.
RawContent : HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Length: 503
Content-Type: application/json
Date: Fri, 16 Jul 2021 03:03:13 GM...
Forms : {}
Headers : {[Connection, keep-alive], [Access-Control-Al
low-Origin, *], [Access-Control-Allow-Credent
ials, true], [Content-Length, 503]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 503
Отправляемые на сервер параметры командлет Invoke-WebRequest автоматически приводит к формату application/x-www-form-urlencoded, который используется при передаче данных из веб-форм. Ответ в поле Content говорит о том, что сервер принял наши параметры и определил, что они были отправлены из формы.
Иногда бывает нужно передавать на сервер данные в формате JSON, а не в application/x-www-form-urlencoded. В этом случае следует указать параметр -ContentType со значением application/json. Например:
PS C:\Users\andrv> $json_params = "{ 'name':'Andrey', 'lastName':'Popov' }"
PS C:\Users\andrv> Invoke-WebRequest -Uri http://httpbin.org/post -ContentType "application/json" -Method POST -Body $json_params
StatusCode : 200
StatusDescription : OK
Content : {
"args": {},
"data": "{ 'name':'Andrey', 'lastName':'Popov
' }",
"files": {},
"form": {},
"headers": {
"Content-Length": "39",
"Content-Type": "application/json",
"Host": "...
RawContent : HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Length: 475
Content-Type: application/json
Date: Fri, 16 Jul 2021 03:43:04 GM...
Forms : {}
Headers : {[Connection, keep-alive], [Access-Control-Allo
w-Origin, *], [Access-Control-Allow-Credentials
, true], [Content-Length, 475]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 475
Как видим, при таком способе отправки запроса серверный скрипт, обрабатывающий обращения к ресурсу http://httpbin.org/post, извлек полученные данные и при формировании своего JSON-ответа поместил их в поле data, а не в поле form.
КОМАНДЛЕТ INVOKE-RESTMETHOD
Если мы обращаемся к веб-сервису, поддерживающему REST API, то ответ сервера, скорее всего, будет содержать структурированные данные в формате JSON или XML. Так, в предыдущем примере мы с помощью командлета Invoke-WebReques посылали POST-запрос на ресурс http://httpbin.org/post и получали в ответ JSON-строку:
PS C:\Users\andrv> $web = Invoke-WebRequest -Uri http://httpbin.org/post -Method POST -Body @{name='Andrey'; lastName='Popov'}
PS C:\Users\andrv> $web.Content
{
"args": {},
"data": "",
"files": {},
"form": {
"lastName": "Popov",
"name": "Andrey"
},
"headers": {
"Content-Length": "26",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT; Windows NT 10.0; ru-RU) WindowsPowerShell/5.1.19041.1023",
"X-Amzn-Trace-Id": "Root=1-60f2a42c-18f152db3b76b5d66d173e31"
},
"json": null,
"origin": "85.95.179.209",
"url": "http://httpbin.org/post"
}
Чтобы работать с полями ответа, мы должны преобразовать JSON в объект PowerShell с помощью командлета ConvertFrom-Json:
PS C:\Users\andrv> $response = $web.Content | ConvertFrom-Json
PS C:\Users\andrv> $response
args :
data :
files :
form : @{lastName=Popov; name=Andrey}
headers : @{Content-Length=26; Content-Type=application/x-www-form-urlencoded; Host=htt pbin.org; User-Agent=Mozilla/5.0 (Windows NT; Windows NT 10.0; ru-RU) Windows
PowerShell/5.1.19041.1023; X-Amzn-Trace-Id=Root=1-60f2a42c-18f152db3b76b5d66d
173e31}
json :
origin : 85.95.179.209
url : http://httpbin.org/post
PS C:\Users\andrv> $response.form.name
Andrey
В подобных случаях, когда от сервера мы получаем структурированные данные, удобнее пользоваться командлетом Invoke-RestMethod, который действует аналогично Invoke-WebRequest и имеет такие же параметры, но при этом автоматически преобразует ответ от сервера в объект PowerShell.
Выполним наш запрос с помощью Invoke-RestMethod, сохранив результат в переменной $result:
PS C:\Users\andrv> $result = Invoke-RestMethod -Uri http://httpbin.org/post -Method POST -Body @{name='Andrey'; lastName='Popov'}
Проверим тип и содержимое переменной $result:
PS C:\Users\andrv> Get-Member -InputObject $result
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
args NoteProperty System.Management.Automation.PSCusto...
data NoteProperty string data=
files NoteProperty System.Management.Automation.PSCusto...
form NoteProperty System.Management.Automation.PSCusto...
headers NoteProperty System.Management.Automation.PSCusto...
json NoteProperty object json=null
origin NoteProperty string origin=85.95.179.209
url NoteProperty string url=http://httpbin.org/post
PS C:\Users\andrv> $result
args :
data :
files :
form : @{lastName=Popov; name=Andrey}
headers : @{Content-Length=26; Content-Type=application/x-www-form-urlencoded; Host=htt
pbin.org; User-Agent=Mozilla/5.0 (Windows NT; Windows NT 10.0; ru-RU) Windows
PowerShell/5.1.19041.1023; X-Amzn-Trace-Id=Root=1-60f2a897-6354ce272644b6d412
560a9b}
json :
origin : 85.95.179.209
url : http://httpbin.org/post
Как видим, вместо строки в формате JSON мы получаем PowerShell-объект типа System.Management.Automation.PSCustomObject и можем сразу обращаться к нужным свойствам:
PS C:\Users\andrv> $result.form.lastName
Popov
Итак, для обращения к веб-ресурсам по протоколу HTTP в PowerShell имеются два стандартных командлета: Invoke-WebRequest и Invoke-RestMethod. Для выполнения HTTP-запросов к веб-ресурсам, возвращающим HTML-страницы, удобнее использовать командлет Invoke-WebRequest, а для работы с внешними REST API, возвращающими структурированные данные, лучше подойдет командлет Invoke-RestMethod.
Действуют эти командлеты аналогично друг другу, за исключением того, что Invoke-RestMethod автоматически преобразует ответ от сервера в объект PowerShell.
Автору спасибо, а нам удачи.
Слава Украине!
No comments:
Post a Comment
А что вы думаете по этому поводу?