为什么re.split() 返回的结果中会出现空字符串?
请考虑以下内容
t = re.split(r'([!\s])', "Hello! How are you?")
print(t)
它返回:
['Hello', '!', '', ' ', 'How', ' ', 'are', ' ', 'you?']
为什么第三个元素是一个空字符串?
在问题“为什么split() 的结果中会返回空字符串?”中,本回答 指出这使得 join 能够还原分割。
例如:
a = '/segment/segment/'.split('/')
print(a)
# ['', 'segment', 'segment', '']
"/".join(a)
# '/segment/segment/'
"/".join([ch for ch in a if ch])
# 'segment/segment'
但在这里并非如此,因为这里我们使用捕获组来保留分隔符:
t = re.split(r'([!\s])', "Hello! How are you?")
print(t)
# ['Hello', '!', '', ' ', 'How', ' ', 'are', ' ', 'you?']
t1 = "".join(t)
print(t1)
# 'Hello! How are you?'
stripped = [ch for ch in t if ch]
print(stripped)
# ['Hello', '!', ' ', 'How', ' ', 'are', ' ', 'you?']
t2 = "".join(stripped)
print(t2)
# Hello! How are you?
t1==t2
# True
请注意,问题并非如何去除空字符串。
问题在于为什么一开始会出现空字符串。
这个问题也不是对 "在re.split返回的列表开头和结尾为什么会有额外的空字符串?" 的重复,因为在那种情况下,空字符串实际上需要用来恢复原始字符串,因为那位提问者没有使用捕获组,因此分隔符并未被保留,而这里我们使用捕获组来保留分隔符。
解决方案
用 str.join 重建输入,是对 re.split 行为在模式没有捕获组时的一个很好的解释。显然,这不适用于捕获模式,因为没有一个(通用的)字符串可用作“连接”的分隔符。不过,我们仍然希望以某种可预测的方式来对 re.split 进行推理。
在这种情况下,不变量是交错的片段和分隔符。换句话说,给定结果中的一个索引,你始终知道它对应的是一个片段还是一个分隔符。让我们从最简单的捕获组开始(下面所有片段假设 import re):
>>> re.split(r"(a)", "bacab")
['b', 'a', 'c', 'a', 'b']
一切都好,一切清晰:我们得到的列表形状为 [segment, delimiter, segment, delimiter, ..., segment] —— 0对或更多不匹配的片段与匹配的分隔符组成的对,后跟一个尾随的剩余片段。
让模式更有趣一些:
>>> re.split(r"(a|d)", "bacabde")
['b', 'a', 'c', 'a', 'b', 'd', 'e']
形状仍然相同。现在让我们使用一个具有连续分隔符(匹配项)的字符串:
>>> re.split(r"(a|c)", "bacab")
['b', 'a', '', 'c', '', 'a', 'b']
请注意,分隔符不能被“折叠”在一起,因为捕获组承诺里面只有恰好一个字符。如果不是这些空字符串,c 将出现在我们期望的字符串不匹配部分的位置。因此,在你的示例中 ! 与一个空格(匹配 \s)是连续的,而 re.split 必须补充一个空字符串,以在它们之间占据片段的位置。
当然,若你愿意读一些简单的C 代码,源代码 中的内容都在那里。
动机(这部分是我的猜测——今天不想挖掘十多年关于 _sre 的历史)在于支持以成对的 (token, remainder) 处理分割结果,而不必每次都重新检查一个字符串是否是令牌(匹配的分隔符)。
一般而言,你常常想知道输出的 re.split 的哪一部分是分隔符,哪一部分是未匹配的字符串,要在一个简单的字符串列表中通过位置来判断是唯一的方式。标准库作者并不想剥夺我们这样的能力,因此如果两个分隔符之间没有未匹配的片段,列表就会用空字符串进行填充。