在Python中解析HTML时获取位置信息

Obtaining position info when parsing HTML in Python

我正在尝试找到一种方法来解析(可能格式错误的)HTML in Python 并且,如果满足一组条件,则输出该文档的位置(行、列)。位置信息是我在这里绊倒的原因。需要明确的是,我不需要构建对象树。我只是想找到某些数据及其在原始文档中的位置(想想拼写检查器,例如:'word "foo" at line x, column y, is misspelled)'

作为示例,我想要这样的东西(使用 ElementTree 的 Target API):

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.

有趣的是,Python 标准库中的 HTMLParser class 确实提供了获取位置信息的支持(使用 getpos() 方法),但这太可怕了在处理格式错误的 HTML 时,已作为可能的解决方案被淘汰。我需要在不破坏解析器的情况下解析真实单词中存在的 HTML。

我知道有两个 HTML 解析器可以很好地解析格式错误的 HTML,即 lxml and html5lib。事实上,我更愿意使用其中任何一个,而不是 Python.

中可用的任何其他选项

但是,据我所知,html5lib 不提供任何事件 API,并且需要将文档解析为树对象。然后我将不得不遍历树。当然,到那时,与源文档没有关联,所有位置信息都丢失了。所以,html5lib 出局了,这很遗憾,因为它似乎是处理格式错误 HTML.

的最佳解析器

lxml 库提供了一个目标 API,它主要反映了 ElementTree 的目标,但同样,我不知道有什么方法可以访问每个事件的位置信息。看一眼源代码也没有任何提示。

lxml 还提供了一个 API 到 SAX 事件。有趣的是,Python 的标准库提到 SAX 支持 Locator Objects, but offers little documentation about how to use them. This SO Question 提供了一些信息(当使用 SAX 解析器时),但我看不出这与对 SAX 事件的有限支持有何关系lxml 提供。

最后,在有人建议 Beautiful Soup 之前,我要指出的是,如主页上所述,"Beautiful Soup sits on top of popular Python parsers like lxml and html5lib"。它给我的只是一个对象,可以从中提取数据,而与原始源文档无关。与 html5lib 一样,在我访问数据时所有位置信息都丢失了。我 want/need 直接访问解析器。

为了扩展我在开头提到的拼写检查器示例,我只想检查文档文本中单词的拼写(而不是标签名称或属性),并且可能希望跳过检查特定内容的内容标签(如脚本或代码标签)。因此,我需要一个真正的 HTML 解析器。但是,在报告错别字时,我只对错别字在原始源文档中的位置感兴趣,不需要构建树对象。需要明确的是,这只是一种潜在用途的示例。我可能会将它用于完全不同的事情,但需求本质上是相同的。事实上,我曾经使用 HTMLParser 构建了一些非常相似的东西,但从未使用过它,因为错误处理不适用于该用例。那是几年前的事了,我似乎把那个文件弄丢了。这次我想改用 lxml 或 html5lib。

所以,有什么我想念的吗?我很难相信 none 这些解析器(除了最无用的 HTMLParser 之外)有任何方法可以访问位置信息。但如果他们这样做了,那一定是没有记录的,这对我来说似乎很奇怪。

只是一种回答 — html5lib 不提供流式传输 API 因为在解析 HTML 时通常无法提供流式传输 API 没有缓冲或致命的错误(例如考虑输入 <table>xxx)。但是,最好为 html5lib 提供一个流 API,它只对那些阻止流式传输的解析错误使用致命错误。实施起来不是特别容易,也不是特别困难。

在 html5lib 中将位置信息获取到树中应该不是太多的工作(解析错误具有位置信息的事实清楚地表明它是可能获取的!),并且有几个错误, one general, and one specific to lxml.

请注意,不可能单独使用 html5lib 分词器来实现此目的 — 分词器的状态在不同点的树构造步骤中发生了变化。因此,您必须实现一个最小的树构造函数(至少必须维护一堆开放元素,尽管我认为仅此而已)以保持分词器正确。一旦你想根据当前元素开始过滤,你基本上需要整个树构建步骤,所以你又回到了上面的流 API 问题。

经过一些额外的研究和更仔细地审查 html5lib, I discovered that html5lib.tokenizer.HTMLTokenizer 的源代码后,确实保留了部分位置信息。 "partial," 我的意思是它知道给定标记的最后一个字符的行和列。不幸的是,它没有保留标记开始的位置(我想它可以被推断出来,但这感觉就像反向重新实现大部分标记器——不,使用前一个标记器的结束位置不会如果令牌之间有白色 space,则工作。

无论如何,我能够包装 HTMLTokenizer 并创建一个 HTMLParser clone which mostly replicates the API. You can find my work here: https://gist.github.com/waylan/7d5b7552078f1abc6fac

但是,由于分词器只是 html5lib 实现的解析过程的一部分,我们放弃了 html5lib 的优点。例如,在此过程的那个阶段没有进行规范化,因此您得到的是原始(可能无效)标记而不是规范化文档。正如那里的评论所述,它并不完美,我怀疑它是否有用。

事实上,我还发现 Python 标准库中包含的 HTMLParser 已 updated 用于 Python 3.3,并且不再因无效输入而严重崩溃。据我所知,它更好(对于我的用例),因为它确实提供了实际有用的位置信息(一如既往)。在所有其他方面,我的 html5lib 包装器并没有好坏之分(当然,除了它可能接受了更多的测试,因此更稳定)。不幸的是,该更新尚未移植到 Python 2 或更早的 Python 3 版本。虽然,我不认为自己会那么困难。

无论如何,我决定继续使用标准库中的 HTMLParser 并拒绝我自己的 html5lib 包装器。您可以看到早期的成果 here,它似乎只需要最少的测试就可以正常工作。


根据 Beautiful Soup docs,HTMLParser 已更新以支持 Python 2.7.3 和 3.2.2 中的无效输入,该版本早于 3.3。

Interestingly, the HTMLParser class in the Python Standard Lib does offer support for obtaining the location info (with a getpos() method), but it is horrible at handling malformed HTML and has been eliminated as a possible solution.

我之前使用的一种技术是使用 BeautilfulSoup.prettify() 修复格式错误的 html,然后使用 HTMLParser 对其进行解析。