Предисловие
Прежде всего хочу отметить, я не программист. Я админ, пока. Хотелось бы конечно зваться архитектором, но в обозримом пространстве подходящих вакансий, с адекватными требованиями, а главное, зарплатами за эти требования нет. А жаль.
Собственно говоря, в рамках этой заметки хочу рассказать о полезных плюшках новой версии Powershell. В частности, о возможности быстро и уверенно парсить веб-странички и делать это «параллельно».
Задача
Итак, задача, которая стояла передо мной была довольно простой. Есть некий сайт, если пройти через начальную форму, на которой нужно выбрать начальную и конечную дату попадаем вот на такую страничку:
Количество таких страниц может быть большим в рамках одного периода дат. Но не больше 999. То есть, если к примеру, нужно выбрать данные за 5 лет, то они все в 999 страниц не влезут. Эта страница – каталог, меня интересовали только данные на которые она ведет по ссылке в колонке Permit NO:
В общем, поскольку я не программист, моих знаний не хватало чтобы воспользоваться возможностями C# или чего-то такого. В общем, мой любимый инструмент – powershell помог и тут.
Решение
Я решил пойти в два этапа. Сначала выгрузить и разобрать каталог со ссылками, а затем пройтись по каталогу выбрать документы, на которые он ссылается. Примитивная задачка для программиста. У меня это заняло в районе 16 часов. Правда с учетом того что я делал это используя новые для меня команды не только с целью решить задачу, но и с целью изучить новые для меня команды и фишки powershell 3, который в тот момент только-что вышел.
Мне повезло, что сайт принимал параметры прямо в строке URL, вот так:
http://[skip]/[skip]?allcount=$allcount&allstartdate_month=$allstartdate_month [skip]
потому как работать с HTML формами я не умею. Потому я решил просто запрашивать нужные странички, меняя параметры запроса. Для этого я использовал командлет Invoke-WebRequest. Он позволяет в простейшем виде отправить запрос и получить результат без использования .NET классов напрямую или COM объектов IE. В результате получается разобранный HTML документ, который можно разбирать дальше.
Кроме этого, особенностью данной страницы явилось то, что она возвращалась не только с HTML кодом таблицы, но и с вот таким разобранным содержимым этой самой таблицы
Парсинг первой половины
В этой части я выбирал просто каталог. Основная проблема на этом этапе, перебрать все страницы, которые вернулись системой и определить последнюю. Для этого я решил проверять есть ли кнопка Next на странице, или ее нет.
Кроме того, на выходе этой части я хотел получить плоский csv файл, содержащий собственно каталог. И в конце передать этот файл на следующий этап. Для этого родился код ниже. Он просто выбирает все корневые таблички для диапазона дат, разбирает содержимое странички регулярными выражениями, используя указанную выше особенность и возвращает объект, который содержит всю указанную информацию.
function Get-AppList { [CmdletBinding()] param( [datetime] $startDate = '01.01.2012', [datetime] $endDate = '01.01.2012', [string] $allpermittype = "SG", [string] $allcount = "0000", [string] $requestid= "1" ) begin{ [string] $allstartdate_month = "{0:d2}" -f $startDate.Month [string] $allstartdate_day= "{0:d2}" -f $startDate.Day [string] $allstartdate_year= $startDate.Year [string] $allenddate_month = "{0:d2}" -f $endDate.Month [string] $allenddate_day = "{0:d2}" -f $endDate.Day [string] $allenddate_year = $endDate.Year $fields = @{Regex="\[0:PtAppFirstName\]\{(?<PtAppFirstName>.+)\}";Column="PtAppFirstName"}, @{Regex="\[1:PtAppLastName\]\{(?<PtAppLastName>.+)\}";Column="PtAppLastName"}, @{Regex="\[2:PtAppMI\]\{(?<PtAppMI>.+)\}";Column="PtAppMI"}, @{Regex="\[3:PtJobNum\]\{(?<PtJobNum>.+)\}";Column="PtJobNum"}, @{Regex="\[4:PtJobDocNum\]\{(?<PtJobDocNum>.+)\}";Column="PtJobDocNum"}, @{Regex="\[5:PtJobType\]\{(?<PtJobType>.+)\}";Column="PtJobType"}, @{Regex="\[6:PtPermitType\]\{(?<PtPermitType>.+)\}";Column="PtPermitType"}, @{Regex="\[7:PtPermitSubtype\]\{(?<PtPermitSubtype>.+)\}";Column="PtPermitSubtype"}, @{Regex="\[8:PtPermitSeqNum\]\{(?<PtPermitSeqNum>.+)\}";Column="PtPermitSeqNum"}, @{Regex="\[9:PtIssuanceDate\]\{(?<PtIssuanceDate>.+)\}";Column="PtIssuanceDate"}, @{Regex="\[10:PtFilingDate\]\{(?<PtFilingDate>.+)\}";Column="PtFilingDate"}, @{Regex="\[11:PtExpirationDate\]\{(?<PtExpirationDate>.+)\}";Column="PtExpirationDate"}, @{Regex="\[12:PtBin\]\{(?<PtBin>.+)\}";Column="PtBin"}, @{Regex="\[13:JHouseNumber\]\{(?<JHouseNumber>.+)\}";Column="JHouseNumber"}, @{Regex="\[14:JStreetName\]\{(?<JStreetName>.+)\}";Column="JStreetName"}, @{Regex="\[15:PermitIsn\]\{(?<PermitIsn>.+)\}";Column="PermitIsn"} $uri = "http://[skip]/bisweb/[skip]?allcount=$allcount&allstartdate_month=$allstartdate_month&allstartdate_day=$allstartdate_day&allstartdate_year=$allstartdate_year&allenddate_month=$allenddate_month&allenddate_day=$allenddate_day&allenddate_year=$allenddate_year&allpermittype=$allpermittype&go13=+GO+&requestid=0&navflag=T&requestid=$requestid" } process{ do { # выбираем очередную страницу. сохраняем сессию $a = Invoke-WebRequest -Uri $uri -SessionVariable sv $s = $a.ParsedHtml.childNodes| % data $s2 = ($s[3] -split "\[\d+\]") $obj = @{} $s2 | % { $item = $_ if ($item) { $fields | % { $res = $item -match $_.regex if ($res) { $obj[$_.Column] = $matches[$_.Column] } else { $obj[$_.Column]= $null } } if (($obj.PtPermitType -ne $null) -and ($obj.PtPermitType -ne " ")) { new-object psobject -Property $obj } } } # проверка, последняя ли это страница.специфично только для этого сайта $form = $a.Forms | where id -EQ "frmnext" if ($form) { $allstartdate_month=$form.Fields["allstartdate_month"] $allstartdate_day=$form.Fields["allstartdate_day"] $allstartdate_year=$form.Fields["allstartdate_year"] $allenddate_month = $form.Fields["allenddate_month"] $allenddate_day = $form.Fields["allenddate_day"] $allenddate_year = $form.Fields["allenddate_year"] $allpermittype = $form.Fields["allpermittype"] $allcount = $form.Fields["allcount"] $requestid = $form.Fields["requestid"] $uri = "http://[skip]/skip?allcount=$allcount&allstartdate_month=$allstartdate_month&allstartdate_day=$allstartdate_day&allstartdate_year=$allstartdate_year&allenddate_month=$allenddate_month&allenddate_day=$allenddate_day&allenddate_year=$allenddate_year&allpermittype=$allpermittype&go13=+GO+&requestid=0&navflag=T&requestid=$requestid" } } while ($form) } }
Парсинг второй половины
Во второй части возникла еще одна проблема. Количество страничек, которые нужно было запросить становилось малость больше. Раз эдак в 30. Потому, перебор результатов первого этапа и выбор страничек по одной занимал много времени. Потому я решил воспользоваться еще одной фишкой powershell v3 – powershell workflow. Ну верней сказать оператором foreach –parallel. На самом деле workflow предназначены для совсем другого, но в данном случае сошло и так. Сразу скажу, это не средство для распараллеливания задач с целью увеличения производительности, потому не стоит ожидать от него этого. Так вот, в данном случае идея была в том, чтобы воспользоваться этой возможностью, чтобы запускать запросы для каждой строчки каталога «параллельно». На самом деле эта команда запускает отдельный процесс, и их количество ограничено. Я не задавался вопросом можно ли изменить их максимальное количество. Этот механизм позволяет просто упростить код для получения «параллелизма». В кавычках не потому что они не параллельны. Они параллельны, просто запускаются не в легких потоках а в тяжелых процессах в рамках .NET Workflow и результаты передавать вынуждены через границы процессов. Поэтому это не слишком производительно, но зато, «как говорит наш любимы шеф, дешево удобно и практично», а самое главное для админа всего 2 строки кода. Потеря нескольких секунд в на отдельную задачу не играет роли относительно задачи в целом. В общем, годная штука.
Код вышел вот таким.
workflow Get-AppDetails2 ($list) { $webList = @() foreach -parallel ($i in $list){ $PermitIsn = $i.PermitIsn $queryUri = "http://[skip]/bisweb/[skip]?allisn=$PermitIsn&allbin=&requestid=1" Invoke-WebRequest -Uri $queryUri } }
Выводы
В общем и целом, все это доказывает, что powershell мощная и полезная штука, годная для всяких важных и полезных дел.
ссылка на оригинал статьи http://habrahabr.ru/post/194792/
Добавить комментарий