Заменить не-ASCII символы одним пробелом

245

Мне нужно заменить все не-ASCII (\ x00- \ x7F) символы пробелом. Я удивлен, что это не так просто в Python, если я что-то упустил. Следующая функция просто удаляет все не-ASCII символы:

def remove_non_ascii_1(text):

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

И этот заменяет не-ASCII символы количеством пробелов согласно количеству байтов в кодовой точке символа (т.е. символ заменяется 3 пробелами):

def remove_non_ascii_2(text):

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

Как заменить все символы, не входящие в ASCII, одним пробелом?

Из за несметного из подобных SO вопросов , ни один адрес символов замены в противоположность к зачистки , и дополнительно обратиться ко всем не-ASCII символы не конкретный характер.

dotancohen
источник
46
вау, вы действительно приложили много усилий, чтобы показать так много ссылок. +1, как только день возобновится!
shad0w_wa1k3r
3
Вы, кажется, пропустили этот один stackoverflow.com/questions/1342000/…
Стюарт
Мне интересно увидеть пример ввода, который имеет проблемы.
Дстромберг
5
@Stuart: Спасибо, но это первое, что я упомянул.
Dotancohen
1
@dstromberg: Я упоминаю проблемный пример символ в вопросе: . Это этот парень .
dotancohen

Ответы:

244

Ваше ''.join()выражение фильтрует , удаляет все, что не является ASCII; вместо этого вы можете использовать условное выражение:

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

Это обрабатывает символы один за другим и все равно будет использовать один пробел на замененный символ.

Ваше регулярное выражение должно просто заменить последовательные символы, не входящие в ASCII, пробелом:

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

Обратите внимание на +там.

Мартейн Питерс
источник
18
@dstromberg: медленнее; str.join() нужен список (он дважды передаст значения), и выражение генератора сначала будет преобразовано в единицу. Дать ему понимание списка просто быстрее. Смотрите этот пост .
Мартин Питерс
1
Первый фрагмент кода вставит несколько пробелов на символ, если вы передадите ему байтовую строку UTF-8.
Марк Рэнсом
@MarkRansom: Я предполагал, что это Python 3.
Мартин Питерс
2
« символ заменяется на 3 пробела» в вопросе подразумевает, что входные данные являются строкой байтов (не Unicode) и, следовательно, используется Python 2 (в противном случае ''.joinпроизойдет сбой). Если OP хочет один пробел на кодовую точку Unicode, то вход должен быть сначала декодирован в Unicode.
JFS
Это мне очень помогло!
Мухаммед Хасиб
55

Чтобы вы получили наиболее похожее представление вашей исходной строки, я рекомендую модуль unidecode :

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

Тогда вы можете использовать его в строке:

remove_non_ascii("Ceñía")
Cenia
Альваро Фуэнтес
источник
интересное предложение, но оно предполагает, что пользователь желает non ascii стать правилами unidecode. Это, однако, ставит вопрос к задающему вопрос о том, почему они настаивают на пробелах, возможно, для замены другим персонажем?
jxramos
Спасибо, это хороший ответ. Это не работает для целей этого вопроса, потому что большая часть данных, с которыми я имею дело, не имеет ASCII-подобного представления. Такие как דותן. Тем не менее, в общем смысле это здорово, спасибо!
dotancohen
1
Да, я знаю, что это не работает для этого вопроса, но я приземлился здесь, пытаясь решить эту проблему, поэтому я подумал, что просто поделюсь своим решением своей собственной проблемы, что, я думаю, очень распространено для людей, как @dotancohen, которые имеют дело с не-ASCII символов все время.
Альваро Фуэнтес
В прошлом были некоторые уязвимости безопасности с такими вещами. Просто будьте осторожны, как вы реализуете это!
deweydb
Кажется, не работает с текстовыми строками в кодировке UTF-16
user5359531
23

Для символьной обработки, использовать строки 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'

Но учтите, что у вас все еще будет проблема, если ваша строка содержит разложенные символы Unicode (например, отдельный символ и комбинацию знаков ударения):

>>> 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'
Марк Толонен
источник
Спасибо, это важное наблюдение. Если вы найдете логический способ справиться со случаем комбинированных меток, я бы с радостью добавил награду к этому вопросу. Я полагаю, что было бы лучше просто убрать метку объединения, но оставить некомбинированного персонажа в покое.
dotancohen
1
Частичное решение состоит в том, чтобы использовать ud.normalize('NFC',s)для объединения меток, но не все комбинированные комбинации представлены отдельными кодовыми точками. Вам нужно более разумное решение, глядя на ud.category()персонажа.
Марк Толонен
1
@dotancohen: в Unicode существует понятие «пользовательский символ», которое может охватывать несколько кодовых точек Unicode. \X(расширенный кластер графем) regex (поддерживается regexмодулем) позволяет выполнять итерации по таким символам (примечание: «графемы не обязательно объединяют последовательности символов, а объединение последовательностей символов не обязательно является графемами» ).
JFS
10

Если символ замены может быть '?' вместо пробела я бы предложил 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(),
))

Полученные результаты:

0.7208260721400134
0.009975979187503592
AXO
источник
Заменить ? с другим символом или пробелом впоследствии, если это необходимо, и вы все равно будете быстрее.
Мориц
7

Что насчет этого?

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
источник
1
Хотя это довольно не элегантно, это очень читабельно. Спасибо.
dotancohen
1
+1 для обработки юникода ... @dotancohen IMNSHO "читабельный" подразумевает "практический", который добавляет к "элегантному", так что я бы сказал "немного не элегантный"
qneill
3

В качестве нативного и эффективного подхода вам не нужно использовать ordкакой-либо цикл над символами. Просто закодируйте сascii и игнорируйте ошибки.

Следующее просто удалит не-ascii символы:

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

Теперь, если вы хотите заменить удаленные символы, просто сделайте следующее:

final_string = new_string + b' ' * (len(old_string) - len(new_string))
Kasramvd
источник
В python3 это encodeвозвратит строку байтов, так что имейте это в виду. Кроме того, этот метод не удаляет такие символы, как символ новой строки.
Кайл Гибсон
-1

Возможно, для другого вопроса, но я предоставляю свою версию ответа @ Alvero (используя unidecode). Я хочу сделать "обычную" полосу для моих строк, то есть начало и конец моей строки для пробельных символов, а затем заменить только другие пробельные символы на "обычный" пробел, т.е.

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

Сначала мы заменим все не-юникодные пробелы обычным пробелом (и снова включим его),

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

И затем мы разделяем это снова, с нормальным разделением Python, и удаляем каждый "бит",

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

И, наконец, присоединиться к ним снова, но только если строка проходит ifтест,

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

И с этим safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')правильно возвращается 'Ceñía mañana'.

seaders
источник