在Python中拆分字符串的最有效方法

时间:2012-03-07 14:00:27

标签: python optimization split

我当前的Python项目需要大量的字符串拆分来处理传入的包。由于我将在一个非常慢的系统上运行它,我想知道最有效的方法是什么。字符串的格式如下:

Item 1 | Item 2 | Item 3 <> Item 4 <> Item 5

说明:此特定示例来自列表,其中前两个项目是标题和日期,而第3项到第5项将被邀请人员(其数量可以是从零到n的任何数字,其中n是服务器上注册用户的数量。)

从我看来,我有以下选择:

  1. 反复使用split()
  2. 使用正则表达式和正则表达式函数
  3. 我还没有想到的其他一些Python函数(可能还有一些)
  4. 解决方案1将包括在|处拆分,然后在此示例中将结果列表的最后一个元素拆分为<>,而解决方案2可能会生成正则表达式,如:

    ((.+)|)+((.+)(<>)?)+

    好的,这个RegEx太可怕了,我可以看到自己。它也是未经测试的。但是你明白了。

    现在,我正在寻找a)花费最少时间和b)理想地使用最少内存量的方式。如果只有两个中的一个是可能的,我宁愿更少的时间。理想的解决方案也适用于包含更多项目|和完全没有<>的字符串的字符串。至少基于正则表达式的解决方案会这样做

    我的理解是split()将使用更多内存(因为你基本上得到两个结果列表,一个分为|,第二个分为<>),但是我不太了解Pythons实现常规表达式来判断RegEx将如何执行。 split()的动态性也低于正则表达式,如果它与不同数量的项目和第二个分隔符的缺失有关。尽管如此,我无法撼动python在没有正则表达式的情况下可以做得更好的印象,这就是我要问的原因

    一些注意事项:

    • 是的,我可以对两个解决方案进行基准测试,但我正在尝试了解一般的python以及它在这里是如何工作的,如果我只是对这两个进行基准测试,我仍然不知道我错过了什么python函数
    • 是的,只有高性能的东西才真正需要在这个级别进行优化,但正如我所说,我正在尝试学习有关python的东西。
    • 添加:在原始问题中,我完全忘记提到我需要能够区分|与分隔符<>分开的部分{{1因此,由re.split(\||<>,input)生成的简单平面列表(由@obmarg提出)将无法正常工作。适合这个标准的解决方案非常受欢迎。

    总结一下这个问题:出于什么原因,哪种解决方案最有效。

    由于多次请求,我在split() - 解决方案和@obmarg提出的第一个正则表达式以及@mgibsonbr和@duncan的解决方案上运行了一些时间:

    import timeit
    import re
    
    def splitit(input):
        res0 = input.split("|")
        res = []
        for element in res0:
            t = element.split("<>")
            if t != [element]:
                res0.remove(element)
                res.append(t)
        return (res0, res)
    
    def regexit(input):
        return re.split( "\||<>", input )
    
    
    def mgibsonbr(input): # Solution by @mgibsonbr
        items = re.split(r'\||<>', input) # Split input in items
        offset = 0
        result = [] # The result: strings for regular itens, lists for <> separated ones
        acc = None
        for i in items:
            delimiter = '|' if offset+len(i) < len(input) and input[offset+len(i)] == '|' else '<>'
            offset += len(i) + len(delimiter)
            if delimiter == '<>': # Will always put the item in a list
                if acc is None:
                    acc = [i] # Create one if doesn't exist
                    result.append(acc)
                else:
                    acc.append(i)
            else:
                if acc is not None: # If there was a list, put the last item in it
                    acc.append(i)
                else:
                    result.append(i) # Add the regular items
                acc = None # Clear the list, since what will come next is a regular item or a new list
        return result
    
    def split2(input): # Solution by @duncan
        res0 = input.split("|")
        res1, res2 = [], []
        for r in res0:
            if "<>" in r:
                res2.append(r.split("<>"))
            else:
                res1.append(r)
        return res1, res2
    
    print "mgibs:", timeit.Timer("mgibsonbr('a|b|c|de|f<>ge<>ah')","from __main__ import mgibsonbr").timeit()
    print "split:", timeit.Timer("splitit('a|b|c|de|f<>ge<>ah')","from __main__ import splitit").timeit()
    print "split2:", timeit.Timer("split2('a|b|c|de|f<>ge<>ah')","from __main__ import split2").timeit()
    print "regex:", timeit.Timer("regexit('a|b|c|de|f<>ge<>ah')","from __main__ import regexit").timeit()
    print "mgibs:", timeit.Timer("mgibsonbr('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import mgibsonbr").timeit()
    print "split:", timeit.Timer("splitit('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import splitit").timeit()
    print "split:", timeit.Timer("split2('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import split2").timeit()
    print "regex:", timeit.Timer("regexit('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import regexit").timeit()
    

    结果:

    mgibs: 14.7349407408
    split: 6.403942732
    split2: 3.68306812233
    regex: 5.28414318792
    mgibs: 107.046683735
    split: 46.0844590775
    split2: 26.5595985591
    regex: 28.6513302646
    

    目前,看起来像@duncan的split2击败了所有其他算法,无论长度如何(至少使用这个有限的数据集),而且看起来@ mgibsonbr的解决方案也有一些性能问题(抱歉'但是,但是谢谢你的解决方案。)

    感谢大家的投入。

5 个答案:

答案 0 :(得分:17)

我有点惊讶split()在您的代码中执行得非常糟糕,所以我更仔细地看了一下,注意到你在内循环中调用了list.remove()。你也在每个字符串上调用split()额外的时间。摆脱那些和解决方案使用split()击败正则表达式在较短的字符串上移动,并在较长的字符串上紧随其后。

import timeit
import re

def splitit(input):
    res0 = input.split("|")
    res = []
    for element in res0:
        t = element.split("<>")
        if t != [element]:
            res0.remove(element)
            res.append(t)
    return (res0, res)

def split2(input):
    res0 = input.split("|")
    res1, res2 = [], []
    for r in res0:
        if "<>" in r:
            res2.append(r.split("<>"))
        else:
            res1.append(r)
    return res1, res2

def regexit(input):
    return re.split( "\||<>", input )

rSplitter = re.compile("\||<>")

def regexit2(input):
    return rSplitter.split(input)

print("split:", timeit.Timer("splitit('a|b|c|de|f<>ge<>ah')","from __main__ import splitit").timeit())
print("split2:", timeit.Timer("split2('a|b|c|de|f<>ge<>ah')","from __main__ import split2").timeit())
print("regex:", timeit.Timer("regexit('a|b|c|de|f<>ge<>ah')","from __main__ import regexit").timeit())
print("regex2:", timeit.Timer("regexit2('a|b|c|de|f<>ge<>ah')","from __main__ import regexit2").timeit())
print("split:", timeit.Timer("splitit('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import splitit").timeit())
print("split2:", timeit.Timer("split2('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import split2").timeit())
print("regex:", timeit.Timer("regexit('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import regexit").timeit())
print("regex2:", timeit.Timer("regexit2('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import regexit2").timeit())

这给出了以下结果:

split: 1.8427431439631619
split2: 1.0897291360306554
regex: 1.6694280610536225
regex2: 1.2277749050408602
split: 14.356198082969058
split2: 8.009285948995966
regex: 9.526430513011292
regex2: 9.083608677960001

当然split2()给出了你想要的嵌套列表,而正则表达式解决方案没有。

编辑:我已经更新了这个答案,包括@ F1Rumors建议编译正则表达式将提高性能。它确实略有不同,但Python缓存编译的正则表达式,因此节省的数量不如您预期的那么多。我认为通常不值得为速度而做(尽管在某些情况下可能会这样),但通常值得让代码更清晰。

我还更新了代码,因此它在Python 3上运行。

答案 1 :(得分:10)

我不确定它是否效率最高,但最简单的代码似乎是这样的:

>>> input = "Item 1 | Item 2 | Item 3 <> Item 4 <> Item 5"
>>> re.split( "\||<>", input )
>>> ['Item 1 ', ' Item 2 ', ' Item 3 ', ' Item 4 ', ' Item 5']

我认为它有可能比普通的旧分裂更有效(取决于输入数据),因为你需要对第一次分割的每个字符串输出执行第二次分割操作,对于记忆或时间来说似乎都没有效率。

虽然说过我可能很容易出错,但唯一能确定的方法就是计时。

答案 2 :(得分:3)

多次调用split可能是不合适的,因为它可能会创建不需要的中间字符串。使用像你提议的正则表达式将无法工作,因为捕获组只会获取最后一项,而不是每一项。像obmarg建议的那样使用正则表达式进行拆分似乎是最好的路线,假设您正在寻找“扁平”列表。

如果您不想要拼合列表,可以先使用正则表达式进行拆分,然后迭代结果,检查原始输入以查看使用了哪个分隔符:

items = re.split(r'\||<>', input)
offset = 0
for i in items:
    delimiter = '|' if input[offset+len(i)] == '|' else '<>'
    offset += len(i) + len(delimiter)
    # Do something with i, depending on whether | or <> was the delimiter

最后,如果您根本不想创建子串(例如,仅使用开始和结束索引来节省空间),re.finditer可能会完成这项工作。迭代分隔符,并根据找到的分隔符(|<>)对它们之间的文本执行某些操作。这是一个更复杂的操作,因为你必须处理许多极端情况,但根据你的需要可能是值得的。

对于您的特定情况

更新,输入格式是统一的,obmarg的解决方案是最好的。如果必须,请将结果后处理为具有嵌套列表:

split_result = re.split( "\||<>", input )
result = [split_result[0], split_result[1], [i for i in split_result[2:] if i]]

(最后一次列表理解是为了确保在最后[]之后没有项目的情况下,您将获得['']而不是|

更新2:在阅读更新后的问题后,我终于明白了您要实现的目标。以下是使用前面提到的框架的完整示例:

items = re.split(r'\||<>', input) # Split input in items
offset = 0
result = [] # The result: strings for regular itens, lists for <> separated ones
acc = None
for i in items:
    delimiter = '|' if offset+len(i) < len(input) and input[offset+len(i)] == '|' else '<>'
    offset += len(i) + len(delimiter)
    if delimiter == '<>': # Will always put the item in a list
        if acc is None:
            acc = [i] # Create one if doesn't exist
            result.append(acc)
        else:
            acc.append(i)
    else:
        if acc is not None: # If there was a list, put the last item in it
            acc.append(i)
        else:
            result.append(i) # Add the regular items
        acc = None # Clear the list, since what will come next is a regular item or a new list
print result

用你的例子测试过,结果是:

['a', 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c','de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'aha'],
 'b', 'c', 'de', ['f', 'ge', 'ah']]

答案 3 :(得分:2)

如果您知道<>不会出现在字符串的其他位置,那么您可以替换'&lt;&gt;'用'|'然后是一个单独的分裂:

>>> input = "Item 1 | Item 2 | Item 3 <> Item 4 <> Item 5"
>>> input.replace("<>", "|").split("|")
['Item 1 ', ' Item 2 ', ' Item 3 ', ' Item 4 ', ' Item 5']

这几乎肯定会比进行多次拆分更快。它可能会或可能不会比使用re.split更快 - timeit是你的朋友。

修改: 在我的系统上,你提供的示例字符串我的版本比re.split快三倍以上:

>>> timeit input.replace("<>", "|").split("|")
1000000 loops, best of 3: 980 ns per loop
>>> import re
>>> timeit re.split(r"\||<>", input)
100000 loops, best of 3: 3.07 us per loop

(N.B。这是使用ipython,它有timeit作为内置命令)。

答案 4 :(得分:1)

您可以使用替换。首先将<>替换为|,然后再分为|

def replace_way(input):
    return input.replace('<>','|').split('|')

时间表现:

import timeit
import re

def splitit(input):
    res0 = input.split("|")
    res = []
    for element in res0:
        t = element.split("<>")
        if t != [element]:
            res0.remove(element)
            res.append(t)
    return (res0, res)

def regexit(input):
    return re.split( "\||<>", input )

def replace_way(input):
    return input.replace('<>','|').split('|')


print "split:", timeit.Timer("splitit('a|b|c|de|f<>ge<>ah')","from __main__ import splitit").timeit()
print "regex:", timeit.Timer("regexit('a|b|c|de|f<>ge<>ah')","from __main__ import regexit").timeit()
print "replace:",timeit.Timer("replace_way('a|b|c|de|f<>ge<>ah')","from __main__ import replace_way").timeit()
print "split:", timeit.Timer("splitit('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import splitit").timeit()
print "regex:", timeit.Timer("regexit('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import regexit").timeit()
print "replace:",timeit.Timer("replace_way('a|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>aha|b|c|de|f<>ge<>ah')","from __main__ import replace_way").timeit()

我的机器上的结果:

split: 11.8682055461
regex: 12.7430856814
replace: 2.54299265006
split: 79.2124379066
regex: 68.6917008003
replace: 10.944842347