使用字符串列表作为模式拆分字符串

ojy*_*ojy 3 python regex split

考虑输入字符串:

mystr = "just some stupid string to illustrate my question"
Run Code Online (Sandbox Code Playgroud)

以及指示输入字符串拆分位置的字符串列表:

splitters = ["some", "illustrate"]
Run Code Online (Sandbox Code Playgroud)

输出应该是这样的

result = ["just ", "some stupid string to ", "illustrate my question"] 
Run Code Online (Sandbox Code Playgroud)

我写了一些代码来实现以下方法.对于每个字符串splitters,我在输入字符串中找到它的出现,并插入一些我知道肯定不会是我输入字符串的部分(例如,这个'!!').然后我使用刚刚插入的子字符串拆分字符串.

for s in splitters:
    mystr = re.sub(r'(%s)'%s,r'!!\1', mystr)

result = re.split('!!', mystr)
Run Code Online (Sandbox Code Playgroud)

这个解决方案看起来很难看,有没有更好的方法呢?

hlt*_*hlt 5

拆分re.split将始终从输出中删除匹配的字符串(注意,这不完全正确,请参阅下面的编辑).因此,必须使用正向前瞻表达式((?=...))进行匹配而不删除匹配项.但是,re.split 忽略空匹配,因此只使用前瞻表达式不起作用.相反,你将在每次拆分时至少丢失一个字符(即使尝试re用"边界"匹配技巧(\b)也不起作用).如果你不关心在每个项目的末尾丢失一个空格/非单词字符(假设你只分裂为非单词字符),你可以使用像

re.split(r"\W(?=some|illustrate)")
Run Code Online (Sandbox Code Playgroud)

这会给

["just", "some stupid string to", "illustrate my question"]
Run Code Online (Sandbox Code Playgroud)

(注意后面just和之后的空格to).然后,您可以使用编程方式生成这些正则表达式str.join.请注意,每个拆分标记都被转义,re.escape因此项目中的特殊字符splitters不会以任何不希望的方式影响正则表达式的含义(想象一下,例如,)在其中一个字符串中,否则会导致正则表达式语法错误).

the_regex = r"\W(?={})".format("|".join(re.escape(s) for s in splitters))
Run Code Online (Sandbox Code Playgroud)

编辑(HT到@Arkadiy):对实际匹配进行分组,即使用(\W)代替\W,将作为单独项目插入列表中的非单词字符返回.然后,连接每两个后续项目也将根据需要生成列表.然后,您还可以通过使用(.)而不是\W:删除具有非单词字符的要求:

the_new_regex = r"(.)(?={})".format("|".join(re.escape(s) for s in splitters))
the_split = re.split(the_new_regex, mystr)
the_actual_split = ["".join(x) for x in itertools.izip_longest(the_split[::2], the_split[1::2], fillvalue='')]
Run Code Online (Sandbox Code Playgroud)

由于普通文本和辅助字符交替,the_split[::2]包含正常的分割文本和the_split[1::2]辅助字符.然后,itertools.izip_longest用于将每个文本项与相应的删除字符和最后一个项(在删除的字符中不匹配)组合fillvalue,即''.然后,使用这些元组中的每一个进行连接"".join(x).请注意,这需要itertools导入(当然,您可以在一个简单的循环中执行此操作,但itertools为这些内容提供了非常干净的解决方案).另请注意,在Python 3中itertools.izip_longest调用itertools.zip_longest.

这导致正则表达式的进一步简化,因为可以用简单的匹配组((some|interesting)而不是(.)(?=some|interesting))替换前瞻,而不是使用辅助字符:

the_newest_regex = "({})".format("|".join(re.escape(s) for s in splitters))
the_raw_split = re.split(the_newest_regex, mystr)
the_actual_split = ["".join(x) for x in itertools.izip_longest([""] + the_raw_split[1::2], the_raw_split[::2], fillvalue='')]
Run Code Online (Sandbox Code Playgroud)

这里,切片索引the_raw_split已经交换过,因为现在必须将偶数项目添加到项目中而不是在前面.还要注意[""] +部件,这是配置第一个项目""以修复订单所必需的.

(编辑结束)

或者,您可以(如果您愿意)使用string.replace而不是re.sub每个拆分器(我认为这是您的情况下的优先选择,但通常它可能更有效)

for s in splitters:
    mystr = mystr.replace(s, "!!" + s)
Run Code Online (Sandbox Code Playgroud)

此外,如果您使用固定令牌来指示拆分的位置,则您不需要re.split,但可以使用string.split:

result = mystr.split("!!")
Run Code Online (Sandbox Code Playgroud)

您还可以做什么(而不是依赖替换标记不在其他任何地方的字符串中或依赖于每个分割位置前面有非单词字符)是使用string.find和使用字符串切片在输入中查找拆分字符串提取件:

def split(string, splitters):
    while True:
        # Get the positions to split at for all splitters still in the string
        # that are not at the very front of the string
        split_positions = [i for i in (string.find(s) for s in splitters) if i > 0]
        if len(split_positions) > 0:
            # There is still somewhere to split
            next_split = min(split_positions)
            yield string[:next_split] # Yield everything before that position
            string = string[next_split:] # Retain the rest of the string
        else:
            yield string # Yield the rest of the string
            break # Done.
Run Code Online (Sandbox Code Playgroud)

在这里,[i for i in (string.find(s) for s in splitters) if i > 0]生成一个可以找到分割器的位置列表,对于字符串中的所有分割器(为此,i < 0被排除)而不是在开头(我们(可能)刚分割的位置,因此i == 0也被排除) .如果字符串中有任何剩余,我们产生(这是一个生成器函数)一切直到(不包括)第一个拆分器(at min(split_positions))并用剩余部分替换该字符串.如果没有,我们产生字符串的最后一部分并退出该函数.因为它使用yield,它是一个生成器函数,所以你需要使用list它将它变成一个实际的列表.

请注意,你也可以yield whatever用一个调用替换some_list.append(some_list前面提到你定义)并some_list在最后返回,但我不认为这是一个非常好的代码风格.


TL; DR

如果您对使用正则表达式没问题,请使用

the_newest_regex = "({})".format("|".join(re.escape(s) for s in splitters))
the_raw_split = re.split(the_newest_regex, mystr)
the_actual_split = ["".join(x) for x in itertools.izip_longest([""] + the_raw_split[1::2], the_raw_split[::2], fillvalue='')]
Run Code Online (Sandbox Code Playgroud)

否则,使用string.find以下拆分功能也可以实现相同:

def split(string, splitters):
    while True:
        # Get the positions to split at for all splitters still in the string
        # that are not at the very front of the string
        split_positions = [i for i in (string.find(s) for s in splitters) if i > 0]
        if len(split_positions) > 0:
            # There is still somewhere to split
            next_split = min(split_positions)
            yield string[:next_split] # Yield everything before that position
            string = string[next_split:] # Retain the rest of the string
        else:
            yield string # Yield the rest of the string
            break # Done.
Run Code Online (Sandbox Code Playgroud)