Reemplazar caracteres no ASCII con un solo espacio

244

Necesito reemplazar todos los caracteres que no sean ASCII (\ x00- \ x7F) con un espacio. Me sorprende que esto no sea fácil en Python, a menos que me falte algo. La siguiente función simplemente elimina todos los caracteres que no son ASCII:

def remove_non_ascii_1(text):

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

Y este reemplaza los caracteres no ASCII con la cantidad de espacios según la cantidad de bytes en el punto de código de caracteres (es decir, el carácter se reemplaza con 3 espacios):

def remove_non_ascii_2(text):

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

¿Cómo puedo reemplazar todos los caracteres no ASCII con un solo espacio?

De la miríada de preguntas SO similares , ninguna aborda el reemplazo de caracteres en lugar de la eliminación , y adicionalmente aborda todos los caracteres no ascii que no son un carácter específico.

dotancohen
fuente
46
wow, realmente hiciste un gran esfuerzo para mostrar tantos enlaces. ¡+1 tan pronto como el día se renueve!
shad0w_wa1k3r
3
Parece que te has perdido este stackoverflow.com/questions/1342000/…
Stuart
Estoy interesado en ver un ejemplo de entrada que tiene problemas.
dstromberg
55
@Stuart: Gracias, pero esa es la primera que menciono.
dotancohen
1
@dstromberg: menciono ejemplo un carácter problemático en la pregunta: . Es este chico .
dotancohen

Respuestas:

243

Su ''.join()expresión es filtrar , eliminar todo lo que no sea ASCII; podrías usar una expresión condicional en su lugar:

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

Esto maneja los caracteres uno por uno y aún usaría un espacio por carácter reemplazado.

Su expresión regular debería reemplazar los caracteres no ASCII consecutivos con un espacio:

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

Tenga en cuenta el +allí.

Martijn Pieters
fuente
18
@dstromberg: más lento; str.join() necesita una lista (pasará los valores dos veces) y una expresión generadora se convertirá primero en una. Darle una lista de comprensión es simplemente más rápido. Ver este post .
Martijn Pieters
1
El primer fragmento de código insertará varios espacios en blanco por carácter si lo alimenta con una cadena de bytes UTF-8.
Mark Ransom
@ MarkRansom: estaba asumiendo que esto era Python 3.
Martijn Pieters
2
"el carácter se reemplaza con 3 espacios" en la pregunta implica que la entrada es una cadena de bytes (no Unicode) y, por lo tanto, se usa Python 2 (de lo contrario ''.join, fallaría). Si OP quiere un solo espacio por punto de código Unicode, entonces la entrada debe decodificarse primero en Unicode.
jfs
¡Esto me ayudó mucho!
Muhammad Haseeb
55

Para que obtenga la representación más parecida de su cadena original, le recomiendo el módulo unidecode :

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

Entonces puedes usarlo en una cadena:

remove_non_ascii("Ceñía")
Cenia
Alvaro Fuentes
fuente
sugerencia interesante, pero supone que el usuario desea que los no ascii se conviertan en cuáles son las reglas para unidecode. Sin embargo, esto plantea una pregunta de seguimiento al autor de la pregunta sobre por qué insisten en espacios, ¿quizás para reemplazar con otro personaje?
jxramos
Gracias, esta es una buena respuesta. No funciona para el propósito de esta pregunta porque la mayoría de los datos con los que estoy tratando no tienen una representación similar a ASCII. Tales como דותן. Sin embargo, en el sentido general, esto es genial, ¡gracias!
dotancohen
1
Sí, sé que esto no funciona para esta pregunta, pero llegué aquí tratando de resolver ese problema, así que pensé en compartir mi solución a mi propio problema, que creo que es muy común para las personas como @dotancohen que tratan con caracteres no ascii todo el tiempo.
Alvaro Fuentes
Ha habido algunas vulnerabilidades de seguridad con cosas como esta en el pasado. ¡Solo tenga cuidado de cómo implementa esto!
deweydb
No parece funcionar con cadenas de texto codificadas UTF-16
user5359531
22

Para el procesamiento de caracteres , use cadenas 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'

Pero tenga en cuenta que todavía tendrá un problema si su cadena contiene caracteres Unicode descompuestos (caracteres separados y combinando marcas de acento, por ejemplo):

>>> 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
fuente
Gracias, esta es una observación importante. Si encuentra una manera lógica de manejar el caso de las marcas combinadas, con gusto agregaría una recompensa a la pregunta. Supongo que simplemente eliminar la marca de combinación y dejar solo el carácter no combinado sería lo mejor.
dotancohen
1
Una solución parcial es usar ud.normalize('NFC',s)para combinar marcas, pero no todas las combinaciones de combinación están representadas por puntos de código únicos. Necesitarías una solución más inteligente mirando ud.category()el personaje.
Mark Tolonen
1
@dotancohen: hay una noción de "carácter percibido por el usuario" en Unicode que puede abarcar varios puntos de código Unicode. \X(grupo de grafemas extendido) regex (soportado por el regexmódulo) permite iterar sobre dichos caracteres (nota: "los grafemas no necesariamente combinan secuencias de caracteres, y la combinación de secuencias de caracteres no son necesariamente grafemas" ).
jfs
10

Si el personaje de reemplazo puede ser '?' en lugar de un espacio, entonces sugeriría 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
fuente
Reemplace la ? con otro personaje o espacio después si es necesario, y aún así serás más rápido.
Moritz
7

¿Qué hay de 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
analizador
fuente
1
Aunque esto es poco elegante, es muy legible. Gracias.
dotancohen
1
+1 para el manejo de Unicode ... @dotancohen IMNSHO "legible" implica "práctico" que se suma a "elegante", así que diría "un poco poco elegante"
qneill
3

Como enfoque nativo y eficiente, no necesita usar ordni ningún ciclo sobre los caracteres. Simplemente codifique asciie ignore los errores.

Lo siguiente solo eliminará los caracteres que no sean ascii:

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

Ahora, si desea reemplazar los caracteres eliminados, haga lo siguiente:

final_string = new_string + b' ' * (len(old_string) - len(new_string))
Kasramvd
fuente
En python3, esto encodedevolverá una cadena de bytes, así que tenlo en cuenta. Además, este método no eliminará caracteres como la nueva línea.
Kyle Gibson el
-1

Potencialmente para una pregunta diferente, pero estoy proporcionando mi versión de la respuesta de @ Alvero (usando unidecode). Quiero hacer una franja "regular" en mis cadenas, es decir, el principio y el final de mi cadena para los espacios en blanco, y luego reemplazar solo otros caracteres de espacios en blanco con un espacio "normal", es decir

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

a

"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)

Primero reemplazamos todos los espacios no Unicode con un espacio regular (y lo unimos de nuevo),

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

Y luego lo dividimos nuevamente, con la división normal de Python, y despojamos cada "bit",

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

Y finalmente, únete a ellos de nuevo, pero solo si la cadena pasa una ifprueba,

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

Y con eso, safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')vuelve correctamente 'Ceñía mañana'.

selladores
fuente