逐步解析HTML(使用lxml?)

8

我目前正在尝试使用 lxml.etree.iterparse迭代解析一个非常大的HTML文档(我知道...很糟糕):

增量解析器。以SAX方式将XML解析为树并生成元组(event, element)

我正在使用增量/迭代/SAX方法来减少内存使用量(我不想将HTML加载到DOM/tree中,因为文件太大)。

问题在于,我遇到了XML语法错误,例如:

lxml.etree.XMLSyntaxError: Attribute name redefined, line 134, column 59

这就导致所有操作都停止了。

有没有一种方法可以在迭代解析HTML时避免出现语法错误?

目前我从XML语法错误异常中提取行号,然后将该行从文档中删除,然后重新开始整个过程。看起来这是一种相当恶心的解决方案。有更好的方法吗?

编辑:

这是我目前的做法:

context = etree.iterparse(tfile, events=('start', 'end'), html=True)
in_table = False
header_row = True
while context:
    try:
        event, el = context.next()
        
        # do something

        # remove old elements
        while el.getprevious() is not None:
            del el.getparent()[0]

    except etree.XMLSyntaxError, e:
        print e.msg
        lineno = int(re.search(r'line (\d+),', e.msg).group(1))
        remove_line(tfilename, lineno)
        tfile = open(tfilename)
        context = etree.iterparse(tfile, events=('start', 'end'), html=True)
    except KeyError:
        print 'oops keyerror'
5个回答

12

最终完美的解决方案竟然是Python自带的HTMLParser[文档]

这是我最终使用的(相当糟糕的)代码:

class MyParser(HTMLParser):
    def __init__(self):
        self.finished = False
        self.in_table = False
        self.in_row = False
        self.in_cell = False
        self.current_row = []
        self.current_cell = ''
        HTMLParser.__init__(self)

    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        if not self.in_table:
            if tag == 'table':
                if ('id' in attrs) and (attrs['id'] == 'dgResult'):
                    self.in_table = True
        else:
            if tag == 'tr':
                self.in_row = True
            elif tag == 'td':
                self.in_cell = True
            elif (tag == 'a') and (len(self.current_row) == 7):
                url = attrs['href']
                self.current_cell = url


    def handle_endtag(self, tag):
        if tag == 'tr':
            if self.in_table:
                if self.in_row:
                    self.in_row = False
                    print self.current_row
                    self.current_row = []
        elif tag == 'td':
            if self.in_table:
                if self.in_cell:
                    self.in_cell = False
                    self.current_row.append(self.current_cell.strip())
                    self.current_cell = ''

        elif (tag == 'table') and self.in_table:
            self.finished = True

    def handle_data(self, data):
        if not len(self.current_row) == 7:
            if self.in_cell:
                self.current_cell += data
那段代码可以让我做到这一点:
parser = MyParser()
for line in myfile:
    parser.feed(line)

6

目前,lxml etree.iterparse 支持 recover=True 关键字参数,因此您可以将此参数传递给 iterparse 而不是编写自定义 HTMLParser 子类来修复损坏的 HTML。

为了正确解析巨大且损坏的HTML,您只需要执行以下操作:

etree.iterparse(tfile, events=('start', 'end'), html=True, recover=True)

这对我来说是最好的答案。 - Zhang Yiwei
这是一个好建议。但请注意,如果 html 为真且未指定 recover,则 recover 将会是 True,正如文档中所述:“recover: try hard to parse through broken input (default: True for HTML)” 对于 iterparse http://lxml.de/api/lxml.etree.iterparse-class.html - Nate Anderson
看起来在OP提出问题的时候(2011年12月),lxml版本2.3并没有recover参数。根据版本3.3中的代码,可以看出recover参数是在lxml中引入的(发布于2014年1月)。所以感谢@Pawel指出了recover参数!(但OP无需更改其代码(他们使用html=True),只需更新lxml即可!) - Nate Anderson

1
抱歉提出一个老问题,但是对于正在寻找解决方案的后来者,lxml 3.3版本拥有HTMLPullParserXMLPullParser可进行增量解析。您也可以查看lxml解析介绍以获取更多示例。
如果您想要解析一个非常大的文档并节省内存,您可以编写一个自定义目标类作为事件处理程序,以避免构建元素树。类似这样的东西:
class MyParserTarget:
    def start(self, tag, attrib) -> None:
        # do something
    def end(self, tag) -> None:
        # do something
    def data(self, data) -> None:
        # do something
    def close(self):
        # return your result

mytarget = MyParserTarget()
parser = lxml.etree.HTMLPullParser(target=mytarget)
parser.feed(next(content))
# Do other stuff
result = parser.close()

如果您继续像OP问题中那样使用etree.iterparse(..., html=True)它将在底层使用HtmlPullParser。但是,iterparse不会传递自定义目标实例(如我在这里展示的),即使在lxml的最新版本中也是如此。因此,如果您喜欢自定义目标方法(而不是OP中所示的events参数),则可以直接使用HtmlPullParser

1

iterparse的参数htmlhuge_tree中使用True


我目前正在使用 html=True,但仍然会引发 XML 语法错误。我将查看 huge_tree 参数。 - Acorn
1
huge_tree似乎不相关: “huge_tree:禁用安全限制并支持非常深的树”。我的树不是很深,只是很长。 - Acorn

0

尝试使用lxml.html解析您的HTML文档:

自2.0版本以来,lxml带有一个专用的Python包,用于处理HTML:lxml.html。它基于lxml的HTML解析器,但为HTML元素提供了特殊的Element API,以及许多常见HTML处理任务的实用程序。


1
我正在尝试迭代解析该文档,因为它非常大。据我所知,lxml.html没有iterparse函数。 - Acorn
1
我建议使用lxml.html,因为在OP中没有提到尝试过lxml.html。我认为对我的回答进行负评是相当误导的。 - Raymond Yee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接