Zamień znaki spoza ASCII na pojedynczą spację

244

Muszę zastąpić wszystkie znaki spoza ASCII (\ x00- \ x7F) spacją. Dziwi mnie, że w Pythonie nie jest to łatwe, chyba że czegoś mi brakuje. Następująca funkcja po prostu usuwa wszystkie znaki spoza ASCII:

def remove_non_ascii_1(text):

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

I ten zastępuje znaki spoza ASCII ilością spacji odpowiadającą liczbie bajtów w punkcie kodu znaku (tzn. Znak jest zastępowany 3 spacjami):

def remove_non_ascii_2(text):

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

Jak mogę zastąpić wszystkie znaki spoza ASCII pojedynczą spacją?

Od tej niezliczonej o podobnej SO pytania , żaden adres charakter zamiennych w przeciwieństwie do rozbiórki , a dodatkowo nie uwzględniają wszystkich znaków spoza ASCII specyficzny charakter.

dotancohen
źródło
46
wow, naprawdę starałeś się pokazać tak wiele linków. +1, gdy tylko odnowi się dzień!
shad0w_wa1k3r
3
Wygląda na to, że przegapiłeś ten jeden stackoverflow.com/questions/1342000/...
Stuart,
Chcę zobaczyć przykładowe dane wejściowe, które mają problemy.
dstromberg,
5
@Stuart: Dzięki, ale to pierwszy, o którym wspomniałem.
dotancohen,
1
@dstromberg: Wspominam problematyczny charakter przykładowy w pytaniu: . To ten facet .
dotancohen,

Odpowiedzi:

243

Twoje ''.join()wyrażenie filtruje , usuwając wszystko inne niż ASCII; zamiast tego możesz użyć wyrażenia warunkowego:

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

To obsługuje znaki jeden po drugim i nadal używałoby jednego miejsca na zastąpioną postać.

Twoje wyrażenie regularne powinno po prostu zastąpić kolejne znaki spoza ASCII spacją:

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

Uwaga +tam.

Martijn Pieters
źródło
18
@dstromberg: wolniej; str.join() potrzebuje listy (dwukrotnie przejdzie przez wartości), a wyrażenie generatora zostanie najpierw przekonwertowane na jedno. Zrozumienie listy jest po prostu szybsze. Zobacz ten post .
Martijn Pieters
1
Pierwszy fragment kodu wstawi wiele pustych znaków na znak, jeśli podasz mu ciąg bajtów UTF-8.
Mark Ransom,
@MarkRansom: Zakładałem, że jest to Python 3.
Martijn Pieters
2
znak jest zastąpiony 3 spacjami” w pytaniu oznacza, że ​​wejście jest bajtowaniem (nie Unicode) i dlatego używany jest Python 2 (inaczej ''.joinby się nie udał ). Jeśli OP chce pojedynczej spacji na kodod Unicode, wówczas dane wejściowe należy najpierw zdekodować do Unicode.
jfs
To mi bardzo pomogło!
Muhammad Haseeb
55

Dla uzyskania najbardziej podobnej reprezentacji oryginalnego ciągu polecam moduł unidecode :

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

Następnie możesz użyć go w ciągu:

remove_non_ascii("Ceñía")
Cenia
Alvaro Fuentes
źródło
ciekawa sugestia, ale zakłada, że ​​użytkownik nie chce ascii, aby stał się tym, jakie są reguły dla unidecode. To jednak pytanie zadaje pytanie pytającemu, dlaczego nalegają na spacje, by być może zastąpić je inną postacią?
jxramos
Dziękuję, to dobra odpowiedź. Nie działa na potrzeby tego pytania, ponieważ większość danych, z którymi mam do czynienia, nie ma reprezentacji podobnej do ASCII. Takich jak דותן. Jednak w ogólnym znaczeniu jest to świetne, dziękuję!
dotancohen
1
Tak, wiem, że to nie działa w przypadku tego pytania, ale wylądowałem tutaj, próbując rozwiązać ten problem, więc pomyślałem, że po prostu podzielę się swoim rozwiązaniem mojego własnego problemu, który moim zdaniem jest bardzo powszechny dla ludzi jako @dotancohen, którzy mają do czynienia z non-ascii przez cały czas.
Alvaro Fuentes
W przeszłości występowały pewne luki w zabezpieczeniach związane z takimi rzeczami. Uważaj tylko, jak to zaimplementować!
deweydb
Nie działa z ciągami tekstowymi zakodowanymi w UTF-16
user5359531
22

Do przetwarzania znaków użyj ciągów 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'

Ale zauważ, że nadal będziesz mieć problem, jeśli Twój ciąg znaków zawiera rozłożone znaki Unicode (na przykład oddzielny znak i łączące znaki akcentu):

>>> 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
źródło
Dziękuję, to ważna obserwacja. Jeśli znajdziesz logiczny sposób radzenia sobie z przypadkiem łączenia znaków, chętnie dodam nagrodę do pytania. Przypuszczam, że po prostu usunięcie łączącego znaku, a pozostawienie niepołączonej postaci w spokoju byłoby najlepsze.
dotancohen,
1
Częściowym rozwiązaniem jest użycie ud.normalize('NFC',s)do łączenia znaków, ale nie wszystkie kombinacje kombinacji są reprezentowane przez pojedyncze punkty kodowe. Potrzebowałbyś mądrzejszego rozwiązania, patrząc na ud.category()postać.
Mark Tolonen,
1
@dotancohen: w Unicode istnieje pojęcie „znaku postrzeganego przez użytkownika”, który może obejmować kilka punktów kodowych Unicode. \X(rozszerzony klaster grafemowy) regex (obsługiwany przez regexmoduł) pozwala na iterację takich znaków (uwaga: „grafemy niekoniecznie łączą sekwencje znaków, a łączenie sekwencji znaków niekoniecznie jest grafem” ).
jfs
10

Jeśli zamiennym znakiem może być „?” zamiast spacji sugerowałbym 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(),
))

Wyniki:

0.7208260721400134
0.009975979187503592
AXO
źródło
Zastąp ? w razie potrzeby z inną postacią lub spacją, a ty nadal będziesz szybszy.
Moritz
7

A co z tym?

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
źródło
1
Chociaż jest to raczej nieeleganckie, jest bardzo czytelne. Dziękuję Ci.
dotancohen
1
+1 za obsługę Unicode ... @dotancohen IMNSHO „czytelny” oznacza „praktyczny”, co dodaje się do „eleganckiego”, więc powiedziałbym „trochę nieelegancki”
qneill
3

Jako natywne i wydajne podejście, nie musisz używać ordani zapętlać znaków. Wystarczy zakodować asciii zignorować błędy.

Poniższe po prostu usunie znaki inne niż ascii:

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

Teraz, jeśli chcesz zastąpić usunięte znaki, wykonaj następujące czynności:

final_string = new_string + b' ' * (len(old_string) - len(new_string))
Kasramvd
źródło
W python3 encodezwróci to bajtowanie, więc miej to na uwadze. Ponadto ta metoda nie usuwa znaków takich jak znak nowej linii.
Kyle Gibson
-1

Potencjalnie na inne pytanie, ale podaję moją wersję odpowiedzi @ Alvero (używając unidecode). Chcę zrobić „zwykły” pasek na moich ciągach, tj. Początek i koniec mojego ciągu dla białych znaków, a następnie zastąpić tylko inne znaki białych znaków „zwykłą” spacją, tj.

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

do

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

Najpierw zamieniamy wszystkie spacje inne niż Unicode spacją zwykłą (i łączymy ją ponownie),

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

A potem dzielimy to ponownie, normalnym podziałem Pythona, i usuwamy każdy „bit”,

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

I w końcu dołącz do nich ponownie, ale tylko wtedy, gdy ciąg minie if test,

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

I dzięki temu safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')poprawnie zwraca 'Ceñía mañana'.

seaders
źródło