Главная

Friday, 17 June 2022

HTTP-запросы и парсинг web-страниц в PowerShell.

Всем привет.

Эта статья - гла­ва из кни­ги Андрея Попова «Сов­ремен­ный 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

А что вы думаете по этому поводу?