Как разделить, но игнорировать разделители в цитируемых строках, в python?



Мне нужно разбить строку, как это, на точки с запятой. Но я не хочу разбивать на точки с запятой, которые находятся внутри строки (' или "). Я не разбираю файл; просто простая строка без разрывов строк.



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



результат должен быть:




  • часть 1

  • "это ; часть 2;"

  • 'это ; часть 3'

  • часть 4

  • это"; часть" 5


Я полагаю, это можно сделать с помощью регулярного выражения но если нет, я открыт для другого подхода.

598   15  

15 ответов:

большинство ответов кажутся слишком сложными. Ты не нужны обратные ссылки. Ты не нужно зависеть от того, ре или нет.метод findAll дает перекрывающихся совпадений. Учитывая, что вход не может быть проанализирован с помощью модуля csv, поэтому регулярное выражение-это довольно хорошо, все, что вам нужно, это вызвать re.разделить с шаблоном, который соответствует полю.

обратите внимание, что это гораздо проще здесь, чтобы соответствовать полю, чем это, чтобы соответствовать разделитель:

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]

и выход:

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

Как правильно указывает Жан-Люк Насиф Коэльо, это не будет правильно обрабатывать пустые группы. В зависимости от ситуации это может иметь или не иметь значения. Если это имеет значение, можно справиться с этим, например, заменив ';;' С ';<marker>;' здесь <marker> должна быть какая-то строка (без точки с запятой), которая, как вы знаете, не появляется в данных до разделения. Также вам необходимо восстановить данные после:

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

однако это Клудж. Есть предложения получше?

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

каждый раз, когда он находит точку с запятой, lookahead сканирует всю оставшуюся строку, убедившись, что есть четное число одинарных кавычек и четное число двойных кавычек. (Одинарные кавычки внутри полей с двойными кавычками или наоборот игнорируются.) Если lookahead выполняется успешно, точка с запятой является разделителем.

в отличие от Дункан, который соответствует полям, а не разделителям, у этого нет проблем с пустыми полями. (Даже не последний одно: в отличие от многих других split реализации, Python не отбрасывает автоматически конечные пустые поля.)

вот аннотированный 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)

дав

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

С помощью, когда pyparsing предоставил quotedString, вы также получаете поддержку экранированных кавычек.

вам также было неясно, как обрабатывать ведущие пробелы до или после разделителя с запятой, и ни одно из ваших полей в вашем образце текста не имеет. Pyparsing будет анализировать "a; b; c" как:

['a', 'b', 'c']
>>> 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']

у вас, похоже,есть строка с запятой. Почему бы не использовать csv модуль, чтобы сделать всю тяжелую работу?

С верхней части моей головы, это должно работать

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 

это должно дать вам что-то вроде
("part 1", "this is ; part 2;", 'this is ; part 3', "part 4", "this \"is ; part\" 5")

Edit:
К сожалению, это не совсем работает (даже если вы используете StringIO, как я и предполагал), из-за смешанных строковых кавычек (как одиночных, так и двойных). Что вы на самом деле получаете это

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

если вы можете изменить данные, чтобы они содержали только одинарные или двойные кавычки в соответствующих местах, это должно работать нормально, но это немного отрицает вопрос.

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

хотя это может быть сделано с помощью PCRE через lookaheads/behinds/backreferences, на самом деле это не совсем задача, для которой предназначено регулярное выражение из-за необходимости соответствовать сбалансированным парам котировок.

вместо этого, вероятно, лучше всего просто сделать мини-машину состояния и проанализировать строку таким образом.

Edit

как оказалось, благодаря удобной дополнительной функции Python re.findall что гарантирует неперекрывающиеся совпадения, это может быть более простым для сделайте с регулярным выражением в Python, чем это могло бы быть в противном случае. См. комментарии для деталей.

однако, если вам интересно, как может выглядеть реализация без регулярных выражений:

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

мы можем создать собственную функцию

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

это регулярное выражение будет делать это: (?:^|;)("(?:[^"]+|"")*"|[^;]*)

поскольку у вас нет '\n' , используйте его для замены любого '; ' это не в строке кавычек

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

хотя я уверен, что есть чистое регулярное выражение (до сих пор мне нравится ответ @noiflection), вот быстрый и грязный ответ без регулярных выражений.

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

(Я никогда не собрали что-то в этом роде, не стесняйтесь критиковать мою форму!)

мой подход состоит в том, чтобы заменить все нецитируемые вхождения точки с запятой другим символом, который никогда не появится в тексте, а затем разделить на этот символ. Следующий код используется повторно.подфункция с аргументом функции для поиска и замены всех вхождений a srch строка, не заключенная в одинарные или двойные кавычки или парены, скобки или фигурные скобки, с 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

если вы не заботитесь о скобках символов, вы можете упростить этот кода много.
Скажем, вы хотите использовать трубу или вертикальную полосу в качестве заменяющего символа, вы бы сделали:

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

кстати, это использует nonlocal из Python 3.1, измените его на глобальный, если вам нужно.

обобщенное решение:

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

выходы:

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

такое решение:

  • захватывает все пустые группы (в том числе в начале и в конце)
  • работает для большинства популярных разделителей, включая пробел, вкладку и запятая
  • обрабатывает кавычки внутри кавычек другого типа как не специальные символы
  • если встречается несопоставимая некотируемая цитата, остатки строки обрабатываются как цитируется

хотя тема старая и предыдущие ответы работают хорошо, я предлагаю свою собственную реализацию функции split в python.

Это прекрасно работает, если вам не нужно обрабатывать большое количество строк и легко настраивается.

вот моя функция:

# 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

Так что вы можете запускать:

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

результат:

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

преимущество в том, что эта функция работает с пустыми полями и с любым количеством разделителей в строка.

надеюсь, что это помогает!

это показалось мне полу-элегантным решением.

Новое Решение:

import re
reg = re.compile('(\'|").*?\1')
pp = re.compile('.*?;')
def splitter(string):
    #add a last semicolon
    string += ';'
    replaces = []
    s = string
    i = 1
    #replace the content of each quote for a code
    for quote in reg.finditer(string):
        out = string[quote.start():quote.end()]
        s = s.replace(out, '**' + str(i) + '**')
        replaces.append(out)
        i+=1
    #split the string without quotes
    res = pp.findall(s)

    #add the quotes again
    #TODO this part could be faster.
    #(lineal instead of quadratic)
    i = 1
    for replace in replaces:
        for x in range(len(res)):
            res[x] = res[x].replace('**' + str(i) + '**', replace)
        i+=1
    return res

старое решение:

Я выбираю совпадение, если была начальная цитата, и жду ее закрытия, а совпадение-конечная точка с запятой. каждая "часть", которую вы хотите сопоставить, должна заканчиваться точкой с запятой. так что этот матч вещи, как это:

  • ' foobar;.сска';
  • " akjshd; asjkdhkj..,";
  • asdkjhakjhajsd.jhdf;

код:

mm = re.compile('''((?P<quote>'|")?.*?(?(quote)\2|);)''')
res = mm.findall('''part 1;"this is ; part 2;";'this is ; part 3';part 4''')

возможно, вам придется выполнить некоторую постобработку для res, но она содержит то, что вы хотите.

Comments

    Ничего не найдено.