Как заставить SQLAlchemy правильно вставлять многоточие в unicode в таблицу mySQL?
Я пытаюсь разобрать RSS-канал с помощью feedparser и вставить его в таблицу mySQL, используя SQLAlchemy. На самом деле я смог добиться этого, но сегодня у фида был элемент с многоточием в описании, и я получаю следующую ошибку:
UnicodeEncodeError: кодек "latin-1" не может кодировать символ u '\ u2026' в позиции 35: порядковый номер не в диапазоне (256)
Если я добавлю параметр convert_unicode = True в движок, я могу получить вставку, но многоточие не отображает ее просто странные символы. Это, по-видимому, имеет смысл, поскольку, насколько мне известно, горизонтального эллипса в латинском-1 нет. Даже если я установил кодировку в utf-8, это, похоже, не имеет значения. Если я делаю вставку с использованием phpmyadmin и включаю эллипсис, он проходит через штраф.
Я думаю, что просто не понимаю кодировки символов или как заставить SQLAlchemy использовать тот, который я указываю. Кто-нибудь знает, как заставить текст войти без лишних символов?
ОБНОВЛЕНИЕ
Я думаю, что я понял это, но я не совсем уверен, почему это важно...
Вот код:
import sys
import feedparser
import sqlalchemy
from sqlalchemy import create_engine, MetaData, Table
COMMON_CHANNEL_PROPERTIES = [
('Channel title:','title', None),
('Channel description:', 'description', 100),
('Channel URL:', 'link', None),
]
COMMON_ITEM_PROPERTIES = [
('Item title:', 'title', None),
('Item description:', 'description', 100),
('Item URL:', 'link', None),
]
INDENT = u' '*4
def feedinfo(url, output=sys.stdout):
feed_data = feedparser.parse(url)
channel, items = feed_data.feed, feed_data.entries
#adding charset=utf8 here is what fixed the problem
db = create_engine('mysql://user:[email protected]/db?charset=utf8')
metadata = MetaData(db)
rssItems = Table('rss_items', metadata,autoload=True)
i = rssItems.insert();
for label, prop, trunc in COMMON_CHANNEL_PROPERTIES:
value = channel[prop]
if trunc:
value = value[:trunc] + u'...'
print >> output, label, value
print >> output
print >> output, "Feed items:"
for item in items:
i.execute({'title':item['title'], 'description': item['description'][:100]})
for label, prop, trunc in COMMON_ITEM_PROPERTIES:
value = item[prop]
if trunc:
value = value[:trunc] + u'...'
print >> output, INDENT, label, value
print >> output, INDENT, u'---'
return
if __name__=="__main__":
url = sys.argv[1]
feedinfo(url)
Здесь вывод/трассировка от запуска кода без опции charset:
Channel title: [H]ardOCP News/Article Feed
Channel description: News/Article Feed for [H]ardOCP...
Channel URL: http://www.hardocp.com
Feed items:
Item title: Windows 8 UI is Dropping the 'Start' Button
Item description: After 15 years of occupying a place of honor on the desktop, the "Start" button will disappear from ...
Item URL: http://www.hardocp.com/news/2012/02/05/windows_8_ui_dropping_lsquostartrsquo_button/
---
Item title: Which Crashes More? Apple Apps or Android Apps
Item description: A new study of smartphone apps between Android and Apple conducted over a two month period came up w...
Item URL: http://www.hardocp.com/news/2012/02/05/which_crashes_more63_apple_apps_or_android/
---
Traceback (most recent call last):
File "parse.py", line 47, in <module>
feedinfo(url)
File "parse.py", line 36, in feedinfo
i.execute({'title':item['title'], 'description': item['description'][:100]})
File "/usr/local/lib/python2.7/site-packages/sqlalchemy/sql/expression.py", line 2758, in execute
return e._execute_clauseelement(self, multiparams, params)
File "/usr/local/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 2304, in _execute_clauseelement
return connection._execute_clauseelement(elem, multiparams, params)
File "/usr/local/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1538, in _execute_clauseelement
compiled_sql, distilled_params
File "/usr/local/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_context
context)
File "/usr/local/lib/python2.7/site-packages/sqlalchemy/engine/default.py", line 330, in do_execute
cursor.execute(statement, parameters)
File "build/bdist.linux-i686/egg/MySQLdb/cursors.py", line 159, in execute
File "build/bdist.linux-i686/egg/MySQLdb/connections.py", line 264, in literal
File "build/bdist.linux-i686/egg/MySQLdb/connections.py", line 202, in unicode_literal
UnicodeEncodeError: 'latin-1' codec can't encode character u'\u2026' in position 35: ordinal not in range(256)
Итак, похоже, что добавление charset в строку подключения mysql сделало это. Полагаю, он по умолчанию использует латинский-1? Я попытался установить флаг кодирования на content_engine на utf8, и это ничего не сделало. Кто-нибудь знает, почему он будет использовать latin-1, когда для таблиц и полей будет установлен unicode utf8? Я также попробовал кодировать элемент ['description], используя .encode(' cp1252 '), прежде чем отправлять его, и это тоже работало, даже не добавляя параметр charset в строку подключения. Это не должно было работать с латинским-1, но, видимо, так оно и было? У меня есть решение, но я бы хотел ответить:)
Ответы
Ответ 1
Сообщение об ошибке
UnicodeEncodeError: 'latin-1' codec can't encode character u'\u2026'
in position 35: ordinal not in range(256)
похоже, указывает, что некоторый код языка Python пытается преобразовать символ \u2026
в строку Latin-1 (ISO8859-1), и он терпит неудачу. Не удивительно, что этот символ U+2026 HORIZONTAL ELLIPSIS
, который не имеет единого эквивалентного символа в ISO8859-1.
Вы устранили проблему, добавив запрос ?charset=utf8
в свой вызов соединения SQLAlchemy:
import sqlalchemy
from sqlalchemy import create_engine, MetaData, Table
db = create_engine('mysql://user:[email protected]/db?charset=utf8')
Раздел URL-адреса базы данных документации SQLAlchemy сообщает нам, что URL-адрес, начинающийся с mysql
, указывает диалоги MySQL, используя mysql-python
.
В следующем разделе Пользовательские аргументы DBAPI connect() говорится, что аргументы запроса передаются в базовый DBAPI.
Итак, что делает mysql-python
драйвер параметра {charset: 'utf8'}
? Раздел Функции и атрибуты их документации говорит об атрибуте charset
"... Если присутствует, набор символов соединения будет изменен на этот набор символов, если они не равны."
Чтобы узнать, что означает набор символов соединения, переходим к 10.1.4. "Наборы символов соединения и сортировки" справочного руководства MySQL 5.6. Короче говоря, MySQL может интерпретировать входящие запросы как кодировку, отличную от набора символов базы данных, и отличную от кодировки возвращаемых результатов запроса.
Поскольку сообщение об ошибке, о котором вы сообщаете, похоже на Python, а не сообщение об ошибке SQL, я буду предполагать, что что-то в SQLAlchemy или mysql-python пытается преобразовать запрос в кодировку соединения по умолчанию latin-1
перед отправкой, Это то, что вызывает ошибку. Однако строка запроса ?charset=utf8
в вашем вызове connect()
изменяет кодировку соединения, и U+2026 HORIZONTAL ELLIPSIS
может пройти.
Обновление:, вы также спрашиваете: "Если я удалю опцию charset, а затем закодирую описание, используя .encode('cp1252'), это будет очень хорошо. Как можно получить многоточие через cp1252, но не unicode?"
encoding cp1252
имеет горизонтальный символ с многоточием в байтовом значении \x85
. Таким образом, можно кодировать строку Unicode, содержащую U+2026 HORIZONTAL ELLIPSIS
, в cp1252 без ошибок.
Помните также, что в Python строки Unicode и байтовые строки представляют собой два разных типа данных. Разумно предположить, что MySQLdb может иметь политику отправки только байтовых строк по SQL-соединению. Таким образом, он будет кодировать запрос, полученный в виде строки Unicode, в байтовую строку, но оставит запрос, полученный только как байтовая строка. (Это предположение, я не смотрел исходный код.)
В ответе, который вы отправили, последние две строки (ближе всего к месту возникновения ошибки) показывают имена методов literal
, а затем unicode_literal
. Это, как правило, поддерживает теорию, согласно которой MySQLdb кодирует запрос, который он получает как строку Unicode, в байтовую строку.
Когда вы кодируете строку запроса самостоятельно, вы обходите часть MySQLdb, которая делает эту кодировку по-разному. Обратите внимание, однако, что если вы кодируете строку запроса иначе, чем вызовы charset для подключения к MySQL, тогда у вас будет несоответствие в кодировке, и ваш текст, вероятно, будет ошибочным.
Ответ 2
Добавление строки charset=utf8
в строку подключения определенно помогает, но я столкнулся с ситуациями в Python 2.7, когда было добавлено добавление convert_unicode=True
в create_engine
. Документация SQLAlchemy говорит об этом только для повышения производительности, но в моем случае она фактически решила проблему использования неправильного кодировщика.