Eliminar caracteres no imprimibles de una cadena en Python

91

Yo solía correr

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

en Perl para deshacerse de los caracteres no imprimibles.

En Python no hay clases de expresiones regulares POSIX, y no puedo escribir [: print:] si significa lo que quiero. No conozco ninguna forma en Python de detectar si un personaje es imprimible o no.

¿Qué harías?

EDITAR: También debe admitir caracteres Unicode. La forma string.printable felizmente los eliminará de la salida. curses.ascii.isprint devolverá falso para cualquier carácter Unicode.

Vinko Vrsalovic
fuente

Respuestas:

85

Desafortunadamente, la iteración sobre cadenas es bastante lenta en Python. Las expresiones regulares son más rápidas en un orden de magnitud para este tipo de cosas. Solo tienes que construir la clase de personaje tú mismo. El módulo unicodedata es bastante útil para esto, especialmente la función unicodedata.category () . Consulte Base de datos de caracteres Unicode para obtener descripciones de las categorías.

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)

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

Para algunos casos de uso, pueden ser preferibles categorías adicionales (por ejemplo, todos los del grupo de control , aunque esto podría ralentizar el tiempo de procesamiento y aumentar el uso de la memoria de manera significativa. Número de caracteres por categoría:

  • Cc (control): 65
  • Cf (formato): 161
  • Cs (sustituto): 2048
  • Co (uso privado): 137468
  • Cn (no asignado): 836601

Editar Agregar sugerencias de los comentarios.

Hormigas Aasma
fuente
4
¿Es suficiente 'Cc' aquí? No lo sé, solo estoy preguntando; me parece que algunas de las otras categorías 'C' también pueden ser candidatas para este filtro.
Patrick Johnmeyer
1
Esta función, tal como se publicó, elimina la mitad de los caracteres hebreos. Obtengo el mismo efecto para ambos métodos dados.
dotancohen
1
Desde la perspectiva del rendimiento, ¿no funcionaría string.translate () más rápido en este caso? Ver stackoverflow.com/questions/265960/…
Kashyap
3
Úselo all_chars = (unichr(i) for i in xrange(sys.maxunicode))para evitar el error de construcción estrecho.
danmichaelo
4
Para mí control_chars == '\x00-\x1f\x7f-\x9f'(probado en Python 3.5.2)
AXO
74

Hasta donde yo sé, el método más pitónico / eficiente sería:

import string

filtered_string = filter(lambda x: x in string.printable, myStr)
William Keller
fuente
10
Probablemente desee filter_string = '' .join (filter (lambda x: x in string.printable, myStr) para obtener una cadena.
Nathan Shively-Sanders
12
Lamentablemente, string.printable no contiene caracteres Unicode y, por lo tanto, ü o ó no estarán en la salida ... ¿tal vez haya algo más?
Vinko Vrsalovic
17
Debería utilizar una comprensión de lista o expresiones generadoras, no filtro + lambda. Uno de estos será el 99,9% de las veces más rápido. '' .join (s for s en myStr si s en string.printable)
habnabit
3
@AaronGallagher: ¿99,9% más rápido? ¿De dónde sacas esa figura? La comparación de rendimiento no es tan mala.
Chris Morgan
4
Hola William. Este método parece eliminar todos los caracteres que no son ASCII. ¡Hay muchos caracteres imprimibles que no son ASCII en Unicode!
dotancohen
17

Puede intentar configurar un filtro usando la unicodedata.category()función:

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

Consulte la Tabla 4-9 en la página 175 en las propiedades de caracteres de la base de datos Unicode para conocer las categorías disponibles.

Ber
fuente
comenzaste una lista de comprensión que no terminaba en tu última línea. Le sugiero que retire el soporte de apertura por completo.
tzot
Gracias por señalar esto. Edité la publicación en consecuencia
Ber
1
Este parece el método más directo y sencillo. Gracias.
dotancohen
1
@CsabaToth Los tres son válidos y producen el mismo conjunto. El suyo es quizás la forma más agradable de especificar un literal establecido.
Ber
1
@AnubhavJhalani Puede agregar más categorías Unicode al filtro. Para reservar espacios y dígitos además de las letras, useprintable = {'Lu', 'Ll', Zs', 'Nd'}
Ber
11

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

Vea esta publicación de StackOverflow sobre cómo eliminar la puntuación para ver cómo se compara .translate () con regex & .replace ()

Los rangos se pueden generar mediante el nonprintable = (ord(c) for c in (chr(i) for i in range(sys.maxunicode)) if unicodedata.category(c)=='Cc')uso de las categorías de la base de datos de caracteres Unicode como lo muestra @Ants Aasma.

Shawnrad
fuente
Sería mejor usar rangos Unicode (ver la respuesta de @Ants Aasma). El resultado sería text.translate({c:None for c in itertools.chain(range(0x00,0x20),range(0x7f,0xa0))}).
darkdragon
9

Lo siguiente funcionará con la entrada Unicode y es bastante rápido ...

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

Mis propias pruebas sugieren que este enfoque es más rápido que las funciones que iteran sobre la cadena y devuelven un resultado usando str.join.

ChrisP
fuente
Esta es la única respuesta que me funciona con caracteres Unicode. ¡Increíble que hayas proporcionado casos de prueba!
pir
1
Si desea permitir saltos de línea, agregue LINE_BREAK_CHARACTERS = set(["\n", "\r"])y and not chr(i) in LINE_BREAK_CHARACTERSal crear la tabla.
pir
5

Esta función usa listas por comprensión y str.join, por lo que se ejecuta en tiempo lineal en lugar de O (n ^ 2):

from curses.ascii import isprint

def printable(input):
    return ''.join(char for char in input if isprint(char))
Kirk Strauser
fuente
2
filter(isprint,input)
2013
5

Otra opción más en Python 3:

re.sub(f'[^{re.escape(string.printable)}]', '', my_string)
c6401
fuente
Esto funcionó muy bien para mí y su 1 línea. gracias
Chop Labalagun
1
por alguna razón, esto funciona muy bien en Windows, pero no puedo usarlo en Linux, tuve que cambiar la f por una r, pero no estoy seguro de que esa sea la solución.
Chop Labalagun
Parece que su Python de Linux era demasiado antiguo para admitir cadenas f en ese momento. Las r-strings son bastante diferentes, aunque se podría decir r'[^' + re.escape(string.printable) + r']'. (No creo que re.escape()sea ​​del todo correcto aquí, pero si funciona ...)
tripleee
2

Lo mejor que se me ocurrió ahora es (gracias a los python-izers anteriores)

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

Esta es la única forma en que he descubierto que funciona con caracteres / cadenas Unicode

¿Alguna mejor opción?

Vinko Vrsalovic
fuente
1
A menos que esté en Python 2.3, los [] s internos son redundantes. "return" .join (c para c ...) "
habnabit
No del todo redundantes, tienen diferentes significados (y características de desempeño), aunque el resultado final es el mismo.
Miles
¿No debería protegerse también el otro extremo del rango ?: "ord (c) <= 126"
Gearoid Murphy
7
Pero hay caracteres Unicode que tampoco se pueden imprimir.
tripleee
2

El de abajo funciona más rápido que los de arriba. Echar un vistazo

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

En Python no hay clases de expresiones regulares POSIX

Hay cuando se usa la regexbiblioteca: https://pypi.org/project/regex/

Está bien mantenido y es compatible con expresiones regulares Unicode, expresiones regulares Posix y muchos más. El uso (firmas de métodos) es muy similar al de Python re.

De la documentación:

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

Se admiten las clases de caracteres POSIX. Normalmente se tratan como una forma alternativa de \p{...}.

(No estoy afiliado, solo un usuario).

Risadinha
fuente
2

Según la respuesta de @ Ber, sugiero eliminar solo los caracteres de control como se define en las categorías de la base de datos de caracteres Unicode :

import unicodedata
def filter_non_printable(s):
    return ''.join(c for c in s if not unicodedata.category(c).startswith('C'))
Dragón oscuro
fuente
¡Esta es una respuesta genial!
tdc
Puede que esté en algo, startswith('C')pero esto fue mucho menos eficaz en mis pruebas que cualquier otra solución.
Big McLargeHuge
big-mclargehuge: El objetivo de mi solución fue la combinación de integridad y simplicidad / legibilidad. Podría intentar usar if unicodedata.category(c)[0] != 'C'en su lugar. ¿Funciona mejor? Si prefiere la velocidad de ejecución sobre los requisitos de memoria, puede precalcular la tabla como se muestra en stackoverflow.com/a/93029/3779655
darkdragon
0

Para eliminar los 'espacios en blanco',

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))
saber parque
fuente
En realidad, tampoco necesitas los corchetes.
tripleee
0

Adaptado de las respuestas de Ants Aasma y 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)

probado en Python 3.7.7

Joe
fuente