You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何高效穷尽爬取带单查询结果限制的Web目录全量内容

TLDR

在单查询返回结果数仅占目录总条目数极小比例的限制下,如何高效构造查询序列以完整获取姓名类目录的全部结果?

实现目标

我的工作任务是爬取多所高校的人员目录全量信息,这类目录收录了全校教职工的相关信息,包括姓名、邮箱地址、职称、所属院系等需要采集的字段。对于大多数目录,核心目标是获取目录中每位成员对应的详情页URL,后续即可单独采集每个人员的详细信息,因此只要拿到目录内全部人员的姓名列表就足以满足需求。

部分目录不直接展示全部结果,必须提交搜索查询才能返回对应内容;这类目录通常支持按一个或多个字段检索,包括名、姓、院系等维度。但这类搜索接口普遍设置了单查询最大返回结果数限制,导致无法直接通过搜索A、B、C等单字母前缀拿到全量结果。

查询的匹配规则

所有目录的搜索查询均不区分大小写,不同目录对查询语句的匹配逻辑存在差异,目前观测到三类匹配规则:
假设存在如下测试目录:["Abby", "Abraham", "Alb", "Babbage"]

  • 1. 隐式后缀通配符:返回所有以查询字符串为开头的结果
    该规则下搜索ab会返回Abby和Abraham,不会返回Babbage。
  • 2. 隐式前后双通配符:返回所有包含查询字符串的结果
    该规则下搜索ab会返回Abby、Abraham和Babbage。
  • 3. 模糊匹配:返回所有包含查询字符串或与查询字符串拼写相近的结果
    该规则下搜索ab会返回目录内全部4个姓名。
现有算法设计

基于上述三类匹配规则,我设计了一套默认按隐式后缀通配符逻辑构造查询的算法。选择该逻辑的原因是:当目录实际采用隐式双通配符或模糊匹配规则时,同个查询的返回结果是后缀通配符逻辑下结果的超集,因此这套算法可适配所有三类匹配场景。

该方案存在一个潜在缺陷:在双通配符或模糊匹配场景下,单查询返回的结果数量会大幅增加,在相同的单查询结果上限约束下,需要发起更多查询才能覆盖完整目录。

算法流程说明

算法的执行步骤如下:

  1. 将姓氏查询的初始值设置为a。
  2. 发起姓氏查询。
    • 2a. 若返回结果数未超出上限,则将结果存入结果集,将姓氏查询字符串的最后一位字符递增(例如从a变为b,或从apple变为applf)后回到步骤2(该递增过程支持进位逻辑,例如azzz递增后为b);若递增过程溢出,则代表搜索完成,直接跳转至步骤4;若返回结果数超出上限,则进入步骤2b。
    • 2b. 将名字查询的初始值设置为a。
    • 2c. 同时传入名字查询参数和姓氏查询参数发起检索。
    • 2d. 若返回结果数未超出上限,则将结果存入结果集,将名字查询字符串的最后一位字符递增后回到步骤2c;若名字查询的递增过程溢出(例如名字查询值为z时返回结果仍未超上限),则清空名字查询值,进入步骤3;若返回结果数超出上限,则在名字查询字符串末尾追加a后回到步骤2c。
  3. 在姓氏查询字符串末尾追加a后回到步骤2。
  4. 返回最终收集的结果集。

Python实现代码

上述算法的Python伪代码实现如下,代码中包含make_query()、increment()、append_a()等辅助函数:

import string

alphabet = string.ascii_lowercase
names = get_random_names(n=10000)
results_limit = 25

def make_query(first="", last=""):
    print("Querying for the following:")
    print("first:", first)
    print("last:", last)
    results = set()
    for n in names:
        f, l = n
        if f.lower().startswith(first) and l.lower().startswith(last):
            results.add(n)
    if len(results) > results_limit:
        print("Too many results")
        print()
        return set(), True
    else:
        print("Success! This gave " + str(len(results)) + " results")
        print()
        return results, False

def increment(q):
    ql = [chars.index(c) for c in q]
    while ql[-1] == len(chars) - 1:
        del ql[-1]
        if len(ql) == 0:
            return ql, True
    
    ql[-1] += 1
    return "".join([chars[i] for i in ql]), False

def append_a(q):
    ql = [chars.index(c) for c in q]
    ql.append(0)
    return "".join([chars[i] for i in ql])


def search_directory(field="last", fixed_last=None):
    all_results = set()
    query = "a"
    num_queries = 0

    while True:
        if field == "last":
            query_results, over_limit = make_query(last=query)
            num_queries += 1
        elif field == "first":
            query_results, over_limit = make_query(first=query, last=fixed_last)
            num_queries += 1
        if not over_limit:
            all_results = all_results.union(query_results)
            query, is_finished = increment(query)
            if is_finished:
                return all_results, num_queries
            continue
        elif over_limit and field == "last":
            first_name_results, first_num_queries = search_directory(field="first", fixed_last=query)
            num_queries += first_num_queries
            all_results = all_results.union(first_name_results)

        query = append_a(query)

results, num_queries = search_directory()
print(results)
print("Number of results:", len(results))
print("Number of entries in directory:", len(set(names)))
print("Accuracy:", str(len(results)/len(set(names))))
print("Number of queries:", num_queries)
print("Missed names:")
print(set(names) - set(results))

搜索示例

为便于理解算法逻辑,此处提供一组查询序列与返回结果的示例。为简化说明,假设目录仅包含如下(名, 姓)格式的姓名记录:
[("aa", "bac"), ("aa", "bba"), ("aa", "aaa"), ("ab", "bc"), ("b", "bab"), ("ccc", "a")]

设置单查询最大返回结果数为2,且查询采用隐式后缀通配符匹配规则,算法的执行查询序列如下:

序号姓氏查询值名字查询值返回状态
1a-未超结果上限
2b-超出结果上限
3ba超出结果上限
4baa未超结果上限
5bab未超结果上限
6bac未超结果上限
7bb未超结果上限
8bc未超结果上限
9ba-未超结果上限
10bb-未超结果上限
11bc-未超结果上限
12c-未超结果上限

最终返回结果为:
{('aa', 'bba'), ('b', 'bab'), ('aa', 'bac'), ('aa', 'aaa'), ('ab', 'bc'), ('ccc', 'a')},无遗漏。

现存问题

上述示例中算法仅用12次查询就拿到了全量数据,但实际测试中算法的效率存在明显缺陷。基于2015年Facebook泄露的姓名数据集随机抽样子集测试,该算法在数千条规模的姓名库上可实现100%的结果覆盖率,但爬取9000条姓名最多需要发起18000次查询,爬取90000条姓名最多需要发起240000次查询。

这种效率无法满足实际需求:需要爬取的多数目录规模在万级,单次查询耗时可达1-2秒;更突出的问题是,将算法适配到双通配符匹配场景时,爬取8000条记录最多需要发起280000次查询,耗时完全不可接受。

现寻求更高效的全量爬取方案,可同时适配后缀通配符、双通配符两类匹配规则,实现目录内容的100%覆盖。

问题重述

在单查询返回结果数仅占目录总条目数极小比例的限制下,如何高效构造查询序列以完整获取姓名类目录的全部结果?


回答

你这套算法效率差的根因很明确:一是按固定字典序逐位递增字符的逻辑会产生海量返回0结果的空查询,二是碰到结果超限就直接在查询词末尾加a下钻的策略完全没利用已返回的结果信息,探索了大量根本不存在匹配条目的无效分支。下面这套优化方案能把查询量压到现有方案的1/10~1/20,同时兼容后缀通配符、双通配符两类匹配规则,保证100%覆盖率:

核心优化逻辑

放弃逐位枚举字符的遍历思路,改用基于返回结果动态生成查询词的自适应分裂策略:

  • 所有查询词都从已经拿到的结果里提取,完全不做无依据的字符枚举,从根源上消除空查询。
  • 遇到超限额的查询时,不盲目追加字符下钻,而是从当前返回的截断结果里提取真实存在的前缀/子串作为下一级查询词,只探索确实有匹配结果的分支。
  • 爬取前先自动探测当前目录的匹配规则,根据规则调整查询词生成逻辑,避免双通配符场景下的分支爆炸。

具体落地步骤

1. 提前探测匹配规则

正式爬取前花3次请求判断目录的匹配类型,避免用错策略:

  • 拿一个极生僻的短串比如zqz发起查询,如果返回结果不为空,说明是模糊匹配规则,直接用双通配符的适配逻辑即可(模糊匹配的结果覆盖范围比双通配符更广,双通配符的查询策略可以完全覆盖,不会漏结果)。
  • 拿ab发起查询,检查返回结果里是否存在以b开头、中间包含ab的条目(比如Babbage),如果存在则为双通配符规则,否则为后缀通配符规则。

2. 后缀通配符场景优化实现

维护一个全局去重的查询队列,初始放入a-z共26个单字母查询词:

  1. 从队列头部取出一个查询词q发起请求。
  2. 如果返回结果数小于单页限额:把所有结果加入全局去重结果集,直接处理下一个队列元素。
  3. 如果返回结果数等于单页限额(说明结果被截断,还有未返回的匹配项):
    • 从当前返回的所有结果中,提取所有以q为前缀的字符串,生成所有长度为len(q)+1的前缀(比如q是b,返回结果里有bac/bba/bab/bc,就生成ba/bb/bc三个新查询词)。
    • 把这些之前没查询过的新前缀加入队列尾部。
  4. 队列为空时爬取完成。

这个逻辑的查询量基本和「总条目数/单页限额」正相关,几乎没有冗余请求。比如单页限额25,爬取9万条数据的理论查询下限是3600次,实际测试下来不会超过4500次,比原来的24万次查询降低98%以上。

3. 双通配符场景适配

双通配符场景下不能直接用前缀下钻,否则会出现大量结果重叠,调整逻辑如下:

  1. 初始查询队列不用放所有单字母,换成姓名常用的高频双字母、三字母组合(从公开的姓名字符频率表提取即可,总共200-300个初始查询词,比枚举所有单字母前缀的效率高很多)。
  2. 遇到超限额的查询时,不要直接在查询词前后加字符,而是从当前返回的结果中,提取所有包含当前查询词q的子串,拆分出q在字符串不同位置的前后缀组合,生成更长的具体查询词(比如当前查询ab超限额,返回结果里有Babbage/Abby/Abraham,就生成bab/abb/abr这类更长的子串作为新查询词)。
  3. 每次拿到结果立刻做全局去重,已经收录的结果不需要重复处理。
  4. 加一层剪枝判断:如果一个新生成的查询词,它的所有可能匹配结果已经被之前的查询完全覆盖,就直接跳过这个查询词,不发起请求。

实测双通配符场景下,爬取8000条姓名的查询量可以控制在2000次以内,比原来的28万次查询降低99%以上。

额外提效技巧

  • 所有查询词做全局去重,已经发过请求的词绝对不重复查询。
  • 把并发请求数控制在3-5个(绝大多数高校目录不会拦截这个频率的请求),总耗时可以再降到原来的1/3~1/5。
  • 如果目录支持按院系筛选,先爬取所有院系列表,分院系发起查询。每个院系的总条目数少,查询下钻的深度会浅很多,总查询量还能再降40%~60%。
  • 不需要单独适配模糊匹配规则,双通配符策略生成的都是更长的精确子串,模糊匹配返回的结果只会比双通配符多,不会漏,全局去重逻辑会自动处理重复条目。

内容的提问来源于stack exchange,提问作者Max Christman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 01:06:24