用单个空格替换非ASCII字符

325

我需要将所有非ASCII(\x00-\x7F)字符替换为空格。我很惊讶在Python中这并不是很简单,除非我漏了什么。下面的函数只是删除所有非ASCII字符:

def remove_non_ascii_1(text):

    return ''.join(i for i in text if ord(i)<128)

这个函数会将非ASCII字符替换为字符编码点所对应的字节数量的空格(例如,字符会被替换为3个空格):

def remove_non_ascii_2(text):

    return re.sub(r'[^\x00-\x7F]',' ', text)

我该如何用一个空格替换所有非ASCII字符?

众多类似的SO问题中,没有一个处理字符替换删除更好的解决方法并且额外处理所有非ASCII字符而不是特定字符。


74
哇,你真的花了很多心思展示这么多链接。一旦新的一天开始,我会点赞加一! - shad0w_wa1k3r
3
你好像错过了这个问题:https://dev59.com/mHM_5IYBdhLWcg3whznx - Stuart
8
@Stuart:谢谢,但那正是我提到的第一个。 - dotancohen
1
@dstromberg:我在问题中提到了一个有问题的字符示例:“–”。 这是 - dotancohen
1
在这个问题的生命周期的这个阶段,也许即使它们不是正题,sedawkperl的答案也很有趣。但我建议将它们放在一个统一的“X/Y答案”中,而不是分开回答。通常情况下,如果代码是从例如bash CLI运行的地方(其中四个都通常可用),而不是实际运行Python脚本的地方,sedawkperl的答案可以替代Python答案。 - dotancohen
显示剩余5条评论
12个回答

311

你的''.join()表达式是在进行过滤,移除任何非ASCII字符;你可以使用条件表达式代替:

return ''.join([i if ord(i) < 128 else ' ' for i in text])

这样处理一个字符一遍,仍然使用每个替换的字符一个空格。

您的正则表达式应该只用一个空格来替换连续的非ASCII字符:

re.sub(r'[^\x00-\x7F]+',' ', text)

请注意其中的+

20
@dstromberg说需要更慢的方式; str.join() 需要一个列表(它会两次遍历值),而生成器表达式首先会被转换为列表。给它一个列表推导式只是更快。请参阅 此帖子 - Martijn Pieters
1
如果您向它提供一个UTF-8字节字符串,第一段代码将在每个字符后插入多个空格。 - Mark Ransom
@MarkRansom:我假设这是Python 3。 - Martijn Pieters
3
问题中的"-字符用3个空格替换"意味着输入是字节串(不是Unicode),因此使用了Python 2(否则,''.join将失败)。如果提问者想要每个Unicode码点一个空格,则应首先将输入解码为Unicode。 - jfs

70
为了让您获得与原始字符串最相似的表示,我建议使用unidecode模块

Python 2

from unidecode import unidecode
def remove_non_ascii(text):
    return unidecode(unicode(text, encoding = "utf-8"))

然后您可以在字符串中使用它:

remove_non_ascii("Ceñía")
Cenia

Python 3

from unidecode import unidecode
unidecode("Ceñía")

有趣的建议,但它假设用户希望非ASCII字符成为unidecode规则。然而,这引出了一个后续问题,询问者为什么坚持使用空格,也许可以用另一个字符替换? - jxramos
谢谢,这是一个好答案。对于这个问题来说,它并不适用,因为我处理的大部分数据没有类似ASCII的表示方式,比如דותן。但是从一般意义上讲,这很棒,谢谢! - dotancohen
1
是的,我知道这不能解决这个问题,但我在尝试解决那个问题时来到了这里,所以我想分享一下我自己的解决方案,因为我认为像@dotancohen这样经常处理非ASCII字符的人都会遇到这个普遍的问题。 - Alvaro Fuentes
7
@AlvaroFuentes,如何处理/重写您精彩的Python 3代码,因为this?错误:NameError: global name 'unicode' is not defined - Igor Savinkin
1
这适用于Python3 - 如果您使用unidecode(text)。我通过这种方式在爬行过程中得到了一些有趣的Unicode字符引号。 - rjurney
显示剩余2条评论

30

对于字符处理,请使用Unicode字符串:

PythonWin 3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] on win32.
>>> s='ABC马克def'
>>> import re
>>> re.sub(r'[^\x00-\x7f]',r' ',s)   # Each char is a Unicode codepoint.
'ABC  def'
>>> b = s.encode('utf8')
>>> re.sub(rb'[^\x00-\x7f]',rb' ',b) # Each char is a 3-byte UTF-8 sequence.
b'ABC      def'

请注意,如果您的字符串包含分解的Unicode字符(例如单独的字符和组合重音符号),仍然会有问题:

>>> s = 'mañana'
>>> len(s)
6
>>> import unicodedata as ud
>>> n=ud.normalize('NFD',s)
>>> n
'mañana'
>>> len(n)
7
>>> re.sub(r'[^\x00-\x7f]',r' ',s) # single codepoint
'ma ana'
>>> re.sub(r'[^\x00-\x7f]',r' ',n) # only combining mark replaced
'man ana'

谢谢,这是一个重要的观察。如果您找到了处理组合标记的逻辑方法,我会很乐意为这个问题添加赏金。我认为最好的方法是简单地删除组合标记,但保留未组合的字符。 - dotancohen
1
一个部分的解决方案是使用 ud.normalize('NFC',s) 来组合标记,但并不是所有的组合都由单个代码点表示。你需要一个更智能的解决方案来查看字符的 ud.category() - Mark Tolonen
1
@dotancohen:在Unicode中有一个“用户感知字符”的概念,它可能跨越多个Unicode代码点。\X(扩展字形簇)正则表达式(由regex模块支持)允许迭代这样的字符(注意:“字形不一定是组合字符序列,组合字符序列也不一定是字形”)。 - jfs

15
如果替换字符可以是“?”而不是空格,那么我建议使用result = text.encode('ascii', 'replace').decode()
"""Test the performance of different non-ASCII replacement methods."""


import re
from timeit import timeit


# 10_000 is typical in the project that I'm working on and most of the text
# is going to be non-ASCII.
text = 'Æ' * 10_000


print(timeit(
    """
result = ''.join([c if ord(c) < 128 else '?' for c in text])
    """,
    number=1000,
    globals=globals(),
))

print(timeit(
    """
result = text.encode('ascii', 'replace').decode()
    """,
    number=1000,
    globals=globals(),
))

结果:

0.7208260721400134
0.009975979187503592

如果需要的话,将?替换为另一个字符或空格,你仍然会更快。 - Moritz

9
作为一种本地且高效的方法,你不需要使用ord或任何循环遍历字符。只需使用ascii进行编码并忽略错误即可。
以下代码将仅移除非ascii字符:
new_string = old_string.encode('ascii',errors='ignore')

现在如果你想替换已删除的字符,只需按照以下步骤进行:

final_string = new_string + b' ' * (len(old_string) - len(new_string))

在Python3中,此“encode”将返回一个字节串,请记住这一点。此外,此方法不会剥离换行符等字符。 - Kyle Gibson
2
new_string = old_string.encode('ascii', errors='ignore').decode() 新字符串 = 旧字符串.encode('ascii', errors='ignore').decode() - Hamid Fadishei

9
这个怎么样?
def replace_trash(unicode_string):
     for i in range(0, len(unicode_string)):
         try:
             unicode_string[i].encode("ascii")
         except:
              #means it's non-ASCII
              unicode_string=unicode_string[i].replace(" ") #replacing it with a single space
     return unicode_string

2
虽然这样做相对不太优雅,但是非常易读。谢谢。 - dotancohen
1
+1 对于 Unicode 处理非常重要... @dotancohen 在我看来,“可读性”意味着“实用性”,这增加了“优雅”的含义,因此我会说“有点不太优雅”。 - qneill
将非 ASCII 字符称为“垃圾”,其概念上的值为 -1。 - axolotl
@axolotl 我并没有冒犯的意思。如果我没记错的话,当我写这个时,我确实在处理一些不属于任何字母表的非常奇怪的字符。 - parsecer
1
我知道 :) 这是一个轻松的评论。 - axolotl

2
当我们使用ascii()函数时,它会转义非ASCII字符,并且不能正确地处理ASCII字符。因此,我的主要想法是遍历字符串并检查字符是否被更改。如果更改了,则用给定的替换器替换它。
例如:' '(一个空格)或'?'(带有问号)。
def remove(x, replacer):

     for i in x:
        if f"'{i}'" == ascii(i):
            pass
        else:
            x=x.replace(i,replacer)
     return x
remove('hái',' ')

结果: "h i" (单个空格分隔)。

语法: remove(str,non_ascii_replacer)
str = 在这里输入您想要处理的字符串。
non_ascii_replacer = 在这里输入您想要用来替换所有非ASCII字符的替换器。


不错的编辑,加上了解释。:-) 现在我理解了你的代码思路,我喜欢这种方法。(正如我承诺的那样,我尽力为您格式化它;希望您喜欢。) - Yunnosch

1

使用Raku(前身为Perl_6)进行预处理

~$ raku -pe 's:g/ <:!ASCII>+ / /;' file

示例输入:

Peace be upon you
السلام عليكم
שלום עליכם
Paz sobre vosotros

样例输出:

Peace be upon you


Paz sobre vosotros

请注意,您可以使用以下代码获取有关匹配项的详细信息:

~$ raku -ne 'say s:g/ <:!ASCII>+ / /.raku;' file
$( )
$(Match.new(:orig("السلام عليكم"), :from(0), :pos(6)), Match.new(:orig("السلام عليكم"), :from(7), :pos(12)))
$(Match.new(:orig("שלום עליכם"), :from(0), :pos(4)), Match.new(:orig("שלום עליכם"), :from(5), :pos(10)))
$( )
$( )

或者更简单地,您可以直接将替换空格可视化:
~$ raku -ne 'say S:g/ <:!ASCII>+ / /.raku;' file
"Peace be upon you"
"   "
"   "
"Paz sobre vosotros"
""

https://docs.raku.org/language/regexes#Unicode_properties
https://www.codesections.com/blog/raku-unicode/
https://raku.org


1
谢谢jubilatious。我已经点赞了,因为这对我来说是非常有用的知识,尽管它与Python问题无关。在Raku/Perl问题上,你非常有帮助,我非常感激! - dotancohen

1
def filterSpecialChars(strInput):
    result = []
    for character in strInput:
        ordVal = ord(character)
        if ordVal < 0 or ordVal > 127:
            result.append(' ')
        else:
            result.append(character)
    return ''.join(result)

然后像这样调用:

result = filterSpecialChars('Ceñía mañana')
print(result)

你为什么要检查ord()是否返回负数?Unicode代码点都是非负整数,但我很乐意学习新知识。我同意这是一个很好的防御措施,但在此之前,我会尝试捕获例如TypeError异常。 - dotancohen

0

我的问题是,我的字符串包含像BelgiÃ这样的内容,表示België,以及&#x20AC表示€符号。我不想用空格替换它们,而是要用正确的符号本身。

我的解决方案是string.encode('Latin1').decode('utf-8')


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