Как я сайт на 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 мощная и полезная штука, годная для всяких важных и полезных дел.
Метки:
  • +12
  • 18,4k
  • 2
Поделиться публикацией
Комментарии 2
  • +2
    Паралимпийские игры по программированию.
    Люблю такие вещи)
    • 0
      Я для таких целей использую python + grab
      Замечательные решения получаются

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.