Обходное решение: ошибка при создании текстовых частей с символами не из диапазона ASCII

obhodnoe reshenie oshibka pri sozdanii tekstovyh chastej s simvolami ne iz diapazona ascii Сценарии на стороне клиента

Последняя проблема, связанная с поддержкой Юникода в пакете email, является столь же серьезной, как и предыдущая: изменения, подобные тем, о которых рассказывалось в предыдущем разделе, вызвали еще одну регрессию в работе механизма создания новых почтовых сообщений. Она выражается в невозможности создавать текстовые части сообщений без адаптации под различные кодировки Юникода.

Тексты некоторых типов автоматически подвергаются преобразованию в формат MIME, пригодный для отправки. К сожалению, из-за строгого разделения типов str/bytes класс MIMEText в пакете email теперь требует указывать разные типы строковых объектов для различных кодировок Юникода. В результате вам теперь необходимо точно знать, как пакет email обрабатывает текстовые данные при создании объектов текстовых сообщений, или повторять значительную часть логики этого класса в своих подклассах.

Например, ниже показано, как в настоящее время необходимо обрабатывать текст наиболее распространенных типов, чтобы корректно создать заголовки с информацией о кодировках и применить необходимое преобразование MIME:

>>> m = MIMEText(‘abc‘, _charset=’ascii‘) # передать текст

# для кодировки ascii

>>> print(m)

MIME-Version: 1.0

Content-Type: text/plain; charset="us-ascii"

Content-Transfer-Encoding: 7bit

abc

>>> m = MIMEText(‘abc’, _charset=’latin-1′) # текст для кодировки latin-1

>>> print(m) # но не для latin1′: см. далее

MIME-Version: 1.0

Content-Type: text/plain; charset="iso-8859-1"

Content-Transfer-Encoding: quoted-printable

abc

>>> m = MIMEText(b’abc’, _charset=’utf-8′) # передать строку bytes для utf8

>>> print(m)

Content-Type: text/plain; charset="utf-8"

MIME-Version: 1.0

Content-Transfer-Encoding: base64

YWJj

Все работает, но если посмотреть внимательно, можно заметить, что в первых двух случаях мы должны передавать строку str, а в третьем — строку bytes. Требование такого особого подхода к типам представления Юникода исходит из особенностей внутренней реализации пакета. Передача типов, отличных от тех, что ожидаются для представления Юникода, вызывает ошибку, что обусловлено появлением внутри пакета email в версии 3.1 недопустимых комбинаций типов str/bytes:

>>> m = MIMEText(‘abc’, _charset=’ascii’)

>  >> m = MIMEText(b’abc’, _charset=’ascii’) # ошибка: предполагается str 2.X Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\encoders.py", line 60, in encode_7or8bit orig.encode(‘ascii’)

AttributeError: ‘bytes’ object has no attribute ‘encode’ (AttributeError: объект ‘bytes’ не имеет атрибута ‘encode’)

>  >> m = MIMEText(‘abc’, _charset=’latin-1′)

>  >> m = MIMEText(b’abc’, _charset=’latin-1′) # ошибка: qp использует str Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\quoprimime.py", line 176, in body_encode if line.endswith(CRLF):

TypeError: expected an object with the buffer interface (TypeError: ожидается объект с интерфейсом буфера)

>  >> m = MIMEText(b’abc’, _charset=’utf-8′)

>  >> m = MIMEText(‘abc’, _charset=’utf-8′) # ошибка: base64 использует bytes Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\base64mime.py", line 94, in body_encode enc = b2a_base64(s[i:i + max_unencoded]).decode("ascii")

TypeError: must be bytes or buffer, not str

(TypeError: должен быть объект типа bytes или buffer, а не str)

Кроме того, пакет email более придирчив к синонимам имен кодировок, чем Python и большинство других инструментов: кодировка «latin-1» определяется как MIME-тип quoted-printable, но кодировка «latin1» считается неизвестной и поэтому она определяется как MIME-тип по умолчанию Base64. Именно по этой причине для кодировки «latin1» выбирался MIME-тип Base64 в приведенных ранее примерах этого раздела — такой выбор формата MIME считается некорректным для любых получателей, которые распознают синоним «latin1», включая сам Python. К сожалению, это также означает, что при использовании синонима, который не распознается пакетом, мы должны передавать строку другого типа:

>>> m = MIMEText(‘abc’, _charset=’latin-1′) # str для ‘latin-1’

>>> print(m)

MIME-Version: 1.0

Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable

abc

>>> m = MIMEText(‘abc’, _charset=’latin1′)

Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\base64mime.py", line 94, in body_encode enc = b2a_base64(s[i:i + max_unencoded]).decode("ascii")

TypeError: must be bytes or buffer, not str

(TypeError: должен быть объект типа bytes или buffer, а не str)

>>> m = MIMEText(b’abc’, _charset=’latin1′) # bytes для ‘latin1’!

>>> print(m)

Content-Type: text/plain; charset="latin1"

MIME-Version: 1.0

Content-Transfer-Encoding: base64

YWJj

Существуют разные способы добавления новых типов кодировок и псевдонимов в пакет email, но они не являются стандартными. Программы, для которых надежность имеет большое значение, должны перепроверять имена кодировок, введенные пользователем, которые могут быть допустимыми для Python, но незнакомыми пакету email. То же относится и к данным, которые вообще не являются символами ASCII, — вам сначала придется декодировать их в текст, чтобы использовать ожидаемое имя «latin-1», потому что для преобразования в формат MIME quoted-printable требуется строка str, хотя для преобразования в формат MIME Base64, когда указывается кодировка «latin1», требуется строка bytes:

>>> m = MIMEText(b’A\xe4B’, _charset=’latin1′)

>>> print(m)

Content-Type: text/plain; charset="latin1"

MIME-Version: 1.0

Content-Transfer-Encoding: base64

QeRC

>>> m = MIMEText(b’A\xe4B’, _charset=’latin-1′)

Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\quoprimime.py", line 176, in body_encode if line.endswith(CRLF):

TypeError: expected an object with the buffer interface

(TypeError: ожидается объект с интерфейсом буфера)

>>> m = MIMEText(b’A\xe4B’.decode(‘latin1′), _charset=’latin-1’)

>>> print(m)

MIME-Version: 1.0

Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable

A=E4B

Фактически объект текстового сообщения не проверяет, являются ли данные, предназначенные вами для преобразования в формат MIME, допустимыми символами Юникода — мы сможем отправить недопустимый текст в кодировке UTF, но у получателя могут возникнуть проблемы при попытке декодировать его:

>>> m = MIMEText(b’A\xe4B’, _charset=’utf-8′)

>>> print(m)

Content-Type: text/plain; charset="utf-8"

MIME-Version: 1.0

Content-Transfer-Encoding: base64

QeRC

>>> b’A\xe4B’.decode(‘utf8’)

UnicodeDecodeError: ‘utf8’ codec can’t decode bytes in position 1-2: unexpected

(UnicodeDecodeError: кодек ‘utf8’ не может декодировать байты

в позиции 1-2: неожиданный…)

>>> import base64

>>> base64.b64decode(bQeRC‘)

bA\xe4B

>>> base64.b64decode(b’QeRC’).decode(‘utf’)

UnicodeDecodeError: ‘utf8’ codec can’t decode bytes in position 1-2: unexpected

(UnicodeDecodeError: кодек ‘utf8’ не может декодировать байты

а позиции 1-2: неожиданный…)

Так как же быть, если потребуется прикрепить текст к составляемому почтовому сообщению, когда выбор типа данных косвенно зависит от имени его кодировки? Обобщенный суперкласс Message не сможет оказать прямую помощь, если мы укажем кодировку, так как он проявляет точно такое же поведение, зависимое от кодировки:

>>> m = Message()

>>> m.set_payload(‘spam’, charset=’us-ascii’)

>>> print(m)

MIME-Version: 1.0

Content-Type: text/plain; charset="us-ascii"

Content-Transfer-Encoding: 7bit

spam

>   >> m = Message()

>   >> m.set_payload(b’spam’, charset=’us-ascii’)

AttributeError: ‘bytes’ object has no attribute ‘encode’ (AttributeError: объект ‘bytes’ не имеет атрибута ‘encode’)

>   >> m.set_payload(‘spam’, charset=’utf-8′)

TypeError: must be bytes or buffer, not str

(TypeError: должен быть объект типа bytes или buffer, а не str)

Мы могли бы попробовать обойти эти проблемы, повторив большую часть программного кода, который выполняет пакет email, но эта избыточность накрепко привязала бы нас к текущей реализации и ввела бы в зависимость от изменений в будущем. Следующий пример как попугай повторяет шаги, выполняемые пакетом email при создании объекта текстового сообщения с текстом в кодировке ASCII. В отличие от приема с использованием класса MIMEText, данный подход позволяет читать любые данные из файлов в виде строк двоичных байтов, даже если эти данные — простой текст ASCII:

>   >> m = Message()

>   >> m.add_header(‘Content-Type’, ‘text/plain’)

>>> m[‘MIME-Version’] = ‘1.0’

>>> m.set_param(‘charset’, ‘us-ascii’)

>>> m.add_header(‘Content-Transfer-Encoding’, ‘7bit’)

>>> data = b’spam’

>>> m.set_payload(data.decode(‘ascii’)) # данные читаются в двоичном виде

>>> print(m)

MIME-Version: 1.0

Content-Type: text/plain; charset="us-ascii"

Content-Transfer-Encoding: 7bit

spam

>>> print(MIMEText(‘spam’, _charset=’ascii’)) # то же самое, MIME-Version: 1.0 # но уже зависит от типа

Content-Type: text/plain; charset="us-ascii"

ContentTransferEncoding: 7bit

spam

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

>   >> m = Message()

>   >> m.add_header(‘Content-Type’, ‘text/plain’)

>   >> m[‘MIME-Version’] = ‘1.0’

>  >> m.set_param(‘charset’, ‘utf-8’)

>  >> m.add_header(‘Content-Transfer-Encoding’, ‘base64’)

>  >> data = b’spam’

>  >> from binascii import b2a_base64 # преобразование MIME, если необходимо

>>> data = b2a_base64(data) # здесь также читаются двоичные данные

>>> m.set_payload(data.decode(‘ascii’))

>>> print(m)

MIME-Version: 1.0

Content-Type: text/plain; charset="utf-8"

Content-Transfer-Encoding: base64

c3BhbQ==

>  >> print(MIMEText(b’spam’, _charset=’utf-8′)) # то же самое

Content-Type: text/plain; charset="utf-8" # но уже зависит от типа MIME-Version: 1.0

Content-Transfer-Encoding: base64

c3BhbQ==

Этот прием действует, но помимо избыточности и зависимости, создаваемыми им, для широкого использования такого подхода его необходимо обобщить, — чтобы его можно было применять со всеми возможными кодировками Юникода и форматами MIME, как это уже делает пакет email. Нам может также потребоваться реализовать поддержку синонимов имен кодировок для большей гибкости и тем самым еще больше увеличить избыточность. Иными словами, потребуется выполнить массу дополнительной работы, и в конце нам все равно необходимо будет специализировать свою реализацию для различных типов Юникода.

Какой бы путь мы ни избрали, похоже, что в настоящее время нам не удастся избежать некоторой зависимости от текущей реализации. Пожалуй, самое лучшее, что можно сделать в такой ситуации, кроме как сидеть и надеяться на появление улучшений в пакете email через несколько лет, — это указывать кодировки Юникода в вызовах конструкторов текстовых сообщений и надеяться, что имена кодировок будут известны пакету, а данные в сообщениях будут совместимы с выбранной кодировкой. Ниже приводится, возможно, немного непонятный программный код, который используется в следующем ниже пакете mailtools (пример 13.23) для выбора текстовых типов:

>  >> from email.charset import Charset, BASE64, QP

>  >> for e in (‘us-ascii’, ‘latin-1’, ‘utf8’, ‘latin1’, ‘ascii’):

cset = Charset(e)

benc = cset.body_encoding

if benc in (None, QP):

print(e, benc, ‘text‘) # прочитать/получить данные как str

else:

print(e, benc, ‘binary’) # прочитать/получить данные как bytes us-ascii None text latin-1 1 text utf8 2 binary latin1 2 binary ascii None text

Мы продолжим действовать в этом направлении далее в книге, помня, что это практически наверняка потребует внесения изменений в программный код в будущем, потому что при таком подходе образуется тесная связь с текущей реализацией email.

Самые последние новости: Есть сведения, что эта ошибка, как и в предыдущем разделе, также будет исправлена в Python 3.2, что сделает данное обходное решение ненужным в этой и в последующих версиях Python. Однако пока неизвестно, каким будет это исправление, и нам по-прежнему требуется решение для версии Python, текущей на момент, когда писалась эта глава. Перед самой публикацией книги уже вышла альфа-версия Python 3.2, в которой по-прежнему присутствует некоторая зависимость от типа, но теперь принимаются текстовые данные в виде str или bytes и при необходимости вызывается преобразование в формат Base64, вместо простого сохранения данных в виде строки bytes.

Использованная литература:

Марк Лутц — Программирование на Python, 4-е издание, II том, 2011

Каталог сайтов Всего.ру
Оцените статью
Секреты программирования
Добавить комментарий