如何检查Python Unicode字符串是否包含非西方字母?

40
我有一个Python Unicode字符串。我希望确保它只包含来自罗马字母表(A到Z)以及欧洲语言中常见的字母,例如ß、ü、ø、é、à和î。它不应该包含其他语言文字的字符(如中文、日文、韩文、阿拉伯文、斯拉夫文、希伯来文等)。如何最好地实现这个功能?
目前我正在使用下面这段代码,但我不知道是否是最佳方式:
def only_roman_chars(s):
    try:
        s.encode("iso-8859-1")
        return True
    except UnicodeDecodeError:
        return False

(我正在使用Python 2.5。同时,我正在使用Django框架进行操作,因此如果Django框架有处理此类字符串的方法,我可以使用该功能--但是我尚未发现任何类似的东西。)


你过滤这些字符的目的是什么?我想不出一个好的理由来做这件事,除非在代码库的其他地方存在问题的症状。 - Daenyth
过滤邮寄地址。我们的发货部门不想填写标签,例如中国地址。 - mipadi
你不能按国家进行过滤吗?(否则,这是一个有趣的问题+1) - ChristopheD
并不完全是这样。有人可能会选择“中国”,但仍然输入一个合适的地址,例如。 - mipadi
1
https://github.com/EliFinkelshteyn/alphabet-detector/blob/master/alphabet_detector/alphabet_detector.py - alvas
8个回答

48
import unicodedata as ud

latin_letters= {}

def is_latin(uchr):
    try: return latin_letters[uchr]
    except KeyError:
         return latin_letters.setdefault(uchr, 'LATIN' in ud.name(uchr))

def only_roman_chars(unistr):
    return all(is_latin(uchr)
           for uchr in unistr
           if uchr.isalpha()) # isalpha suggested by John Machin

>>> only_roman_chars(u"ελληνικά means greek")
False
>>> only_roman_chars(u"frappé")
True
>>> only_roman_chars(u"hôtel lœwe")
True
>>> only_roman_chars(u"123 ångstrom ð áß")
True
>>> only_roman_chars(u"russian: гага")
False

3
建议使用uchr.isalpha()代替unicodedata.category(uchr).startswith('L')。建议在模块加载时构建一个集合:okletters = set(unichr(i) for i in xrange(sys.maxunicode+1) if unicodedata.name(unichr(i), "").startswith('LATIN ')) ,即使用 uchr in okletters 代替 'LATIN' in unicodedata.name(uchr) - John Machin
@John:uchr.isalpha是一个更好的建议,谢谢;我会更新我的答案。对于优化建议,我会选择一个记忆化风格的函数。 - tzot
对于is_latin函数,适当覆盖__missing__defaultdict子类也是一个不错的解决方案。 - tzot
+1. 这非常不容易。我正在尝试做类似的事情,我想将西里尔字母转换为拉丁字母,而不改变其他任何东西。谢谢! - Bogdan Vasilescu
1
仍然是不完整的解决方案,因为一些非常可疑的字符在Unicode中被称为“LATIN”。例如,请查看https://unicode-table.com/en/A7B7/。 - greatvovan

37

@tzot 的回答非常好,但是我认为应该有一个适用于所有脚本的库。 所以,我做了一个(在那个回答的基础上)。

pip install alphabet-detector

然后直接使用它:

from alphabet_detector import AlphabetDetector
ad = AlphabetDetector()

ad.only_alphabet_chars(u"ελληνικά means greek", "LATIN") #False
ad.only_alphabet_chars(u"ελληνικά", "GREEK") #True
ad.only_alphabet_chars(u'سماوي يدور', 'ARABIC')
ad.only_alphabet_chars(u'שלום', 'HEBREW')
ad.only_alphabet_chars(u"frappé", "LATIN") #True
ad.only_alphabet_chars(u"hôtel lœwe 67", "LATIN") #True
ad.only_alphabet_chars(u"det forårsaker første", "LATIN") #True
ad.only_alphabet_chars(u"Cyrillic and кириллический", "LATIN") #False
ad.only_alphabet_chars(u"кириллический", "CYRILLIC") #True

此外,还有一些主要语言的方便方法:

ad.is_cyrillic(u"Поиск") #True  
ad.is_latin(u"howdy") #True
ad.is_cjk(u"hi") #False
ad.is_cjk(u'汉字') #True

1
我想使用库来检测我的Twitch(IRC)聊天室中是否有人输入西里尔文,但我不喜欢当Unicode字符串包含单个“?”时is_cyrillic返回true的情况。另外,我也不喜欢它无法触发像“hello Поиск”这样的内容。现在我正在使用带有范围的正则表达式,只是想提供一些反馈 :) - telina
1
感谢您的反馈并尝试使用该库!您提出了一些很好的观点。您是否介意在 Github 存储库上打开一个问题,以便我们可以讨论它们? - Eli

4

标准的string包含所有的拉丁字母数字符号。您可以从文本中删除这些值,如果还有其它内容,则表示这些不是拉丁字符。我已经完成了这个操作:

In [1]: from string import printable                                                                                                                                                                           

In [2]: def is_latin(text): 
   ...:     return not bool(set(text) - set(printable)) 
   ...:                                                                                                                                                                                                        

In [3]: is_latin('Hradec Králové District,,Czech Republic,')                                                                                                                                                   
Out[3]: False

In [4]: is_latin('Hradec Krlov District,,Czech Republic,')                                                                                                                                                     
Out[4]: True

我无法检查所有非拉丁字符,如果有人能够做到,请告诉我。谢谢。


1

请检查 django.template.defaultfilters.slugify 中的代码

import unicodedata
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')

如果这是你正在寻找的,那么你可以将结果字符串与原始字符串进行比较。

我不想把所有的字母变成小写或者把空格转换成破折号,只是想检查一个字符串是否有不需要的字符。我修改了问题以避免使用“过滤”这个词。 - mipadi

1

检查 ISO-8559-1 会忽略像 'œ' 和 '€' 这样的合理西方字符。解决方案取决于您如何定义“西方”,以及您想如何处理非字母字符。以下是一种方法:

import unicodedata

def is_permitted_char(char):
    cat = unicodedata.category(char)[0]
    if cat == 'L': # Letter
        return 'LATIN' in unicodedata.name(char, '').split()
    elif cat == 'N': # Number
        # Only DIGIT ZERO - DIGIT NINE are allowed
        return '0' <= char <= '9'
    elif cat in ('S', 'P', 'Z'): # Symbol, Punctuation, or Space
        return True
    else:
        return False

def is_valid(text):
    return all(is_permitted_char(c) for c in text)

(1) 只需使用return unicodedata.name(char, '').startswith('LATIN ')即可。 (2) 对函数结果进行记忆化可能是个好主意,可以通过将通常的字符[-A-Za-z0-9,./ ']等预加载到备忘录中来进一步优化。 (3) 符号/标点相当广泛。 (4) 应该用'\x20'替换类别空格吗? - John Machin

1

根据您所说的想要做的事情,您的方法是正确的。如果您正在运行Windows,则建议使用cp1252而不是iso-8859-1。您还可以考虑使用cp1250,这将涵盖东欧国家,如波兰、捷克共和国、斯洛伐克、罗马尼亚、斯洛文尼亚、匈牙利、克罗地亚等,这些国家的字母表基于拉丁字母。其他的cp125x包括土耳其语和马耳他语...

您还可以考虑从西里尔文转录为拉丁文;据我所知,有几个系统,其中一个可能得到了万国邮政联盟(UPU)的认可。

我对您的评论“我们的发货部门不想填写标签,例如中国地址”有点好奇...三个问题:(1)您是否指“某个国家的地址”或“用X语字符书写的地址”(2)您的系统打印标签会不会更好?(3)如果订单未通过您的测试,该如何发货?


(1) 后者(用X语字符书写的地址)。 (2) 可能。目前还没有。该表单是Web应用程序的一部分;数据被传送到完全不同的另一个系统,该系统处理订单等管理工作。 (3) 表单未通过验证并提示用户输入适当的地址。 - mipadi
2
我建议无论您是否在Windows上,都不要使用cp125x,因为它与正确的标准字符集和编码不兼容。它将法定西方字符放入Unicode和ISO中保留为“C1”控制字符范围的位置。古老的“CP”代码页编码是有限的字符集空间的一种解决方法,在所有现代代码中应该避免使用。 - Stephen P
@StephenP:OP已经有Unicode字符串了;我建议他考虑一个强烈的可能性,那就是他需要注意的字符可能在cp125x字符集中找到;Windows 用户无法避免地使用cp125x编码的数据。这是生活的事实。虽然ISO-8859-x编码已经被标准认可,但更加受限,应该在代码中避免使用;使用UTF-8、UTF-16或GB18030。如果有Unicode数据,其代码点为0080到009F,则概率(C1控制) == 0.1%,概率(cp125x编码的数据解码为latin1) == 99.9%。 - John Machin

0

如果你是Django用户,也许这个可以解决问题?

from django.template.defaultfilters import slugify 

def justroman(s):
  return len(slugify(s)) == len(s)

0

使用內建的unicodedata庫簡單化tzot的答案,對我來說似乎有效:

import unicodedata as ud

def is_latin(word):
    return all(['LATIN' in ud.name(c) for c in word])

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