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:
Je suppose que cela peut être fait avec une regex mais sinon; Je suis ouvert à une autre approche.
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?
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.)
>>> 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']
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']
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.
>>> 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']
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.
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']
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
Cette regex fera cela: (?:^|;)("(?:[^"]+|"")*"|[^;]*)
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']
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']
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.
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!)
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!
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: