web-dev-qa-db-fra.com

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

J'ai besoin de scinder une chaîne comme celle-ci, en point-virgule. Mais je ne veux pas diviser les points-virgules à l'intérieur d'une chaîne ('ou "). Je ne suis pas en train d'analyser un fichier, mais une simple chaîne 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
  • "ceci est; partie 2;"
  • 'c'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.

58
Sylvain

La plupart des réponses semblent excessivement compliquées. Vous n'avez pas besoin de références en arrière. Vous ne pas devez 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, une expression régulière est le moyen le plus pratique, il vous suffit d'appeler re.split avec un modèle correspondant à un champ.

Notez qu'il est beaucoup plus facile de faire correspondre un champ à 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 le résultat est:

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

Comme le souligne correctement Jean-Luc Nacif Coelho, cela ne gérera pas correctement les groupes vides. Selon la situation, cela peut être important ou non. Par exemple, en remplaçant ';;' par ';<marker>;', où <marker> doit obligatoirement être constitué d'une chaîne (sans point-virgule) qui, à votre connaissance, ne figure pas dans les données avant la division. Aussi, vous devez 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'"]

Cependant c'est un kludge. De meilleures suggestions?

49
Duncan
re.split(''';(?=(?:[^'"]|'[^']*'|"[^"]*")*$)''', data)

Chaque fois qu'il trouve un point-virgule, le préfixe analyse l'intégralité de la chaîne restante en s'assurant qu'il existe un nombre pair de guillemets simples et un nombre pair de guillemets doubles. (Les guillemets simples dans les champs à guillemets doubles, ou vice-versa, sont ignorés.) Si la recherche anticipée aboutit, le point-virgule est un délimiteur.

Contrairement à la solution de Duncan , qui correspond aux champs plutôt qu'aux délimiteurs, celle-ci ne présente aucun problème avec les champs vides. (Même pas le dernier: contrairement à beaucoup d'autres implémentations split, celles de Python ne suppriment pas automatiquement les champs vides qui se trouvent en fin de liste.)

25
Alan Moore
>>> a='A,"B,C",D'
>>> a.split(',')
['A', '"B', 'C"', 'D']

It failed. Now try csv module
>>> import csv
>>> from StringIO import StringIO
>>> data = StringIO(a)
>>> data
<StringIO.StringIO instance at 0x107eaa368>
>>> reader = csv.reader(data, delimiter=',') 
>>> for row in reader: print row
... 
['A,"B,C",D']
14

Voici une approche annotée pyparsing :

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)

donnant

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

En utilisant la variable quotedString fournie par pyparsing, vous bénéficiez également d'une assistance pour les citations échappées.

Vous ne saviez pas non plus comment gérer les espaces avant et après un séparateur de point-virgule et aucun de vos champs de votre exemple de texte n'en contient. Pyparsing analyserait "a; b; c" comme suit:

['a', 'b', 'c']
11
PaulMcG

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

De mémoire, cela devrait marcher

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

Modifier:
Malheureusement, cela ne fonctionne pas vraiment (même si vous utilisez StringIO, comme je le souhaitais), en raison des guillemets de chaînes 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 nie en quelque sorte la question.

9
Simon Callan
>>> x = '''part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5'''
>>> import re
>>> re.findall(r'''(?:[^;'"]+|'(?:[^']|\\.)*'|"(?:[^']|\\.)*")+''', x)
['part 1', "this is ';' part 2", "'this is ; part 3'", 'part 4', 'this "is ; part" 5']
3
Max Shawabkeh

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çue en raison de la nécessité de faire correspondre des paires de guillemets équilibrés.

Au lieu de cela, il est probablement préférable de créer une mini machine à états et d’analyser la chaîne de cette façon.

Modifier

En fin de compte, en raison de la fonctionnalité supplémentaire très pratique de Python re.findall qui garantit des correspondances ne se chevauchant pas, cela peut être plus simple à faire avec une expression rationnelle en Python. 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']
3
Amber

nous pouvons créer une fonction qui lui est propre

def split_with_commas_outside_of_quotes(string):
    arr = []
    start, flag = 0, False
    for pos, x in enumerate(string):
        if x == '"':
            flag= not(flag)
        if flag == False and x == ',':
            arr.append(string[start:pos])
            start = pos+1
    arr.append(string[start:pos])
    return arr
2
Pradeep Pathak

Cette regex fera cela: (?:^|;)("(?:[^"]+|"")*"|[^;]*)

1
dawg

puisque vous n'avez pas '\ n', utilisez-le pour remplacer n'importe quel ';' ce n'est pas dans une chaîne de guillemets

>>> new_s = ''
>>> is_open = False

>>> for c in s:
...     if c == ';' and not is_open:
...         c = '\n'
...     Elif c in ('"',"'"):
...         is_open = not is_open
...     new_s += c

>>> result = new_s.split('\n')

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

Au lieu de séparer un motif de séparation, capturez simplement ce dont vous avez besoin:

>>> import re
>>> data = '''part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5'''
>>> re.findall(r';([\'"][^\'"]+[\'"]|[^;]+)', ';' + data)
['part 1', '"this is ; part 2;"', "'this is ; part 3'", 'part 4', 'this "is ', ' part" 5']
0
michael

Mon approche consiste à remplacer toutes les occurrences non-citées du point-virgule par un autre caractère qui n'apparaîtra jamais dans le texte, puis sera divisé sur ce caractère. Le code suivant utilise la fonction re.sub avec un argument de fonction pour rechercher et remplacer toutes les occurrences d'une chaîne srch, non placées entre guillemets simples ou doubles ni entre parenthèses, crochets ou accolades, avec une chaîne repl 

def srchrepl(srch, repl, string):
    """
    Replace non-bracketed/quoted occurrences of srch with repl in string.
    """
    resrchrepl = re.compile(r"""(?P<lbrkt>[([{])|(?P<quote>['"])|(?P<sep>["""
                          + srch + """])|(?P<rbrkt>[)\]}])""")
    return resrchrepl.sub(_subfact(repl), string)


def _subfact(repl):
    """
    Replacement function factory for regex sub method in srchrepl.
    """
    level = 0
    qtflags = 0
    def subf(mo):
        nonlocal level, qtflags
        sepfound = mo.group('sep')
        if  sepfound:
            if level == 0 and qtflags == 0:
                return repl
            else:
                return mo.group(0)
        Elif mo.group('lbrkt'):
            if qtflags == 0:
                level += 1
            return mo.group(0)
        Elif mo.group('quote') == "'":
            qtflags ^= 1            # toggle bit 1
            return "'"
        Elif mo.group('quote') == '"':
            qtflags ^= 2            # toggle bit 2
            return '"'
        Elif mo.group('rbrkt'):
            if qtflags == 0:
                level -= 1
            return mo.group(0)
    return subf

Si vous ne vous souciez pas des caractères entre crochets, vous pouvez beaucoup simplifier ce code.
Supposons que vous souhaitiez utiliser un tuyau ou une barre verticale comme caractère de substitution, vous feriez: 

mylist = srchrepl(';', '|', mytext).split('|')

BTW, cela utilise nonlocal de Python 3.1, changez-le en global si vous en avez besoin. 

0
Don O'Donnell

Bien que je sois certain qu'il existe une solution de regex propre (jusqu'à présent, j'aime bien la réponse de @ noiflection), voici une réponse rapide et déformée.

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

inQuotes = False
current = ""
results = []
currentQuote = ""
for c in s:
    if not inQuotes and c == ";":
        results.append(current)
        current = ""
    Elif not inQuotes and (c == '"' or c == "'"):
        currentQuote = c
        inQuotes = True
    Elif inQuotes and c == currentQuote:
        currentQuote = ""
        inQuotes = False
    else:
        current += c

results.append(current)

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

(Je n'ai jamais mis en place quelque chose de ce genre, n'hésitez pas à critiquer ma forme!)

0
Ipsquiggle

Bien que le sujet soit ancien et que les réponses précédentes fonctionnent bien, je propose ma propre implémentation de la fonction split en python.

Cela fonctionne bien si vous n'avez pas besoin de traiter un grand nombre de chaînes et est facilement personnalisable.

Voici ma fonction:

# l is string to parse; 
# splitchar is the separator
# ignore char is the char between which you don't want to split

def splitstring(l, splitchar, ignorechar): 
    result = []
    string = ""
    ignore = False
    for c in l:
        if c == ignorechar:
            ignore = True if ignore == False else False
        Elif c == splitchar and not ignore:
            result.append(string)
            string = ""
        else:
            string += c
    return result

Pour que vous puissiez courir:

line= """part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5"""
splitted_data = splitstring(line, ';', '"')

résultat:

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

L'avantage est que cette fonction fonctionne avec des champs vides et avec un nombre quelconque de séparateurs dans la chaîne.

J'espère que cela t'aides!

0
Florian Luciano

Une solution généralisée:

import re
regex = '''(?:(?:[^{0}"']|"[^"]*(?:"|$)|'[^']*(?:'|$))+|(?={0}{0})|(?={0}$)|(?=^{0}))'''

delimiter = ';'
data2 = ''';field 1;"field 2";;'field;4';;;field';'7;'''
field = re.compile(regex.format(delimiter))
print(field.findall(data2))

Les sorties:

['', 'field 1', '"field 2"', '', "'field;4'", '', '', "field';'7", '']

Cette solution:

  • capture tous les groupes vides (y compris au début et à la fin) 
  • fonctionne pour les délimiteurs les plus courants, y compris l'espace, la tabulation et la virgule
  • traite les guillemets entre guillemets de l'autre type comme des caractères non spéciaux
  • si une citation non citée sans correspondance est rencontrée, traite les restes de la ligne comme cités
0
Roman