如何过滤(或replace)UTF-8中需要超过3个字节的unicode字符?
我正在使用Python和Django,但是我遇到了由于MySQL限制而导致的问题。 根据MySQL 5.1文档 ,他们的utf8
实现不支持4字节的字符。 MySQL 5.5将使用utf8mb4
支持4字节字符; 而且将来有一天, utf8
也可能会支持它。
但是我的服务器还没有准备好升级到MySQL 5.5,因此我只能使用3个字节或更less的UTF-8字符。
我的问题是: 如何过滤(或replace)将需要超过3个字节的Unicode字符?
我想用官方的\ufffd
( U + FFFDreplace字符 )replace所有的4字节字符,还是用?
。
换句话说,我想要一个与Python自己的str.encode()
方法非常相似的行为(当传递'replace'
参数时)。 编辑:我想要一个类似于encode()
的行为,但我不想实际编码string。 我想在过滤后仍然有一个unicodestring。
在存储到MySQL之前,我不想逃避这个angular色,因为那意味着我需要将我从数据库中获得的所有string都取消,这是非常烦人和不可行的。
也可以看看:
- 将某些unicode字符保存到MySQL (在Django票证系统中) 时出现“错误的string值”警告
- '𠂉'不是一个有效的unicode字符,但在unicode字符集? (在堆栈溢出)
[编辑]增加了有关build议解决scheme的testing
所以我得到了很好的答案。 谢谢,人民! 现在,为了select其中之一,我做了一个快速testing,find最简单,最快速的一个。
#!/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()
在0.139个CPU秒内完成了515个函数调用sub()
内buildsub()
为0.138个CPU秒) -
filter_using_python()
在3.413 CPU秒 (在join()
调用时为1.511 CPU秒,在生成器expression式为1.900 CPU秒时filter_using_python()
做了2097923次函数调用。 - 我没有使用
itertools
testing,因为…itertools
…这个解决scheme虽然很有趣,但是相当庞大和复杂。
结论
RegEx解决scheme是迄今为止最快的解决scheme。
范围\ u0000- \ uD7FF和\ uE000- \ uFFFF中的Unicode字符在UTF8中将有3个字节(或更less)的编码。 \ u800-uDFFF范围用于多字节UTF16。 我不知道Python,但你应该能够build立一个正则expression式来匹配这些范围之外。
pattern = re.compile("[\uD800-\uDFFF].", re.UNICODE) pattern = re.compile("[^\u0000-\uFFFF]", re.UNICODE)
在问题主体中编辑从DenilsonSá的脚本中添加Python:
re_pattern = re.compile(u'[^\u0000-\uD7FF\uE000-\uFFFF]', re.UNICODE) filtered_string = re_pattern.sub(u'\uFFFD', unicode_string)
您可以跳过解码和编码步骤,直接检测每个字符的第一个字节(8位string)的值。 根据UTF-8:
#1-byte characters have the following format: 0xxxxxxx #2-byte characters have the following format: 110xxxxx 10xxxxxx #3-byte characters have the following format: 1110xxxx 10xxxxxx 10xxxxxx #4-byte characters have the following format: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
据此,你只需要检查每个字符的第一个字节的值来过滤掉4个字节的字符:
def filter_4byte_chars(s): i = 0 j = len(s) # you need to convert # the immutable string # to a mutable list first s = list(s) while i < j: # get the value of this byte k = ord(s[i]) # this is a 1-byte character, skip to the next byte if k <= 127: i += 1 # this is a 2-byte character, skip ahead by 2 bytes elif k < 224: i += 2 # this is a 3-byte character, skip ahead by 3 bytes elif k < 240: i += 3 # this is a 4-byte character, remove it and update # the length of the string we need to check else: s[i:i+4] = [] j -= 4 return ''.join(s)
跳过解码和编码部分将节省您一些时间,对于大部分为1字节字符的较小string,这甚至可能比正则expression式过滤更快。
而只是为了它的乐趣,一个itertools
怪物:)
import itertools as it, operator as op def max3bytes(unicode_string): # sequence of pairs of (char_in_string, u'\N{REPLACEMENT CHARACTER}') pairs= it.izip(unicode_string, it.repeat(u'\ufffd')) # is the argument less than or equal to 65535? selector= ft.partial(op.le, 65535) # using the character ordinals, return 0 or 1 based on `selector` indexer= it.imap(selector, it.imap(ord, unicode_string)) # now pick the correct item for all pairs return u''.join(it.imap(tuple.__getitem__, pairs, indexer))
编码为UTF-16,然后重新编码为UTF-8。
>>> t = u'𝐟𝐨𝐨' >>> e = t.encode('utf-16le') >>> ''.join(unichr(x).encode('utf-8') for x in struct.unpack('<' + 'H' * (len(e) // 2), e)) '\xed\xa0\xb5\xed\xb0\x9f\xed\xa0\xb5\xed\xb0\xa8\xed\xa0\xb5\xed\xb0\xa8'
请注意,join后不能进行编码,因为替代对可能在重新编码之前被解码。
编辑:
MySQL(至less5.1.47)处理代理对没有问题:
mysql> create table utf8test (t character(128)) collate utf8_general_ci; Query OK, 0 rows affected (0.12 sec) ... >>> cxn = MySQLdb.connect(..., charset='utf8') >>> csr = cxn.cursor() >>> t = u'𝐟𝐨𝐨' >>> e = t.encode('utf-16le') >>> v = ''.join(unichr(x).encode('utf-8') for x in struct.unpack('<' + 'H' * (len(e) // 2), e)) >>> v '\xed\xa0\xb5\xed\xb0\x9f\xed\xa0\xb5\xed\xb0\xa8\xed\xa0\xb5\xed\xb0\xa8' >>> csr.execute('insert into utf8test (t) values (%s)', (v,)) 1L >>> csr.execute('select * from utf8test') 1L >>> r = csr.fetchone() >>> r (u'\ud835\udc1f\ud835\udc28\ud835\udc28',) >>> print r[0] 𝐟𝐨𝐨
根据MySQL 5.1文档 :“ucs2和utf8字符集不支持位于BMP之外的补充字符。” 这表明代理对可能存在问题。
请注意, Unicode标准5.2第3章实际上禁止将代理对编码为两个3字节的UTF-8序列,而不是一个4字节的UTF-8序列…参见例如第93页“”“因为替代码点不是Unicode标量值,否则映射到代码点D800..DFFF的任何UTF-8字节序列都是不合格的。“”“然而,就我所知,这种禁止在很大程度上未知或被忽略。
检查MySQL用代理对做什么是一个好主意。 如果他们不被保留,这个代码将提供一个简单的检查:
all(uc < u'\ud800' or u'\ue000' <= uc <= u'\uffff' for uc in unicode_string)
而这段代码将用u\ufffd
replace任何“ u\ufffd
u''.join( uc if uc < u'\ud800' or u'\ue000' <= uc <= u'\uffff' else u'\ufffd' for uc in unicode_string )
我猜这不是最快的,但很直接(“pythonic”:):
def max3bytes(unicode_string): return u''.join(uc if uc <= u'\uffff' else u'\ufffd' for uc in unicode_string)
注意:这段代码没有考虑到Unicode在U + D800-U + DFFF范围内具有替代字符的事实。