Substitua caracteres não ASCII por um único espaço

244

Preciso substituir todos os caracteres não ASCII (\ x00- \ x7F) por um espaço. Estou surpreso que isso não seja fácil no Python, a menos que esteja faltando alguma coisa. A função a seguir simplesmente remove todos os caracteres não ASCII:

def remove_non_ascii_1(text):

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

E este substitui caracteres não ASCII pela quantidade de espaços conforme a quantidade de bytes no ponto de código do caractere (ou seja, o caractere é substituído por 3 espaços):

def remove_non_ascii_2(text):

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

Como posso substituir todos os caracteres não ASCII por um único espaço?

Da miríade de semelhantes SO perguntas , nenhum endereço de caráter de substituição como oposição a descascar , e , adicionalmente, tratar todos os caracteres não-ascii não um personagem específico.

dotancohen
fonte
46
Uau, você realmente se esforçou muito para mostrar tantos links. +1 assim que o dia for renovado!
shad0w_wa1k3r
3
Você parece ter esquecido este stackoverflow.com/questions/1342000/… #
Stuart
Estou interessado em ver um exemplo de entrada com problemas.
Dstromberg 19/11
5
@ Stuart: Obrigado, mas esse é o primeiro que eu mencionei.
dotancohen
1
@dstromberg: I mencionar um personagem exemplo problemático na questão: . É esse cara .
dotancohen

Respostas:

243

Sua ''.join()expressão é filtrada , removendo qualquer coisa que não seja ASCII; você poderia usar uma expressão condicional:

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

Isso lida com os caracteres um por um e ainda usaria um espaço por caractere substituído.

Sua expressão regular deve substituir apenas caracteres não ASCII consecutivos por um espaço:

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

Observe o +lá.

Martijn Pieters
fonte
18
@stromberg: mais lento; str.join() precisa de uma lista (ela passará os valores duas vezes) e uma expressão de gerador será primeiro convertida em uma. Fornecer uma lista de compreensão é simplesmente mais rápido. Veja este post .
Martijn Pieters
1
O primeiro trecho de código inserirá vários espaços em branco por caractere se você alimentar uma sequência de bytes UTF-8.
precisa
@ MarkRansom: Eu estava assumindo que fosse o Python 3. #
Martijn Pieters
2
" caractere é substituído por 3 espaços" na pergunta implica que a entrada é uma bytestring (não Unicode) e, portanto, o Python 2 é usado (caso contrário ''.join, falharia). Se o OP desejar um espaço único por ponto de código Unicode, a entrada deverá ser decodificada primeiro no Unicode.
jfs
Isso me ajudou muito!
Muhammad Haseeb
55

Para você obter a representação mais semelhante da sua string original, recomendo o módulo unidecode :

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

Então você pode usá-lo em uma string:

remove_non_ascii("Ceñía")
Cenia
Alvaro Fuentes
fonte
sugestão interessante, mas assume que o usuário deseja que não ascii se torne o que são as regras para unidecode. Isso, no entanto, coloca uma pergunta de acompanhamento ao solicitante sobre por que eles insistem em espaços, talvez para substituir por outro personagem?
jxramos
Obrigado, esta é uma boa resposta. Não funciona para o propósito desta pergunta, porque a maioria dos dados com os quais estou lidando não possui uma representação semelhante a ASCII. Tais como דותן. No entanto, no sentido geral, isso é ótimo, obrigado!
dotancohen
1
Sim, eu sei que isso não funciona para esta pergunta, mas cheguei aqui tentando resolver esse problema, então pensei em compartilhar minha solução para o meu próprio problema, o que acho muito comum para pessoas como @dotancohen que lidam com caracteres não-ascii o tempo todo.
Alvaro Fuentes
Houve algumas vulnerabilidades de segurança com coisas como essa no passado. Apenas tenha cuidado ao implementar isso!
deweydb
Parece não funcionar com cadeias de texto codificadas em UTF-16
#
22

Para processamento de caracteres , use cadeias 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'

Mas observe que você ainda terá um problema se sua string contiver caracteres Unicode decompostos (caracteres separados e sinais de destaque combinados, por exemplo):

>>> 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'
Mark Tolonen
fonte
Obrigado, esta é uma observação importante. Se você encontrar uma maneira lógica de lidar com o caso das marcas combinadas, eu adicionaria felizmente uma recompensa à pergunta. Suponho que simplesmente remover a marca combinada e deixar o caractere não combinado sozinho seria o melhor.
dotancohen
1
Uma solução parcial é usar ud.normalize('NFC',s)para combinar marcas, mas nem todas as combinações são representadas por pontos de código únicos. Você precisaria de uma solução mais inteligente olhando para ud.category()o personagem.
Mark Tolonen
1
@dotancohen: existe uma noção de "caractere percebido pelo usuário" no Unicode que pode abranger vários pontos de código Unicode. \X(cluster de grafemas expandidos) O regex (suportado pelo regexmódulo) permite iterar sobre esses caracteres (nota: "grafemas não necessariamente combinam seqüências de caracteres e combinações de caracteres não são necessariamente grafemas" ).
jfs
10

Se o caractere de substituição puder ser '?' em vez de um espaço, sugiro 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(),
))

Resultados:

0.7208260721400134
0.009975979187503592
AXO
fonte
Substitua o ? com outro personagem ou espaço depois, se necessário, e você ainda será mais rápido.
Moritz
7

Que tal este?

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
parsecer
fonte
1
Embora isso seja bastante deselegante, é muito legível. Obrigado.
dotancohen 21/08/16
1
+1 para o manuseio unicode ... IMNSHO @dotancohen "legível" implica "prático" que contribui para "elegante", então eu diria que "um pouco deselegante"
qneill
3

Como uma abordagem nativa e eficiente, você não precisa usar ordnenhum loop sobre os caracteres. Apenas codifique asciie ignore os erros.

A seguir, apenas os caracteres não-ascii serão removidos:

new_string = old_string.encode('ascii',errors='ignore')

Agora, se você deseja substituir os caracteres excluídos, faça o seguinte:

final_string = new_string + b' ' * (len(old_string) - len(new_string))
Kasramvd
fonte
No python3, isso encoderetornará uma bytestring, portanto, lembre-se disso. Além disso, esse método não remove caracteres como nova linha.
Kyle Gibson
-1

Possivelmente para uma pergunta diferente, mas estou fornecendo minha versão da resposta do @ Alvero (usando o unidecode). Eu quero fazer uma faixa "regular" nas minhas strings, ou seja, o começo e o fim da minha string para caracteres de espaço em branco e, em seguida, substituir apenas outros caracteres de espaço em branco por um espaço "regular", ou seja,

"Ceñíaㅤmañanaㅤㅤㅤㅤ"

para

"Ceñía mañana"

,

def safely_stripped(s: str):
    return ' '.join(
        stripped for stripped in
        (bit.strip() for bit in
         ''.join((c if unidecode(c) else ' ') for c in s).strip().split())
        if stripped)

Primeiro, substituímos todos os espaços não unicode por um espaço regular (e os juntamos novamente),

''.join((c if unidecode(c) else ' ') for c in s)

E então dividimos isso novamente, com a divisão normal do python, e removemos cada "bit",

(bit.strip() for bit in s.split())

E, por fim, junte-os novamente, mas somente se a sequência for aprovada em um ifteste,

' '.join(stripped for stripped in s if stripped)

E com isso, safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')retorna corretamente 'Ceñía mañana'.

seaders
fonte