Suppression des caractères non imprimables d'une chaîne en python

88

J'ai l'habitude de courir

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

sur Perl pour se débarrasser des caractères non imprimables.

En Python, il n'y a pas de classes regex POSIX, et je ne peux pas écrire [: print:] pour que cela signifie ce que je veux. Je ne connais aucun moyen en Python de détecter si un caractère est imprimable ou non.

Qu'est-ce que tu ferais?

EDIT: Il doit également prendre en charge les caractères Unicode. La méthode string.printable les supprimera volontiers de la sortie. curses.ascii.isprint retournera false pour tout caractère Unicode.

Vinko Vrsalovic
la source

Réponses:

83

L'itération sur des chaînes est malheureusement assez lente en Python. Les expressions régulières sont plus rapides d'un ordre de grandeur pour ce genre de chose. Il vous suffit de créer vous-même la classe de personnage. Le module unicodedata est très utile pour cela, en particulier la fonction unicodedata.category () . Voir Base de données de caractères Unicode pour une description des catégories.

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)

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

Pour certains cas d'utilisation, des catégories supplémentaires (par exemple, toutes du groupe témoin peuvent être préférables, bien que cela puisse ralentir le temps de traitement et augmenter considérablement l'utilisation de la mémoire. Nombre de caractères par catégorie:

  • Cc (contrôle): 65
  • Cf (format): 161
  • Cs (substitut): 2048
  • Co (à usage privé): 137468
  • Cn (non attribué): 836601

Modifier Ajout de suggestions à partir des commentaires.

Fourmis Aasma
la source
4
'Cc' est-il suffisant ici? Je ne sais pas, je demande simplement - il me semble que certaines des autres catégories «C» peuvent également être candidates pour ce filtre.
Patrick Johnmeyer
1
Cette fonction, telle que publiée, supprime la moitié des caractères hébreux. J'obtiens le même effet pour les deux méthodes données.
dotancohen
1
Du point de vue des performances, string.translate () ne fonctionnerait-il pas plus rapidement dans ce cas? Voir stackoverflow.com/questions/265960/…
Kashyap
3
Utilisez all_chars = (unichr(i) for i in xrange(sys.maxunicode))pour éviter l'erreur de construction étroite.
danmichaelo
4
Pour moi control_chars == '\x00-\x1f\x7f-\x9f'(testé sur Python 3.5.2)
AXO
72

Autant que je sache, la méthode la plus pythonique / efficace serait:

import string

filtered_string = filter(lambda x: x in string.printable, myStr)
William Keller
la source
10
Vous voulez probablement filtered_string = '' .join (filter (lambda x: x dans string.printable, myStr) pour que vous récupériez une chaîne.
Nathan Shively-Sanders
12
Malheureusement, string.printable ne contient pas de caractères Unicode, et donc ü ou ó ne sera pas dans la sortie ... peut-être qu'il y a autre chose?
Vinko Vrsalovic
17
Vous devez utiliser une compréhension de liste ou des expressions génératrices, et non filter + lambda. L'un d'eux sera 99,9% du temps plus rapide. '' .join (s pour s dans myStr si s dans string.printable)
habnabit
3
@AaronGallagher: 99,9% plus rapide? D'où tirez-vous ce chiffre? La comparaison des performances est loin d'être aussi mauvaise.
Chris Morgan
4
Salut William. Cette méthode semble supprimer tous les caractères non ASCII. Il existe de nombreux caractères non ASCII imprimables en Unicode!
dotancohen
17

Vous pouvez essayer de configurer un filtre en utilisant la unicodedata.category()fonction:

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

Reportez-vous au Tableau 4-9 à la page 175 dans les propriétés des caractères de la base de données Unicode pour les catégories disponibles

Ber
la source
vous avez commencé une compréhension de liste qui ne s'est pas terminée dans votre dernière ligne. Je vous suggère de retirer complètement le support d'ouverture.
tzot le
Merci de l'avoir signalé. J'ai modifié le message en conséquence
Ber
1
Cela semble la méthode la plus directe et la plus simple. Merci.
dotancohen
1
@CsabaToth Les trois sont valides et donnent le même ensemble. Le vôtre est peut-être le meilleur moyen de spécifier un ensemble littéral.
Ber
1
@AnubhavJhalani Vous pouvez ajouter d'autres catégories Unicode au filtre. Pour réserver des espaces et des chiffres en plus des lettres, utilisezprintable = {'Lu', 'Ll', Zs', 'Nd'}
Ber
10

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

Voir cet article de StackOverflow sur la suppression de la ponctuation pour savoir comment .translate () se compare à regex et .replace ()

Les plages peuvent être générées en nonprintable = (ord(c) for c in (chr(i) for i in range(sys.maxunicode)) if unicodedata.category(c)=='Cc')utilisant les catégories de base de données de caractères Unicode comme indiqué par @Ants Aasma.

Shawnrad
la source
Il serait préférable d'utiliser des gammes Unicode (voir la réponse de @Ants Aasma). Le résultat serait text.translate({c:None for c in itertools.chain(range(0x00,0x20),range(0x7f,0xa0))}).
darkdragon
8

Ce qui suit fonctionnera avec l'entrée Unicode et est plutôt rapide ...

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

Mes propres tests suggèrent que cette approche est plus rapide que les fonctions qui itèrent sur la chaîne et retournent un résultat en utilisant str.join.

ChrisP
la source
C'est la seule réponse qui fonctionne pour moi avec des caractères unicode. Génial que vous ayez fourni des cas de test!
pir
1
Si vous souhaitez autoriser les sauts de ligne, ajoutez LINE_BREAK_CHARACTERS = set(["\n", "\r"])et and not chr(i) in LINE_BREAK_CHARACTERSlors de la création du tableau.
pir
5

Cette fonction utilise des compréhensions de liste et str.join, donc elle s'exécute en temps linéaire au lieu de O (n ^ 2):

from curses.ascii import isprint

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

Encore une autre option dans python 3:

re.sub(f'[^{re.escape(string.printable)}]', '', my_string)
c6401
la source
Cela a très bien fonctionné pour moi et sa 1 ligne. merci
Chop Labalagun
1
pour une raison quelconque, cela fonctionne très bien sur Windows mais je ne peux pas l'utiliser sous Linux, j'ai dû changer le f pour un r mais je ne suis pas sûr que ce soit la solution.
Chop Labalagun
On dirait que votre Python Linux était trop ancien pour prendre en charge les chaînes f à l'époque. Les r-strings sont assez différents, bien que l'on puisse dire r'[^' + re.escape(string.printable) + r']'. (Je ne pense pas que ce re.escape()soit tout à fait correct ici, mais si cela fonctionne ...)
tripleee
2

Le meilleur que j'ai trouvé maintenant est (grâce aux python-izers ci-dessus)

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

C'est le seul moyen que j'ai découvert qui fonctionne avec les caractères / chaînes Unicode

Y a-t-il de meilleures options?

Vinko Vrsalovic
la source
1
Sauf si vous êtes sur python 2.3, les [] internes sont redondants. "return '' .join (c for c ...)"
habnabit
Pas tout à fait redondants - ils ont des significations (et des caractéristiques de performance) différentes, bien que le résultat final soit le même.
Miles le
Est-ce que l'autre extrémité de la plage ne devrait pas être protégée aussi?: "Ord (c) <= 126"
Gearoid Murphy
7
Mais il existe également des caractères Unicode qui ne sont pas imprimables.
tripleee
2

Celui ci-dessous fonctionne plus rapidement que les autres ci-dessus. Regarde

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

En Python, il n'y a pas de classes regex POSIX

Il y en a lors de l'utilisation de la regexbibliothèque: https://pypi.org/project/regex/

Il est bien entretenu et prend en charge les expressions régulières Unicode, Posix regex et bien d'autres. L'utilisation (signatures de méthode) est très similaire à celle de Python re.

De la documentation:

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

Les classes de caractères POSIX sont prises en charge. Ceux-ci sont normalement traités comme une forme alternative de \p{...}.

(Je ne suis pas affilié, juste un utilisateur.)

Risadinha
la source
1

Sur la base de la réponse de @ Ber, je suggère de supprimer uniquement les caractères de contrôle tels que définis dans les catégories de la base de données de caractères Unicode :

import unicodedata
def filter_non_printable(s):
    return ''.join(c for c in s if not unicodedata.category(c).startswith('C'))
Dragon noir
la source
C'est une excellente réponse!
tdc le
0

Pour supprimer les «espaces»,

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))
sachant parc
la source
En fait, vous n'avez pas non plus besoin des crochets.
tripleee
0

Adapté des réponses d' Ants Aasma et 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)

testé sur Python 3.7.7

Joe
la source