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

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

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

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

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 9

    0
    Да, приходится свежеполученные данные принудительно превращать в unicode, нередко при этом пользуясь тормозным chardet. Увы, пока лучше вариантов не нашел.
      +8
      Вам просили передать, что по RFC 2616 (HTTP 1.1) кодировка контента по умолчанию — ISO8859-1 a.k.a. Latin1. Так что библиотечка всё делает правильно, и питон всё делает правильно, а виноваты вебмастеры, не объявляющие кодировку своих страниц. И частично писатели стандарта, выбравшие «плохую» кодировку.
        +13
        Или вы Александр Кошелев или я ничего не понимаю.
        Вот та же самая статья, датированная аж 2009 годом: webnewage.org/2009/11/04/be-ware-lxml-html/
          +3
            0
            во-во… То же самое хотел написать. Копипейст, да еще и старинный. Хотя мне в свое время эта статья помогла!

            А вообще я уже сто лет вот так пишу:

            etree.HTML(body, parser=etree.HTMLParser(encoding='utf-8')
              0
              А я grab юзаю, он сам определяет кодировку по Content-Type из мета-тэга или из http-заголовка. Ну и utf-8, если ничего не нашлось.
                0
                lxml насколько помню тоже в meta — теги смотрит. Но не уверен. Предпочитаю всегда сам указывать.
              0
              Ждём комментарий автора.
                0
                Нет, это не я. Это наглое воровство:-(

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое