Получение информации о позиции при разборе HTML в Python
Я пытаюсь найти способ анализа (потенциально искаженного) HTML в Python и, если выполняется ряд условий, вывести этот фрагмент документа с позицией (строка, столбец). Информация о местоположении - это то, что сбивает меня с толку. И чтобы быть ясным, мне не нужно строить дерево объектов. Я просто хочу найти определенные фрагменты данных и их положение в исходном документе (подумайте о проверке орфографии, например: "слово" foo "в строке x, столбце y, написано с ошибкой)"
В качестве примера я хочу что-то вроде этого (используя Target API от ElementTree):
import xml.etree.ElementTree as ET
class EchoTarget:
def start(self, tag, attrib):
if somecondition():
print "start", tag, attrib, self.getpos()
def end(self, tag):
if somecondition():
print "end", tag, self.getpos()
def data(self, data):
if somecondition():
print "data", repr(data), self.getpos()
target = EchoTarget()
parser = ET.XMLParser(target=target)
parser.feed("<p>some text</p>")
parser.close()
Однако, насколько я могу судить, getpos()
метод (или что-то подобное) не существует. И, конечно же, это использует парсер XML. Я хочу разобрать потенциально искаженный HTML.
Интересно, что класс HTMLParser в Python Standard Lib предлагает поддержку для получения информации о местоположении (с getpos()
метод), но это ужасно при работе с искаженным HTML и было исключено как возможное решение. Мне нужно разобрать HTML, который существует в реальном слове, не нарушая парсер.
Мне известны два HTML-парсера, которые будут хорошо работать при разборе искаженного HTML, а именно lxml и html5lib. И на самом деле, я бы предпочел использовать любой из них по сравнению с любыми другими опциями, доступными в Python.
Однако, насколько я могу судить, html5lib не предлагает API-интерфейсы событий и потребует, чтобы документ был проанализирован в объекте дерева. Тогда мне придется перебирать дерево. Конечно, к этому моменту связь с исходным документом отсутствует, и вся информация о местоположении теряется. Итак, html5lib отсутствует, и это позор, потому что он кажется лучшим парсером для обработки искаженного HTML.
Библиотека lxml предлагает Target API, который в основном отражает ElementTree, но, опять же, я не знаю ни одного способа доступа к информации о местоположении для каждого события. Взгляд на исходный код также не дал никаких подсказок.
lxml также предлагает API для событий SAX. Интересно, что стандартная библиотека Python упоминает, что SAX поддерживает объекты Locator, но предлагает мало документации о том, как их использовать. Этот вопрос SO предоставляет некоторую информацию (при использовании SAX Parser), но я не понимаю, как это связано с ограниченной поддержкой событий SAX, которую предоставляет lxml.
Наконец, прежде чем кто-либо предложит Beautiful Soup, я укажу, что, как указано на домашней странице, "Beautiful Soup стоит поверх популярных парсеров Python, таких как lxml и html5lib". Все, что он дает мне - это объект для извлечения данных без связи с исходным исходным документом. Как и в случае с html5lib, вся информация о местоположении теряется, когда у меня есть доступ к данным. Я хочу / нужен прямой доступ к парсеру напрямую.
Чтобы расширить пример проверки орфографии, о которой я упоминал в начале, я хотел бы проверить написание только слов в тексте документа (но не имен тегов или атрибутов) и, возможно, захотеть пропустить проверку содержимого определенных тегов (например, сценария). или кодовые метки). Поэтому мне нужен настоящий HTML-парсер. Однако меня интересует только положение слов с ошибками в исходном документе, когда дело доходит до сообщения о словах с ошибками, и у меня нет необходимости строить объект дерева. Чтобы было ясно, это только пример одного потенциального использования. Я могу использовать это для чего-то совершенно другого, но потребности будут по существу такими же. Фактически, я когда-то создавал нечто очень похожее с использованием HTMLParser, но никогда не использовал его, так как обработка ошибок не сработала для этого варианта использования. Это было много лет назад, и я, кажется, потерял этот файл где-то вдоль линии. Я хотел бы использовать вместо этого lxml или html5lib.
Итак, я что-то упускаю? Мне трудно поверить, что ни один из этих синтаксических анализаторов (кроме бесполезного HTMLParser) не имеет никакого доступа к информации о местоположении. Но если они это делают, это должно быть недокументировано, что мне кажется странным.
3 ответа
После некоторых дополнительных исследований и более тщательного изучения исходного кода html5lib я обнаружил, что html5lib.tokenizer.HTMLTokenizer
сохраняет частичную информацию о местоположении. Под "частичным" я подразумеваю, что он знает строку и столбец последнего символа данного токена. К сожалению, он не сохраняет позицию начала токена (я полагаю, он может быть экстраполирован, но это похоже на повторную реализацию большей части токенизатора в обратном порядке - и нет, использование конечной позиции предыдущего не будет работать, если между токенами есть пробел).
В любом случае мне удалось обернуть HTMLTokenizer
и создать HTMLParser
клон, который в основном копирует API. Вы можете найти мою работу здесь: https://gist.github.com/waylan/7d5b7552078f1abc6fac.
Однако, поскольку токенизатор является лишь частью процесса синтаксического анализа, реализованного html5lib, мы теряем хорошие части html5lib. Например, на этой стадии процесса нормализация не проводилась, поэтому вы получаете необработанные (потенциально недействительные) токены, а не нормализованный документ. Как указано в комментариях, он не идеален, и я сомневаюсь в его полезности.
Фактически, я также обнаружил, что HTMLParser, включенный в стандартную библиотеку Python, был обновлен для Python 3.3 и больше не дает сбой при неправильном вводе. Насколько я могу судить, это лучше (для моего случая использования), поскольку оно предоставляет действительно полезную информацию о позиции (как это всегда было). В остальном, моя оболочка html5lib не лучше и не хуже (за исключением, конечно, того, что она предположительно прошла гораздо больше тестов и поэтому более стабильна). К сожалению, обновление не было перенесено обратно в Python 2 или более ранние версии Python 3. Хотя я не думаю, что это было бы так сложно сделать самому.
В любом случае, я решил перейти с HTMLParser в стандартной библиотеке и отказаться от своей собственной оболочки вокруг html5lib. Вы можете увидеть ранние усилия здесь, которые, кажется, работают нормально с минимальным тестированием.
Согласно документации Beautiful Soup, HTMLParser был обновлен для поддержки некорректного ввода в Python 2.7.3 и 3.2.2, который является более ранним, чем 3.3.
Только своего рода ответ - html5lib не предоставляет потоковый API, потому что невозможно обеспечить потоковый API при анализе HTML для каждой спецификации в целом без буферизации или фатальных ошибок (рассмотрим ввод <table>xxx
например). Однако было бы неплохо предоставить потоковый API для html5lib, который использовал фатальные ошибки только для тех ошибок разбора, которые мешают потоковой передаче. Не очень легко реализовать, и не очень сложно.
Не должно быть слишком много работы, чтобы получить информацию о местоположении в дерево в html5lib (тот факт, что ошибки синтаксического анализа имеют информацию о местоположении, дает понять, что ее можно получить!), И есть несколько ошибок в этом, одна общая и одна специфично для lxml.
Обратите внимание, что для этого невозможно использовать только токенайзер html5lib - его состояние меняется на разных этапах построения дерева. Поэтому для реализации правильного токенизатора вам придется реализовать минимальный конструктор дерева (который должен поддерживать хотя бы стек из открытых элементов, хотя я не думаю, что больше). Как только вы захотите начать фильтрацию по текущему элементу, вам, по сути, потребуется весь этап построения дерева, поэтому вы вернулись к проблеме потокового API, описанной выше.
Интересно, что класс HTMLParser в Python Standard Lib предлагает поддержку для получения информации о местоположении (с помощью метода getpos()), но он ужасен при работе с искаженным HTML и был исключен как возможное решение.
Техника, которую я использовал ранее, состоит в том, чтобы использовать BeautilfulSoup.prettify(), чтобы исправить искаженный HTML, а затем проанализировать его с помощью HTMLParser.