Замініть символи, що не належать до ASCII, одним пробілом


244

Мені потрібно замінити всі символи, що не належать до 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 символи не конкретний характер.


46
Нічого, ви дійсно доклали зусиль, щоб показати так багато посилань. +1, як тільки день поновиться!
shad0w_wa1k3r

3
Ви, здається, пропустили цей stackoverflow.com/questions/1342000/…
Стюарт

Мені цікаво побачити приклад введення, який має проблеми.
dstromberg

5
@Stuart: Дякую, але це саме перше, що я згадую.
dotancohen

1
@dstromberg: Я згадую проблемний приклад символ в питанні: . Це цей хлопець .
dotancohen

Відповіді:


243

Ваше ''.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() потрібен список (він передасть значення двічі), і вираз генератора спочатку буде перетворено в одне. Ознайомлення зі списком - це просто швидше. Дивіться цю публікацію .
Martijn Pieters

1
Перший фрагмент коду буде вставляти кілька пробілів на символ, якщо ви подаєте йому байт-рядок UTF-8.
Марк Викуп

@MarkRansom: Я вважав, що це Python 3.
Martijn Pieters

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, які мають справу з персонажами, які не мають асацій, весь час.
Альваро Фуентес

У минулому були вразливі місця безпеки. Тільки будьте уважні, як ви це реалізуєте!
deweydb

Здається, не працює з текстовими рядками, закодованими UTF-16,
user5359531

22

Для обробки символів використовуйте рядки 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(eXtended grapheme cluster) регулярний вираз (підтримується 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

Замінити? з іншим символом або пробілом згодом, якщо потрібно, і ви все одно будете швидшими.
Моріц

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

1
Хоча це досить неелегантно, але воно легко читається. Дякую.
dotancohen

1
+1 для обробки unicode ... @dotancohen IMNSHO "читабельний" означає "практичне", що додає "елегантності", тому я б сказав "трохи неелегантно"
qneill

3

Як власний і ефективний підхід, вам не потрібно використовувати ordабо будь-який цикл над символами. Просто кодуйте asciiі ігноруйте помилки.

Далі просто буде видалено символи, які не мають права:

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

Тепер, якщо ви хочете замінити видалені символи, просто виконайте наступне:

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

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

А потім ми розділимо це знову, звичайним розщепленням пітона, і знімемо кожен "шматочок",

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.