71 votes

Comment séparer mais ignorer les séparateurs dans les chaînes citées, en python ?

J'ai besoin de diviser une chaîne de caractères comme ceci, sur des points-virgules. Mais je ne veux pas couper sur les points-virgules qui sont à l'intérieur d'une chaîne (' ou "). Je n'analyse pas un fichier, mais une simple chaîne de caractères sans saut de ligne.

part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5

Le résultat devrait être :

  • partie 1
  • "c'est ; partie 2 ;"
  • "Ceci est ; partie 3
  • partie 4
  • ceci "est ; partie" 5

Je suppose que cela peut être fait avec une regex, mais sinon, je suis ouvert à une autre approche.

0 votes

Avez-vous d'autres exemples ? Ou y a-t-il d'autres types de "pièces" ?

0 votes

Je ne pense pas. Je veux diviser sur les points-virgules et ignorer les points-virgules à l'intérieur des guillemets. Je prendrais en considération toute solution qui ne fait pas exactement que ce n'est pas valable. Pouvez-vous penser à d'autres cas qui pourraient briser les solutions fournies jusqu'à présent ?

0 votes

Les guillemets peuvent-ils être échappés à l'intérieur d'une chaîne de caractères ? "this is a \"quoted\" string" ? Si c'est le cas, une solution de type regex va être extrêmement difficile, voire impossible.

54voto

Duncan Points 25356

La plupart des réponses semblent massivement trop compliquées. Vous Ne le fais pas. besoin de références arrière. Vous Ne le fais pas. doivent dépendre du fait que re.findall donne ou non des correspondances qui se chevauchent. Etant donné que l'entrée ne peut pas être analysée avec le module csv et qu'une expression régulière est donc la seule solution possible, tout ce dont vous avez besoin est d'appeler re.split avec un motif qui correspond à un champ.

Notez qu'il est beaucoup plus facile ici de faire correspondre un champ qu'un séparateur :

import re
data = """part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5"""
PATTERN = re.compile(r'''((?:[^;"']|"[^"]*"|'[^']*')+)''')
print PATTERN.split(data)[1::2]

et la sortie est :

['part 1', '"this is ; part 2;"', "'this is ; part 3'", 'part 4', 'this "is ; part" 5']

Comme le fait remarquer Jean-Luc Nacif Coelho, cela ne permet pas de gérer correctement les groupes vides. Selon la situation, cela peut être important ou non. Si c'est le cas, il peut être possible de le gérer, par exemple en remplaçant ';;' con ';<marker>;' donde <marker> doit être une chaîne de caractères (sans point-virgule) dont vous savez qu'elle n'apparaît pas dans les données avant le fractionnement. Vous devez également restaurer les données après :

>>> marker = ";!$%^&;"
>>> [r.replace(marker[1:-1],'') for r in PATTERN.split("aaa;;aaa;'b;;b'".replace(';;', marker))[1::2]]
['aaa', '', 'aaa', "'b;;b'"]

Toutefois, il s'agit d'une solution de fortune. Avez-vous de meilleures suggestions ?

0 votes

Oh, au fait, [^;"']+ serait meilleur que ([^;"']...)+ Je pense

0 votes

Je ne pense pas que [^;"']+ aide. Vous avez toujours besoin du + en dehors du groupe pour gérer quelque chose qui est un mélange de caractères ordinaires et d'éléments cités. Les éléments qui peuvent se répéter et qui contiennent eux-mêmes des répétitions sont un excellent moyen de tuer la correspondance des expressions régulières et doivent donc être évités autant que possible.

1 votes

Merci beaucoup. J'ai rencontré le même problème mais avec des espaces, j'ai donc remplacé le point-virgule par un espace et ça a marché parfaitement.

43voto

Alan Moore Points 39365
re.split(''';(?=(?:[^'"]|'[^']*'|"[^"]*")*$)''', data)

Chaque fois qu'il trouve un point-virgule, le lookahead parcourt toute la chaîne restante, en s'assurant qu'il y a un nombre pair de guillemets simples et un nombre pair de guillemets doubles. (Les apostrophes simples à l'intérieur de champs à apostrophes doubles, ou vice-versa, sont ignorées.) Si le lookahead réussit, le point-virgule est un délimiteur.

Contrairement à La solution de Duncan qui correspond aux champs plutôt qu'aux délimiteurs, celui-ci n'a aucun problème avec les champs vides. (Pas même le dernier : contrairement à beaucoup d'autres split Python ne rejette pas automatiquement les champs vides de fin de ligne).

0 votes

Merci Alan, j'ai failli manquer cette réponse. Il est similaire à celui de Duncan mais il coupe la corde de manière plus élégante. J'avais un problème similaire et cela a parfaitement fonctionné.

0 votes

Pour chaque ; cette solution exécutera un lookahead en s'assurant que les guillemets sont équilibrés après ce point-virgule (sinon ce point-virgule est cité et doit être omis). Ainsi, la complexité est O(n^2) (en supposant que le nombre de ; est en croissance linéaire avec la longueur de la chaîne).

0 votes

Merci Alan. Tu as sauvé ma journée :)

11voto

Paul McGuire Points 24790

Voici une version annotée pyparsage approche :

from pyparsing import (printables, originalTextFor, OneOrMore, 
    quotedString, Word, delimitedList)

# unquoted words can contain anything but a semicolon
printables_less_semicolon = printables.replace(';','')

# capture content between ';'s, and preserve original text
content = originalTextFor(
    OneOrMore(quotedString | Word(printables_less_semicolon)))

# process the string
print delimitedList(content, ';').parseString(test)

donner

['part 1', '"this is ; part 2;"', "'this is ; part 3'", 'part 4', 
 'this "is ; part" 5']

En utilisant l'outil de pyparsing fourni quotedString vous bénéficiez également d'un soutien pour les devis échappés.

Vous n'avez pas non plus précisé comment gérer les espaces avant ou après un délimiteur point-virgule, et aucun des champs de votre exemple de texte n'en comporte. Pyparsing analyserait "a ; b ; c" comme :

['a', 'b', 'c']

1 votes

+1 J'étais sur le point de poster une solution de pyparsing mais la vôtre est plus élégante.

1 votes

Cette réponse est extrêmement utile. En commençant ici, j'ai pu télécharger, installer et écrire un simple analyseur d'en-tête IMAP en 10 lignes. Merci !

0 votes

C'est génial ! Cependant, dans les cas où une valeur est vide (par exemple :[ ,23,43,38,75,26,19,37,43,19,27,25,20,34,22,23] ), j'obtiens pyparsing.ParseException : Expected {quotedString using single or double quotes | W :(0123...)} (at char 0), (line:1, col:1)

9voto

Simon Callan Points 1411

Vous semblez avoir une chaîne de caractères séparée par un point-virgule. Pourquoi ne pas utiliser le csv module pour faire tout le travail difficile ?

D'après ce que je sais, cela devrait fonctionner.

import csv 
from StringIO import StringIO 

line = '''part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5'''

data = StringIO(line) 
reader = csv.reader(data, delimiter=';') 
for row in reader: 
    print row 

Cela devrait vous donner quelque chose comme
("part 1", "this is ; part 2;", 'this is ; part 3', "part 4", "this \"is ; part\" 5")

Edit :
Malheureusement, cela ne fonctionne pas (même si vous utilisez StringIO, comme je l'avais prévu), à cause des guillemets mixtes (simples et doubles). Ce que vous obtenez réellement est

['part 1', 'this is ; part 2;', "'this is ", " part 3'", 'part 4', 'this "is ', ' part" 5'] .

Si vous pouvez modifier les données pour qu'elles ne contiennent que des guillemets simples ou doubles aux endroits appropriés, cela devrait fonctionner correctement, mais cela annule un peu la question.

1 votes

+1 : csv.reader prend un itérable, donc vous devez envelopper la chaîne d'entrée dans une liste : csv.reader([data], delimiter=';') . En dehors de cela, il fait exactement ce que l'utilisateur veut. Elle gère également les caractères entre guillemets préfixés par une barre oblique inversée.

1 votes

En fait, le module csv n'est pas si intelligent, il ne fonctionne pas quand je l'ai testé. ses données ont à la fois des guillemets simples et doubles, et le module csv ne peut pas les gérer. this "is ; part" 5 en un seul bloc, ce qui donne ['part 1', 'this is ; part 2;', "'this is ", " part 3'", 'part 4', 'this "is ', ' part" 5']

2 votes

Non seulement le module csv ne gère pas plus d'un type de citation, mais il insiste également pour que les champs soient entièrement cités ou ne le soient pas du tout. Cela signifie que la partie 5 sera scindée en deux parce qu'une double citation au milieu d'un champ est juste un littéral qui ne cite pas le contenu. Je crains que dans ce cas, les options soient (a) d'utiliser une expression régulière excessivement complexe, ou (b) de faire modifier le format des données d'entrée pour utiliser une variante reconnaissable du CSV. Si c'était moi, je choisirais l'option (b).

4voto

Amber Points 159296

Bien que cela puisse être fait avec PCRE via lookaheads/behinds/backreferences, ce n'est pas vraiment une tâche pour laquelle regex est conçu en raison de la nécessité de faire correspondre des paires équilibrées de guillemets.

Au lieu de cela, il est probablement préférable de créer une mini-machine à états et d'analyser la chaîne de caractères de cette manière.

Modifier

Il s'avère que, grâce à la fonctionnalité supplémentaire très pratique de Python re.findall qui garantit des correspondances qui ne se chevauchent pas, cela peut être plus simple à faire avec une regex en Python que cela pourrait l'être autrement. Voir les commentaires pour plus de détails.

Cependant, si vous êtes curieux de savoir à quoi pourrait ressembler une implémentation non regex :

x = """part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5"""

results = [[]]
quote = None
for c in x:
  if c == "'" or c == '"':
    if c == quote:
      quote = None
    elif quote == None:
      quote = c
  elif c == ';':
    if quote == None:
      results.append([])
      continue
  results[-1].append(c)

results = [''.join(x) for x in results]

# results = ['part 1', '"this is ; part 2;"', "'this is ; part 3'",
#            'part 4', 'this "is ; part" 5']

1 votes

La question n'exige pas du tout d'équilibrage, mais seulement l'enfermement et l'échappement d'un seul caractère. Il s'agit d'un modèle assez simple (et en fait formellement régulier).

0 votes

En fait, la seule raison findall fonctionne est dû à la restriction supplémentaire mise en œuvre dans Python selon laquelle les correspondances retournées doivent être sans chevauchement . Sinon, une chaîne comme '''part 1;"this 'is' sparta";part 2''' échouerait parce que le motif correspond aussi à un sous-ensemble de la chaîne.

0 votes

J'utilise findall car nous devons extraire la chaîne de caractères. Formellement, les expressions régulières ne font que de la mise en correspondance. Pour faire correspondre, nous pouvons simplement utiliser ^mypattern(;mypattern)*$ .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X