做 POI 相关项目的人都会遇到同一个坎:代码写完了,逻辑也通了,结果跑了两天发现配额没了,数据只抓了三分之一。百度地图的地点检索服务在处理"按名称获取 POI"这类需求时特别好用,但它的配额限制、单次返回上限、分页规则这几条约束叠在一起,会把很多看起来理所当然的做法直接堵死。这篇文章想聊的就是在配额限制下利用百度地图按名称获取 POI的完整思路和实操细节。我把这两年做门店数据、加油站数据、充电桩数据积累下来的经验整了一遍,包括配额怎么算、关键词怎么打包、网格怎么切、缓存怎么做、报错怎么查。适合正在做 POI 数据集、门店地理信息补全、竞品分布分析的朋友,也适合刚接触百度地图开放平台、准备写第一个采集脚本的初学者。不涉及任何绕过平台规则的手段,全部是在服务条款允许范围内的工程优化。
1. 先把配额这件事算明白,再谈怎么省
1.1 配额分三层:日调用量、并发量、权限范围
很多人一上来就写代码,跑到报错才开始翻文档,结果发现配额根本不是"一个数字"那么简单。百度地图开放平台的 Web 服务 API 配额,实际要拆成三个维度来看。
第一层是日调用量配额。这个是最直观的,每天零点重置,用完了当天就没了。不同服务(地点检索、地理编码、逆地理编码、路线规划)的配额池是分开计算的,这一点特别重要——很多新手以为所有服务共享一个总配额,结果不敢用地理编码来辅助纠偏,白白浪费了精度。实际上你只调地点检索,就不会被其他服务的调用挤占。
第二层是并发配额,也就是 QPS。同一时刻允许在途的请求数超了就直接拒绝,返回的是并发超限类的错误码。这里有个细节:并发超限返回的错误一般不消耗你的日配额,但会浪费一次网络往返和一次重试窗口,所以限流器该做还是要做。
第三层是权限范围。未认证、个人认证、企业认证,能调用的服务不一样,配额等级也不一样。如果你做的是商业项目,走企业认证把配额拉上去,是最正当也最稳定的路线,比任何"技术手段"都靠谱。
我的建议是把这三层写进配置文件,启动脚本时先打印一遍,让每次跑批之前心里有数。见过太多次"跑了六个小时,天亮发现配额早爆了"的场面,全都是因为没人算这一笔账。
1.2 400 条天花板:为什么你的结果永远抓不全
地点检索有两个硬约束,几乎所有采集失败都跟它们有关。
page_size最大只能给到 20;page_num从 0 开始,且page_num × page_size的结果不能超过 400。
也就是说,同一个检索条件(同样的 query + 同样的 region 或 bounds),最多只能拿回 400 条。你在城市级别检索"餐厅",一座二线城市可能有几万家,但接口在第 20 页之后就不给数据了。这时候你会看到一个很迷惑的现象:total字段显示一个很大的数字,但你翻到第 19 页就是空的。
这个限制直接决定了整个采集架构的形态——必须做空间分片。分片的目的不是"提高精度"这种虚话,而是让每一片的检索结果数量都落在 400 以内,这样每一片才可能被完整取回。
| 分片方式 | 参数 | 适用场景 | 能否突破 400 |
|---|---|---|---|
| 行政区划检索 | region | 省级、市级粗筛,总量摸底 | 不能,市级别基本必超 |
| 圆形区域检索 | location+radius | 商圈、站点周边、以点为中心 | 能,半径缩小即可 |
| 矩形区域检索 | bounds | 规则网格切分、批量跑批 | 能,格子缩小即可 |
从上表能看出来,真正适合做批量采集的是矩形检索,因为它的切分逻辑最规整,容易做成四叉树递归。圆形检索适合做"某个坐标周围多少米内有什么"这类补漏,不属于主力。
1.3 AK 类型与调用方式的选择
服务端 AK 和浏览器端 AK 是两套东西,别搞混。服务端调用可以直接带ak参数请求,建议同时配上 IP 白名单;浏览器端调用必须走 SN 校验,因为 AK 会出现在前端代码里,暴露是无法避免的。
关于"多 AK 分摊"这件事,我态度很明确:同一个账号下建多个应用、按业务模块分开管理 AK 是合理的工程实践,方便做用量归因和故障隔离。但通过批量注册账号来绕开配额,属于明确违反平台规则的行为,账号回收、服务中断只是时间问题。做长期项目的人应该把精力放在算法和缓存上,而不是这种一次性收益上。我自己在项目里的做法就是:把配额当成一个硬约束条件写进设计文档,所有优化都在这个约束下做,跑得慢一点没关系,跑得稳才重要。
2. 按名称检索:接口怎么调、参数怎么配
2.1 三种检索方式,先搞清楚各自的语义
百度地点检索的 v2 接口路径是/place/v2/search,三种检索方式共用同一个入口,靠参数区分:
- 行政区划区域检索:传
region,可以是城市名("杭州市")也可以是行政区划编码。它是"检索这个行政区内符合条件的 POI",返回结果会跨区边界。 - 圆形区域检索:传
location=lng,lat和radius(单位米,1 到 50000)。语义是"以这个点为中心、半径多少米内"。 - 矩形区域检索:传
bounds=左下纬,左下经,右上纬,右上经。注意顺序是纬度在前、经度在后,而且必须先左下再右上。
这里有个特别容易踩的坑:bounds参数顺序写反了不会报错,接口会照常返回 200,但结果是一堆莫名其妙的点,或者干脆返回空。我见过有人排查了一整天,最后发现是纬度经度写倒了。建议在客户端封装里加一层断言,lat值必须在 -90 到 90 之间,lng在 -180 到 180 之间,写错立刻抛异常。
还有一个认知偏差需要纠正:矩形检索不是严格裁剪。它返回的是"与该矩形相关"的结果,边界附近、甚至矩形外一点的点也可能出现。所以拿回来的数据必须做一次本地裁剪,不能直接入库。
2.2 用符号一次性打包多个关键词——省配额的第一招
这是整个方案里性价比最高的技巧。地点检索的query参数支持多关键词并集检索,不同关键词之间用$符号分隔,最多支持 10 个关键字。
举个例子,你要抓某市的加油站,本来可能要分四轮:
query=加油站 query=中国石化 query=中国石油 query=壳牌加油站改成一次请求:
query=加油站$中国石化$中国石油$壳牌加油站一次请求拿回四类结果,配合本地按关键词归类,等于把 4 次配额消耗压缩成 1 次。如果你的关键词组有 8 组,那就是 8 倍效率差。对于日配额只有几千次的账号来说,这个差别直接决定项目能不能在一天内跑完。
这里必须提醒一句:有些旧资料、二手博客上写的是竖线|分隔,那是其他地图平台的写法。百度地点检索官方文档用的是$。这类细节一定要翻当前版本的官方文档确认,照抄博客的代价往往是"请求成功但结果完全不对",比报错还难排查。
多关键词打包还有几个使用禁忌,我踩过:
- 别把量级差太多的词放一起。"医院$奶茶店"这种组合,奶茶店的结果会把 400 条上限迅速填满,医院反而一条都拿不到。同一组关键词的 POI 密度应该在一个量级上。
- 返回结果不区分来源关键词。接口只是把并集给你,不告诉你是哪个词命中的。所以本地必须做匹配判断,判断逻辑后面第 4 节会讲。
- 关键词数量别打满 10 个。留一两个位置给后续补漏,因为实际跑的时候经常发现某个子类目漏了。
2.3 tag 与 query 的组合用法
tag是分类偏好参数,它不决定"返不返回",但决定"排前面的返回什么"。当query比较泛(比如只写"银行")的时候,加一个tag=银行能让结果更贴近你要的分类。
我的经验用法是:query 负责精准命中,tag 负责兜底召回。比如抓"社区支行",query 写"社区支行",tag 写"银行",这样既拿得到精准名称匹配的结果,也不至于因为命名差异漏掉一堆。
注意tag和query不能同时为空,至少给一个。另外tag有优先级,给了 tag 之后排序会明显偏向该分类,翻页的时候顺序会变,做增量更新时要留意这一点。
2.4 返回字段里真正有用的那几个
接口返回的字段不少,但真正要落库的不多。我一般只留这些:
| 字段 | 说明 | 是否作为主键 |
|---|---|---|
uid | POI 唯一标识 | 是,优先使用 |
name | 名称 | 否,但用于关键词归类 |
location.lat/lng | 坐标,默认 bd09ll 坐标系 | 否 |
address | 地址 | 否 |
province/city/area | 行政区信息 | 否 |
detail_info.tag | 分类标签 | 否,用于交叉验证 |
detail_info.type | 分类编码 | 否 |
street_id | 街景/道路 ID | 否 |
关于scope参数:scope=1返回基础信息,scope=2会带上评分、营业时间、图片这类详情。看起来 scope=2 更划算,但实际不是——详情字段体积大,解析慢,而且很多 POI 本来就没有这些数据。批量采集阶段建议一律用scope=1,等筛出目标 POI 之后,再对少量关键对象单独调详情接口补全。
还有一个隐藏的成本点:详情接口也消耗配额。别对着几万个 POI 无脑补详情,那等于把采集阶段的配额翻倍消耗掉。
3. 省配额的三个核心架构设计
3.1 缓存分三层:请求前先问本地
缓存这件事听起来老生常谈,但在配额受限的场景下,它的收益是数量级的。
我在项目里一般做三层:
- 第一层:进程内 LRU 缓存。同一个 key 在同一进程里重复出现,直接返回内存对象,连网络都不走。适合处理关键词分组时出现的重复组合。
- 第二层:请求日志表。以
(query_group, grid_id, page_num)作为唯一键,记录这次请求的结果条数和时间戳。任何一次新的请求发起之前先查这张表,命中且未过期的直接读库。POI 数据变化很慢,7 天内重复检索同一个网格基本是纯浪费。 - 第三层:POI 主表。以
uid为唯一键做去重,这是最终的数据资产。
第二层是省配额的关键,因为它拦掉的是"完全相同参数的重复请求"。实际跑批时,网格切分产生的重叠区域、多天补跑、调试重跑,全部会在这一层被拦掉。我做过一个对比:加了这层日志之后,同样的采集量,配额消耗下降了大约四成。
3.2 空间分片:四叉树动态细分
固定切网格的做法很浪费。城市建成区 POI 密集,郊区稀疏,你用统一大小的格子切,密集区格子不够用(超过 400 条),郊区格子又太多(每格只有几条结果,白花配额)。
四叉树动态细分的逻辑是这样的:
- 先拿一个覆盖目标范围的外接矩形;
- 用矩形检索跑一次,读返回的
total; - 如果
total小于阈值(我一般设 380,不是 400),说明这一片一次能拿全,标记为完成; - 如果
total触及上限,把这个矩形四等分,对四个子矩形递归执行第 2 步; - 设置最大递归深度(我设 6 层),防止在极端密集区无限细分。
阈值为什么设 380 而不是 400?因为total是个估算值,而且翻页过程中结果集会动态变化,卡在 400 这个临界点上,翻到第 19 页很可能断在半路。留 20 条余量,实际测试下来稳定性明显更好。
这套做法比固定网格省多少?我实测过一个地级市,固定网格切了 1200 格,四叉树只用了 340 格左右,配额消耗差了 3 倍多。原因很简单:大片郊区一次就搞定了。
3.3 任务表 + 断点续跑
批量采集最怕的就是跑到一半中断。所以网格任务的状态必须持久化,不能放在内存里。
表结构大致这样:
CREATE TABLE grid_task ( grid_id TEXT PRIMARY KEY, bounds TEXT NOT NULL, -- 左下纬,左下经,右上纬,右上经 query_group TEXT NOT NULL, depth INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', -- pending / done / failed fetched INTEGER DEFAULT 0, total INTEGER DEFAULT 0, updated_at TEXT );有了这张表,第二天配额刷新之后直接SELECT * FROM grid_task WHERE status='pending',从断点继续跑,无需重新计算网格,也不会重复消耗已经完成的格子。
failed状态要单独处理,不要在循环里立刻重试,而是扫一遍看错误码分布:如果全是配额类错误,说明今天真的没额度了,直接停;如果混着网络超时,才针对性地重试那几个。
3.4 配额预算怎么估:先算后跑
这一步很多人跳过,结果就是跑着跑着发现不够。给个估算公式:
总请求数 ≈ 网格数 × 关键词组数 × 平均翻页数举个具体例子:一个中等城市四叉树细分后得到 300 个网格,关键词分了 8 组,平均每组每格翻 1.2 页,那么:
300 × 8 × 1.2 ≈ 2880 次请求如果日配额是 3000 左右,一天刚好能跑完,但要留出调试和补漏的空间,实际可能要拆成两天。如果日配额只有 1000,那就要重新设计关键词分组,或者把城市按区拆分,分多天完成。
这套"先算后跑"的习惯,能避免最尴尬的情况:跑了 8 个小时,配额定格在 97%,数据半残,既不能交付也不好续跑。算一遍只要五分钟。
4. 手把手:从零跑通一条按名称抓取 POI 的链路
4.1 目录结构与依赖
我把整个项目拆成六个模块,职责分明,方便单独调试:
poi_spider/ ├── config.py # AK、QPS、阈值、关键词组 ├── client.py # 接口封装、SN 计算 ├── limiter.py # 令牌桶限流 ├── grid.py # 四叉树切分与任务表管理 ├── cleaner.py # 名称匹配、坐标去重、边界裁剪 ├── storage.py # SQLite / MySQL 落库 ── main.py # 主流程编排依赖很简单,核心就一个 requests:
pip install requests如果要做重试和退避,可以加tenacity,不过我更倾向于自己写,因为重试策略里要区分错误类型,用现成库反而绕。
4.2 限流器:令牌桶 + 有选择的重试
限流器解决的是并发配额问题:
import time import threading class TokenBucket: def __init__(self, rate, capacity): self.rate = rate # 每秒补充的令牌数,对应 QPS self.capacity = capacity # 桶容量,允许的突发量 self.tokens = capacity self.lock = threading.Lock() self.last = time.time() def acquire(self, n=1): while True: with self.lock: now = time.time() delta = now - self.last self.tokens = min(self.capacity, self.tokens + delta * self.rate) self.last = now if self.tokens >= n: self.tokens -= n return need = (n - self.tokens) / self.rate time.sleep(max(need, 0.01))QPS 建议设成官方限制的 70% 到 80%。设满看起来效率高,但网络抖动导致的偶发超限会触发失败,失败的请求虽然不消耗日配额,却会拖慢整体节奏,反而更亏。
重试策略是这里最容易写错的地方。必须区分错误类型:
- 网络超时、连接重置、5xx 服务器错误 → 值得重试,间隔 1s、2s、4s、8s,最多 4 次;
- 参数错误、权限错误、配额类错误 →不要重试,重试一百次结果也一样,只会白白烧掉重试窗口。
我见过最典型的错误写法是for i in range(3): try: ... except: pass,无脑重试所有异常。结果配额被"失败重试"烧掉一大半,日志里全是重复的失败记录,排查起来还特别费劲。
4.3 客户端封装与 SN 计算
import hashlib import requests from urllib.parse import quote class BaiduPlaceClient: HOST = "https://api.map.baidu.com" PATH = "/place/v2/search" def __init__(self, ak, sk=None, timeout=8): self.ak = ak self.sk = sk self.timeout = timeout self.session = requests.Session() def _gen_sn(self, params): if not self.sk: return None # 参数需按 key 排序后拼接,具体规则以官方 SN 校验文档为准 query = "&".join(f"{k}={params[k]}" for k in sorted(params)) raw = f"{self.PATH}?{query}{self.sk}" return hashlib.md5(quote(raw, safe="/:=&?").encode("utf-8")).hexdigest() def search(self, query, bounds=None, region=None, page_num=0, page_size=20, scope=1): params = { "query": query, "page_num": page_num, "page_size": page_size, "scope": scope, "output": "json", "ak": self.ak, } if bounds: params["bounds"] = bounds if region: params["region"] = region sn = self._gen_sn(params) if sn: params["sn"] = sn resp = self.session.get( self.HOST + self.PATH, params=params, timeout=self.timeout ) resp.raise_for_status() return resp.json()关于坐标系,服务端调用默认返回 bd09ll。如果你的业务数据是 WGS84,需要额外做坐标转换,这部分是独立的一次转换,不消耗接口配额,可以放心在本地批量处理。
4.4 网格切分与递归抓取
四叉树的核心就是两个函数,一个切分,一个递归。
def split_bounds(bounds): """把矩形四等分为四个子矩形""" lat_min, lng_min, lat_max, lng_max = bounds lat_mid = (lat_min + lat_max) / 2 lng_mid = (lng_min + lng_max) / 2 return [ (lat_min, lng_min, lat_mid, lng_mid), (lat_min, lng_mid, lat_mid, lng_max), (lat_mid, lng_min, lat_max, lng_mid), (lat_mid, lng_mid, lat_max, lng_max), ] def fetch_grid(client, query_group, bounds, page_size=20, threshold=380, depth=0, max_depth=6): """递归抓取一个网格,返回 POI 列表""" bounds_str = "{},{},{},{}".format(*bounds) all_poi = [] page_num = 0 total = None while page_num < 20: data = client.search( query=query_group, bounds=bounds_str, page_num=page_num, page_size=page_size, ) if data.get("status") != 0: raise RuntimeError(f"接口返回异常: {data}") results = data.get("results", []) if total is None: total = data.get("total", 0) if not results: break all_poi.extend(results) page_num += 1 # 结果触及上限,且还能继续细分,则四等分递归 if total is not None and total >= threshold and depth < max_depth: merged = [] for sub in split_bounds(bounds): merged.extend( fetch_grid(client, query_group, sub, page_size, threshold, depth + 1, max_depth) ) return merged return all_poi几个实操细节值得说:
total只取第一次响应的值。翻页过程中 total 会变,每次都用新值判断会导致同一片网格一会儿细分一会儿不细分。- 空结果页立刻 break。不要傻乎乎地把 20 页翻完,每翻一页都是一次配额消耗。我在早期版本里就犯过这个错,一个网格白翻了十几页空请求。
max_depth一定要设。极端密集的区域(比如商业综合体内部)递归下去会爆炸,深度 6 相当于把初始矩形分成 4096 份,实际项目里很少需要超过这个深度。
4.5 结果清洗:名称匹配、去重与边界裁剪
接口返回的是并集,本地必须做归类。我的做法是关键词表加一列正则规则:
| 目标类目 | 检索关键词 | 本地匹配规则 |
|---|---|---|
| 便利店 | 便利店$超市$小卖部 | 名称包含"便利""超市""商店" |
| 充电桩 | 充电桩$充电站$换电站 | 名称包含"充电""换电" |
| 药店 | 药店$药房$大药房 | 名称包含"药"且不含"医药公司" |
匹配规则里最常见的坑是过度包含。比如抓药店时"药"这个字会匹配到"医药科技有限公司""兽药经销部",这类必须用排除规则挡掉。更稳的做法是用返回的detail_info.tag做二次确认。
去重的逻辑分两级:有uid的用uid;没有uid的(部分数据会缺),用name+ 坐标四舍五入到小数点后 5 位拼成合成主键。四舍五入到 5 位大约是 1 米精度,足够区分不同门店,又能容忍坐标的微小抖动。
边界裁剪这块,最简单的做法是判断点的经纬度是否落在矩形范围内:
def in_bounds(lat, lng, bounds): lat_min, lng_min, lat_max, lng_max = bounds return lat_min <= lat <= lat_max and lng_min <= lng <= lng_max如果目标是行政区而不是矩形,就需要做点在多边形内的判断。数据量大时可以用射线法配合包围盒预筛,先判断外层包围盒,不在的直接丢弃,能省掉大量计算。
4.6 落库与增量更新
主表设计:
CREATE TABLE poi ( uid TEXT PRIMARY KEY, name TEXT, lng REAL, lat REAL, address TEXT, city TEXT, area TEXT, tag TEXT, source_query TEXT, first_seen TEXT, last_seen TEXT, missing_cnt INTEGER DEFAULT 0 );增量更新的思路:全量跑完之后,下一轮只更新last_seen,不重复插入。如果某条 POI 连续三轮(比如连续三个季度)都没再出现,把missing_cnt加一,超过 3 就标记为失效,从活跃数据里剔出去。这个机制能自动过滤掉已经关店的 POI,比人工维护省事得多。
source_query这列别省。它能告诉你这条 POI 是通过哪个关键词抓到的,后期做覆盖率分析、补充关键词的时候全靠它。
5. 报错与掉坑:常见问题速查
5.1 状态码对照表
| 状态码 | 含义 | 处理建议 |
|---|---|---|
| 0 | 成功 | 正常解析 |
| 1 | 服务内部错误 | 可重试,退避后重发 |
| 2 | 参数错误 | 检查 bounds 顺序、query 是否为空 |
| 3 | 权限校验失败 | 检查 AK 是否开通该服务、SN 是否正确 |
| 4 | 配额校验失败 | 检查日配额和并发,当天可能已用完 |
| 5 | AK 不存在或非法 | 检查 AK 拼写、是否被禁用 |
| 101 及以上 | 服务禁用、白名单、权限类 | 去控制台核对应用配置 |
上表只是常见分类,具体数值以官方控制台和文档为准。我在客户端里会把status非 0 的响应统一记一条日志,包含完整的请求参数,方便事后复现。
注意:
status=0不代表结果就是你要的。名称检索本质是模糊匹配,返回 200 但结果全不相干是很常见的情况,必须做本地过滤。
5.2 配额莫名其妙被跑满的三个原因
第一个原因是失败重试没有区分错误类型。这是最常见的。参数错误、配额错误被无脑重试三次,三次里没有一次可能成功,但请求数是实打实发出去的。
第二个原因是翻空页。分页循环没有遇到空结果就 break,硬翻到第 20 页。一个网格浪费 19 次请求,300 个网格就是 5700 次,直接把一天配额吃光。
第三个原因是定时任务和手工调试共用同一个 AK。半夜定时任务在跑,白天你在本地调试,两边同时消耗一个配额池。跑批之前把调试用的 AK 和生产的 AK 分开,或者至少在跑批期间暂停调试。
5.3 结果少了、重了、错了分别怎么查
结果偏少,按顺序排查三件事:一是total是否达到 400 上限,达到就说明该细分了;二是bounds的经纬度顺序有没有写反;三是关键词分组是不是把低密度类目和高密度类目混在了一起。
结果重复,通常是网格重叠导致的。检查四叉树切分时有没有边界重叠(我用的闭区间写法,相邻网格共享边界,边界上的点会被两边各返回一次),这种情况靠uid去重就能解决。
结果不相关,说明关键词太泛或者本地匹配规则太宽。两个动作:一是收紧匹配规则,加排除词;二是用detail_info.tag做交叉验证,标签不符的直接丢掉。
6. 几条我自己踩出来的经验
6.1 AK 别写死在代码里
本地调试图省事把 AK 硬编码进代码,然后代码一提交,AK 就进了版本历史。这东西删掉也还在 Git 记录里。用环境变量加配置文件的方式管理:
import os AK = os.environ.get("BAIDU_MAP_AK") QPS = float(os.environ.get("BAIDU_MAP_QPS", "3"))如果是前端页面接入,那就必须用浏览器端 AK 加 SN 校验,而且要在控制台配置好 Referer 白名单,把泄露风险控制在可接受范围内。
6.2 关键词的同义词陷阱
同一样东西在不同地区叫法完全不同。"便利店"在有些地方叫"小卖部""杂货店""士多","充电桩"和"充电站"在数据里是两个类目,"社区卫生服务中心"和"社区医院"也是两回事。
我的做法是维护一张关键词表,字段包括主类目、标准词、地区变体、排除词四列。每次开始新一轮采集之前,先拿一个小区块做小样本测试,看看召回率和准确率,再决定要不要全量跑。花二十分钟做这个测试,能省掉一整天的无效配额消耗。
6.3 数据使用的边界
有几条线我在项目里一直守着:不通过注册大量账号绕开配额、不做超出正常业务频率的压力式调用、不把原始数据对外转售、采集频率控制在合理范围。说这些不是为了讲道德,而是工程上的现实考虑——依赖规则漏洞的方案稳定性极差,今天能跑的脚本明天就可能全线失败,维护成本远高于把配额用足、把算法做精。
6.4 抓到什么时候该停手
纯 API 采集有它的边界。超长尾的小类目、名称极其不规范的个体店,靠名称检索的召回率就是上不去,再怎么优化关键词也就那样。
这时候我的做法是转向组合策略:用行政区划做粗筛拿到全量轮廓,用关键词穷举补细节,再抽 5% 的样本做人工核对,算一下整体覆盖率。如果覆盖率能到 85% 以上,对大多数业务场景就够了;硬追最后那 15%,投入产出比会非常难看。
另外提一个交叉验证的小习惯:从别的公开数据源抽一小批点位,看看在你的百度 POI 数据集里能不能找到对应的记录,找不到的比例就是召回率的一个粗略估计。这个方法比内部自查靠谱得多,因为它引入了外部参照。
最后分享一个小技巧,关于跑批时间的安排。把大批量任务放在后半夜执行,一方面避开你白天调试的时间段,另一方面网络的稳定性通常更好,重试次数更少。我自己的定时任务设在凌晨两点启动,跑完之后自动生成一份报表,包含请求总数、配额消耗比例、各网格的完成状态和异常统计。第二天早上看一眼报表就知道要不要调整关键词分组——这比跑到一半发现配额不够要好得多。