Вадим Стеркин

  • Главная
  • Windows
  • SSD
  • Программы
  • Разное
  • Об авторе
Вы тут: Главная → Windows → PowerShell: парсинг веб-страниц и загрузка файлов с Invoke-WebRequest

PowerShell: парсинг веб-страниц и загрузка файлов с Invoke-WebRequest

Рубрики: Windows, Вопрос - Ответ Обновлено: 10.04.2018 комментариев 13

Я изучаю PowerShell по мере возникновения задач. Если надо что-то автоматизировать, я смотрю, как соотносится реализация в PowerShell с моими знаниями. И держу в уме, что мне есть к кому обратиться, если знаний не хватит :)

[+] Сегодня в программе

  • История вопроса и задача
  • Получение содержимого страницы и выбор нужных ссылок
  • Загрузка файлов по ссылкам
  • Парсинг заголовков
  • Экономия на запросах

История вопроса и задача

Есть замечательный ресурс smartfiction.ru, публикующий по будням короткие рассказы. Для меня главная ценность в их автоматической доставке на Kindle. Работает это очень просто: на сайте даете почтовый адрес Kindle, а в настройках Amazon добавляете в разрешенные адрес рассылки, после чего книга скачивает отправленные рассказы автоматически.

Invoke-WebRequest

Точнее – работало, потому что в какой-то момент рассказы перестали приходить. Сервис подписки на другом домене, а он недоступен, как выяснилось. Я написал письмо на адрес обратной связи, но оно осталось без ответа. Однако рассказы на сайте публикуются, и под каждым есть ссылка для загрузки в mobi.

Invoke-WebRequest

Поэтому задача свелась к тому, чтобы автоматизировать закачку этих файлов. Первая мысль была расчехлить консольный wget, но тут же возникла ассоциация с PowerShell. Ведь wget – это псевдоним командлета Invoke-WebRequest.

Получение содержимого страницы и выбор нужных ссылок

Invoke-WebRequest умеет отправлять запросы HTTP/HTTPS/FTP, парсить ответ и возвращать наборы элементов HTML – ссылки, формы, изображения и т.д. Попробуйте любой сайт так:

Invoke-WebRequest -Uri "http://smartfiction.ru"

Для каждой ссылки легко выводится набор атрибутов.

$Site = "http://smartfiction.ru/"
$HttpContent = Invoke-WebRequest -Uri $Site
$HttpContent.Links

innerHTML : smartfiction
innerText : smartfiction
outerHTML : <A title=smartfiction href="http://smartfiction.ru/" rel=home>smartfiction</A>
outerText : smartfiction
tagName   : A
title     : smartfiction
href      : http://smartfiction.ru/
rel       : home

Выше показана только первая ссылка страницы, но нужна конкретная. В Chrome щелкните правой кнопкой мыши по ссылке – «Посмотреть код элемента» и сопоставьте с выводом PowerShell. Интересующие атрибуты – это innerText (текст ссылки) и href (URL).

Invoke-WebRequest

Передав запрос по конвейеру командлету Where-Object, можно получить список всех ссылок на книги в формате mobi (ниже показана только первая).

$HttpContent.Links | Where-Object {$_.innertext -eq "mobi"} | fl innerText, href

innerText : mobi
href      : http://convert.smartfiction.ru/?uri=http%3A%2F%2Fsmartfiction.ru%2Fprose%2Fhot_and_cold_blood%2F&amp;format
            =mobi

Загрузка файлов по ссылкам

Информации выше уже достаточно для первой версии скрипта.

$Site = "http://smartfiction.ru/"
$HttpContent = Invoke-WebRequest -Uri $Site
$HttpContent.Links | Where-Object {$_.innertext -eq "mobi"} | 
%{Invoke-WebRequest -Uri $_.href -OutFile "$(Get-Random 10001)$(".mobi")"}

Первые три строки вы уже видели, поэтому разберу четвертую. Список ссылок по конвейеру передается командлету ForEach-Object (псевдоним %). Он выполняет запрос для каждой ссылки ($_.href) и сохраняет ответ сервера в файл со случайным именем и расширением mobi. Таким образом, со страницы скачиваются все книги в формате mobi.

Случайное имя с числовым значением от 0 до 10001 генерирует командлет Get-Random. Это костыль, потому что имя файла в атрибутах ссылки не содержится. Но до него можно добраться!

Парсинг заголовков

В ответ на запрос о ссылке на книгу сервер выдает такую картину.

Invoke-WebRequest -Uri "http://convert.smartfiction.ru/?uri=http%3A%2F%2Fsmartfiction.ru%2Fprose%2Fhot_and_cold_blood%2F&amp;format=mobi"


StatusCode        : 200
StatusDescription : OK
Content           : {76, 105, 111, 100...}
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Connection: keep-alive
                    Status: 200 OK
                    content-disposition: attachment; filename="hot_and_cold_blood.mobi"
                    content-transfer-encoding: binary
                    x-ua-compat...
Headers           : {[Transfer-Encoding, chunked], [Connection, keep-alive], [Status, 200 OK], [content-disposition, at
                    tachment; filename="hot_and_cold_blood.mobi"]...}
RawContentLength  : 46350

Имя файла тут есть: filename="hot_and_cold_blood.mobi". Но я сразу приуныл, потому что извлечь его можно только регулярным выражением, которые я исторически не осилил. Однако меня быстро утешил в Телеграме Вадимс Поданс :)

$r = Invoke-WebRequest -Uri "http://convert.smartfiction.ru/?uri=http%3A%2F%2Fsmartfiction.ru%2Fprose%2Fhot_and_cold_blood%2F&amp;format=mobi"
$r.Headers["content-disposition"] -match 'filename=\"(.+)\"' | Out-Null
$matches[1]
hot_and_cold_blood.mobi

Регулярное выражение берет из ответа заголовки (Headers) и вытаскивает имя файла из секции content-deposition.

В результате получается такой скрипт.

$Site = "http://smartfiction.ru/"
$HttpContent = Invoke-WebRequest -Uri $Site
$HttpContent.Links | Where-Object {$_.innertext -eq "mobi"} | 
%{
(Invoke-WebRequest -Uri $_.href).Headers["content-disposition"] -match 'filename=\"(.+)\"' | Out-Null
Invoke-WebRequest -Uri $_.href -OutFile $matches[1]
}

Экономия на запросах

Код выше вполне рабочий, но перфекциониста не устроит. Каждая ссылка на книгу запрашивается с сервера дважды: сначала для получения имени файла, затем для загрузки. Убрать лишний запрос из цикла несложно – достаточно присвоить ему переменную. Но я не мог сообразить, как это дело вывести в файл.

Вадимс подсказал командлет Set-Content. В данном случае он сохраняет содержимое ответа в файл с именем, полученным с помощью регулярного выражения.

$Site = "http://smartfiction.ru/"
$HttpContent = Invoke-WebRequest -Uri $Site
$HttpContent.Links | Where-Object {$_.innertext -eq "mobi"} | 
%{
$mobi = Invoke-WebRequest -Uri $_.href
$mobi.Headers["content-disposition"] -match 'filename=\"(.+)\"' | Out-Null
Set-Content -path $matches[1] -value $mobi.content -encoding byte
#sleep -s 3
}

Иногда серверы блокируют слишком частые запросы с одного хоста. Поэтому в качестве последнего штриха я добавил в цикл трехсекундную паузу командлетом Start-Sleep.


Если хотите потренироваться, вот тут масса бесплатных книг Microsoft. А у вас возникают подобные задачи? Как решаете?

Метки: PowerShell, скрипты Информация в статье применима к Windows 7 и новее

Об авторе

Вадим - владелец этого блога, и почти все записи здесь вышли из-под его пера. Подробности о блоге и авторе здесь.

Вас также может заинтересовать:

  • PowerShell: как извлечь список уникальных ссылок с веб-страницы
  • Как в Windows 10 автоматически выполнять задачи на восходе и закате солнца
  • Как скачать видео с YouTube с помощью youtube-dl и PowerShell
  • PowerShell: сохраняем исчезающие блоги Microsoft MSDN и TechNet
  • Трюки утилиты certutil
  • Как массово задать дату изменения или создания файлов в PowerShell
  • Как удалить неудаляемые языки в Windows 10
  • 8 полезных возможностей PowerShell 5.0, о которых вы могли и не знать
  • Как массово переименовать файлы по маске в PowerShell
  • Сбор и анализ сведений о системе с помощью PowerShell
← Как переименовать папку в панели быстрого доступа Windows 10
Создание или восстановление загрузки в разметке GPT →
Telegram logo

Я в Telegram

Подпишитесь на канал и читайте интересные записи чаще! Есть вопросы? Задайте их в чате.

комментариев 13

↓
  1. nett00n

    10.04.2018 в 10:29

    Сначала хотел написать, что при помощи Bash и curl/wget получилось бы проще, потом смоделировал скрипт в голове и понял, что нет, не проще.

    Ваша оценка: Thumb up Thumb down 0
    • Vadim Sterkin

      10.04.2018 в 11:12

      Выбирать инструмент, которым лучше владеешь — нормально, даже если в итоге не проще :) Но PowerShell очень компактно решает эту задачу, благодаря конвейеру. И, как я написал в чате, в первой версии скрипта используются только базовые возможности PS, которые используешь практически всегда: ? | % $_. $()

      Ваша оценка: Thumb up Thumb down +1
      • Lecron

        13.04.2018 в 18:35

        Эту — несомненно. За исключением неявного формирования переменной $matches, все классно. Только выигрывая на простых задачах, слишком легко эту простоту разрушить. Очень быстро догнав и перегнав по сложности кода традиционные скриптовые языки.
        Допустим надо искать не ссылки, а блоки книги. Фильтровать их по некоторым полям, например пользовательскому рейтингу. И только потом, уже в самом блоке, искать нужную ссылку. Четкого якоря, типа «mobi», для которой может не быть.

        Ваша оценка: Thumb up Thumb down 0
  2. Alexey Prikhodko

    10.04.2018 в 10:52

    Думаю, что это можно было сделать с помощью MS Flow (аналог IFTTT, но более мощный) без программирования. Но у них ограничение на количество бесплатных запусков — 75. Хотя в данном случае — этого хватит. :)

    Ваша оценка: Thumb up Thumb down 0
    • Vadim Sterkin

      10.04.2018 в 11:13

      MS Flow умеет парсить веб-страницы?

      Ваша оценка: Thumb up Thumb down 0
  3. Сергей Казнадей

    10.04.2018 в 19:33

    nett00n:
    Сначала хотел написать, что при помощи Bash и curl/wget получилось бы проще, потом смоделировал скрипт в голове и понял, что нет, не проще.

    Выдрать ссылки — проще некуда.
    lynx -dump http://smartfiction.ru |grep 'epub$' |tail -10 |awk '{print $2}' > /home/user/urllist.txt
    |tail -10
    использовал для удобства — скрипт пробный и все ссылки мне не нужны, оставил только первые 10.
    Но при попытке скачать wget файл по ссылке получаю Internal Error 500. curl выдает ту же ошибку.

    Ваша оценка: Thumb up Thumb down 0
    • Vadim Sterkin

      10.04.2018 в 19:48

      Лучше на другом сайте тренироваться, а то сломаем хороший сайт окончательно.

      Вот тут куча бесплатных книг Microsoft.

      Ваша оценка: Thumb up Thumb down 0
  4. Павел Нагаев

    13.04.2018 в 11:14

    Вадим, я делал похожий пример, когда парсил русскоязычную версию сайта Вадимса, для истории :-) , а так же mp3 с djpromo сайта скачивал на флешку в машину.

    Ваша оценка: Thumb up Thumb down 0
  5. Lecron

    13.04.2018 в 14:01

    Публикуется не более одного рассказа в день. Если скрипт запускать ежедневно, и качать только последний, то Start-Sleep не нужен. Иначе нужно вести учет скачанного или проверять существование файла, для исключения повторной закачки. Для перфекциониста это как серпом по :).
    И зачем запрашивать заголовки, если имя файла можно получить сразу из url-а? А так как читалка берет инфу из тегов, и ей имя по-барабану, то можно и суррогатным обойтись. Только не случайное число а дату.

    Ваша оценка: Thumb up Thumb down 0
    • Vadim Sterkin

      13.04.2018 в 18:43

      Lecron: Иначе нужно вести учет скачанного или проверять существование файла, для исключения повторной закачки.

      Я, конечно, думал об этом, но решил не уводить скрипт в сторону, потому что этот момент все-таки специфичен для мой конкретной задачи.

      Lecron: И зачем запрашивать заголовки, если имя файла можно получить сразу из url-а? А так как читалка берет инфу из тегов, и ей имя по-барабану, то можно и суррогатным обойтись.

      Цель поста — показать приемы на конкретной задаче. В данном случае имя файла не имеет значения, конечно. Но в каком-то другом — скорее да, чем нет.

      Можно парсить URL, но парсинг заголовков полезнее. Например, для задачи, где в URL — linkID=45105045.

      Lecron: Только не случайное число а дату.

      :) Дата была в исходном варианте скрипта, но я уже разбирал Get-Date в блоге, а Get-Random — нет.

      Ваша оценка: Thumb up Thumb down +1
      • Lecron

        13.04.2018 в 20:09

        Vadim Sterkin: Я, конечно, думал об этом, но решил не уводить скрипт в сторону, потому что этот момент все-таки специфичен для мой конкретной задачи.

        ОК. Тогда строго по теме. Доступ не только к ссылке, но и извлечение информации из произвольного селектора (div). Итерация по блокам, например книгам «div.post» и обращение к селекторам только внутри него. Пара примеров доступа по классу, id, произвольному свойству, css query.
        Это не выйдет за рамки простого примера, но и не окажется настолько примитивным. Понимаю, что есть книги, но эта информация скорее нужна не для обучения, а для оценки перспектив. Прикинуть, насколько стоит использовать штатный инструмент и когда станет пора расчехлять нечто по-мощнее.

        Ваша оценка: Thumb up Thumb down 0
        • Vadim Sterkin

          13.04.2018 в 20:25

          Я написал на примере практической задачи, причем вполне распространенной (см. ссылку в конце статьи). Примитивно? Нет, но просто.

          А для ваших хотелок у меня практических задач нет. И даже если будут, вряд ли стану писать для 2.5 человек.

          Хотите оценить перспективы — попробуйте сами сделать то, что вы попросили.

          Ваша оценка: Thumb up Thumb down 0
  6. Herz Mein

    14.04.2018 в 17:44

    Такой же подход, изучаю по мере выполнения задач. Накатал скриптов, начиная от задач, связанных с музыкой — рип, конвертация, прослушивание, в основном, как скриптовый фронт-енд к lame, vorbis, упорядочивания встроенных тэгов и т.д. Для загрузки rss-лент, анекдотов, цитат и прочей погоды. Ну и конечно для выполнения повседневных задач, связанных с администрированием личного ноутбука.

    Ваша оценка: Thumb up Thumb down 0

Обсуждение завершено.

Subscribers

Популярные записи

  • 12 мифов об оптимизации SSD, которые никогда не умрут (991)
  • Есть ли у Windows шансы победить в войне платформ? (122)
  • Как массово задать дату изменения или создания файлов в PowerShell (17)
  • Почему Microsoft вас не слушает, и можно ли что-нибудь сделать с этим (116)
  • Зачем ноутбуку маленький SSD, и стоит ли ставить на него Windows (140)
  • Установка Office 365 или Office 2019 с выбором приложений и OneNote 2016 (8)
  • 10 причин, по которым я не могу работать в Windows XP (362)
  • Еще →

Свежие комментарии

  • Vadim Sterkin к записи Как выполнять команды и скрипты от имени системы средствами Windows
  • Vadim Sterkin к записи Поддержите меня подпиской или донатом!
  • Валерий Плотников к записи Поддержите меня подпиской или донатом!
  • Артём Ракчеев к записи Нюансы управления звуком в Windows 10
  • Игорь к записи Поддержите меня подпиской или донатом!
  • Vadim Sterkin к записи Поддержите меня подпиской или донатом!
  • Игорь к записи Поддержите меня подпиской или донатом!
  • Vadim Sterkin к записи Поддержите меня подпиской или донатом!

Рекомендую ресурсы

  • Windows 10, etc — канал этого блога в Telegram
  • Инсайдеры Windows 10 — чат блога в Telegram
  • Community — новости предварительных сборок
  • Николай Павлов — тайны планеты Excel
  • Вадимс Поданс — PKI, PowerShell и Тера Патрик
  • Василий Гусев — PowerShell и другие скрипты
  • Kazun — PowerShell для взрослых

Реклама

Измененная тема eleven40 Pro на платформе Genesis · Архивы и метки · Правила (16+) · О рекламе · Обратная связь · Вход

Допускается копирование материалов без изменений, с указанием имени автора и гиперссылки на сайт.