Возникла задача получить со страницы Confluence все ссылки, в URL которых была последовательность символов XYZ
. Вручную – не вариант, потому что ссылок много.
Я в блоге показывал парсинг веб-страниц с командлетом PowerShell Invoke-WebRequest. Но тут пришлось бы возиться с авторизацией и двухфакторной аутентификацией, поэтому я решил сохранить HTML-страницу локально.
Базовый подход к обработке локальных веб-страниц легко нагугливается вплоть до примеров кода – используется COM-объект и метод IHTMLDocument2.write. После удаления из HTML-кода страницы этой строки:
<meta http-equiv="X-UA-Compatible" content="IE=EDGE,chrome=IE7">
сработал такой парсинг:
$source = Get-Content -Path "C:\temp\page.html" -Raw $page = New-Object -ComObject "HTMLFile" $page.IHTMLDocument2_write($source)
На этом специфика локальной веб-страницы закончилась. Далее я получил список всех URLs с ключевым словом:
$urls = $page.links | Where-Object {$_.href -like '*XYZ*'} $urls | Format-Table href
Однако сразу выяснилось, что нужно немного поработать напильником.
Во-первых, в некоторых случаях ссылки вели на якорь (конкретное место на странице) и имели вид /page#anchor. Лишнее отсекается методом .NET split() по тому же принципу, что и в рассказе про youtube-dl. Разница лишь в разделителе (в данном случае — #
) и необходимости перебрать все ссылки командлетом ForEach-Object.
$urls | ForEach-Object {$_.href.split('#')[0]}
Во-вторых, оказалось, что в списке есть дубликаты, т.е. одинаковые ссылки присутствовали на странице несколько раз. Работа с дубликатами – вполне типовая задача, и в одной из грядущих статей я к ней вернусь. Это решается группировкой с помощью командлета Group-Object, который до сих пор не фигурировал в блоге.
Для наглядности я отсортировал группы по количеству ссылок. Видно, что две ссылки встречаются на странице по четыре раза, а остальные – по одному разу.
Чтобы получить только уникальные ссылки, надо из каждой группы взять по одной ссылке. В данном случае я беру свойство Name
, значением которого выступает уникальный URL.
$urls | ForEach-Object {$_.href.split('#')[0]} | Group-Object | ForEach-Object {$_.Name} | Sort-Object
В итоге получается аккуратный список уникальных ссылок. При необходимости их все можно оптом открыть в браузере, передав список дальше по конвейеру командлету ForEach-Object. В таком случае последний этап конвейера будет примерно таким:
ForEach-Object {Start-Process "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" $_}
Lecron
Правильнее вначале извлекать все ссылки, а потом фильтровать на xyz. Проще повторно использовать код.
Vadim Sterkin
Не понял. Я вроде именно это и делаю в строке 4 с конвейером. Да, я присваиваю переменную $urls уже отфильтрованному списку, но мне прочее и не нужно.
Lecron
Это сейчас. Пройдет время и когда понадобится фильтровать по другому критерию, найти фильтр в конце скрипта, будет гораздо легче чем в середине. Без необходимости восстанавливать в голове всю логику скрипта.
Просто правило хорошего кода, а не влияния на функциональность. Становится важным, потому что после публикации, ваш пример начнет расползаться по клиентским машинам. В идеале, вообще подумать о создании функции Get-Links.
Vadim Sterkin
Я понял вашу мысль, и отчасти согласен. Но в данном случае я демонстрирую пример приемов split и group нежели пример скрипта. Да, группировка для выборки уникальных ссылок — вполне распространенный случай, но отсечка якорей — частный.
Равно как зачем мне применять оба приема ко всему массиву ссылок, если меня интересуют конкретные с XYZ? Правило хорошего кода в данном случае получается неэффективным с точки зрения скорости. Ладно если объектов пара сотен, как в моем случае, а если пара сотен тысяч?
Я думаю, что с учетом индвидуальности каждого случая описанный порядок действий (и последовательности команд) вполне логичный*. Мне нужны ссылки с XYZ — я с этого начинаю, а дальше уже смотрю, что получилось. Может, этого достаточно, и тогда вообще не о чем писать в блоге :)
*Возможно, логичнее было бы сначала выбрать уникальные ссылки, а потом уже отсекать якоря. Но так уж вышло, что сначала я заметил якоря, а потом уже дубликаты.
Lecron
Одно другому не помеха. В человеке все должно быть прекрасно :) Впрочем, мой изначальный комментарий, скорее предназначался читателям статьи, чем Вам.
Преждевременная оптимизация — зло!
Примеры должны быть релевантными (репрезентативными? всегда путаюсь в этих сложных словах))). Такую задачу, буде она возникнет, вряд ли будут решать шелл-скриптом, ибо она явно будет шире. И даже для скрипта, который полностью вписан в .NET это разница в миллисекунды. На фоне парсинга html для извлечения ссылок, вообще 0.
Lecron
Кстати, может действительно не стоит экономить строки и написать более самодокументируемый код.
Vadim Sterkin
Да, это самый наглядный вариант. Но боюсь, что тогда в комментарии придет Вадимс Поданс и поинтересуется, не проходит ли тут чемпионат мира по созданию переменных :)
Василий Гусев в чате написал, что парсит регулярными выражениями, потому что не любит комы :)
Lecron
Каждому свое.
Не знаю, что было бы источником данных, скорее всего BeautifulSoup, но на питоне обсуждаемый код выглядел бы где-то так:
И никаких чемпионатов по переменным :)
Vadim Sterkin
Ну на пошике обсуждаемый код тоже может выглядеть как-то так↓ Но это не для публикации :)
Lecron
Почему не для публикации? Вполне можно в конце статьи писать такой краткий вариант. Вреда не вижу, а пользу вполне.
Andrew D
Тут мысль одна возникла. Я как-то решал задачу через excel, в нем генерировал список ссылок, экспортировал в txt и скармливал Reget Deluxe. Но толи там HTTPS сертификаты протухли, толи оно запрос неверно формирует, в прошлый раз что-то пошло не так.
Можно ли сотворить на PS? Думаю да.
Суть: есть набор ссылок вида https://example.com/path/nnnn где nnnn — число.
Задача: перейти по всем ссылкам по очереди, там где лежит бинарник (прямо или с редиректом — не важно, суть в том что браузер при переходе по верному номеру спрашивает куда класть, а по неверному — страничка о том что не найдено) — взять как есть вместе с именем и сохранить в папку, добавив его nnnn к имени.
$a = Invoke-WebRequest -Uri $url
Можете сходу подсказать команду сохранения бинарно того $a что отдано по ссылке ?
Vadim Sterkin
Так вы настройте браузер, чтобы не спрашивал — будет класть в заданную папку. А так, загрузку файлов я показывал уже — тут.