Sostituisci caratteri non ASCII con un singolo spazio


244

Devo sostituire tutti i caratteri non ASCII (\ x00- \ x7F) con uno spazio. Sono sorpreso che questo non sia assolutamente facile in Python, a meno che non manchi qualcosa. La seguente funzione rimuove semplicemente tutti i caratteri non ASCII:

def remove_non_ascii_1(text):

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

E questo sostituisce i caratteri non ASCII con la quantità di spazi secondo la quantità di byte nel punto del codice carattere (ovvero il carattere viene sostituito con 3 spazi):

def remove_non_ascii_2(text):

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

Come posso sostituire tutti i caratteri non ASCII con un singolo spazio?

Della miriade di simili SO domande , nessuno indirizzo carattere di sostituzione come contrapposizione a nudo , e in aggiunta non affrontare tutti i caratteri non ASCII un carattere specifico.


46
caspita, hai fatto davvero grandi sforzi per mostrare così tanti link. +1 non appena la giornata si rinnova!
shad0w_wa1k3r

3
Sembra che tu abbia perso questo stackoverflow.com/questions/1342000/…
Stuart

Sono interessato a vedere un input di esempio che presenta problemi.
dstromberg,

5
@Stuart: Grazie, ma è il primo che menziono.
dotancohen,

1
@dstromberg: Ho parlare di un personaggio esempio problematico nella domanda: . È questo ragazzo .
dotancohen,

Risposte:


243

La tua ''.join()espressione sta filtrando , rimuovendo qualsiasi cosa non ASCII; puoi invece usare un'espressione condizionale:

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

Questo gestisce i caratteri uno per uno e utilizza comunque uno spazio per carattere sostituito.

La tua espressione regolare dovrebbe semplicemente sostituire caratteri consecutivi non ASCII con uno spazio:

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

Nota +lì.


18
@dstromberg: più lento; str.join() ha bisogno di un elenco (passerà sopra i valori due volte) e un'espressione del generatore verrà prima convertita in uno. Dare una comprensione della lista è semplicemente più veloce. Vedi questo post .
Martijn Pieters

1
Il primo pezzo di codice inserirà più spazi vuoti per carattere se gli dai una stringa di byte UTF-8.
Mark Ransom,

@MarkRansom: supponevo che questo fosse Python 3.
Martijn Pieters

2
"Il carattere viene sostituito con 3 spazi" nella domanda implica che l'input è un bytestring (non Unicode) e quindi viene usato Python 2 (altrimenti ''.joinfallirebbe). Se OP desidera un singolo spazio per punto di codice Unicode, l'input deve essere prima decodificato in Unicode.
jfs,

Questo mi ha aiutato molto!
Muhammad Haseeb,

55

Per te la rappresentazione più simile della tua stringa originale ti consiglio il modulo unidecode :

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

Quindi puoi usarlo in una stringa:

remove_non_ascii("Ceñía")
Cenia

suggerimento interessante, ma presuppone che l'utente desideri non ascii per diventare ciò che sono le regole per unidecode. Questo, tuttavia, pone una domanda di follow-up per chi chiede perché insistono sugli spazi, per sostituire forse con un altro personaggio?
jxramos,

Grazie, questa è una buona risposta. Non funziona ai fini di questa domanda perché la maggior parte dei dati con cui ho a che fare non ha una rappresentazione simile a ASCII. Come דותן. Tuttavia, in generale, questo è fantastico, grazie!
dotancohen,

1
Sì, lo so che non funziona per questa domanda, ma sono atterrato qui cercando di risolvere quel problema, quindi ho pensato di condividere la mia soluzione al mio problema, che penso sia molto comune per le persone come @dotancohen che si occupano con caratteri non ascii per tutto il tempo.
Alvaro Fuentes,

Ci sono state alcune vulnerabilità di sicurezza con cose come questa in passato. Fai solo attenzione a come implementalo!
Deweydb,

Sembra non funzionare con le stringhe di testo codificate UTF-16
user5359531

22

Per l' elaborazione dei caratteri , utilizzare le stringhe 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'

Ma nota che avrai ancora un problema se la tua stringa contiene caratteri Unicode decomposti (carattere separato e combinazione di accenti, per esempio):

>>> 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'

Grazie, questa è un'osservazione importante. Se trovi un modo logico per gestire il caso dei segni combinati, aggiungerei felicemente una generosità alla domanda. Suppongo che sarebbe meglio rimuovere semplicemente il segno combinato ma lasciare solo il personaggio non combinato.
dotancohen,

1
Una soluzione parziale è utilizzare ud.normalize('NFC',s)per combinare i segni, ma non tutte le combinazioni combinate sono rappresentate da singoli punti di codice. Avresti bisogno di una soluzione più intelligente guardando ud.category()il personaggio.
Mark Tolonen,

1
@dotancohen: esiste un concetto di "carattere percepito dall'utente" in Unicode che può comprendere diversi punti di codice Unicode. \X(eXtended grapheme cluster) regex (supportato dal regexmodulo) consente di iterare su tali caratteri (nota: "i grafemi non necessariamente combinano sequenze di caratteri, e combinare sequenze di caratteri non sono necessariamente grafie" ).
jfs,

10

Se il carattere sostitutivo può essere '?' invece di uno spazio, quindi suggerirei 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(),
))

risultati:

0.7208260721400134
0.009975979187503592

Sostituisci il ? con un altro personaggio o spazio in seguito, se necessario, e saresti ancora più veloce.
Moritz,

7

Che dire di questo?

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

1
Anche se questo è piuttosto inelegante, è molto leggibile. Grazie.
dotancohen,

1
+1 per la gestione unicode ... @dotancohen IMNSHO "leggibile" implica "pratico" che si aggiunge a "elegante", quindi direi "un po 'inelegante"
qneill

3

Come approccio nativo ed efficiente, non è necessario utilizzare ordo alcun ciclo sui personaggi. Basta codificare asciie ignorare gli errori.

Quanto segue rimuoverà solo i caratteri non ascii:

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

Ora, se vuoi sostituire i caratteri eliminati, procedi come segue:

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

In python3, questo encoderestituirà un bytestring, quindi tienilo a mente. Inoltre, questo metodo non eliminerà caratteri come newline.
Kyle Gibson,

-1

Potenzialmente per una domanda diversa, ma sto fornendo la mia versione della risposta di @ Alvero (usando unidecode). Voglio fare una striscia "normale" sulle mie stringhe, cioè l'inizio e la fine della mia stringa per i caratteri degli spazi bianchi, e quindi sostituire solo altri caratteri degli spazi bianchi con uno spazio "normale", cioè

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

per

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

Sostituiamo innanzitutto tutti gli spazi non unicode con uno spazio regolare (e lo ricolleghiamo di nuovo),

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

E poi lo dividiamo di nuovo, con la normale divisione di Python, e rimuoviamo ogni "bit",

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

E infine uniscili di nuovo, ma solo se la stringa supera un iftest,

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

E con ciò, safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')ritorna correttamente 'Ceñía mañana'.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.