Pull to refresh

LXML — проблемы с кодировкой при парсинге HTML

Reading time 2 min
Views 15K
Данный пост посвящен извечной проблеме всех питонистов — кодировкам. Недавно я получил письмо, в котором мой знакомый жаловался на то, что у него в программе получаются строчки вида::

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

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

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

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

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

Для начала, я сходил на 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. Но в любом случае обходные пути есть.

Будьте бдительны.
Tags:
Hubs:
+5
Comments 9
Comments Comments 9

Articles