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


67

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

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

结果应为:

  • 第1部分
  • “这是;第2部分;”
  • '这是 ; 第三部分
  • 第4部分
  • 这是“一部分” 5

我想这可以用正则表达式来完成,如果不能的话;我愿意接受另一种方法。


你还有更多的例子吗?还是有更多的“零件”?
msemelman 2010年

我不这么认为。我想分割分号,而忽略引号内的分号。我认为,没有做任何的解决方案恰好是为无效。您能想到其他可能破坏目前提供的解决方案的情况吗?
西尔万

引号可以出现在字符串内吗?例如"this is a \"quoted\" string"?如果是这样,那么正则表达式解决方案将非常困难甚至不可能。
戴夫·柯比

没有; 我不必支持这种情况。
西尔万

示例输出的第二行缺少分号。在下面的答案中是正确的。应该是:"this is ; part 2;"
Harvey

Answers:


52

大多数答案似乎过于复杂。你并不需要反向引用。您并不需要依赖于是否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>;'where <marker>。另外,您还需要在以下时间恢复数据:

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

但是,这太过分了。还有更好的建议吗?


哦,顺便说一句,[^;"']+([^;"']...)+我想象的要好
您2010年

我认为这没有[^;"']+帮助。您仍然需要组外的+来处理由普通字符和带引号的元素混合而成的内容。可以重复并且本身包含重复的元素是杀死正则表达式匹配的好方法,因此应尽可能避免。
邓肯2010年

1
非常感谢-我遇到了相同的问题,但是使用了空格,因此我只用分号代替了空格,并且效果很好。
ds1848 2013年

这不匹配aaa;;aaa
Jean-Luc Nacif Coelho 2015年

1
但是不输出['aaa', '', 'aaa']吗?
Jean-Luc Nacif Coelho

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

每次找到分号时,超前扫描都会扫描剩余的整个字符串,以确保单引号的偶数和双引号的偶数。(忽略双引号字段中的单引号,反之亦然。)如果超前成功,则分号是定界符。

Duncan的解决方案不同,该解决方案匹配字段而不是定界符,而对于空字段则没有问题。(甚至没有最后一个:与许多其他split实现不同,Python不会自动丢弃尾随的空字段。)


谢谢艾伦,我差点错过了这个答复。它与Duncan的相似,但是它可以更优雅地将琴弦切成薄片。我有一个类似的问题,它运行良好。
marshall.ward,

对于每种;解决方案,将先行运行,确保在此分号后的引号是平衡的(否则,此分号将被引用,应省略)。因此,复杂度是O(n^2)(假设数目;随着字符串的长度线性增长)。
ovgolovin

谢谢艾伦。您救了我的一天:)
Painkiller

应该比邓肯的喜欢更多,因为它可以正确处理空字符串!
juan Isaza

1
注意,这似乎不能处理转义的引号,例如'"scarlett o\'hara"; rhett butler'-而Duncan的解决方案可以。
nrflaw

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

2
我向下滚动页面以回答完全相同的问题,可惜这个答案如此之低,csv模块绝对是正确的方法
Edmond Lafay-David

1
在Python3.0中,请执行from io import StringIO而不是StringIO。从docs.python.org/3.0/whatsnew/3.0.html中删除“ StringIO和cStringIO模块。转而导入io模块,并分别使用io.StringIO或io.BytesIO分别用于文本和数据。”
Pritesh Ranjan

11

这是带注释的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']

1
+1我本来打算发布一个pyparsing解决方案,但您的解决方案更优雅
Luper Rouch 2010年

1
这个答案非常有用。从这里开始,我能够用10行dl,安装和编写一个简单的IMAP标头解析器。谢谢!
哈维

这很棒!但是,在值为空的情况下(例如:[[,23,43,38,75,26,19,37,43,19,27,25,20,34,22,23])我得到pyparsing.ParseException :预期{quotedString使用单引号或双引号| W:(0123 ...)}(在char 0处),(行:1,col:1)
chri_chri

9

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

如果您可以将数据更改为在适当位置仅包含单引号或双引号,则它应该可以正常工作,但这种方式会否定该问题。


1
+1:csv.reader采用可迭代的方式,因此您需要将输入字符串包装在列表中: csv.reader([data], delimiter=';')。除此之外,它确实可以满足用户的需求。这还将处理以反斜杠为前缀的嵌入式引号字符。
戴夫·柯比

1
实际上,csv模块不是那么聪明,在我测试时不起作用。他的数据同时包含单引号和双引号,并且csv模块无法将其this "is ; part" 5作为单块处理,从而导致['part 1', 'this is ; part 2;', "'this is ", " part 3'", 'part 4', 'this "is ', ' part" 5']
YOU 2010年

2
csv模块不仅处理不止一种引用类型,而且还坚持要求字段完全引用或完全不引用。这意味着第5部分将被一分为二,因为字段中间的双引号只是不引用内容的文字。在这种情况下,恐怕选项是(a)使用过于复杂的正则表达式,或(b)将输入数据的格式更改为使用某些可识别的CSV变体。如果是我,我会选择(b)。
邓肯2010年

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

无法使用以下字符串:'''part 1;"this is ';' part 2;";'this is ; part 3';part 4'''
琥珀色

对。固定。忘记在第二部分中交换单引号/双引号。
Max Shawabkeh

对不起,我在测试用例中错过了一些东西。请参阅我的问题的第5部分。谢谢
西尔万(Sylvain)2010年

您的第5个测试用例可能会使该解决方案的可行性大大降低。
琥珀色

好的,我真的只想忽略引号内的分号。我不希望引号充当分隔符。
西尔万

3

尽管可以通过先行/后备/反向引用使用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']

1
这个问题根本不需要平衡-只需将其括起来并转义单个字符即可。这是一个非常简单(实际上实际上是常规)的模式。
Max Shawabkeh,2010年

实际上,唯一可行的原因findall是由于Python中实现的附加限制,即返回的匹配项是不重叠的。否则,'''part 1;"this 'is' sparta";part 2'''由于模式也匹配字符串的子集,因此类似的字符串将失败。
琥珀色

我正在使用,findall因为我们需要提取字符串。形式上,正则表达式只进行匹配。为了匹配,我们可以简单地使用^mypattern(;mypattern)*$
Max Shawabkeh,2010年

但是,正如您所指出的那样,这样做放弃了以一种很好的方式提取文本的能力(尽管我想您可以迭代无限次的捕获)。
琥珀色

哦,你的比我的好得多。:)
Ipsquiggle 2010年

2

我们可以创建自己的功能

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

1

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


您还需要为单引号添加另一个选项。
琥珀色

除非您可以在python的re模块中使用向后引用(未记录),否则它将中断。您同时支持两种类型的引号,则有可能与"quoted'vs "quoted' single quote"
dlamotte


@xyld:Python的re模块不支持反向引用。@ killdash10:没关系。OP并未尝试解析嵌套模式。
Max Shawabkeh,2010年

@ killdash10完全一样,但是可以在perl中使用反向引用;)打破了整个抽奖引理,DFA / NFA,因为正则表达式具有状态,非常小的/有限的状态,但是仍然是状态
dlamotte

1

由于您没有'\ 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']

干净简单。由于它只是一个简单的字符串,因此无需担心效率。要处理嵌套引号,可能需要调整elif语句。
丁格尔2010年

0

即使我确定有一个干净的正则表达式解决方案(到目前为止,我喜欢@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

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

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


0

通用解决方案:

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

此解决方案:

  • 捕获所有空组(包括开头和结尾)
  • 适用于大多数流行的定界符,包括空格,制表符和逗号
  • 将另一种类型的引号内的引号视为非特殊字符
  • 如果遇到不匹配的不带引号的引号,则将行的其余部分视为带引号的

0

尽管主题很旧,以前的答案也很有效,但我还是建议使用python实现自己的split函数。

如果您不需要处理大量字符串,并且可以轻松自定义,则效果很好。

这是我的功能:

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

优点是此函数可用于空字段以及字符串中任意数量的分隔符。

希望这可以帮助!


0

无需拆分分隔符模式,只需捕获所需的内容即可:

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

-1

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

新解决方案:

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

您可能需要对资源进行一些后处理,但其中包含您想要的内容。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.