在Python中解析字符串:如何在忽略引号内的换行符的情况下分割换行符

5

我有一个需要在Python中解析的文本。

它是一个字符串,我想将其拆分为一系列行,但是如果换行符(\n)在引号内,则应忽略它。

例如:

abcd efgh ijk\n1234 567"qqqq\n---" 890\n

应该将其解析为以下行的列表:
abcd efgh ijk
1234 567"qqqq\n---" 890

我尝试使用 split('\n'),但我不知道如何忽略引号。
有什么想法吗?
谢谢!

4
如果引号的数量是奇数会怎么样?例如:foo"bar"oh"what - Pavel
引号的数量是偶数。 - Yuval Atzmon
4个回答

8

这里有一个更简单的解决方案。

匹配由(?:"[^"]*"|.)+组成的分组。即,匹配"引号内的内容或非换行符的内容"。

例如:

import re
re.findall('(?:"[^"]*"|.)+', text)

注意:这个函数将多个换行符合并为一个,因为空白行会被忽略。为了避免这种情况,也可以添加一个空字符:(?:"[^"]*"|.)+|(?!\Z)(?!\Z)的意思是“不是字符串的末尾”,有点令人困惑。而(?!)则表示负向前瞻;\Z则表示字符串的结尾。
测试:
import re

texts = (
    'text',
    '"text"',
    'text\ntext',
    '"text\ntext"',
    'text"text\ntext"text',
    'text"text\n"\ntext"text"',
    '"\n"\ntext"text"',
    '"\n"\n"\n"\n\n\n""\n"\n"'
)

line_matcher = re.compile('(?:"[^"]*"|.)+')

for text in texts:
    print("{:>27} → {}".format(
        text.replace("\n", "\\n"),
        " [LINE] ".join(line_matcher.findall(text)).replace("\n", "\\n")
    ))

#>>>                        text → text
#>>>                      "text" → "text"
#>>>                  text\ntext → text [LINE] text
#>>>                "text\ntext" → "text\ntext"
#>>>        text"text\ntext"text → text"text\ntext"text
#>>>    text"text\n"\ntext"text" → text"text\n" [LINE] text"text"
#>>>            "\n"\ntext"text" → "\n" [LINE] text"text"
#>>>    "\n"\n"\n"\n\n\n""\n"\n" → "\n" [LINE] "\n" [LINE] "" [LINE] "\n"

感谢您的解决方案!非常简洁而优雅! - Yuval Atzmon
干得好,赞扬! - georg
迄今为止我遇到过的最专业的答案之一!如果想要在空格处分割(而不是换行符),可以使用:(?:"[^"]*"|[^\s]+?)+ - jakob.j
好奇:为什么正则表达式需要非匹配组((?:...)?使用“普通”组也可以吗? - jakob.j

4
你可以将其拆分,然后缩减它以组合具有奇数个"元素的部分:
txt = 'abcd efgh ijk\n1234 567"qqqq\n---" 890\n'
s = txt.split('\n')
reduce(lambda x, y: x[:-1] + [x[-1] + '\n' + y] if x[-1].count('"') % 2 == 1 else x + [y], s[1:], [s[0]])
# ['abcd efgh ijk', '1234 567"qqqq\n---" 890', '']

说明:

if x[-1].count('"') % 2 == 1
# If there is an odd number of quotes to the last handled element
x[:-1] + [x[-1] + y]
# Append y to this element
else x + [y]
# Else append the element to the handled list

也可以这样写:

def splitWithQuotes(txt):
    s = txt.split('\n')
    res = []
    for item in s:
        if res and res[-1].count('"') % 2 == 1:
            res[-1] = res[-1] + '\n' + item
        else:
            res.append(item)
    return res
splitWithQuotes(txt)
# ['abcd efgh ijk', '1234 567"qqqq\n---" 890', '']

如@Veedrac所指出的那样,这是一个O(n^2)复杂度的算法。但是,通过跟踪双引号"的数量,我们可以避免这种情况的发生。
def splitWithQuotes(txt):
    s = txt.split('\n')
    res = []
    cnt = 0
    for item in s:
        if res and cnt % 2 == 1:
            res[-1] = res[-1] + '\n' + item
        else:
            res.append(item)
            cnt = 0
        cnt += item.count('"')
    return res
splitWithQuotes(txt)
# ['abcd efgh ijk', '1234 567"qqqq\n---" 890', '']

(最后一个空字符串是由输入字符串末尾的最后一个 \n 造成的。)

额...这是一个二次时间复杂度的解决方案,而问题本身只需要O(n)的时间复杂度。 - Veedrac
感谢您的解决方案! - Yuval Atzmon
@njzk2 我刚刚测试了一下,因为它在列表中,所以时间复杂度仍然是O(n²)。在这里查看结果。 - Veedrac
@Veedrac:感谢您进行研究。我从未想过会有这样的结果。我仍然不明白它为什么会这样做。 - njzk2
我一直在尝试各种配置来测试你的程序,但是测试得越多,我就越感到困惑。 - njzk2
显示剩余5条评论

1

好的,这似乎是有效的(假设引号正确平衡):

rx = r"""(?x)
    \n
    (?!
        [^"]*
        "
        (?=
            [^"]*
            (?:
                " [^"]* "
                [^"]*
            )*
            $
        )
    )
"""

测试:
str = """\
first
second "qqq
     qqq
     qqq
     " line
"third
    line" AND "spam
        ham" AND "more
            quotes"
end \
"""

import re


for x in re.split(rx, str):
    print '[%s]' % x

结果:

[first]
[second "qqq
     qqq
     qqq
     " line]
["third
    line" AND "spam
        ham" AND "more
            quotes"]
[end ]

如果上面的看起来太奇怪了,你也可以分两步来做:

str = re.sub(r'"[^"]*"', lambda m: m.group(0).replace('\n', '\x01'), str)
lines = [x.replace('\x01', '\n') for x in str.splitlines()]

for line in lines:
    print '[%s]' % line  # same result

感谢您的解决方案! - Yuval Atzmon

1
有许多方法可以实现这一点。我想出了一个非常简单的方法:
splitted = [""]
for i, x in enumerate(re.split('"', text)):
    if i % 2 == 0:
        lines = x.split('\n')
        splitted[-1] += lines[0]
        splitted.extend(lines[1:])
    else:
        splitted[-1] += '"{0}"'.format(x)

谢谢你的解决方案! - Yuval Atzmon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接