新闻采集字段坑:source/time 是解析文本,不是结构化字段新闻端点有个和其他端点不一样的坑:source(来源媒体)和time(发布时间)不是 Google 直接给的结构化字段,而是解析器从一段紧凑的元数据文本里抠出来的。文档原话:“Source and time are parsed from compact Google metadata text, so clients should treat both as optional.”翻译成工程语言:你脚本里item[source]和item[time]直取,某天就会 KeyError;更隐蔽的是你用.get()兜底拿到了空字符串,然后在来源媒体数量“发布时效分布这类统计里,把解析失败算成了没有来源”。这篇把这个坑讲透,并给一个带填充率监控的采集脚本。字段口径以 SerpBase 官方文档的 news 端点字段表 为准;每次成功请求 1 credit。先说路径:是 /google/news/search端点路径是POST /google/news/search,不是/google/news。和 image/search、video/search 一个路子——单数资源名 /search。如果你从旧教程里抄的路径,先改过来。入参四个:q必填,hl/gl/page可选(默认 en/us/1)。没有 device 参数,那是搜索端点独有的。返回信封里search_type会解析成news,可以拿来断言路由对不对。字段口径:三个必填,其余全靠解析NewsResult 里必填的只有rank、title、link三个。其余全是可选,而且可选的理由分两类:字段可选的原因能不能指望positionrank 的别名,“when available”能用 rank 就别用url归一化后的规范链接一般有,没有就用 linksource_url原始 Google 跳转 URL,调试/溯源用别依赖display_url从展示文本或域名推导别依赖source“when publisher text is parsed”不能time“when Google exposes time text”不能published_at解析到 time 时的归一化别名跟随 timesnippet“when a snippet is parsed”新闻卡常常没有thumbnail_url/thumbnailGoogle 暴露缩略图时别依赖顶层news数组本身也是可选的——解析到新闻结果时才有。零结果时可能整个键缺失,所以必须data.get(news, [])。source 和 time 为什么不可靠新闻卡片在 Google 里的呈现是一行紧凑文本,大概长这样:“媒体名 · 3 小时前 · 来源域名”。解析器要从这一行里切出媒体名和时间文本。这里每一环都可能断:Google 对某些结果不渲染这行元数据(比如聚合页、视频新闻)元数据行的格式随布局变化,解析规则没覆盖到就整条丢时间文本是给人看的相对时间(“3小时前”“昨天”),不是机器时间;published_at只是它的归一化别名,解析不出 time 就没有 published_at所以这两个字段的正确用法是:有就用,没有就记空,统计时单独看填充率。把它们当一定有来写代码,等于把解析器的上游依赖藏进了你的数据管道——出问题那天,你看到的现象是媒体数量突然少了,而真实原因是解析失败率突然高了。带填充率监控的采集脚本importcsv,json,pathlib,requestsfromdatetimeimportdate APIhttps://api.serpbase.dev/google/news/search# 注意路径KEY你的 API KeyHEADERS{X-API-Key:KEY,Content-Type:application/json}TERMS[人工智能,新能源汽车,半导体]defnews(q:str,hl:strzh-CN,gl:strcn,page:int1)-list:resprequests.post(API,headersHEADERS,json{q:q,hl:hl,gl:gl,page:page},timeout30)dataresp.json()ifdata.get(status)!0:print(f{q}p{page}: status{data.get(status)}rid{data.get(request_id)})return[]returndata.get(news,[])# 数组本身可选,必须兜底defcollect(terms:list)-list:rows[]forqinterms:forpagein(1,2):itemsnews(q,pagepage)ifnotitems:break# 空页即停,别继续翻foritinitems:rows.append({词组:q,页:page,位次:it[rank],# 必填,直取标题:it[title],# 必填,直取链接:it[link],# 必填,直取来源:it.get(source,),# 解析字段,兜底时间文本:it.get(time,),# 解析字段,兜底发布时间:it.get(published_at,),# 别名,跟随 time摘要:it.get(snippet,),})returnrowsdeffill_report(rows:list)-dict:来源/时间的填充率,和数据一起落盘。nlen(rows)or1return{总条数:len(rows),来源填充率:round(sum(1forrinrowsifr[来源])/n,3),时间填充率:round(sum(1forrinrowsifr[时间文本])/n,3),去重来源数:len({r[来源]forrinrowsifr[来源]}),}if__name____main__:rowscollect(TERMS)daydate.today().isoformat()withopen(fnews_{day}.csv,w,newline,encodingutf-8-sig)asf:wcsv.DictWriter(f,fieldnameslist(rows[0].keys())ifrowselse[词组,页,位次,标题,链接,来源,时间文本,发布时间,摘要])w.writeheader()w.writerows(rows)reportfill_report(rows)pathlib.Path(fnews_fill_{day}.json).write_text(json.dumps(report,ensure_asciiFalse,indent1),encodingutf-8)print(f{len(rows)}条 → news_{day}.csv)print(f填充率:{report[来源填充率]:.0%}来源 /{report[时间填充率]:.0%}时间)关键点是fill_report:来源/时间填充率必须和数据分开落一份。下次看到去重来源数从 30 掉到 12,先翻填充率文件——如果是 95% 掉到 60%,那是解析退化,不是行业变天。三条实战经验时间字段只做粗聚合,别做时间窗过滤。time是3小时前这种文本,published_at是它的归一化别名但同样依赖解析成功。做周报按天粒度聚合可以用;要精确到最近 24 小时,得用你自己记录请求那一刻的时间戳做上界,再用 time 文本做人工判断。别写WHERE published_at ...这种查询,底层数据撑不住。来源去重前先确认来源字段解析成功了。把来源为空当独立一类统计出来,别让它混进未知来源。前者是数据质量问题,后者是真实的来源分布。翻页到空页就停。news数组可选,空页可能就是这页没解析到结果,继续翻只是白烧 credits。1 credit 一页,10 个词组翻 5 页就是 50 credits,够跑半个月日报。FAQ路径到底是 /google/news 还是 /google/news/search?文档写的是/google/news/search。如果旧代码用的/google/news还能跑,也别依赖——以文档为准改过来。snippet 为空正常吗?正常。它是可选字段,解析到摘要时才有;新闻卡的标题来源行往往就是全部内容,没有摘要。thumbnail 能直接当封面用吗?和图片端点一样有防盗链和可达性问题,thumbnail_url只是 Google 暴露的地址。落库记着,展示层先校验再决定是否代理。为什么搜索端点有 device 参数新闻没有?文档里 news 的入参就是 q/hl/gl/page 四个,device 属于搜索端点。别把搜索端点的参数习惯带到新闻端点。把fill_report加进你现有的新闻采集流程——它不改变你采到什么,只让你知道采到的东西里有多少是真的。