Как разделить, но проигнорировать разделители в заключенных в кавычки строках в Python?

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

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

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

  • часть 1
  • "это; часть 2";
  • 'это; часть 3'
  • часть 4
  • это "; часть" 5

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

63
задан Sylvain 9 February 2012 в 01:03
поделиться

11 ответов

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

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

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

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

50
ответ дан 24 November 2019 в 16:18
поделиться

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

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

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; .sska';
  • "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, но он содержит то, что вы хотите.

-1
ответ дан 24 November 2019 в 16:18
поделиться

Поскольку у вас нет '\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']
1
ответ дан 24 November 2019 в 16:18
поделиться

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

BTW, здесь используется nonlocal из Python 3.1, измените его на global, если нужно.

0
ответ дан 24 November 2019 в 16:18
поделиться

Кажется, у вас есть строка, разделенная точкой с запятой. Почему бы не использовать модуль 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 

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

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

['часть 1', 'это; part 2; ', "' this is", "part 3 '",' part 4 ',' this "is ',' part" 5 '] .

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

9
ответ дан 24 November 2019 в 16:18
поделиться

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

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

Редактировать

Как оказалось, благодаря удобной дополнительной функции Python re.findall, которая гарантирует непересекающиеся совпадения, это может быть более просто сделать с помощью regex в 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']
3
ответ дан 24 November 2019 в 16:18
поделиться

Этот regex сделает это: (?:^|;)("(?:[^"]+|"")*"|[^;]*)

1
ответ дан 24 November 2019 в 16:18
поделиться
re.split(''';(?=(?:[^'"]|'[^']*'|"[^"]*")*$)''', data)

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

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

32
ответ дан 24 November 2019 в 16:18
поделиться

Вот аннотированный 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']
11
ответ дан 24 November 2019 в 16:18
поделиться
>>> 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
ответ дан 24 November 2019 в 16:18
поделиться

Хотя я уверен есть чистое решение с регулярным выражением (пока мне нравится ответ @ 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']

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

0
ответ дан 24 November 2019 в 16:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: