Ganti karakter non-ASCII dengan satu spasi

244

Saya perlu mengganti semua karakter non-ASCII (\ x00- \ x7F) dengan spasi. Saya terkejut bahwa ini tidak mudah mati di Python, kecuali saya kehilangan sesuatu. Fungsi berikut hanya menghapus semua karakter non-ASCII:

def remove_non_ascii_1(text):

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

Dan yang ini menggantikan karakter non-ASCII dengan jumlah spasi sesuai jumlah byte pada titik kode karakter (yaitu karakter diganti dengan 3 spasi):

def remove_non_ascii_2(text):

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

Bagaimana saya bisa mengganti semua karakter non-ASCII dengan satu spasi?

Dari yang segudang dari sejenis SO pertanyaan , tidak ada alamat karakter pengganti sebagai lawan untuk pengupasan , dan juga mengatasi semua karakter non-ascii bukan karakter tertentu.

dotancohen
sumber
46
wow, Anda benar-benar berusaha keras untuk menunjukkan banyak tautan. +1 segera setelah hari diperbarui!
shad0w_wa1k3r
3
Anda sepertinya telah melewatkan stackoverflow.com/questions/1342000/… ini
Stuart
Saya tertarik melihat contoh input yang bermasalah.
dstromberg
5
@ Sitart: Terima kasih, tapi itu yang pertama yang saya sebutkan.
dotancohen
1
@dstromberg: saya menyebutkan contoh karakter bermasalah dalam pertanyaan: . Ini orangnya .
dotancohen

Jawaban:

243

''.join()Ekspresi Anda memfilter , menghapus apa pun yang bukan ASCII; Anda bisa menggunakan ekspresi kondisional sebagai gantinya:

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

Ini menangani karakter satu per satu dan masih akan menggunakan satu ruang per karakter yang diganti.

Ekspresi reguler Anda seharusnya hanya mengganti karakter non-ASCII berturut - turut dengan spasi:

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

Perhatikan di +sana.

Martijn Pieters
sumber
18
@ dokromberg: lebih lambat; str.join() membutuhkan daftar (itu akan melewati nilai dua kali), dan ekspresi generator pertama-tama akan dikonversi menjadi satu. Memberikannya daftar pemahaman hanya lebih cepat. Lihat posting ini .
Martijn Pieters
1
Sepotong kode pertama akan menyisipkan beberapa kosong per karakter jika Anda memberinya string UTF-8 byte.
Mark Ransom
@MarkRansom: Saya berasumsi ini Python 3.
Martijn Pieters
2
" karakter diganti dengan 3 spasi" dalam pertanyaan menyiratkan bahwa input adalah bytestring (bukan Unicode) dan oleh karena itu Python 2 digunakan (jika tidak ''.joinakan gagal). Jika OP ingin satu ruang per Unicode codepoint maka input harus diterjemahkan ke dalam Unicode terlebih dahulu.
jfs
Ini sangat membantu saya!
Muhammad Haseeb
55

Untuk Anda yang mendapatkan representasi paling mirip dari string asli Anda, saya sarankan modul unidecode :

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

Kemudian Anda bisa menggunakannya dalam sebuah string:

remove_non_ascii("Ceñía")
Cenia
Alvaro Fuentes
sumber
saran yang menarik, tetapi mengasumsikan keinginan pengguna non ascii untuk menjadi seperti apa aturan unidecode. Namun ini menimbulkan pertanyaan lanjutan kepada penanya tentang mengapa mereka bersikeras pada ruang, untuk mungkin mengganti dengan karakter lain?
jxramos
Terima kasih, ini jawaban yang bagus. Tidak berfungsi untuk tujuan pertanyaan ini karena sebagian besar data yang saya hadapi tidak memiliki representasi seperti ASCII. Seperti דותן. Namun, secara umum ini bagus, terima kasih!
dotancohen
1
Ya, saya tahu ini tidak berfungsi untuk pertanyaan ini , tapi saya mendarat di sini mencoba menyelesaikan masalah itu, jadi saya pikir saya hanya akan berbagi solusi untuk masalah saya sendiri, yang menurut saya sangat umum bagi orang-orang sebagai @dotancohen yang berurusan dengan karakter non-ascii sepanjang waktu.
Alvaro Fuentes
Ada beberapa kerentanan keamanan dengan hal-hal seperti ini di masa lalu. Hanya berhati-hatilah bagaimana Anda menerapkan ini!
deweydb
Tampaknya tidak berfungsi dengan string teks yang disandikan UTF-16
user5359531
22

Untuk pemrosesan karakter , gunakan string 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'

Tetapi perhatikan Anda masih akan memiliki masalah jika string Anda berisi karakter Unicode terurai (karakter yang terpisah dan menggabungkan tanda aksen, misalnya):

>>> 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
sumber
Terima kasih, ini pengamatan penting. Jika Anda menemukan cara logis untuk menangani kasus penggabungan-tanda, saya akan dengan senang hati menambahkan hadiah untuk pertanyaan tersebut. Saya kira hanya menghapus tanda menggabungkan namun meninggalkan karakter yang tidak digabungkan saja akan menjadi yang terbaik.
dotancohen
1
Solusi parsial digunakan ud.normalize('NFC',s)untuk menggabungkan tanda, tetapi tidak semua kombinasi kombinasi diwakili oleh satu titik kode tunggal. Anda membutuhkan solusi yang lebih cerdas untuk melihat ud.category()karakter.
Mark Tolonen
1
@dotancohen: ada gagasan "karakter yang dirasakan pengguna" di Unicode yang dapat menjangkau beberapa titik kode Unicode. \X(eXtended grapheme cluster) regex (didukung oleh regexmodul) memungkinkan untuk beralih lebih dari karakter tersebut (catatan: "grapheme tidak harus menggabungkan urutan karakter, dan menggabungkan urutan karakter tidak harus grapheme" ).
jfs
10

Jika karakter pengganti bisa '?' alih-alih spasi, maka saya sarankan 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(),
))

Hasil:

0.7208260721400134
0.009975979187503592
AXO
sumber
Ganti? dengan karakter atau spasi lain sesudahnya jika diperlukan, dan Anda akan tetap lebih cepat.
Moritz
7

Bagaimana dengan yang ini?

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
sumber
1
Meskipun ini agak tidak sopan, itu sangat mudah dibaca. Terima kasih.
dotancohen
1
+1 untuk penanganan unicode ... @dotancohen IMNSHO "dapat dibaca" menyiratkan "praktis" yang menambah "elegan", jadi saya akan mengatakan "agak tidak
sopan
3

Sebagai pendekatan asli dan efisien, Anda tidak perlu menggunakan ordatau mengulang karakter apa pun. Cukup enkode denganascii dan mengabaikan kesalahan.

Berikut ini hanya akan menghapus karakter non-ascii:

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

Sekarang jika Anda ingin mengganti karakter yang dihapus lakukan saja hal berikut:

final_string = new_string + b' ' * (len(old_string) - len(new_string))
Kasramvd
sumber
Di python3, ini encodeakan mengembalikan bytestring, jadi ingatlah itu. Selain itu, metode ini tidak akan menghapus karakter seperti baris baru.
Kyle Gibson
-1

Berpotensi untuk pertanyaan yang berbeda, tapi saya memberikan versi saya dari jawaban @ Alvero (menggunakan unidecode). Saya ingin melakukan strip "biasa" pada string saya, yaitu awal dan akhir string saya untuk karakter spasi, dan kemudian ganti hanya karakter spasi putih lainnya dengan ruang "biasa", yaitu

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

untuk

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

Kami pertama-tama mengganti semua ruang non-unicode dengan ruang reguler (dan bergabung kembali lagi),

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

Dan kemudian kita membaginya lagi, dengan split normal python, dan strip masing-masing "bit",

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

Dan terakhir bergabung kembali dengan mereka lagi, tetapi hanya jika string melewati iftes,

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

Dan dengan itu, safely_stripped('ㅤㅤㅤㅤCeñíaㅤmañanaㅤㅤㅤㅤ')kembali dengan benar 'Ceñía mañana'.

pelaut
sumber