Remplacez les caractères non ASCII par un seul espace

245

Je dois remplacer tous les caractères non ASCII (\ x00- \ x7F) par un espace. Je suis surpris que ce ne soit pas facile à faire en Python, sauf si je manque quelque chose. La fonction suivante supprime simplement tous les caractères non ASCII:

def remove_non_ascii_1(text):

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

Et celui-ci remplace les caractères non ASCII par la quantité d'espaces selon la quantité d'octets dans le point de code de caractère (c'est-à-dire que le caractère est remplacé par 3 espaces):

def remove_non_ascii_2(text):

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

Comment remplacer tous les caractères non ASCII par un seul espace?

Parmi la myriade de questions SO similaires , aucune ne concerne le remplacement de caractère par opposition à la suppression , et traite également tous les caractères non-ascii et non un caractère spécifique.

dotancohen
la source
46
wow, vous avez vraiment fait de gros efforts pour montrer autant de liens. +1 dès que la journée se renouvelle!
shad0w_wa1k3r
3
Vous semblez avoir manqué celui-ci stackoverflow.com/questions/1342000/…
Stuart
Je suis intéressé à voir un exemple d'entrée qui a des problèmes.
dstromberg
5
@Stuart: Merci, mais c'est le tout premier que je mentionne.
dotancohen
1
@dstromberg: Je mentionne un caractère problématique exemple dans la question: . C'est ce mec .
dotancohen

Réponses:

244

Votre ''.join()expression filtre , supprimant tout ce qui n'est pas ASCII; vous pouvez utiliser une expression conditionnelle à la place:

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

Cela gère les caractères un par un et utilise toujours un espace par caractère remplacé.

Votre expression régulière doit simplement remplacer les caractères non ASCII consécutifs par un espace:

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

Notez le +là.

Martijn Pieters
la source
18
@dstromberg: plus lent; str.join() a besoin d' une liste (elle passera deux fois sur les valeurs), et une expression de générateur sera d'abord convertie en une. Lui donner une liste de compréhension est tout simplement plus rapide. Voir cet article .
Martijn Pieters
1
Le premier morceau de code insérera plusieurs blancs par caractère si vous lui introduisez une chaîne d'octets UTF-8.
Mark Ransom
@MarkRansom: Je supposais qu'il s'agissait de Python 3.
Martijn Pieters
2
"le caractère est remplacé par 3 espaces" dans la question implique que l'entrée est un bytestring (pas Unicode) et donc Python 2 est utilisé (sinon ''.joinéchouerait). Si OP souhaite un seul espace par point de code Unicode, l'entrée doit d'abord être décodée en Unicode.
jfs
Cela m'a beaucoup aidé!
Muhammad Haseeb
55

Pour vous obtenir la représentation la plus similaire de votre chaîne d'origine, je recommande le module unidecode :

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

Ensuite, vous pouvez l'utiliser dans une chaîne:

remove_non_ascii("Ceñía")
Cenia
Alvaro Fuentes
la source
suggestion intéressante, mais elle suppose que l'utilisateur souhaite que non ascii devienne ce que sont les règles pour unidecode. Cela pose cependant une question de suivi au demandeur sur la raison pour laquelle ils insistent sur les espaces, pour peut-être les remplacer par un autre personnage?
jxramos
Merci, c'est une bonne réponse. Cela ne fonctionne pas aux fins de cette question, car la plupart des données que je traite n'ont pas de représentation de type ASCII. Tels que דותן. Cependant, dans le sens général, c'est super, merci!
dotancohen
1
Oui, je sais que cela ne fonctionne pas pour cette question, mais j'ai atterri ici en essayant de résoudre ce problème, alors j'ai pensé partager ma solution à mon propre problème, qui je pense est très courant pour les gens comme @dotancohen qui s'occupent avec des caractères non-ascii tout le temps.
Alvaro Fuentes
Il y a eu des vulnérabilités de sécurité avec des trucs comme ça dans le passé. Faites juste attention à la façon dont vous implémentez cela!
deweydb
Ne semble pas fonctionner avec les chaînes de texte encodées UTF-16
user5359531
23

Pour le traitement des caractères , utilisez des chaînes 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'

Mais notez que vous aurez toujours un problème si votre chaîne contient des caractères Unicode décomposés (caractère séparé et combinaison de marques d'accentuation, par exemple):

>>> 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
la source
Merci, c'est une observation importante. Si vous trouvez un moyen logique de gérer le cas des marques de combinaison, je serais heureux d'ajouter une prime à la question. Je suppose que simplement supprimer la marque de combinaison tout en laissant le caractère non combiné seul serait le mieux.
dotancohen
1
Une solution partielle consiste à utiliser ud.normalize('NFC',s)pour combiner des marques, mais toutes les combinaisons de combinaisons ne sont pas représentées par des points de code uniques. Vous auriez besoin d'une solution plus intelligente en regardant ud.category()le personnage.
Mark Tolonen
1
@dotancohen: il existe une notion de "caractère perçu par l'utilisateur" dans Unicode qui peut s'étendre sur plusieurs points de code Unicode. \X(eXtended grapheme cluster) regex (pris en charge par le regexmodule) permet d'itérer sur de tels caractères (note: "les graphèmes ne combinent pas nécessairement des séquences de caractères, et la combinaison de séquences de caractères ne sont pas nécessairement des graphèmes" ).
jfs
10

Si le caractère de remplacement peut être «?» au lieu d'un espace, alors je suggère 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(),
))

Résultats:

0.7208260721400134
0.009975979187503592
AXO
la source
Remplace le ? avec un autre personnage ou espace par la suite si nécessaire, et vous seriez toujours plus rapide.
Moritz
7

Et celui-ci?

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
la source
1
Bien que ce soit plutôt inélégant, il est très lisible. Je vous remercie.
dotancohen
1
+1 pour la gestion unicode ... @dotancohen IMNSHO "lisible" implique "pratique" qui ajoute à "élégant", donc je dirais "un peu inélégant"
qneill
3

En tant qu'approche native et efficace, vous n'avez pas besoin d'utiliser ordou de boucle sur les personnages. Il suffit d'encoder asciiet d'ignorer les erreurs.

Ce qui suit supprimera simplement les caractères non ascii:

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

Maintenant, si vous souhaitez remplacer les caractères supprimés, procédez comme suit:

final_string = new_string + b' ' * (len(old_string) - len(new_string))
Kasramvd
la source
En python3, cela encoderetournera un bytestring, alors gardez cela à l'esprit. De plus, cette méthode ne supprime pas les caractères tels que la nouvelle ligne.
Kyle Gibson
-1

Potentiellement pour une question différente, mais je fournis ma version de la réponse de @ Alvero (en utilisant unidecode). Je veux faire une bande "régulière" sur mes chaînes, c'est-à-dire le début et la fin de ma chaîne pour les caractères d'espacement, puis remplacer uniquement les autres caractères d'espacement par un espace "régulier", c'est-à-dire

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

à

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

Nous remplaçons d'abord tous les espaces non-unicode par un espace régulier (et le rejoignons à nouveau),

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

Et puis nous avons divisé à nouveau, avec la division normale de python, et dépouiller chaque "bit",

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

Enfin, rejoignez-les à nouveau, mais uniquement si la chaîne réussit un iftest,

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

Et avec cela, safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')revient correctement 'Ceñía mañana'.

seaders
la source