Замініть символи, що не належать до 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.