Я использую Python и Django, но у меня проблема, связанная с ограничением MySQL. Согласно документации MySQL 5.1, их реализация utf8
не поддерживает 4-байтовые символы. MySQL 5.5 будет поддерживать 4-байтовые символы, используя utf8mb4
; и, когда-нибудь в будущем, utf8
также может поддержать его.
Но мой сервер не готов к обновлению до MySQL 5.5, и поэтому я ограничен символами UTF-8, которые занимают 3 байта или меньше.
Мой вопрос: Как фильтровать (или заменять) символы Unicode, которые занимают более 3 байтов?
Я хочу заменить все 4-байтные символы официальным \ufffd
(U + FFFD REPLACEMENT CHARACTER) или ?
.
Другими словами, я хочу, чтобы поведение было похоже на собственный метод str.encode()
Python (при передаче параметра 'replace'
). Изменить: мне нужно поведение, подобное encode()
, но я не хочу кодировать строку. Я хочу по-прежнему иметь строку unicode после фильтрации.
Я НЕ хочу, чтобы избежать символа перед хранением в MySQL, потому что это означало бы, что мне нужно было бы unescape все строки, которые я получаю из базы данных, что очень раздражает и неосуществимо.
См. также:
- Предупреждение "Неверное строковое значение" при сохранении некоторых символов Unicode в MySQL (в системе билетов Django)
- '𠂉 Недействительный символ юникода, но в наборе символов Unicode? (при переполнении стека)
[EDIT] Добавлены тесты о предлагаемых решениях
Итак, до сих пор я получил хорошие ответы. Спасибо, люди! Теперь, чтобы выбрать один из них, я быстро проверил, чтобы найти самый простой и быстрый.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vi:ts=4 sw=4 et
import cProfile
import random
import re
# How many times to repeat each filtering
repeat_count = 256
# Percentage of "normal" chars, when compared to "large" unicode chars
normal_chars = 90
# Total number of characters in this string
string_size = 8 * 1024
# Generating a random testing string
test_string = u''.join(
unichr(random.randrange(32,
0x10ffff if random.randrange(100) > normal_chars else 0x0fff
)) for i in xrange(string_size) )
# RegEx to find invalid characters
re_pattern = re.compile(u'[^\u0000-\uD7FF\uE000-\uFFFF]', re.UNICODE)
def filter_using_re(unicode_string):
return re_pattern.sub(u'\uFFFD', unicode_string)
def filter_using_python(unicode_string):
return u''.join(
uc if uc < u'\ud800' or u'\ue000' <= uc <= u'\uffff' else u'\ufffd'
for uc in unicode_string
)
def repeat_test(func, unicode_string):
for i in xrange(repeat_count):
tmp = func(unicode_string)
print '='*10 + ' filter_using_re() ' + '='*10
cProfile.run('repeat_test(filter_using_re, test_string)')
print '='*10 + ' filter_using_python() ' + '='*10
cProfile.run('repeat_test(filter_using_python, test_string)')
#print test_string.encode('utf8')
#print filter_using_re(test_string).encode('utf8')
#print filter_using_python(test_string).encode('utf8')
Результаты:
-
filter_using_re()
выполнил 515 вызовов функций в 0.139 секунд процессора (0.138 секунды процессора при встроенномsub()
) -
filter_using_python()
выполнил 2097923 вызовы функций 3.413 секунд процессора (1,511 секунды процессора при вызовеjoin()
и 1,900 секунд процессора, вычисляющих выражение генератора) - Я не тестировал, используя
itertools
, потому что... ну... это решение, хотя и интересное, было довольно большим и сложным.
Заключение
Решение RegEx было, безусловно, самым быстрым.