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

в 19:43, , рубрики: powershell, powershell v3, windows, метки:

Предисловие

Прежде всего хочу отметить, я не программист. Я админ, пока. Хотелось бы конечно зваться архитектором, но в обозримом пространстве подходящих вакансий, с адекватными требованиями, а главное, зарплатами за эти требования нет. А жаль.
Собственно говоря, в рамках этой заметки хочу рассказать о полезных плюшках новой версии 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 мощная и полезная штука, годная для всяких важных и полезных дел.

Автор: eosfor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js