HTML编码与lxml解析

10 投票
2 回答
12333 浏览
提问于 2025-04-17 18:27

我正在尝试解决一些编码问题,这些问题是在用lxml抓取HTML时出现的。这里有三个我遇到的HTML示例:

1.

<!DOCTYPE html>
<html lang='en'>
<head>
   <title>Unicode Chars: 은 —’</title>
   <meta charset='utf-8'>
</head>
<body></body>
</html>

2.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko-KR" lang="ko-KR">
<head>
    <title>Unicode Chars: 은 —’</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body></body>
</html>

3.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Unicode Chars: 은 —’</title>
</head>
<body></body>
</html>

这是我基本的脚本:

from lxml.html import fromstring
...

doc = fromstring(raw_html)
title = doc.xpath('//title/text()')[0]
print title

结果是:

Unicode Chars: ì ââ
Unicode Chars: 은 —’
Unicode Chars: 은 —’

显然,第一个示例有问题,因为缺少了<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />标签。这里的解决方案来自这个链接,它能正确识别第一个示例为utf-8,因此在功能上与我原来的代码是等效的。

lxml的文档似乎有些矛盾:

这个链接中,示例似乎建议我们使用UnicodeDammit将标记编码为unicode。

from BeautifulSoup import UnicodeDammit

def decode_html(html_string):
    converted = UnicodeDammit(html_string, isHTML=True)
    if not converted.unicode:
        raise UnicodeDecodeError(
            "Failed to detect encoding, tried [%s]",
            ', '.join(converted.triedEncodings))
    # print converted.originalEncoding
    return converted.unicode

root = lxml.html.fromstring(decode_html(tag_soup))

然而在这个链接中,它说:

[Y]ou will get errors when you try [to parse] HTML data in a unicode string that specifies a charset in a meta tag of the header. You should generally avoid converting XML/HTML data to unicode before passing it into the parsers. It is both slower and error prone.

如果我试着按照lxml文档中的第一个建议来做,我的代码现在是:

from lxml.html import fromstring
from bs4 import UnicodeDammit
...
dammit = UnicodeDammit(raw_html)
doc = fromstring(dammit.unicode_markup)
title = doc.xpath('//title/text()')[0]
print title

我现在得到的结果是:

Unicode Chars: 은 —’
Unicode Chars: 은 —’
ValueError: Unicode strings with encoding declaration are not supported.

第一个示例现在可以正常工作,但第三个示例由于<?xml version="1.0" encoding="utf-8"?>标签而出现错误。

有没有一种正确的方法来处理所有这些情况?有没有比下面的解决方案更好的方法?

dammit = UnicodeDammit(raw_html)
try:
    doc = fromstring(dammit.unicode_markup)
except ValueError:
    doc = fromstring(raw_html)

2 个回答

3

这个问题可能是因为 <meta charset> 是一个比较新的标准(如果我没记错的话,是HTML5引入的,之前其实并没有广泛使用)。

lxml.html 这个库更新之前,你需要特别处理这种情况。

如果你只关心 ISO-8859-* 和 UTF-8 的编码,并且可以忽略掉一些不兼容 ASCII 的编码(比如 UTF-16 或者东亚的传统字符集),你可以对字节字符串进行正则表达式替换,把新的 <meta charset> 替换成旧的 http-equiv 格式。

否则,如果你需要一个更好的解决方案,最好的办法就是自己修补这个库(顺便把修复的代码贡献给大家)。你可以问问 lxml 的开发者,他们是否有针对这个特定问题的半成品代码,或者他们是否在他们的bug追踪系统中记录了这个问题。

18

lxml这个库在处理Unicode(也就是我们常说的中文、日文等字符)时,有一些问题讨论。所以现在最好是使用字节(bytes),同时明确指定字符编码,这样会更稳妥:

#!/usr/bin/env python
import glob
from lxml import html
from bs4 import UnicodeDammit

for filename in glob.glob('*.html'):
    with open(filename, 'rb') as file:
        content = file.read()
        doc = UnicodeDammit(content, is_html=True)

    parser = html.HTMLParser(encoding=doc.original_encoding)
    root = html.document_fromstring(content, parser=parser)
    title = root.find('.//title').text_content()
    print(title)

输出结果

Unicode Chars: 은 —’
Unicode Chars: 은 —’
Unicode Chars: 은 —’

撰写回答