【问题标题】:Longest Prefix Matches for URLsURL 的最长前缀匹配
【发布时间】:2011-07-23 00:45:54
【问题描述】:

我需要有关可用于 URL 上“最长前缀匹配”的任何标准 python 包的信息。我已经浏览了两个标准包http://packages.python.org/PyTrie/#pytrie.StringTrie & 'http://pypi.python.org/pypi/trie/0.1.1' 但它们似乎对 URL 上的最长前缀匹配任务没有用。

例如,如果我的设置有这些 URL 1->http://www.google.com/mail、2->http://www.google.com/document、3->http://www。 facebook.com 等。

现在如果我搜索“http://www.google.com/doc”,那么它应该返回 2,搜索“http://www.face”应该返回 3。

我想确认是否有任何标准的 python 包可以帮助我做到这一点,或者我应该实现一个 Trie 来进行前缀匹配。

我不是在寻找正则表达式类型的解决方案,因为它无法随着 URL 数量的增加而扩展。

非常感谢。

【问题讨论】:

  • 这对您有帮助吗?帮助(str.startswith)
  • 如果搜索http://www.google会怎样?是否应支持“ww.google.com/d”之类的搜索?
  • 你希望它匹配整个搜索字符串,还是搜索字符串中最长的前缀?换句话说,搜索“googl.com”会起作用(返回 1 或 2)还是会失败?

标签: python url trie longest-prefix


【解决方案1】:

下面的函数将返回最长匹配的索引。其他有用的信息也可以很容易地提取出来。

from os.path import commonprefix as oscp

def longest_prefix(s, slist):
    pfx_idx = ((oscp([s, url]), i) for i, url in enumerate(slist))
    len_pfx_idx = map(lambda t: (len(t[0]), t[0], t[1]), pfx_idx)
    length, pfx, idx = max(len_pfx_idx)
    return idx

slist = [
    'http://www.google.com/mail',
    'http://www.google.com/document',
    'http://www.facebook.com',
]

print(longest_prefix('http://www.google.com/doc', slist))
print(longest_prefix('http://www.face', slist))

【讨论】:

  • 这会返回一个不匹配的搜索索引。
  • 好点,它不检查零长度匹配。没时间打磨。
  • 非常感谢您的回复,但我不是在寻找一种正则表达式的解决方案,因为它随着不同 URL 数量的增加而无法扩展。我正在寻找的是基于 Trie 的最长前缀匹配解决方案,其中字符串是 URL。
【解决方案2】:

此示例适用于小型 url 列表,但不能很好地扩展。

def longest_prefix_match(search, urllist):
    matches = [url for url in urllist if url.startswith(search)]
    if matches:
        return max(matches, key=len)
    else:
        raise Exception("Not found")

使用trie 模块的实现。

import trie


def longest_prefix_match(prefix_trie, search):
    # There may well be a more elegant way to do this without using
    # "hidden" method _getnode.
    try:
        return list(node.value for node in prefix_trie._getnode(search).walk())
    except KeyError:
        return list()

url_list = [ 
    'http://www.google.com/mail',
    'http://www.google.com/document',
    'http://www.facebook.com',
]

url_trie = trie.Trie()

for url in url_list:
    url_trie[url] = url 

searches = ("http", "http://www.go", "http://www.fa", "http://fail")

for search in searches:
    print "'%s' ->" % search, longest_prefix_match(url_trie, search)

结果:

'http' -> ['http://www.facebook.com', 'http://www.google.com/document', 'http://www.google.com/mail']
'http://www.go' -> ['http://www.google.com/document', 'http://www.google.com/mail']
'http://www.fa' -> ['http://www.facebook.com']
'http://fail' -> []

或使用PyTrie 给出相同的结果,但列表的顺序不同。

from pytrie import StringTrie


url_list = [ 
    'http://www.google.com/mail',
    'http://www.google.com/document',
    'http://www.facebook.com',
]

url_trie = StringTrie()

for url in url_list:
    url_trie[url] = url 

searches = ("http", "http://www.go", "http://www.fa", "http://fail")

for search in searches:
    print "'%s' ->" % search, url_trie.values(prefix=search)

我开始认为从内存使用的角度来看,radix tree / patricia tree 会更好。这就是基数树的样子:

而 trie 看起来更像:

【讨论】:

  • 非常感谢您的回复,但我不是在寻找一种正则表达式的解决方案,因为它随着不同 URL 数量的增加而无法扩展。我正在寻找的是基于 Trie 的最长前缀匹配解决方案,其中字符串是 URL。
  • 可能return ''在这里比raise Exception更合适
  • @Amit 很公平。您如何存储 URL 列表?如果您将它们放在为 URL 编制索引的数据库中,则可能会提供一种简单有效的解决方案。 @J.F.塞巴斯蒂安,好的,谢谢。
  • @Stephen 我没有将它们存储在数据库中,我有一个 URL 列表,其中包含与之关联的唯一随机数,现在我想将它存储在 trie 中,然后匹配一个新 URL并找出最接近的前缀匹配
【解决方案3】:

如果您愿意用 RAM 换取时间性能,那么SuffixTree 可能会有用。它具有很好的算法属性,例如它允许在线性时间内解决最长的公共子串问题。

如果您总是搜索前缀而不是任意子字符串,那么您可以在填充 SubstringDict() 时添加唯一前缀:

from SuffixTree import SubstringDict

substr_dict = SubstringDict()
for url in URLS: # urls must be ascii (valid urls are)
    assert '\n' not in url
    substr_dict['\n'+url] = url #NOTE: assume that '\n' can't be in a url

def longest_match(url_prefix, _substr_dict=substr_dict):
    matches = _substr_dict['\n'+url_prefix]
    return max(matches, key=len) if matches else ''

SuffixTree 的这种用法似乎不是最理想的,但它比@StephenPaulger's solution [基于.startswith()] 的数据快 20-150 倍(没有SubstringDict() 的构建时间)我已经尝试过并且它可能已经足够好了。

要安装SuffixTree,运行:

pip install SuffixTree -f https://hkn.eecs.berkeley.edu/~dyoo/python/suffix_trees

【讨论】:

  • 到目前为止,这是一个比我更好的解决方案。遗憾的是标准 python 库中没有树实现。 SuffixTrees 是否可以序列化或生成它们的速度如此之快以至于重新创建它们都没有关系?
  • @Sebastian 这个库正在做后缀匹配而不是前缀,这是我需要的。我不确定如何将它用于我的任务,所以如果我使用这些 st['abcd.com/']=1,st['abcd.com/news']=2,st['abcd.com/sports']=3 构建后缀树(st),然后搜索'abcd.com' 通过执行 st['abcd.com'] 我得到 {1,2,3} 作为答案,而它应该只返回 1 作为前缀匹配
  • @StephenPaulger:SuffixTreestrmat C 库的简单 SWIG 包装器。我怀疑是否有序列化支持,但它肯定会有用。 SuffixTree 解决方案如果您从一开始就为每个新的 url_prefix 重新生成它,则没有意义。
  • @Amit: SubstringDict 进行 substring 匹配。您可以使用我的回答中明确指出的唯一前缀,例如 '\n'。使用 Markdown 格式(单击“添加评论”下的 help)以避免损坏代码片段。
  • @Sebastian :感谢您的帮助,但您提到的方法对于“前缀”匹配失败
【解决方案4】:

性能对比

suffixtree vs. pytrie vs. trie vs. datrie vs. startswith -函数

设置

记录的时间是 3 次重复 1000 次搜索中的最短时间。包含一个 trie 构建时间并在所有搜索中传播。搜索是对从 1 到 1000000 个项目的主机名集合执行的。

搜索字符串的三种类型:

  • non_existent_key - 没有匹配的字符串
  • rare_key - 大约百万分之二十
  • frequent_key - 出现次数与集合大小相当

结果

一百万个 url 的最大内存消耗:
| function    | memory, | ratio |
|             |     GiB |       |
|-------------+---------+-------|
| suffix_tree |   0.853 |   1.0 |
| pytrie      |   3.383 |   4.0 |
| trie        |   3.803 |   4.5 |
| datrie      |   0.194 |   0.2 |
| startswith  |   0.069 |   0.1 |
#+TBLFM: $3=$2/@3$2;%.1f

要重现结果,run the trie benchmark code

  • rare_key/nonexistent_key 情况

    如果 url 的数量小于 10000,那么 datrie 是最快的,对于 N>10000 - suffixtree 更快,startwith 平均明显更慢。

  • 轴:

    • 垂直(时间)刻度约为 1 秒(2**20 微秒)
    • 横轴显示每种情况下的网址总数:N= 1、10、100、1000、10000、100000 和 1000000(一百万)。

  • 频繁键

    最多 N=100000 datrie 是最快的(对于一百万个网址,时间是 由 trie 构建时间主导)。

    在找到的匹配项中查找最长的匹配项花费的时间最多。所以所有函数的行为都与预期相似。

startswith - 时间性能与密钥类型无关。

triepytrie 的行为彼此相似。

无需尝试构建时间的性能

  • datrie - 最快、体面的内存消耗

  • startswith 在这里更加不利,因为其他方法不会因构建 trie 所需的时间而受到惩罚。

  • datrie, pytrie, trie - 对于稀有/不存在的密钥几乎 O(1)(恒定时间)

拟合(近似)已知函数的多项式以进行比较(与图中相同的对数/对数标度):

| Fitting polynom              | Function          |
|------------------------------+-------------------|
| 0.15  log2(N)   +      1.583 | log2(N)           |
| 0.30  log2(N)   +      3.167 | log2(N)*log2(N)   |
| 0.50  log2(N)   +  1.111e-15 | sqrt(N)           |
| 0.80  log2(N)   +  7.943e-16 | N**0.8            |
| 1.00  log2(N)   +  2.223e-15 | N                 |
| 2.00  log2(N)   +  4.446e-15 | N*N               |

【讨论】:

  • 优秀的比较。我能问一下您使用什么工具来衡量统计数据和制作图表吗?我希望我有时间和精力来实现基数树,以便可以将其包含在比较中。
  • @StephenPaulger:timeit 模块用于测量统计数据,make-figures.py 脚本用于绘制简单的*.xy 文件(例如,pytrie-non_existent_key-7-1000000.xy)。该脚本起源于Compare sorting algorithms' performance
  • @MikhailKorobov:我需要longest_match() 函数进行比较。 Is it correct usage of datrie?
  • @MikhailKorobov:我已经抛弃了the code for the benchmark at github。你可以自己运行它。我会看看我是否设法调试计时过程(我不明白结果)然后我会用新结果更新答案。
  • @MikhailKorobov:我想通了。旧结果对应于上面的“没有尝试构建时间的性能”案例。我添加了datrie 进行比较。
猜你喜欢
  • 2013-06-03
  • 1970-01-01
  • 1970-01-01
  • 2013-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-11
相关资源
最近更新 更多