3

If I have a collection of strings is there a data structure or function that could improve the speed of checking if any of the elements of the collections are substrings on my main string?

Right now I'm looping through my array of strings and using the in operator. Is there a faster way?

import timing

## string match in first do_not_scan
## 0:00:00.029332

## string not in do_not_scan
## 0:00:00.035179
def check_if_substring():
    for x in do_not_scan:
        if x in string:
            return True
    return False

## string match in first do_not_scan
## 0:00:00.046530

## string not in do_not_scan
## 0:00:00.067439
def index_of():
    for x in do_not_scan:
        try:
            string.index(x)
            return True
        except:
            return False

## string match in first do_not_scan
## 0:00:00.047654

## string not in do_not_scan
## 0:00:00.070596
def find_def():
    for x in do_not_scan:
        if string.find(x) != -1:
            return True
    return False

string = '/usr/documents/apps/components/login'
do_not_scan = ['node_modules','bower_components']

for x in range(100000):
    find_def()
    index_of()
    check_if_substring()
4
  • Is it possible that you pasted something wrong here. Or is string = 'a' just a sample. Because node_modules will never be in string. With that said, can you use a map. Where the keys are the items of do_not_scan. Then the search is O(1) Commented Mar 4, 2016 at 18:13
  • just a sample to demonstrate string may not contain any elements of do_not_scan. I've never used a map before how would you go about doing that? Commented Mar 4, 2016 at 18:19
  • Do you want the analog of grep -l -Ff collections_of_strings main_string? where collections_of_strings file contains a collections of strings (one per line) and main_string file contains the main string (as is). Commented Mar 5, 2016 at 16:04
  • I will edit, meant is there a better data structure eg. put things in a set instead of an array that would make the search faster Commented Mar 6, 2016 at 2:50

4 Answers 4

3

No, there is no faster built-in way.

If you have large amounts of strings to test for, then you may be better off using a third-party Aho-Corasick package, as J.F. Sebastian's answer shows.


Using the built-in methods, the worst-case scenario is: there is no match, which means you have tested every item in the list and nearly every offset in every item.

Fortunately, the in operator is very quick (at least in CPython) and was faster by nearly a factor of three in my tests:

0.3364804992452264  # substring()
0.867534976452589   # any_substring()
0.8401796016842127  # find_def()
0.9342398950830102  # index_of()
2.7920695478096604  # re implementation

Here is the script I used for testing:

from timeit import timeit
import re

def substring():
    for x in do_not_scan:
        if x in string:
            return True
    return False

def any_substring():
    return any(x in string for x in do_not_scan)

def find_def():
    for x in do_not_scan:
        if string.find(x) != -1:
            return True
    return False

def index_of():
    for x in do_not_scan:
        try:
            string.index(x)
            return True
        except:
            return False

def re_match():
    for x in do_not_scan:
        if re.search(string, x):
            return True
    return False

string = 'a'
do_not_scan = ['node_modules','bower_components']

print(timeit('substring()', setup='from __main__ import substring'))
print(timeit('any_substring()', setup='from __main__ import any_substring'))
print(timeit('find_def()', setup='from __main__ import find_def'))
print(timeit('index_of()', setup='from __main__ import index_of'))
print(timeit('re_match()', setup='from __main__ import re_match'))
Sign up to request clarification or add additional context in comments.

1 Comment

it is incorrect. You can do better than O(n*m) e.g., Aho–Corasick algorithm is O(n + m) in time. grep may use it for fixed strings
2

I don't have a large dataset to try:

But maybes something like this will work?

python3

from builtins import any
import timeit

do_not_scan = ['node_modules', 'bower_components']
string = 'a'


def check_if_substring():
    return any(string in x for x in do_not_scan)


result = timeit.Timer("check_if_substring()", "from __main__ import check_if_substring")
count = 10000
print(result.timeit(count)/count)

Or the other way around:

def check_if_substring():
    return any(x in string for x in do_not_scan)

My results: 6.48119201650843e-07

3 Comments

Just curious -- why are you renaming any and why that way?
It was a copy and past from and old code. Doesn't make sense in this context. Ill fix it
any is as slow as find_def and index_of.
2
def check():
    if any(w in string for w in do_not_scan):
        return True
    else:
        return False

Or simpler:

def check():
    return any(w in string for w in do_not_scan)

as mentioned by @Two-Bit Alchemist

3 Comments

string match in first do_not_scan = 0:00:00.085493 | string not in do_not_scan = 0:00:00.074540
Simpler: return any(w in string for w in do_not_scan)
any is as slow as find_def and index_of.
2

Yes, there is a faster way to perform found = any(s in main_string for s in collection_of_strings) e.g., there is Aho-Corasick_algorithm that allows to improve any()-based O(n*m*k) algorithm to O(n + m*k) in time operation where n is len(main_string), m is len(collections_of_strings), and k represents individual lengths of the strings in the collection.

#!/usr/bin/env python
import noaho # $ pip install noaho

trie = noaho.NoAho()
for s in collection_of_strings:
    trie.add(s)
found = trie.find_short(main_string)[0] is not None

Note: there is no point to measure the time performance on tiny strings such as string = 'a' if you are interested in Big-O behavior. Either use a more representative sample for the benchmark or you don't need a faster (asymptotically) algorithm in your case.

2 Comments

Can you offer any guidance of where the cut-off is to use an Aho-Corasick algorithm over in?
Only your profiler knows. Constant factors depend on the quality of the implementation e.g., str.translate() in Python 3.5+ on ASCII-only input may be 50 times faster than the same code on previous Python 3 versions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.