在Python列表中高效搜索部分字符串

时间:2019-01-03 11:17:18

标签: python string list dictionary

寻找一种有效的方法来搜索Python(3.6+)列表中的部分字符串。

我有两个列表。 listA是路径名+唯一文件名的字符串列表:

['/pathname/uniquestring.ext', '/pathname/uniquestring.ext', '/pathname/uniquestring.ext' ...]

(使用glob()创建,文件名已全部给出并且已经存在)

listB是词典列表。每个字典具有相同的键集,但具有唯一的值。

[{key1:value1, key2:value2}, {key1:value3, key2:value4}, ...]

(也已经给出)

listB中每个字典中的一个key:value对将具有一个 listA中一个唯一项包含的值。

但是,值在listA的每个项目中出现的位置是不确定的。

我想要的是:对于listB中的每个项目,在listA中找到包含与dict中的k:v对匹配的子字符串的项目, 并创建一个新的字典(或元组列表)作为“查找表”(目的是更正一组图像文件中损坏的exif创建日期)。

示例:

listA = ['/pathname/abdce_654321.ext', '/pathname/a3b4c5_123456.ext', '/pathname/cbeebie_645321_abcde.ext', ...]

listB = [{"id": "123456", "create_date": "23/05/2014"}, ...]

new_dict = {"/pathname/a3b4c5_123456.ext": "23/05/2014, ...}

我完全可以从dict补偿中获得想要的东西,如下所示:

{j:i['create_date'] for j in listA for i in listB  if i['id'] in j}

但是,即使对于很小的文件(约5500个项目),这在我(相当老)的笔记本电脑上也要花费12s。

大概是因为我必须使用我的方法遍历整个listB〜5500次。

在Python中有更有效的方法吗?

(nb我不是在寻求有关如何使用python纠正exif数据的建议;这是有关列表中字符串查找的一般化问题)

更正和澄清

  1. 在我的示例中,我忽略了将引号放在值“ 123456”周围,这当然意味着它是整数;在现实数据中,它不是,我处理的实际数据中的任何等效值也不是。
  2. 列表中出现的'id'子字符串几乎总是由下划线定界,但并不总是出现在整个字符串的同一位置;因此,对每个项目执行split('_')并不总是将'id'字符串放在位置[-1]或[-2]或[-3​​],尽管[-1]会注意约80%的案例。
  3. 所有“ id”都是唯一的,在两个列表中它们都不会出现多次;每个文件名在listA中都是唯一的;每个“ id”都不会出现在多个词典中。

谢谢大家到目前为止的关注。

3 个答案:

答案 0 :(得分:2)

我可以看到两条评论的内容。最大的问题是:是否需要使用in,因为仅当我们不知道id在路径字符串中的位置时才需要这样做吗?如果它总是在特定的地方,我们可以提取它并使用恒定时间查找:

def extract_id(path):
    # todo
ids = {item['id']: item['create_date'] for item in listB}
new_dict = {path: ids[extract_id(path)] for path in listA}

只有O(N),而不是您当前的O(N**2)

答案 1 :(得分:0)

首先,这是有助于测试的通用列表:

listA = ['/pathname/abdce_%s.ext' % str(x) for x in range(10000)]

listB = [{'id': str(number), "create_date": "23/05/2014"} for number in range(10000)]

hello = {j: i['create_date'] for j in listA for i in listB if i['id'] in j}

以10000个值运行时,我的机器平均花费8.8秒。 (如果我之后打印字典,则为9.5秒)

现在,如果我们将代码编译为Cython(在C上运行的python超集),那么我的时间将减少到4.4秒。

请参见下面的代码

cpdef dict main():
    cdef int x
    cdef int number
    cdef char j
    cdef dict i

    listA = ['/pathname/abdce_%s.ext' % str(x) for x in range(10000)]

    listB = [{'id': str(number), "create_date": "23/05/2014"} for number in range(10000)]

    hello = {j: i['create_date'] for j in listA for i in listB if i['id'] in j}

    return hello

答案 2 :(得分:0)

我写了一个小测试台,它会生成类似于您的随机数据,并尝试使用您的原始词典理解功能,以及一个具有优化功能的版本,例如在找到匹配项时提前退出并删除使用过的标签。

match(您的原件)和match2(我的)都打印出结果数量,以尝试确保它们等效。

结果颇具说服力...希望能有所帮助。

我的MBP上5000/10000个项目的编号:

  • 原始:1.771 / 7.391
  • 优化:0.054 / 0.203
  • 不删除使用过的标签(如果这不是可接受的业务规则):0.917 / 3.789

import random
import timeit
import string

random.seed(42)


def genrand(n):
    return "".join(
        random.choice(string.ascii_lowercase + string.digits) for x in range(n)
    )


filenames = []
tags = []

for x in range(5000):
    id = genrand(8)
    filenames.append("/pathname/%s_%s.ext" % (genrand(6), id))
    if random.random() < 0.95:
        tags.append({"id": id, "date": "date for %s" % id})


def match():
    x = {j: i["date"] for j in filenames for i in tags if i["id"] in j}
    print(len(x))


def match2():
    x = {}
    available_tags = tags[:]
    for filename in filenames:
        for tag in available_tags:
            if tag["id"] in filename:
                x[filename] = tag
                available_tags.remove(tag)  # we've used this tag, remove it
                break
    print(len(x))


print(timeit.timeit(match, number=1))
print(timeit.timeit(match2, number=1))