wake-up-neo.com

Wie kann ich Trennzeichen in zitierten Zeichenfolgen in Python teilen, aber ignorieren?

Ich muss einen String auf Semikolons teilen. Ich möchte jedoch nicht Semikolons trennen, die sich innerhalb einer Zeichenfolge befinden ('oder "). Ich analysiere keine Datei, sondern eine einfache Zeichenfolge ohne Zeilenumbrüche.

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

Ergebnis sollte sein:

  • teil 1
  • "das ist; Teil 2;"
  • 'das ist ; Teil 3 '
  • teil 4
  • dies "ist; Teil" 5

Ich nehme an, dass dies mit einer Regex gemacht werden kann, wenn nicht; Ich bin offen für einen anderen Ansatz.

58
Sylvain

Die meisten Antworten scheinen massiv zu kompliziert. Sie benötigen nicht Rückverweise. Sie müssen nicht davon abhängen, ob re.findall überlappende Übereinstimmungen liefert. Da die Eingabe nicht mit dem csv-Modul analysiert werden kann, ist ein regulärer Ausdruck der einzige Weg, um dies zu erreichen. Sie müssen lediglich re.split mit einem Muster aufrufen, das einem Feld entspricht.

Beachten Sie, dass es hier viel einfacher ist, ein Feld als ein Trennzeichen zuzuordnen:

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]

und die Ausgabe ist:

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

Wie Jean-Luc Nacif Coelho richtig betont, werden leere Gruppen nicht richtig behandelt. Abhängig von der Situation, die möglicherweise oder möglicherweise nicht von Bedeutung ist. Wenn es darauf ankommt, kann dies beispielsweise dadurch geschehen, dass ';;' durch ';<marker>;' ersetzt wird, wobei <marker> eine Zeichenfolge (ohne Semikolon) sein muss, von der Sie wissen, dass sie nicht angezeigt wird in den Daten vor dem Split. Außerdem müssen Sie die Daten wiederherstellen, nachdem:

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

Dies ist jedoch ein Trick. Bessere Vorschläge?

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

Jedes Mal, wenn ein Semikolon gefunden wird, durchsucht der Lookahead die gesamte verbleibende Zeichenfolge, um sicherzustellen, dass es eine gerade Anzahl von Anführungszeichen und eine gerade Anzahl von Anführungszeichen gibt. (Einfache Anführungszeichen in Feldern in Anführungszeichen oder umgekehrt werden ignoriert.) Wenn der Lookahead erfolgreich ist, ist das Semikolon ein Trennzeichen.

Im Gegensatz zu Duncans Lösung , die eher zu den Feldern als zu den Trennzeichen passt, hat dieses keine Probleme mit leeren Feldern. (Nicht einmal die letzte: Im Gegensatz zu vielen anderen split-Implementierungen löscht Python leere Felder nicht automatisch.)

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']

Hier ist ein kommentierter Pyparsing Ansatz:

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)

geben

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

Durch die Verwendung von quotedString von pyparsing erhalten Sie auch Unterstützung für Escape-Anführungszeichen.

Sie waren auch unklar, wie mit führenden Leerzeichen vor oder nach einem Trennzeichen für Semikolons umgegangen werden soll, und keines Ihrer Felder in Ihrem Beispieltext enthält eines. Pyparsing würde "a; b; c" wie folgt analysieren:

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

Sie scheinen eine durch Semikolon getrennte Zeichenfolge zu haben. Warum nicht das Modul csv verwenden, um all die harte Arbeit zu erledigen?

Ganz oben auf meinem Kopf sollte dies funktionieren

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 

Das sollte Ihnen etwas geben
("part 1", "this is ; part 2;", 'this is ; part 3', "part 4", "this \"is ; part\" 5")

Bearbeiten:
Leider funktioniert das nicht ganz (auch wenn Sie StringIO wie gewünscht verwenden), aufgrund der gemischten String-Anführungszeichen (sowohl single als auch double). Was Sie tatsächlich bekommen, ist

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

Wenn Sie die Daten so ändern können, dass sie an den entsprechenden Stellen nur einfache oder doppelte Anführungszeichen enthalten, sollte dies einwandfrei funktionieren, was die Frage jedoch etwas negiert.

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

Während dies mit PCRE über Lookaheads/Behinds/Backreferences möglich ist, ist es nicht wirklich eine Aufgabe, für die regex entwickelt wurde, da ausgeglichene Anführungszeichenpaare erforderlich sind.

Stattdessen ist es wahrscheinlich am besten, eine Mini-Statusmaschine zu erstellen und die Zeichenfolge so zu analysieren.

Bearbeiten

Es stellt sich heraus, dass aufgrund der praktischen zusätzlichen Funktion von Python re.findall, die nicht überlappende Übereinstimmungen garantiert, dies mit einem regulären Ausdruck in Python einfacher zu tun ist, als es sonst der Fall wäre. Siehe Kommentare für Details.

Wenn Sie wissen möchten, wie eine Implementierung ohne Regex aussehen könnte:

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

wir können eine eigene Funktion erstellen

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

Diese Regex führt das aus: (?:^|;)("(?:[^"]+|"")*"|[^;]*)

1
dawg

da Sie nicht über\n verfügen, ersetzen Sie jedes beliebige;; das ist nicht in einer Anführungszeichenfolge

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

Anstatt ein Trennmuster aufzuteilen, erfassen Sie einfach das, was Sie brauchen:

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

Mein Ansatz ist es, alle nicht zitierten Vorkommen des Semikolons durch ein anderes Zeichen zu ersetzen, das niemals im Text erscheinen wird, und dann auf dieses Zeichen aufgeteilt. Der folgende Code verwendet die Funktion re.sub mit einem Funktionsargument zum Suchen und Ersetzen aller Vorkommen einer srch-Zeichenfolge, die nicht in einfache oder doppelte Anführungszeichen oder Parens, Klammern oder Klammern eingeschlossen ist, durch eine repl-Zeichenfolge: 

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

Wenn Sie sich nicht für die eingeklammerten Zeichen interessieren, können Sie diesen Code erheblich vereinfachen.
Angenommen, Sie wollten eine Pipe oder einen vertikalen Balken als Ersatzzeichen verwenden, würden Sie Folgendes tun: 

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

Übrigens, dies verwendet nonlocal aus Python 3.1, wenn nötig, ändern Sie es in global. 

0
Don O'Donnell

Auch wenn ich sicher bin, dass es eine saubere Regex-Lösung gibt (bisher gefällt mir die Antwort von @ noiflection), gibt es hier eine schnelle und schmutzige Antwort, die kein Regex ist.

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']

(Ich habe noch nie so etwas zusammengestellt, ich kann meine Form nicht kritisieren!)

0
Ipsquiggle

Obwohl das Thema alt ist und die bisherigen Antworten gut funktionieren, schlage ich meine eigene Implementierung der Split-Funktion in Python vor.

Dies funktioniert gut, wenn Sie keine große Anzahl von Strings verarbeiten müssen und leicht angepasst werden können.

Hier ist meine Funktion:

# 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

So kannst du laufen:

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

ergebnis:

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

Der Vorteil ist, dass diese Funktion mit leeren Feldern und mit einer beliebigen Anzahl von Trennzeichen in der Zeichenfolge arbeitet.

Hoffe das hilft!

0
Florian Luciano

Eine verallgemeinerte Lösung:

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

Ausgänge:

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

Diese Lösung:

  • erfasst alle leeren Gruppen (einschließlich am Anfang und am Ende) 
  • funktioniert für die gängigsten Trennzeichen, einschließlich Leerzeichen, Tabulatorzeichen und Komma
  • behandelt Anführungszeichen in Anführungszeichen des anderen Typs als nicht-Sonderzeichen
  • wenn ein nicht übereinstimmendes, nicht in Anführungszeichen angegebenes Zitat gefunden wird, werden die Reste der Zeile als Zitate behandelt
0
Roman