如何拆分但忽略引号字符串中的分隔符,在 python 中?

我需要像这样在分号上分割字符串。但是我不想在字符串(’或“)中的分号上拆分。我没有解析文件; 只是一个没有换行符的简单字符串。

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

结果应该是:

  • 第一部分
  • “这是; 第二部分;”
  • 这是,第三部分
  • 第四部分
  • 这“是; 部分”5

我想这可以用正则表达式来完成,但是如果不能,我可以采用另一种方法。

72193 次浏览

这个正则表达式将做到这一点: (?:^|;)("(?:[^"]+|"")*"|[^;]*)

虽然可以通过 lookahead/behind/backreference 使用 PCRE 完成,但是由于需要匹配平衡的引号对,regex 实际上并不是为此设计的。

相反,最好只是创建一个迷你状态机,然后像这样解析字符串。

剪辑

事实证明,由于 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']
>>> 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']

在我看来,这似乎是一个半优雅的解决方案。

新解决方案:

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

你可能需要做一些后处理,但它包含了你想要的。

尽管我确信有一个干净的正则表达式解决方案(到目前为止,我喜欢@noiflections 的答案) ,但是这里有一个简单的非正则表达式答案。

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

(我从来没有把这样的东西放在一起,请随意批评我的形式!)

您似乎有一个分号分隔的字符串。为什么不使用 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")

编辑:
不幸的是,由于混合字符串引号(单引号和双引号) ,这种方法并不能很好地工作(即使按照我的意图使用 StringIO)。你真正得到的是

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

如果您可以将数据更改为在适当的位置只包含单引号或双引号,那么它应该可以正常工作,但这有点否定了这个问题。

我的方法是用另一个永远不会出现在文本中的字符替换分号中所有未引号的出现,然后在该字符上进行分割。下面的代码使用 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('|')

顺便说一句,这里使用了 Python 3.1中的 nonlocal,如果需要,可以将其改为 global。

大多数答案似乎都过于复杂。你需要背景资料。不要需要依赖于 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']

正如让-吕克•纳西夫•科埃略(Jean-Luc Nacif Coelho)正确指出的那样,这种方法无法正确处理空组。根据情况而定,可能是重要的,也可能不重要。如果确实重要,可以通过以下方式来处理它,例如,用 ';<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)

每当它找到一个分号,前瞻扫描整个剩余的字符串,确保有偶数个单引号和偶数个双引号。(双引号字段中的单引号或反之亦然,将被忽略。)如果前瞻成功,则分号为分隔符。

与匹配字段而不是分隔符的 邓肯的解决方案不同,这个函数对空字段没有问题。(甚至不是最后一个: 与其他许多 split实现不同,Python 不会自动丢弃尾随的空字段。)

下面是一个带注释的 胡言乱语方法:

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

因为没有 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']
>>> 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']

一个通用的解决方案:

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

解决办法:

  • 捕获所有的空组(包括开头和结尾)
  • 适用于大多数流行的分隔符,包括空格、制表符和 逗号
  • 将其他类型的引号中的引号视为非特殊字符
  • 如果遇到无匹配的无引号引号,则将该行的其余部分视为引号引号

我们可以创建一个自己的函数

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

尽管这个主题已经过时了,而且以前的答案都很好,但是我还是提出了自己在 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
>>> 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']

最简单的方法是使用 shlex (简单词法分析)—— Python 中的一个内置模块

import shlex
shlex.split("""part 1;"this is ; part 2;";'this is ; part 3';part 4;this "is ; part" 5 """ )


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