Usuwanie niedrukowalnych znaków z łańcucha w Pythonie

91

Zwykłem biegać

$s =~ s/[^[:print:]]//g;

na Perlu, aby pozbyć się niedrukowalnych znaków.

W Pythonie nie ma klas wyrażeń regularnych POSIX i nie mogę pisać [: print:] mając to na myśli, co chcę. Nie znam żadnego sposobu w Pythonie, aby wykryć, czy znak można wydrukować, czy nie.

Co byś zrobił?

EDYCJA: Musi również obsługiwać znaki Unicode. Sposób string.printable szczęśliwie usunie je z wyjścia. curses.ascii.isprint zwróci wartość false dla każdego znaku Unicode.

Vinko Vrsalovic
źródło

Odpowiedzi:

85

Iterowanie po łańcuchach jest niestety raczej powolne w Pythonie. Wyrażenia regularne są o rząd wielkości szybsze w tego rodzaju sytuacjach. Musisz tylko sam zbudować klasę postaci. Unicodedata moduł jest bardzo pomocne w tym, w szczególności unicodedata.category () funkcja. Aby zapoznać się z opisami kategorii, zobacz Baza danych znaków Unicode .

import unicodedata, re, itertools, sys

all_chars = (chr(i) for i in range(sys.maxunicode))
categories = {'Cc'}
control_chars = ''.join(c for c in all_chars if unicodedata.category(c) in categories)
# or equivalently and much more efficiently
control_chars = ''.join(map(chr, itertools.chain(range(0x00,0x20), range(0x7f,0xa0))))

control_char_re = re.compile('[%s]' % re.escape(control_chars))

def remove_control_chars(s):
    return control_char_re.sub('', s)

W przypadku Python2

import unicodedata, re, sys

all_chars = (unichr(i) for i in xrange(sys.maxunicode))
categories = {'Cc'}
control_chars = ''.join(c for c in all_chars if unicodedata.category(c) in categories)
# or equivalently and much more efficiently
control_chars = ''.join(map(unichr, range(0x00,0x20) + range(0x7f,0xa0)))

control_char_re = re.compile('[%s]' % re.escape(control_chars))

def remove_control_chars(s):
    return control_char_re.sub('', s)

W niektórych przypadkach preferowane mogą być dodatkowe kategorie (np. Wszystkie z grupy kontrolnej , chociaż może to spowolnić czas przetwarzania i znacznie zwiększyć użycie pamięci. Liczba znaków na kategorię:

  • Cc (kontrola): 65
  • Cf (format): 161
  • Cs (surogat): 2048
  • Co (do użytku prywatnego): 137468
  • Cn (nieprzypisane): 836601

Edytuj Dodawanie sugestii z komentarzy.

Ants Aasma
źródło
4
Czy wystarczy tutaj „Cc”? Nie wiem, tylko pytam - wydaje mi się, że niektóre inne kategorie „C” również mogą być kandydatami do tego filtra.
Patrick Johnmeyer,
1
Ta funkcja, zgodnie z opublikowaną wersją, usuwa połowę znaków hebrajskich. Uzyskuję ten sam efekt dla obu podanych metod.
dotancohen
1
Z punktu widzenia wydajności, czy string.translate () nie działałby w tym przypadku szybciej? Zobacz stackoverflow.com/questions/265960/…
Kashyap
3
Użyj, all_chars = (unichr(i) for i in xrange(sys.maxunicode))aby uniknąć wąskiego błędu kompilacji.
danmichaelo
4
Dla mnie control_chars == '\x00-\x1f\x7f-\x9f'(testowane na Pythonie 3.5.2)
AXO
73

O ile wiem, najbardziej pythonowa / wydajna metoda to:

import string

filtered_string = filter(lambda x: x in string.printable, myStr)
William Keller
źródło
10
Prawdopodobnie chcesz filter_string = '' .join (filter (lambda x: x in string.printable, myStr), aby otrzymać ciąg znaków.
Nathan Shively-Sanders
12
Niestety string.printable nie zawiera znaków Unicode, a więc ü lub ó nie będzie na wyjściu ... może jest coś jeszcze?
Vinko Vrsalovic
17
Powinieneś używać rozumienia listowego lub wyrażeń generatora, a nie filtru + lambda. Jeden z nich w 99,9% przypadków będzie szybszy. '' .join (s for s in myStr if s in
string.printable
3
@AaronGallagher: 99,9% szybciej? Skąd wyrywasz tę figurę? Porównanie wydajności nie jest wcale takie złe.
Chris Morgan,
4
Cześć William. Wydaje się, że ta metoda usuwa wszystkie znaki spoza zestawu ASCII. Istnieje wiele drukowalnych znaków spoza ASCII w Unicode!
dotancohen
17

Możesz spróbować skonfigurować filtr za pomocą unicodedata.category()funkcji:

import unicodedata
printable = {'Lu', 'Ll'}
def filter_non_printable(str):
  return ''.join(c for c in str if unicodedata.category(c) in printable)

Informacje na temat dostępnych kategorii zawiera Tabela 4-9 na stronie 175, opisująca właściwości znaków bazy danych Unicode

Ber
źródło
zacząłeś rozumieć listę, która nie kończyła się w ostatniej linii. Proponuję całkowicie usunąć wspornik otwierający.
tzot
Dziękuję za zwrócenie uwagi. Odpowiednio zredagowałem post
Ber
1
Wydaje się, że jest to najbardziej bezpośrednia i prosta metoda. Dzięki.
dotancohen
1
@CsabaToth Wszystkie trzy są prawidłowe i dają ten sam zestaw. Twój jest chyba najładniejszym sposobem określenia literału zestawu.
Ber
1
@AnubhavJhalani Możesz dodać więcej kategorii Unicode do filtra. Aby zarezerwować spacje i cyfry oprócz liter, użyjprintable = {'Lu', 'Ll', Zs', 'Nd'}
Ber
11

W Pythonie 3

def filter_nonprintable(text):
    import itertools
    # Use characters of control category
    nonprintable = itertools.chain(range(0x00,0x20),range(0x7f,0xa0))
    # Use translate to remove all non-printable characters
    return text.translate({character:None for character in nonprintable})

Zobacz ten post StackOverflow na temat usuwania znaków interpunkcyjnych, aby dowiedzieć się, jak .translate () porównuje się z wyrażeniem regularnym i .replace ()

Zakresy można generować za nonprintable = (ord(c) for c in (chr(i) for i in range(sys.maxunicode)) if unicodedata.category(c)=='Cc')pomocą kategorii bazy danych znaków Unicode, jak pokazano na @Ants Aasma.

shawnrad
źródło
Byłoby lepiej używać zakresów Unicode (zobacz odpowiedź @Ants Aasma). Wynik byłby text.translate({c:None for c in itertools.chain(range(0x00,0x20),range(0x7f,0xa0))}).
darkdragon
9

Poniższe będzie działać z wejściem Unicode i jest dość szybkie ...

import sys

# build a table mapping all non-printable characters to None
NOPRINT_TRANS_TABLE = {
    i: None for i in range(0, sys.maxunicode + 1) if not chr(i).isprintable()
}

def make_printable(s):
    """Replace non-printable characters in a string."""

    # the translate method on str removes characters
    # that map to None from the string
    return s.translate(NOPRINT_TRANS_TABLE)


assert make_printable('Café') == 'Café'
assert make_printable('\x00\x11Hello') == 'Hello'
assert make_printable('') == ''

Moje własne testy sugerują, że to podejście jest szybsze niż funkcje, które iterują po ciągu i zwracają wynik za pomocą str.join.

ChrisP
źródło
To jedyna odpowiedź, która działa dla mnie ze znakami Unicode. Wspaniale, że udostępniłeś przypadki testowe!
pir
1
Jeśli chcesz zezwolić na podziały wierszy, dodaj LINE_BREAK_CHARACTERS = set(["\n", "\r"])i and not chr(i) in LINE_BREAK_CHARACTERSpodczas budowania tabeli.
pir
5

Ta funkcja wykorzystuje wyrażenia listowe i str.join, więc działa w czasie liniowym zamiast O (n ^ 2):

from curses.ascii import isprint

def printable(input):
    return ''.join(char for char in input if isprint(char))
Kirk Strauser
źródło
2
filter(isprint,input)
wysłano
5

Jeszcze inna opcja w Pythonie 3:

re.sub(f'[^{re.escape(string.printable)}]', '', my_string)
c6401
źródło
To zadziałało świetnie dla mnie i jego 1 linii. dzięki
Chop Labalagun
1
z jakiegoś powodu działa to świetnie w systemie Windows, ale nie mogę go używać w systemie Linux, musiałem zmienić f na r, ale nie jestem pewien, czy to jest rozwiązanie.
Chop Labalagun
Wygląda na to, że twój Linux Python był zbyt stary, aby obsługiwać f-stringi. r-strings są zupełnie inne, choć można powiedzieć r'[^' + re.escape(string.printable) + r']'. (Myślę, że nie re.escape()jest to do końca poprawne, ale jeśli
zadziała
2

Najlepsze, co teraz wymyśliłem, to (dzięki python-izers powyżej)

def filter_non_printable(str):
  return ''.join([c for c in str if ord(c) > 31 or ord(c) == 9])

To jedyny sposób, w jaki się dowiedziałem, że działa ze znakami / ciągami znaków Unicode

Jakieś lepsze opcje?

Vinko Vrsalovic
źródło
1
Jeśli nie korzystasz z Pythona 2.3, wewnętrzne [] s są zbędne. "return '' .join (c jak c ...)"
habnabit,
Niezupełnie zbędne - mają różne znaczenia (i charakterystykę działania), chociaż efekt końcowy jest taki sam.
Miles
Czy drugi koniec zakresu również nie powinien być chroniony ?: "ord (c) <= 126"
Gearoid Murphy
7
Ale są też znaki Unicode, których nie można wydrukować.
tripleee
2

Ten poniżej działa szybciej niż pozostałe powyżej. Spójrz

''.join([x if x in string.printable else '' for x in Str])
Nilav Baran Ghosh
źródło
"".join([c if 0x21<=ord(c) and ord(c)<=0x7e else "" for c in ss])
evandrix
2

W Pythonie nie ma klas wyrażeń regularnych POSIX

Podczas korzystania z regexbiblioteki są: https://pypi.org/project/regex/

Jest dobrze utrzymany i obsługuje Unicode regex, Posix regex i wiele innych. Użycie (sygnatury metod) jest bardzo podobne do tego w Pythonie re.

Z dokumentacji:

[[:alpha:]]; [[:^alpha:]]

Obsługiwane są klasy znaków POSIX. Są one zwykle traktowane jako alternatywna forma \p{...}.

(Nie jestem zrzeszony, tylko użytkownik).

Risadinha
źródło
2

Opierając się na odpowiedzi @ Bera, proponuję usunąć tylko znaki sterujące zdefiniowane w kategoriach bazy danych znaków Unicode :

import unicodedata
def filter_non_printable(s):
    return ''.join(c for c in s if not unicodedata.category(c).startswith('C'))
Ciemny smok
źródło
To świetna odpowiedź!
tdc
Być może masz do czynienia z czymś, startswith('C')ale w moich testach było to znacznie mniej wydajne niż jakiekolwiek inne rozwiązanie.
Big McLargeHuge
big-mclargehuge: Celem mojego rozwiązania było połączenie kompletności i prostoty / czytelności. Możesz spróbować użyć if unicodedata.category(c)[0] != 'C'zamiast tego. Czy działa lepiej? Jeśli wolisz szybkość wykonywania niż wymagania dotyczące pamięci, możesz wstępnie obliczyć tabelę, jak pokazano na stackoverflow.com/a/93029/3779655
darkdragon
0

Aby usunąć „białe znaki”,

import re
t = """
\n\t<p>&nbsp;</p>\n\t<p>&nbsp;</p>\n\t<p>&nbsp;</p>\n\t<p>&nbsp;</p>\n\t<p>
"""
pat = re.compile(r'[\t\n]')
print(pat.sub("", t))
knowpark
źródło
Właściwie wtedy też nie potrzebujesz nawiasów kwadratowych.
tripleee
0

Na podstawie odpowiedzi Ants Aasma i Shawnrad :

nonprintable = set(map(chr, list(range(0,32)) + list(range(127,160))))
ord_dict = {ord(character):None for character in nonprintable}
def filter_nonprintable(text):
    return text.translate(ord_dict)

#use
str = "this is my string"
str = filter_nonprintable(str)
print(str)

przetestowano na Pythonie 3.7.7

Joe
źródło