使用Python,最快的去除和替换高Unicode字符的方法是什么?

5
我想替换一个大文档中的所有高Unicode字符,例如重音E、左右引号等,用低范围内的“正常”对应字符,例如普通的'E'和直引号。我需要经常在一个非常大的文档上执行此操作。我在这里看到了一个示例,可能是perl:http://www.designmeme.com/mtplugins/lowdown.txt 有没有一种快速的方法在Python中完成这个任务,而不使用s.replace(...).replace(...).replace(...)...? 我尝试了只替换几个字符,但文档剥离变得非常缓慢。
编辑,我的版本unutbu的代码似乎不起作用:
# -*- coding: iso-8859-15 -*-
import unidecode
def ascii_map():
    data={}
    for num in range(256):
        h=num
        filename='x{num:02x}'.format(num=num)
        try:
            mod = __import__('unidecode.'+filename,
                             fromlist=True)
        except ImportError:
            pass
        else:
            for l,val in enumerate(mod.data):
                i=h<<8
                i+=l
                if i >= 0x80:
                    data[i]=unicode(val)
    return data

if __name__=='__main__':
    s = u'“fancy“fancy2'
    print(s.translate(ascii_map()))
5个回答

8
# -*- encoding: utf-8 -*-
import unicodedata

def shoehorn_unicode_into_ascii(s):
    return unicodedata.normalize('NFKD', s).encode('ascii','ignore')

if __name__=='__main__':
    s = u"éèêàùçÇ"
    print(shoehorn_unicode_into_ascii(s))
    # eeeaucC

请注意,正如@Mark Tolonen所指出的那样,上面的方法会删除一些字符,例如ß‘’“”。如果上面的代码截断了您希望翻译的字符,则可能需要使用字符串的translate方法手动解决这些问题。另一种选择是使用unidecode(请参见J.F. Sebastian的答案)。
当您有一个大的Unicode字符串时,使用其translate方法将比使用replace方法快得多。
编辑:unidecode具有更完整的Unicode代码点到ASCII的映射。但是,unidecode.unidecode通过字符串逐个字符地循环(在Python循环中),这比使用translate方法要慢。
下面的辅助函数使用unidecode的数据文件和translate方法来获得更好的速度,特别是对于长字符串。
在我的1-6 MB文本文件测试中,使用ascii_mapunidecode.unidecode快4-6倍。
# -*- coding: utf-8 -*-
import unidecode
def ascii_map():
    data={}
    for num in range(256):
        h=num
        filename='x{num:02x}'.format(num=num)
        try:
            mod = __import__('unidecode.'+filename,
                             fromlist=True)
        except ImportError:
            pass
        else:
            for l,val in enumerate(mod.data):
                i=h<<8
                i+=l
                if i >= 0x80:
                    data[i]=unicode(val)
    return data

if __name__=='__main__':
    s = u"éèêàùçÇ"
    print(s.translate(ascii_map()))
    # eeeaucC

编辑2:如果# -*- encoding: utf-8 -*-导致SyntaxError错误,请尝试使用# -*- encoding: cp1252 -*-。声明哪种编码取决于您的文本编辑器用于保存文件的编码。Linux倾向于使用utf-8,而(似乎)Windows倾向于cp1252。


1
不,NFKD规范会将像é这样的字符分解成e和一个组合重音符号。使用忽略编码为ASCII会保留e并删除非ASCII组合重音符号。问题在于,并非所有的非ASCII字符都有由ASCII和组合字符组成的分解形式,因此像ß‘’“”这样的字符只能使用忽略方式删除。 - Mark Tolonen
我现在遇到的问题是错误发生在另一个我没有源代码的库中。 - Rhubarb
@Rhubarb:哎呀……看起来你已经做到了。 - unutbu
@unutbu,我尝试了你的更新代码,但是收到了这个错误:SyntaxError: 非ASCII字符'\xe9'在文件t.py的第21行,但没有声明编码; 详情请见http://www.python.org/peps/pep-0263.html(t.py, line 21)第21行指的是: s = u"éèêàùçÇ" - Rhubarb
@~unutbu,我想知道这个问题是否是由于较旧的unidecode版本引起的。还有其他人能够重现这个问题吗? - Leeks and Leaks
显示剩余19条评论

4
没有所谓的“高ASCII字符”。ASCII字符集的范围仅限于128个序数。除此之外,这是一个常见问题。以下是一个答案。一般来说,你应该熟悉str.translate()和unicode.translate()——它们非常方便,可以进行单个字节/字符的多次替换。注意不要采用只提到unicodedata.normalize()技巧的答案;那只是解决方案中的一部分。更新:目前被接受的答案会消除没有分解的字符,正如Mark Tolonen所指出的那样。似乎缺乏关于unicode.translate()的知识。它可以将一个字符翻译成多个字符。以下是help(unicode.translate)的输出:S.translate(table)-> unicode 返回字符串S的副本,其中所有字符都已通过给定的翻译表映射,该表必须是Unicode序数到Unicode序数、Unicode字符串或None的映射。未映射的字符保持不变。映射到None的字符将被删除。下面是一个例子:
>>> u"Gau\xdf".translate({0xdf: u"ss"})
u'Gauss'
>>>

这是我指向的解决方案中的修复列表:

CHAR_REPLACEMENT = {
    # latin-1 characters that don't have a unicode decomposition
    0xc6: u"AE", # LATIN CAPITAL LETTER AE
    0xd0: u"D",  # LATIN CAPITAL LETTER ETH
    0xd8: u"OE", # LATIN CAPITAL LETTER O WITH STROKE
    0xde: u"Th", # LATIN CAPITAL LETTER THORN
    0xdf: u"ss", # LATIN SMALL LETTER SHARP S
    0xe6: u"ae", # LATIN SMALL LETTER AE
    0xf0: u"d",  # LATIN SMALL LETTER ETH
    0xf8: u"oe", # LATIN SMALL LETTER O WITH STROKE
    0xfe: u"th", # LATIN SMALL LETTER THORN
    }

这可以轻松扩展以适应在cp1252及其衍生版本中找到的花式引号和其他非Latin-1字符。

谢谢,我是指Unicode,但在这个时间点上,这就是我得到的。 - Rhubarb

3

我认为unicodedata不能处理花式引号。在这种情况下,你可以使用Unidecode

import unidecode
print unidecode.unidecode(u"ß‘’“”")
# -> ss''""

1

如果像~unubtu建议的那样使用unicodedata.normalize()不能满足需求,例如您想更好地控制映射关系,您应该研究一下
str.translate()
以及str.maketrans(),它是生成映射表的一个实用工具。 str.translate 对于此类转换既高效又便捷。
在 Python 2.x 中对于 Unicode 字符串,需要使用 unicode.translate() 而不是 str.translate(),并使用与下面代码片段中所示类似的技巧来代替 maketrans()。(感谢 John Machin 指出这一点!)

这些方法在Python 3.x中也是可用的,例如可以查看Python 3.1.2文档(由于某种原因,我曾经注意到这可能已经在Python 3.x中发生了变化)。当然,在Python 3下,所有字符串都是Unicode字符串,但这是另一个问题。

#Python 3.1
>>> intab = 'àâçêèéïîôù'
>>> outtab = 'aaceeeiiou'
>>> tmap = str.maketrans(intab, outtab)
>>> s = "à la fête de l'été, où il fait bon danser, les Français font les drôles"
>>> s
"à la fête de l'été, où il fait bon danser, les Français font les drôles"
>>> s.translate(tmap)
"a la fete de l'ete, ou il fait bon danser, les Francais font les droles"
>>>


#Python 2.6
>>> intab = u'àâçêèéïîôù'
>>> outtab = u'aaceeeiiou'
>>> s = u"à la fête de l'été, où il fait bon danser, les Français font les drôles"
>>> #note the trick to replace maketrans() since for unicode strings the translation
>>> #     map expects integers (unicode ordinals) not characters.
>>> tmap = dict(zip(map(ord, intab), map(ord, outtab))) 
>>> s.translate(tmap)
u"a la fete de l'ete, ou il fait bon danser, les Francais font les droles"
>>>

错误。在Python2.x中,请使用unicode.translate()而不是str.translate() - John Machin
@John Machin:没错!感谢您指出这一点。我已经进行了编辑,并添加了3.1和2.6的代码片段。 - mjv

0

这里有一个处理Latin-1字符的解决方案(基于2003年的Usenet线程):

>>> accentstable = str.join("", map(chr, range(192))) + "AAAAAAACEEEEIIIIDNOOOOOxOUUUUYTsaaaaaaaceeeeiiiidnooooo/ouuuuyty"
>>> import string
>>> s = u"éèêàùçÇ"
>>> print string.translate(s.encode('latin1', 'ignore'), accentstable)
eeeaucC

一些映射并不完美,例如Thorn映射到T而不是Th,但它做得还算可以接受。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接