Remoção de caracteres não imprimíveis de uma string em python

88

Eu costumava correr

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

em Perl para se livrar de caracteres não imprimíveis.

Em Python não há classes regex POSIX e não posso escrever [: print:] porque significa o que eu quero. Não conheço nenhuma maneira em Python de detectar se um caractere pode ser impresso ou não.

O que você faria?

EDIT: Ele também deve oferecer suporte a caracteres Unicode. A forma string.printable irá facilmente eliminá-los da saída. curses.ascii.isprint retornará false para qualquer caractere Unicode.

Vinko Vrsalovic
fonte

Respostas:

83

A iteração em strings é, infelizmente, bastante lenta em Python. As expressões regulares são mais do que uma ordem de magnitude mais rápidas para esse tipo de coisa. Você apenas tem que construir a classe de personagem sozinho. O módulo unicodedata é bastante útil para isso, especialmente a função unicodedata.category () . Consulte o banco de dados de caracteres Unicode para obter as descrições das categorias.

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 alguns casos de uso, categorias adicionais (por exemplo, todas do grupo de controle podem ser preferíveis, embora isso possa diminuir o tempo de processamento e aumentar o uso de memória significativamente. Número de caracteres por categoria:

  • Cc (controle): 65
  • Cf (formato): 161
  • Cs (substituto): 2048
  • Co (uso privado): 137468
  • Cn (não atribuído): 836601

Editar Adicionando sugestões dos comentários.

Formigas Aasma
fonte
4
'Cc' é suficiente aqui? Não sei, estou apenas perguntando - parece-me que algumas das outras categorias 'C' também podem ser candidatas a esse filtro.
Patrick Johnmeyer
1
Esta função, conforme publicada, remove metade dos caracteres hebraicos. Obtenho o mesmo efeito para os dois métodos fornecidos.
dotancohen
1
Do ponto de vista do desempenho, string.translate () não funcionaria mais rápido neste caso? Consulte stackoverflow.com/questions/265960/…
Kashyap,
3
Use all_chars = (unichr(i) for i in xrange(sys.maxunicode))para evitar o erro de construção restrito.
danmichaelo
4
Para mim control_chars == '\x00-\x1f\x7f-\x9f'(testado em Python 3.5.2)
AXO
72

Pelo que eu sei, o método mais pitônico / eficiente seria:

import string

filtered_string = filter(lambda x: x in string.printable, myStr)
William Keller
fonte
10
Você provavelmente deseja filter_string = '' .join (filter (lambda x: x em string.printable, myStr) para que você receba uma string.
Nathan Shively-Sanders
12
Infelizmente string.printable não contém caracteres Unicode e, portanto, ü ou ó não estarão na saída ... talvez haja algo mais?
Vinko Vrsalovic
17
Você deve usar uma compreensão de lista ou expressões geradoras, não filtro + lambda. Um deles será 99,9% das vezes mais rápido. '' .join (s para s em myStr se s em string.printable)
habnabit de
3
@AaronGallagher: 99,9% mais rápido? De onde você tira essa figura? A comparação de desempenho está longe de ser tão ruim.
Chris Morgan
4
Olá, William. Este método parece remover todos os caracteres não ASCII. Existem muitos caracteres não ASCII imprimíveis em Unicode!
dotancohen
17

Você pode tentar configurar um filtro usando a unicodedata.category()função:

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

Consulte a Tabela 4-9 na página 175 nas propriedades de caracteres do banco de dados Unicode para as categorias disponíveis

Ber
fonte
você iniciou uma compreensão de lista que não terminou em sua linha final. Eu sugiro que você remova o colchete de abertura completamente.
tzot
Obrigado por apontar isto. Eu editei a postagem de acordo
Ber,
1
Este parece o método mais direto e direto. Obrigado.
dotancohen
1
@CsabaToth Todos os três são válidos e geram o mesmo conjunto. Seu é talvez a melhor maneira de especificar um conjunto literal.
Ber
1
@AnubhavJhalani Você pode adicionar mais categorias Unicode ao filtro. Para reservar espaços e dígitos, além do uso de letrasprintable = {'Lu', 'Ll', Zs', 'Nd'}
Ber
10

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

Veja esta postagem StackOverflow sobre como remover pontuação para saber como .translate () se compara a regex e .replace ()

Os intervalos podem ser gerados nonprintable = (ord(c) for c in (chr(i) for i in range(sys.maxunicode)) if unicodedata.category(c)=='Cc')usando as categorias do banco de dados de caracteres Unicode, conforme mostrado por @Ants Aasma.

Shawnrad
fonte
Seria melhor usar intervalos Unicode (veja a resposta de @Ants Aasma). O resultado seria text.translate({c:None for c in itertools.chain(range(0x00,0x20),range(0x7f,0xa0))}).
darkdragon
8

O seguinte funcionará com entrada Unicode e é 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('') == ''

Meus próprios testes sugerem que essa abordagem é mais rápida do que funções que iteram na string e retornam um resultado usando str.join.

ChrisP
fonte
Esta é a única resposta que funciona para mim com caracteres Unicode. Incrível que você forneceu casos de teste!
pir
1
Se você quiser permitir quebras de linha, adicione LINE_BREAK_CHARACTERS = set(["\n", "\r"])e and not chr(i) in LINE_BREAK_CHARACTERSao construir a tabela.
pir
5

Esta função usa compreensões de lista e str.join, por isso é executado em tempo linear em vez de O (n ^ 2):

from curses.ascii import isprint

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

Mais uma opção em python 3:

re.sub(f'[^{re.escape(string.printable)}]', '', my_string)
c6401
fonte
Isso funcionou muito bem para mim e sua 1 linha. obrigado
Chop Labalagun
1
por algum motivo, isso funciona muito bem no windows, mas não consigo usá-lo no linux, eu tive que mudar o f por um r, mas não tenho certeza de que é a solução.
Chop Labalagun
Parece que o seu Linux Python era muito antigo para suportar strings F na época. cordas r são bastante diferentes, embora você possa dizer r'[^' + re.escape(string.printable) + r']'. (Eu não acho que re.escape()esteja totalmente correto aqui, mas se funcionar ...)
tripleee
2

O melhor que descobri agora é (graças aos python-izers acima)

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

Esta é a única maneira que descobri que funciona com caracteres / strings Unicode

Alguma opção melhor?

Vinko Vrsalovic
fonte
1
A menos que você esteja no python 2.3, os [] internos são redundantes. "return '' .join (c for c ...)"
habnabit
Não é totalmente redundante - eles têm significados diferentes (e características de desempenho), embora o resultado final seja o mesmo.
Milhas
A outra extremidade do intervalo também não deve ser protegida ?: "ord (c) <= 126"
Gearoid Murphy
7
Mas existem caracteres Unicode que também não podem ser impressos.
tripleee
2

O que está abaixo tem um desempenho mais rápido do que os outros acima. Dê uma olhada

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

Em Python não há classes POSIX regex

Existem ao usar a regexbiblioteca: https://pypi.org/project/regex/

É bem mantido e oferece suporte a Unicode regex, Posix regex e muitos mais. O uso (assinaturas de método) é muito semelhante ao do Python re.

Da documentação:

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

Classes de caracteres POSIX são suportadas. Estes são normalmente tratados como uma forma alternativa de\p{...} .

(Não sou afiliado, apenas um usuário.)

Risadinha
fonte
1

Com base na resposta de @Ber, sugiro remover apenas os caracteres de controle, conforme definido nas categorias do banco de dados 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ão escuro
fonte
Esta é uma ótima resposta!
tdc
0

Para remover 'espaços em branco',

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
fonte
Na verdade, você também não precisa dos colchetes.
tripleee
0

Adaptado das respostas de 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)

testado em Python 3.7.7

Joe
fonte