Rimozione di caratteri non stampabili da una stringa in Python


88

Io uso per correre

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

su Perl per eliminare i caratteri non stampabili.

In Python non ci sono classi regex POSIX e non posso scrivere [: print:] visto che significa quello che voglio. Non conosco alcun modo in Python per rilevare se un carattere è stampabile o meno.

Cosa faresti?

EDIT: deve supportare anche i caratteri Unicode. Il modo string.printable li rimuoverà felicemente dall'output. curses.ascii.isprint restituirà false per qualsiasi carattere Unicode.

Risposte:


83

L'iterazione delle stringhe è purtroppo piuttosto lenta in Python. Le espressioni regolari sono più veloci di un ordine di grandezza per questo genere di cose. Devi solo costruire la classe del personaggio da solo. Il modulo unicodedata è molto utile per questo, specialmente la funzione unicodedata.category () . Vedere Database dei caratteri Unicode per le descrizioni delle categorie.

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)

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

Per alcuni casi d'uso, potrebbero essere preferibili categorie aggiuntive (ad es. Tutte dal gruppo di controllo , sebbene ciò potrebbe rallentare il tempo di elaborazione e aumentare notevolmente l'utilizzo della memoria. Numero di caratteri per categoria:

  • Cc (controllo): 65
  • Cf (formato): 161
  • Cs (surrogato): 2048
  • Co (uso privato): 137468
  • Cn (non assegnato): 836601

Modifica Aggiunta di suggerimenti dai commenti.


4
"Cc" è sufficiente qui? Non lo so, sto solo chiedendo: mi sembra che anche alcune delle altre categorie "C" possano essere candidate per questo filtro.
Patrick Johnmeyer

1
Questa funzione, come pubblicata, rimuove la metà dei caratteri ebraici. Ottengo lo stesso effetto per entrambi i metodi indicati.
dotancohen

1
Dal punto di vista delle prestazioni, string.translate () non funzionerebbe più velocemente in questo caso? Vedi stackoverflow.com/questions/265960/…
Kashyap

3
Utilizzare all_chars = (unichr(i) for i in xrange(sys.maxunicode))per evitare l'errore di build ristretto.
danmichaelo

4
Per me control_chars == '\x00-\x1f\x7f-\x9f'(testato su Python 3.5.2)
AXO

72

Per quanto ne so, il metodo più pitonico / efficiente sarebbe:

import string

filtered_string = filter(lambda x: x in string.printable, myStr)

10
Probabilmente vuoi filtered_string = '' .join (filter (lambda x: x in string.printable, myStr) in modo da ottenere una stringa.
Nathan Shively-Sanders

12
Purtroppo string.printable non contiene caratteri Unicode, quindi ü o ó non saranno nell'output ... forse c'è qualcos'altro?
Vinko Vrsalovic

17
Dovresti usare una comprensione dell'elenco o espressioni del generatore, non un filtro + lambda. Uno di questi sarà il 99,9% delle volte più veloce. '' .join (s for s in myStr if s in string.printable)
habnabit

3
@AaronGallagher: 99,9% più veloce? Da dove prendi quella cifra? Il confronto delle prestazioni non è affatto male.
Chris Morgan

4
Ciao William. Questo metodo sembra rimuovere tutti i caratteri non ASCII. Ci sono molti caratteri non ASCII stampabili in Unicode!
dotancohen

17

Potresti provare a impostare un filtro utilizzando la unicodedata.category()funzione:

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

Vedere la Tabella 4-9 a pagina 175 nelle proprietà dei caratteri del database Unicode per le categorie disponibili


hai iniziato una comprensione della lista che non è finita nella tua riga finale. Ti consiglio di rimuovere completamente la staffa di apertura.
tzot

Grazie per averlo fatto notare. Ho modificato il post di conseguenza
Ber

1
Questo sembra il metodo più diretto e diretto. Grazie.
dotancohen

1
@CsabaToth Tutti e tre sono validi e producono lo stesso set. Il tuo è forse il modo più carino per specificare un set letterale.
Ber

1
@AnubhavJhalani Puoi aggiungere più categorie Unicode al filtro. Per prenotare spazi e cifre oltre alle lettere utilizzareprintable = {'Lu', 'Ll', Zs', 'Nd'}
Ber

10

In Python 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})

Vedi questo post StackOverflow sulla rimozione della punteggiatura per il confronto tra .translate () e regex e .replace ()

Gli intervalli possono essere generati nonprintable = (ord(c) for c in (chr(i) for i in range(sys.maxunicode)) if unicodedata.category(c)=='Cc')utilizzando le categorie del database dei caratteri Unicode come mostrato da @Ants Aasma.


Sarebbe meglio usare gli intervalli Unicode (vedi la risposta di @Ants Aasma). Il risultato sarebbe text.translate({c:None for c in itertools.chain(range(0x00,0x20),range(0x7f,0xa0))}).
Darkdragon

8

Quanto segue funzionerà con l'input Unicode ed è piuttosto veloce ...

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('') == ''

I miei test suggeriscono che questo approccio è più veloce delle funzioni che iterano sulla stringa e restituiscono un risultato usando str.join.


Questa è l'unica risposta che funziona per me con i caratteri Unicode. Fantastico che tu abbia fornito casi di test!
pir

1
Se vuoi consentire le interruzioni di riga, aggiungi LINE_BREAK_CHARACTERS = set(["\n", "\r"])e and not chr(i) in LINE_BREAK_CHARACTERSdurante la creazione della tabella.
pir

5

Questa funzione utilizza le liste di comprensione e str.join, quindi viene eseguita in tempo lineare invece di O (n ^ 2):

from curses.ascii import isprint

def printable(input):
    return ''.join(char for char in input if isprint(char))

2
filter(isprint,input)
yingted il

5

Ancora un'altra opzione in Python 3:

re.sub(f'[^{re.escape(string.printable)}]', '', my_string)

Questo ha funzionato alla grande per me e la sua riga 1. grazie
Chop Labalagun

1
per qualche motivo questo funziona alla grande su Windows ma non posso usarlo su Linux, ho dovuto cambiare la f per una r ma non sono sicuro che sia la soluzione.
Chop Labalagun

Sembra che il tuo Linux Python fosse troppo vecchio per supportare le stringhe f allora. le stringhe r sono abbastanza diverse, anche se potresti dire r'[^' + re.escape(string.printable) + r']'. (Non penso re.escape()sia del tutto corretto qui, ma se funziona ...)
tripleee

2

Il meglio che ho trovato ora è (grazie ai python-izers sopra)

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

Questo è l'unico modo in cui ho scoperto che funziona con caratteri / stringhe Unicode

Eventuali opzioni migliori?


1
A meno che tu non sia su Python 2.3, le [] interne sono ridondanti. "return" .join (c for c ...) "
habnabit

Non del tutto ridondanti: hanno significati (e caratteristiche di prestazione) diversi, sebbene il risultato finale sia lo stesso.
Miglia

Non dovrebbe essere protetta anche l'altra estremità dell'intervallo ?: "ord (c) <= 126"
Gearoid Murphy

7
Ma ci sono anche caratteri Unicode che non sono stampabili.
tripleee

2

Quello sotto è più veloce degli altri sopra. Guarda

''.join([x if x in string.printable else '' for x in Str])

"".join([c if 0x21<=ord(c) and ord(c)<=0x7e else "" for c in ss])
evandrix

2

In Python non ci sono classi regex POSIX

Ci sono quando si utilizza la regexlibreria: https://pypi.org/project/regex/

È ben mantenuto e supporta Unicode regex, Posix regex e molti altri. L'utilizzo (firme del metodo) è molto simile a quello di Python re.

Dalla documentazione:

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

Le classi di caratteri POSIX sono supportate. Questi sono normalmente trattati come una forma alternativa di \p{...}.

(Non sono affiliato, solo un utente.)


1

Sulla base della risposta di @ Ber, suggerisco di rimuovere solo i caratteri di controllo come definiti nelle categorie del database dei caratteri Unicode :

import unicodedata
def filter_non_printable(s):
    return ''.join(c for c in s if not unicodedata.category(c).startswith('C'))

Questa è un'ottima risposta!
tdc

0

Per rimuovere "spazi vuoti",

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

In realtà non hai nemmeno bisogno delle parentesi quadre.
tripla

0

Adattato dalle risposte di Ants Aasma e 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)

testato su Python 3.7.7

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.