Python 字符串子串定位性能比较

项目最近遇到一个需求:

  1. 给定一组文本文件,每个文本包含若干行,每一行是一条数据记录;
  2. 每一行各字段按照如下方式排布,首先是n个metafield字段,紧接着是最多4个keyfield字段,然后是m个valuefield字段,每个字段用"|"分隔,key从哪个字段开始以及key有几个字段已知metafield_1|metafield_2|...|metafield_n|keyfield_1|...|keyfield_4|valuefield_1|valuefield_2|....|valuefield_m
  3. 任务是对这组文件按keyfields_string除重

除开业务细节,这个任务本质是:

  1. 遍历每个文件的每一行;
  2. 然后截取出keyfield字段集合;
  3. 然后对其进行重复判断;
  4. 最后按照判断结果决定本行是否插入新文件中。

Python很适合完成这种文本处理任务,字符串重复判断这种任务可以使用dict来完成,本文中不做深入探讨。本文想探讨的是在给定了key字段在字段列表中开始下标和key字段个数后,如何在整行字符串中定位到key字符串的起始位置。简而言之,就是确定keyfield_1前一个和keyfield_p后一个“|”字符的位置。

解决这个问题,我想到了三种思路:

  1. 将整个字符串用"|"分割(split),并根据key字段的下标计算首尾两个"|"的位置;
  2. 使用(index/find)函数,通过设置搜索起始位置,按顺序逐个查找"|"字符的位置,直到找到目标“|”位置
  3. 先通过正则表达式或字符串遍历的方式查找出所有"|"的位置生成list,然后根据key字段下标找到目标“|”位置

有同学会说方法1既然每个字段都已经分割开了,将其按照顺序组合就能得到keyfields_string,为何还要查找“|”字符的位置,我想说在这里只是比较在字符串中查找子串的各种方法。

针对以上三个思路,我一共有七种实现,后面会对比其效率:

字符串分割思路

Split

def get_pos_split(line, key_start):
    pos = 0
    tmp_line_list = line.split('|')
    for i in xrange(key_start):
        if i >= len(tmp_line_list):
            return len(line)
        pos += len(tmp_line_list[i]) + 1
    return pos

逐个查找子串位置思路

这个思路我写了三种方法,分别用 index/find来实现,需要注意的是,index函数在未找到子串的情况下会抛出ValueError错误,需要用try except处理,而find在找不到子串的情况下返回-1,两者效率基本一致。并且在查找下一个子串的方式上有少许不同,一种是当找到当前子串位置后,记录下该位置,然后下一次从本次找到的位置+1开始查找,另一种是每找到一个子串,就去掉前缀部分,然后下一次在剩下的字符串中查找。

Find

#使用find查找,记录查找位置,下一次从本次找到的位置+1开始查找
def get_pos_find(line, key_start):
    if key_start == 0: 
        return 0
    pos = line.find('|')
    while pos >= 0 and key_start > 1:
        pos = line.find('|', pos+1)
        key_start -= 1
    return len(line) if pos == -1 else pos+1

Index

#使用index查找,记录查找位置,下一次从本次找到的位置+1开始查找
def get_pos_index(line, key_start):
    pos = 0
    for i in xrange(key_start):
        try:  
            pos = line.index('|', pos+1)
        except ValueError,e:
            return len(line)
    return 0 if pos == 0 else pos+1

Index Cut

#使用index查找,每次找到第一个子串后,就去掉前缀部分,拷贝后缀部分,后续不断在后缀部分查找
def get_pos_index_2(line, key_start):
    tmp_line = line
    pos = 0
    for i in xrange(key_start):
        try:
            pos += tmp_line.index('|')+1
            tmp_line = tmp_line[tmp_line.index('|')+1:]
        except ValueError, e:
            return len(line)
    return pos

定位所有子串思路

针对这个思路,分别使用正则表达式模块,列表推导式以及lambda、map、filter组合方式实现。

  • 正则表达式 re.finditer 方法会返回字符串中所有子串位置的迭代器
  • 列表推倒式将遍历整个字符串并输出子串位置的列表
  • 组合复杂函数的方法,首先用map扫描字符串中所有匹配子串的位置,不匹配的输出-1,再通过filter与lambda函数结合的方式在刚才的结果中过滤掉-1元素

Regex

#通过正则表达式re模块查找匹配所有子串位置
def get_pos_re(line, key_start):
    pos_idx = [p.start() for p in re.finditer('\|', line)]
    return 0 if key_start == 0 else (pos_idx[key_start-1]+1 if key_start <= len(pos_idx) else len(line))

LC

#通过列表推导式(list comprehensions)实现
def get_pos_lc(line, key_start):
    pos_idx = [i for i, x in enumerate(line) if x == '|']
    return 0 if key_start == 0 else (pos_idx[key_start-1]+1 if key_start <= len(pos_idx) else len(line))

Filter

#通过 lambda、map、filter 组合实现
def get_pos_filter(line, key_start):
    def func_in(t):
        return t[0] if t[1] == '|' else -1
    pos_idx = filter(lambda x: x!=-1, map(func_in, enumerate(line)))
    return 0 if key_start == 0 else (pos_idx[key_start-1]+1 if key_start <= len(pos_idx) else len(line))

测试对比

首先,测试在相同单条记录,不同的记录条数条件下,各种方法的耗时,结果如上图所示。

然后,测试在记录条数一定,不同记录长度条件下,各种方法耗时,结果如上图所示。

第三,测试在相同单条记录,相同记录条数情况下取不同位置的字段各种方法耗时,结果如上图所示。

结论

通过测试对比可以看到,字符串分割逐个查找子串位置的思路在总体上都比定位所有子串位置的思路效率更高。

逐个查找子串位置思路中通过find和index定位子串位置的效率最高,拆分子串的方式次之。影响性能的因素是单条记录长度以及所需要查找的字段位置。

字符串分割,影响性能的因素是单条记录长度以及所需要查找的字段位置。

定位所有子串因为要定位到每个字段的位置,相当于扫描全数据,所以效率最低。在这个思路的三种方法中,正则表达式的实现效率最高,其他两种效率都很差。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小詹同学

Leetcode打卡 | No.24 两两交换链表中的节点

欢迎和小詹一起定期刷leetcode,每周一和周五更新一题,每一题都吃透,欢迎一题多解,寻找最优解!这个记录帖哪怕只有一个读者,小詹也会坚持刷下去的!

12440
来自专栏lhyt前端之路

js版本的(广、深)度优先搜索0. 前言1.队列、栈2.BFS1.1 矩阵形式的图的遍历1.2 树的BFS举例3.DFS

广度优先搜索(BFS)和深度优先搜索(DFS),大家可能在oj上见过,各种求路径、最短路径、最优方法、组合等等。于是,我们不妨动手试一下js版本怎么玩。

13920
来自专栏云霄雨霁

查找----基于散列表(线性探测法)

59400
来自专栏互联网开发者交流社区

HashMap相关(二)

13050
来自专栏python成长之路

集合常用操作

17740
来自专栏xiaoxi666的专栏

最长公共子串+最长公共子序列

1、先建立一个二维数组array[str1.size()][str2.size()](全部初始化为0),初始化第一行和第一列(元素相同处置1),然后进入状态方程

1.5K20
来自专栏小詹同学

Leetcode打卡 | No.22 括号生成

欢迎和小詹一起定期刷leetcode,每周一和周五更新一题,每一题都吃透,欢迎一题多解,寻找最优解!这个记录帖哪怕只有一个读者,小詹也会坚持刷下去的!

30010
来自专栏企鹅号快讯

Python算法分享系列-查找,排序,递归

iTesting,爱测试,爱分享 沉寂了一段时间,继续学习。 算法这个系列我想分享很久了,奈何本身对算法不是特别了解,又找不到合适的载体来分享。 最近看了本有趣...

47460
来自专栏racaljk

Leetcode 8. String to Integer (atoi) atoi函数实现 (字符串)

这道题的corner cases非常多,请务必确保下面cases都能通过的情况下再提交。

23530
来自专栏SeanCheney的专栏

《利用Python进行数据分析·第2版》 附录A NumPy高级应用A.1 ndarray对象的内部机理A.2 高级数组操作A.3 广播A.4 ufunc高级应用A.5 结构化和记录式数组A.6 更多

在这篇附录中,我会深入NumPy库的数组计算。这会包括ndarray更内部的细节,和更高级的数组操作和算法。 这章包括了一些杂乱的章节,不需要仔细研究。 A.1...

73760

扫码关注云+社区

领取腾讯云代金券