Получение информации о позиции при разборе 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 поддерживает получение местоположения. info (с методом getpos()), но он ужасно обрабатывает деформированный HTML и был исключен как возможное решение. Мне нужно разобрать HTML, который существует в реальном слове, не нарушая синтаксический анализатор.

Я знаю два парсера HTML, которые хорошо работают при анализе искаженного HTML, а именно lxml и html5lib. И на самом деле, я бы предпочел использовать любой из них любым другим вариантам, доступным в Python.

Однако, насколько я могу судить, html5lib не предлагает API событий и требует, чтобы документ анализировался в объект дерева. Тогда мне пришлось бы перебирать дерево. Конечно, к этому моменту связь с исходным документом отсутствует, и вся информация о местоположении теряется. Итак, html5lib отсутствует, и это позор, потому что он кажется лучшим парсером для обработки искаженного HTML.

Библиотека lxml предлагает целевой API, который в основном отражает ElementTree, но опять же, я не знаю никакого способа получить доступ к информации о местоположении для каждого события. Взгляд на исходный код также не дал никаких подсказок.

lxml также предлагает API для событий SAX. Интересно, что в стандартной библиотеке Python упоминается, что SAX поддерживает объекты локатора., но предлагает мало документации о том, как их использовать. Этот SO Question содержит некоторую информацию (при использовании SAX Parser), но я не понимаю, как это связано с ограниченной поддержкой событий SAX, которую предоставляет lxml.

Наконец, прежде чем кто-либо предложит Beautiful Soup, я отмечу, что, как указано на главной странице , «Beautiful Soup работает поверх популярных парсеров Python, таких как lxml и html5lib». Все, что он мне дает, — это объект для извлечения данных без связи с исходным документом. Как и в случае с html5lib, вся информация о местоположении теряется к тому времени, когда я получаю доступ к данным. Мне нужен/нужен необработанный доступ к парсеру напрямую.

Чтобы расширить пример проверки орфографии, о котором я упоминал в начале, я хотел бы проверять правописание только слов в тексте документа (но не имен тегов или атрибутов) и, возможно, захотеть пропустить проверку содержимого определенных тегов (например, скрипт или теги кода). Поэтому мне нужен настоящий парсер HTML. Однако меня интересует только положение слов с ошибками в исходном документе, когда дело доходит до сообщения о словах с ошибками, и мне не нужно строить объект дерева. Чтобы было ясно, это только пример одного потенциального использования. Я могу использовать его для чего-то совершенно другого, но потребности будут по существу теми же. На самом деле, когда-то я создал что-то очень похожее с помощью HTMLParser, но никогда не использовал его, так как обработка ошибок не работала для этого варианта использования. Это было много лет назад, и я, кажется, где-то потерял этот файл. На этот раз я бы хотел использовать lxml или html5lib.

Итак, есть что-то, что я упускаю? Мне трудно поверить, что ни один из этих парсеров (кроме в основном бесполезного HTMLParser) не имеет никакого доступа к информации о позиции. Но если они это делают, это должно быть недокументировано, что мне кажется странным.


person Waylan    schedule 25.02.2015    source источник


Ответы (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.

person Waylan    schedule 04.06.2015

Единственный вид ответа — html5lib не предоставляет потокового API, потому что невозможно предоставить потоковый API при разборе HTML по спецификации в целом без буферизации или фатальных ошибок (например, рассмотрим ввод <table>xxx). Однако было бы неплохо предоставить потоковый API для html5lib, который использовал бы фатальные ошибки только для тех ошибок синтаксического анализа, которые препятствуют потоковой передаче. Не очень легко реализовать, но и не очень сложно.

Не должно быть слишком много работы, чтобы получить информацию о местоположении в дереве в html5lib (тот факт, что ошибки синтаксического анализа содержат информацию о местоположении, ясно дает понять, что это возможно!), и в этом есть пара ошибок, один общий и один для lxml.

Обратите внимание, что для достижения этой цели нельзя использовать только токенизатор html5lib — состояние токенизатора изменяется на этапе построения дерева в различных точках. Вам нужно будет реализовать конструктор минимального дерева (который должен будет поддерживать по крайней мере стек открытых элементов, хотя я не думаю, что больше), чтобы сохранить правильность токенизатора. Как только вы захотите начать фильтрацию на основе текущего элемента, вам в основном понадобится весь шаг построения дерева, поэтому вы вернетесь к проблеме потокового API, описанной выше.

person gsnedders    schedule 01.05.2015

Интересно, что класс HTMLParser в стандартной библиотеке Python предлагает поддержку для получения информации о местоположении (с помощью метода getpos()), но он ужасно обрабатывает деформированный HTML и был исключен как возможное решение.

Техника, которую я использовал раньше, заключается в использовании BeautilfulSoup.prettify() для исправления искаженного HTML, а затем его разборе с помощью HTMLParser.

person onelivesleft    schedule 21.07.2018