Как я сайт на powershell парсил

от автора

Предисловие

Прежде всего хочу отметить, я не программист. Я админ, пока. Хотелось бы конечно зваться архитектором, но в обозримом пространстве подходящих вакансий, с адекватными требованиями, а главное, зарплатами за эти требования нет. А жаль.
Собственно говоря, в рамках этой заметки хочу рассказать о полезных плюшках новой версии Powershell. В частности, о возможности быстро и уверенно парсить веб-странички и делать это «параллельно».

Задача

Итак, задача, которая стояла передо мной была довольно простой. Есть некий сайт, если пройти через начальную форму, на которой нужно выбрать начальную и конечную дату попадаем вот на такую страничку:

image

Количество таких страниц может быть большим в рамках одного периода дат. Но не больше 999. То есть, если к примеру, нужно выбрать данные за 5 лет, то они все в 999 страниц не влезут. Эта страница – каталог, меня интересовали только данные на которые она ведет по ссылке в колонке Permit NO:

image

В общем, поскольку я не программист, моих знаний не хватало чтобы воспользоваться возможностями C# или чего-то такого. В общем, мой любимый инструмент – powershell помог и тут.

Решение

Я решил пойти в два этапа. Сначала выгрузить и разобрать каталог со ссылками, а затем пройтись по каталогу выбрать документы, на которые он ссылается. Примитивная задачка для программиста. У меня это заняло в районе 16 часов. Правда с учетом того что я делал это используя новые для меня команды не только с целью решить задачу, но и с целью изучить новые для меня команды и фишки powershell 3, который в тот момент только-что вышел.
Мне повезло, что сайт принимал параметры прямо в строке URL, вот так:

http://[skip]/[skip]?allcount=$allcount&allstartdate_month=$allstartdate_month [skip] 

потому как работать с HTML формами я не умею. Потому я решил просто запрашивать нужные странички, меняя параметры запроса. Для этого я использовал командлет Invoke-WebRequest. Он позволяет в простейшем виде отправить запрос и получить результат без использования .NET классов напрямую или COM объектов IE. В результате получается разобранный HTML документ, который можно разбирать дальше.
Кроме этого, особенностью данной страницы явилось то, что она возвращалась не только с HTML кодом таблицы, но и с вот таким разобранным содержимым этой самой таблицы

image

Парсинг первой половины

В этой части я выбирал просто каталог. Основная проблема на этом этапе, перебрать все страницы, которые вернулись системой и определить последнюю. Для этого я решил проверять есть ли кнопка 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *