Александр Кошелев
Александр Кошелев Python-разработчик

Бойтесь lxml, html парсящий

Недавно на форуме случился топик посвященный извечной проблеме всех питонистов – кодировкам. Человек жаловался на то, что у него в программе получаются строчки вида:

u'\xd0\x9a\xd1\x83\xd1\x80\xd1\x83\xd0\xbc\xd0\xbe\xd1\x87'

Вы заметили что что-то не так? И я вот. Строчки как бы уникодные, но внутри них закодированные utf-8 байты. Что-то pдесь не так. Разбираясь дальше и потребовав скрипт, которые такое генерирует, становится понятно, что данные берутся из веба. Вполне обычным способом через urllib и потом скармливаются в lxml.html для разбора. Поскольку urllib оперирует только байтовыми строками, то он не мог их так превратить в уникод, а значит во всем виноват lxml.

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

Но тут немного другой случай. Тут используется парсер html. И именно в нем происходят эти неприятные метаморфозы со строками.

Я решил понять в чем же всё-таки и дело и как побороть такое поведение.

Для начала, я сходил на http://yandex.ru/ и посмотрел что за html там отдается. Кодировка контента utf8. Сразу что бросилось в глаза – это отсутствие декларации кодировки Он не обязателен, но всё-же довольно часто используется. Сделав похожий снипет html:

data = """<html>
<head>
</head>
<body>Привет мир</body>
</html>"""
html = lxml.html.document_fromstring(data)

и засунув его в lxml.html, получил, увы, уже ожидаемый результат:

>>> s
u'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82 \xd0\xbc\xd0\xb8\xd1\x80'
>>> print s
Привет м

s – это как раз и есть строчка “Привет мир”, выдранная через xpath. Как видно, она в нераскодированном виде. По большому счету это проблему можно решить на месте. Есть такой специальный кодек raw-unicode-escape, который из такой строчки сделайт байтовую но тоже без конвертации:

>>> print s.encode('raw-unicode-escape')
Привет мир

Но такое решение плохое. Надо как-то заставить lxml.html не издеваться над не-ASCII символами.

Что будет если указать кодировку в нелюбимом мною мета-заголовке html?

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>Привет мир</body>
</html>

Всё сразу встаёт на свои места:

>>> print s
Привет мир

Конечно логичней было бы брать информацию о кодировке из http заголовков, но для lxml.html протокол по которому пришли данные загадка и он не может на него опираться.

Ещё один способ решения – это уже на вход lxml.html давать не байтовую строчку, а уникод (если вы конечно точно знаете кодировку сами):

>>> html = lxml.html.document_fromstring(data.decode('utf-8'))
...
>>> print s
Привет мир

На мой взгляд было бы правильней, чтобы lxml.html не пытался “выжить любой ценой” и портить контент, а явным образом сообщать о том что не задана кодировка – как кстати он же и поступает в случае разбора xml. Но в любом случае обходные пути есть.

Будьте бдительны.

comments powered by Disqus