Парсинг HTML.

Зачем это надо, если есть прекрасный парсер XYZ?

Ага. Есть. Вот только этот парсер надо ещё найти, скачать и поставить. Причём вместе с последней версией любимого ЯП автора этого парсера. Ладно, если-бы автор любит перл, перл везде есть. А если там какой-нибудь Ruby нужен? Или ещё что похуже...

Но это-то ладно, хуже другое - «парсить хтмл» это всего-лишь часть задачи - обычно нужно что-то совсем другое, а парсинг - только часть общей задачи. Например мне тут потребовалось написать скрипт, который гуляет по сайту и совершает там разные действия. «Зайти на страничку» это просто: достаточно использовать программу wget, она с лёгкостью скачает нужную страничку, и кроме того, может использовать авторизацию, и многое другое. Затем данную страничку можно просмотреть нашей любимой sed, и выполнить необходимые действия. Проблема в том, что страничка предназначена вовсе не для sed, а для браузера, который покажет её юзеру. Потому напрямую обрабатывать эту страничку очень проблематично. Основная беда в том, что строки в HTML файле не совпадают с единицами обработки - например теги могут занимать несколько строк, а так-же возможна ситуация, когда в одной строке несколько разных тегов.

Первичная обработка HTML.

Прежде всего следует переформатировать текст так, что-бы в одной строке был-бы только один тег. Либо тегов в строке не было-бы вовсе (для кусков собственно текста).

Похожая задача нам уже встречалась при выравнивание текста - там так-же нужно было резать и склеивать строки.

#!/bin/sed -rnf

# форматирование HTML, все теги собираются
# каждый в свою строку

# загрузка первой строки...
# удаление ведущих пробелов
s/^\s+//
/^</{
	 # данная строка начинается с тега
	:tag
	/>/{
		s/^[^>]*>/\L&\n/
		P
		D
	}
	# строка содержит не целый тег
	$ b error
	N
	s/\r?\n/ /
	b tag
}

/</{
	# строка не начинается с тега, но, однако, его содержит
	s/\s*</\n</
	P
	D
}

# проверяем наличие символа > вне тегов
/>/ b error

s/\s+$//
# проверяем, не является-ли строка пустой, и печатаем её
/^$/! p
b

:error
s/.*/\x1b[31;1mErorr in line '&'\x1b[0m/p
q 77

Здесь у нас так-же вложенный цикл - во внешнем цикле мы отделяем и выводим строки, которые склеиваем в внутреннем цикле. Этот внутренний цикл начинается с метки tag и заканчивается командой b tag. Склейка строк осуществляется командой N, после которой мы удаляем символ перевода строки. Во многих текстах из сети строки переводятся не символом «\n», а символами «\r\n» (так принято в маздае). На самом деле, в HTML допускаются любые комбинации любых пробельных символов - все они заменяются на одиночный пробел. А символ «\r» вообще-то также является пробельным. Именно по этой причине мы срезаем не /\n/, а /\r?\n/, что можно перевести как символ «\n» перед которым может стоять символ «\r». В других местах скрипта я так-же удаляю пробельные символы в конце строк, там-же удаляется и «\r», для упрощения дальнейшего анализа.

Строка может начинаться с тега (с символа «<»), а может и не начинаться. Если строка не начинается с тега, но его содержит, то я отделяю то, что перед тегом символом «\n», а затем вывожу эту башку и продолжаю анализ (командами P и D). Если строка не содержит тегов, то я её просто печатаю.

Самый интересный случай: это если строка начинается с тега, с символа «<». В этом случае мы прежде всего проверяем, есть-ли в строке символ «>», который закрывает теги. Если в строке есть хотя-бы один такой символ - значит строка содержит закрытый хтмл-тег. Его нужно отделить и вывести, это делается так-же тремя командами: «s», «P», «D». Причём последняя ещё и предаёт управление в начала скрипта для продолжения анализа хвоста после тега.

В случае, когда тег не закрыт, приходится подгружать следующие строки, пока тег не будет полностью загружен.

Вырезание комментариев, скриптов и стилей.

К сожалению, вышеприведённый скрипт работает только в теории. В реальных текстах часто присутствуют комментарии, стили, и скрипты на других языках (обычно на JavaScript). В этих вставках часто попадаются символы < и >, которые приводят к ошибкам.

Логично собрать все эти вставки в одну строку, для упрощения дальнейшего анализа. Ну например так:

/^<!--/{
	:comment
	# эта строка начинается с коментария
	# насколько я понимаю, в комментах можно всё, кроме -->
	/-->/{
		# строка так-же содержит завершение коменнта
		# убираем его
		:remove_substring
		s//&\n/
		P
		D
	}
	# строка не содержит конца коммента, грузим след.
	# проверяем особый случай: незавершённый коммент
	$ b error
	N
	s/\r?\n/<\\n>/
	b comment
	}

Если строка содержит начало комментария, то я начинаю собирать все дальнейшие строчки в одну, учитывая тот факт, что комментарии мне так-же могут пригодится для дальнейшего анализа. При этом переводы строк я так-же сохраняю - я их изменяю на собственный тег <\n>. В некоторых случаях комментарии также важны для анализа.

Проблемы с быстродействием.

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

Проблема в том, что это очень долго - например реальный файл с форума обрабатывается таким образом за 2.85 секунд - это неприемлемо!. Связано это вот с чем: после загрузки каждой строки нам нужно проверить, не является-ли загруженный кусок окончанием данной строки. Для этого нам придётся просмотреть не только загруженную подстроку, но и все предыдущие (если мы ищем окончание комментария, стиля, или яваскрипта. «Все» в данном контексте означает «все строки этого стиля/скрипта/коммента».).

Дело в том, что стили и скрипты можно вводить в HTML двумя способами, во первых отдельным файлом, который будет подгружаться браузером при необходимости (обычно браузер всегда и подгружает), либо напрямую вставляя в HTML. Конечно, для нас второй способ нафиг не нужен, однако, мы не в силах переписать все движки всех сайтов так, как это нам было-бы удобнее...

Как всегда возможно несколько решений. Основное время у нас отнимает команда s/ЗАВЕРШЕНИЕ_СТИЛЯ_ИЛИ_СКРИПТА//, и что-бы её ускорить, нам нужно проводить поиск не по всему стилю/скрипту, а только по его последней строке. Для этого надо грузить строку командой n, которая затирает предыдущие строки, а что-бы сохранить стиль/скрипт нужно его добавлять в область удержания командой H.

Это общее решение. И именно его я-бы и и применял. Однако, изначально у нас была другая задача: мы хотели обрабатывать автономно странички, без участия человека. Т.е. создать бота, который сам ползает по Сети. Но зачем нашему боту скрипты и стили? Зачем выполнять яваскрипт, единственная функциональность которого - выдать табличку «Вы уверенны?»? Это не нужно ни нам, ни нашему боту. Потому мы можем смело постирать все скрипты и стили, оставив вместо них просто

<style type="text/css"></style>

Ну типа - «здесь был стиль.»

Замечание

С этой оптимизацией скрипт выполняется не за 2.85 секунд, а за 0.14 - такое время примерно соответствует нулю - 0.14 секунд это меньше, чем погрешность измерения.

Готовый скрипт для парсинга HTML.

Пример 4.10. Скрипт для парсинга HTML (предварительная резка и склейка строк).


Работа над ошибками.

Как всегда, в реальной жизни возможны ошибки и неточности. Например наш скрипт может чего-то не понять в загруженном файле. Кроме того, возможен вариант, что веб-мастер будет использовать какие-то хитрые и неизвестные нам конструкции HTML. Если-бы наша программа была интерактивной, то нам было-бы достаточно просто вывести табличку «Быдлокодерская страничка!», однако, у нас бот работающий автономно. Потому нам приходится действовать по другому.

Если документ для анализа слишком кривой, и обработка его невозможна, наш скрипт переходит к метке :error, в которой он во первых выводит раскрашенную ярко-красным проблемную строку, а во вторых прерывает скрипт с кодом 77. Скрипт, который вызвал данный скрипт может проверить код ошибки и предпринять какие-то меры для её устранения. Самое простое - отправить сообщение в журнал, и завершить работу.

Вы можете обсудить этот документ на форуме. Текст предоставляется по лицензии GNU Free Documentation License (Перевод лицензии GFDL).

Вы можете пожертвовать небольшую сумму яндекс-денег на счёт 41001666004238 для оплаты хостинга, интернета, и прочего. Это конечно добровольно, однако это намного улучшит данный документ (у меня будет больше времени для его улучшения). На самом деле, проект часто находится на грани закрытия, ибо никаких денег никогда не приносил, и приносить не будет. Вы можете мне помочь. Спасибо.