我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

DuckDuckGo多类型搜索接口实战:从参数配置到统一封装

DuckDuckGo多类型搜索接口实战:从参数配置到统一封装 0. 写在前面为什么我会盯上 DuckDuckGo 的搜索接口如果你做过内容聚合、舆情采集、知识库自动扩充或者单纯想给个人项目加一个搜索增强的能力应该都遇到过同一个尴尬大厂搜索 API 要么要申请 Key要么有严格的调用配额要么账单吓人。我自己在一个模拟项目X里折腾过好几轮最后发现DuckDuckGo 提供的搜索接口反而是最省事的一条路——不需要注册、不需要 API Key、请求结构简单而且一次能覆盖网页、图片、新闻、视频、词典、即时答案等多种搜索类型。这篇文章不会讲什么花哨的架构就老老实实把我从选型到落地、从踩坑到稳定的全过程拆给你看。你会搞清楚它到底提供哪几种搜索能力、每种类型的关键参数怎么配、返回的数据长什么样、怎么把这些零散调用封装成一个统一搜索服务以及最容易被忽略的限流和长期稳定性问题。如果你是刚接触搜索类接口的新手跟着走一遍就能跑通如果你已经在用了后面关于错误处理和工程封装的几节应该也能帮你省点时间。1. 多类型搜索到底多在哪先把需求拆清楚1.1 DuckDuckGo 能返回的不只是网页结果很多做搜索集成的朋友第一次看到搜索 API四个字下意识就以为是网页搜索然后照着网页结果设计了一套 schema后期发现还要图片、要新闻整个数据结构返工。这个坑我踩过所以先把能力边界列出来。DuckDuckGo 相关的接口能力和普通网页搜索最大的区别是它把搜索分成多个独立端点每种端点返回的信息结构完全不同网页搜索结果标题、链接、摘要正文适合做通用检索和信息发现。图片搜索结果图片直链、缩略图、原始页面来源、尺寸宽高适合做素材采集和视觉检索。新闻搜索结果新闻标题、摘要、来源媒体、发布时间适合舆情监控和热点追踪。视频搜索结果视频标题、描述、封面图、嵌入播放地址、发布方适合做视频聚合。即时答案Instant Answer针对事实型问题的结构化答案比如某个概念的定义、某部作品的简介适合做问答机器人的语料补充。词典与联想词返回单词释义和搜索联想词在做搜索框自动补全、自然语言处理预处理时很有用。我实际用下来感受最深的一点是这些类型不是互相独立的多个接口它们其实是围绕同一个关键词从不同信息维度给你返回结果。你在做内容聚合系统时如果能把这几类结果放在同一套检索框架里效果远好过只接一个网页搜索。1.2 典型应用场景决定了技术选型方向在动手写代码之前先想清楚你要解决什么问题。我是这么归纳的内容聚合与知识库构建定期抓取某几个领域的关键词把网页摘要、新闻标题、图片源统一入库再配合向量化做 RAG 检索。这类场景对结果数量要求不高单关键词 10-30 条足够但对时间窗新闻时效性和结果分类很敏感。舆情与热点监控核心需求是最新新闻搜索加 timelimit 参数就比网页搜索合适得多因为它天然带发布时间字段。搜索增强的辅助召回在主搜索源之外用 DuckDuckGo 做补充召回。这个场景我最推荐因为它的结果质量和老牌搜索引擎有差距但覆盖度不错而且完全免费。工具型小应用比如命令行快速搜图、搜索词联想补全、多语言词典查询这种轻量需求用它的 answers 和 suggestions 就能很快搞定。不同场景对接口的调用频率、结果数量、字段完整度要求都不一样。比如舆情监控需要高频调用新闻接口那你重点要考虑限流和去重知识库构建需要稳定的大批量抓取那你更该关注翻页逻辑和异常重试。需求没拆清楚之前不要急着上代码。2. 两种接入方式裸 HTTP 请求还是用现成库2.1 先搞清楚 DuckDuckGo 的接口形态很多人被API这个词带偏了以为 DuckDuckGo 会提供一个干净的官方 JSON API。实际上它的接口形态有点特殊我梳理了一下差不多有三类第一类是官方的 Instant Answer API端点地址是 https://api.duckduckgo.com/返回真正的 JSON 结构化数据。但它的能力很有限主要返回即时答案块和相关的主题列表网页级搜索结果是拿不到的。第二类是 HTML 端点包括 https://html.duckduckgo.com/html/ 和更轻量的 https://lite.duckduckgo.com/lite/。这两个端点本质上是可以 POST 关键词然后返回 HTML 页面的搜索页你需要自己解析 HTML 才能拿到结果。它们没有官方文档也没有请求配额说明普通做法是构造 POST 表单参数从返回的 HTML 里提取结果区块。第三类是开源社区维护的第三方库最常见的是 duckduckgo_search。它的做法是在底层封装了上面那些 HTML 端点和解析逻辑对外提供干净的 Python 方法帮你省掉写表单参数、解析 HTML、维护 headers 的麻烦。这三个形态我画个对比给你参考接入方式数据结构维护成本推荐程度Instant Answer APIJSON低但能力太窄只做问答类场景裸请求 HTML 端点需自写解析器高页面结构一变就崩不推荐新手第三方库封装Python 对象/字典中库本身在持续更新最推荐当初我图省事一开始想直接 requests 打 html 端点解析逻辑写了大几十行结果有一天页面结构调整解析出来的结果全是空列表。所以后来我彻底转到了第三方库。2.2 推荐方案用 duckduckgo_search 库快速起步直接说结论个人项目、中小型采集任务、快速原型验证无脑选 duckduckgo_search。理由很简单第一它把所有搜索类型都封装成了同风格的方法text、images、news、videos、answers、suggestions 都是传关键词返回列表的模式几乎没有学习成本。第二它内置了请求头、超时、翻页、基础反爬处理正常情况下你不需要关心底层 HTML 长什么样。第三它同时提供同步和异步两个入口异步版本在并发采集时很方便。安装就是一条命令pip install duckduckgo_search装完跑一个最小例子十秒钟就能出结果from duckduckgo_search import DDGS with DDGS() as ddgs: results ddgs.text(搜索接口 多类型, max_results10) for r in results: print(r[title], r[href], r[body][:100])这里有一个很多人不知道的细节with语句不是摆设它会帮你管理内部会话关闭时清理连接资源。如果你在循环里反复DDGS()还不写 with跑久了可能出现连接数堆积。另外库默认返回的是字典列表不是对象这个在下游处理时心里要有数。2.3 什么时候才需要裸请求 API第三方库不是万能的。我遇到过两种必须放弃库、自己写请求的情况一种是需要对请求做深度定制。比如你有一套成熟的分布式采集框架所有请求都要走统一的代理出口、自定义指纹、特定 User-Agent这时候库内置的简单请求逻辑反而成了阻碍。另一种是库的更新节奏跟不上页面变化。某次 DDG 改了 HTML 结构库作者还没发新版本你的生产任务又等不了这时候就只能自己解析。但我要提醒你裸请求 HTML 端点的成本和风险被很多人低估了。DDG 的 HTML 端点和普通网页一样的里面埋着各种校验参数和隐藏表单字段不同地区、不同 UA 拿到的页面结构还不完全一样。我自己的经验是能靠库解决的就别自己造轮子库解决不了的再手动补一个适配层而不是一上来就全部手写。3. 高频搜索类型逐个拆解参数、返回结构与使用要点3.1 text 网页搜索最通用但别迷信参数text 是最基础的搜索类型也是我日常用得最多的。它的核心参数其实就四个keywords关键词、region地区、safesearch安全搜索、timelimit时间过滤。代码长这样from duckduckgo_search import DDGS with DDGS() as ddgs: results ddgs.text( Python 异步编程 实践指南, regionwt-wt, safesearchmoderate, timelimity, max_results20, )每个参数的取值都有讲究。region 不是语言参数是地区参数默认值wt-wt表示全球无特定地区。如果你想搜中文内容可以试试cn-zh但我实测它的中文结果质量不稳定很多时候还不如wt-wt配合中文关键词效果好。safesearch 建议开发期用moderate上线后根据业务调整。timelimit 可选值只有d一天内、w一周内、m一个月内、y一年内如果你想搜所有时间的结果直接不要传这个参数就行传了反而会把大量老结果过滤掉。返回结果里三个关键字段是 title、href、body。body 一般是 100-300 字左右的摘要不是全文所以不要指望拿它来做正文抽取。有个小技巧是如果你只需要高权重的高质量结果可以对 href 做一次域名过滤把明显的内容农场域名剔除掉相关性会提升一大截。3.2 images 图片搜索字段最丰富但容易忽略图片源图片搜索的参数比 text 多一截但每一个都有明确的业务含义with DDGS() as ddgs: results ddgs.images( 城市夜景 长曝光, regionwt-wt, safesearchmoderate, sizeWallpaper, type_imagephoto, colorblack, max_results20, )size 可以传 Small、Medium、Large、Wallpaper我通常用 Wallpaper 来快速筛高清大图。type_image 传 photo 是照片、clipart 是剪贴画、gif 是动图、transparent 是透明背景图。color 这个参数有点小陷阱它只支持英文颜色词比如传black、white、red传中文大概率返回空列表。返回字段里 image 是图片直链thumbnail 是缩略图url 是图片所在的原页面。这里有个容易踩的坑图片直链的域名分散在几百个图床站上很多图床有防盗链你的服务端如果设置了 Referer 校验直接拿 image 字段去请求会返回 403。我的做法是拿到图片后立刻转存到自己对象的存储空间不直接对外暴露原图链接既解决了防盗链也避免原图被删导致裂图。3.3 news 新闻搜索时效性最强但去重是硬仗新闻搜索在舆情监控里是刚需它的最大优势是返回结果里直接带日期字段with DDGS() as ddgs: results ddgs.news( 芯片 产业链, regionwt-wt, safesearchmoderate, timelimitw, max_results20, )返回的每条结果有 title、url、body、date、source 五个主要字段。date 是 ISO 8601 格式的字符串比如2025-01-15T08:30:0000:00用的时候记得转成 datetime别直接拿字符串排序不同时区的偏移量会干扰排序。新闻结果里有一个特别烦人的问题重复率高。同一则通稿会被几十家媒体转载title 可能一模一样body 略有差异。我的去重方案是先对 title 做归一化把标点、空格、全半角统一然后取 MD5 哈希存到集合里。如果两个结果的标题哈希一样只保留发布时间最新的一条。再狠一点可以再对 body 的前 50 个字做一次相似度判断能去掉大部分洗稿重复。3.4 videos 视频搜索适合做聚合播放列表视频搜索我用的相对少但做视频整合类应用时很需要。它的参数主要有 duration 和 resolutionwith DDGS() as ddgs: results ddgs.videos( 纪录片 深海, regionwt-wt, safesearchmoderate, durationmedium, resolutionhigh, max_results15, )duration 可选 short4 分钟以下、medium4-20 分钟、long20 分钟以上。返回字段包括 title、description、embed_url、image、publisher、published 等。embed_url 是嵌入播放页的地址不是视频文件直链如果你想拿视频二进制内容还得自己去解析播放页这部分 DuckDuckGo 帮不上忙。我注意到视频搜索返回的 description 很多时候是空的或者只有一句话。所以在设计搜索结果页或者下游数据处理时不要把 description 当必填字段做好缺省显示。3.5 answers、dictionaries、suggestions被低估的问答三件套这三类能力在一般的搜索 API里很难找到却是 DuckDuckGo 独有的小惊喜。answers 走的是 Instant Answer 数据源适合回答某某是什么之类的事实型问题。它的返回结构和网页搜索完全不同是结构化程度很高的 AnswerType AbstractText Infobox比如查一个编程概念它会直接给出定义摘要和别名。我在做问答机器人的候选检索源时就把 answers 的返回结果单独存了一个表效果比全文检索精准。dictionaries 能返回指定语言的单词释义。suggestions 则是搜索联想词在搜索框做自动补全时非常省事几行代码就能实现一个输入前缀出联想的功能with DDGS() as ddgs: suggestions ddgs.suggestions(DuckDuckGo API) answers ddgs.answers(什么是内存映射) translations ddgs.translate(hello, tozh-CN) # 部分版本支持这几个方法的限制是只支持英文和少数语种的词典数据中文联想词覆盖率一般但作为一个免费补充源已经足够香了。4. 把零散调用封装成统一搜索服务4.1 统一返回 Schema先把数据结构定死同时接好几种搜索类型之后最痛的点不是调接口而是每种类型返回的字段都不一样。text 返回 bodynews 返回 source 和 dateimages 返回 image 和 thumbnail。如果你下游对接的是数据库表或者 LLM 上下文这种字段差异会逼着你写一堆 if else。我的解决方案是定义一套统一的返回结构。所有搜索类型的结果最终都归一化成下面这个形状from dataclasses import dataclass from datetime import datetime from typing import Optional dataclass class SearchResult: source_type: str # text / news / image / video title: str url: str excerpt: str # 摘要或描述 published_at: Optional[datetime] source: Optional[str] # 媒体名 / 图床名 / 发布方 thumbnail: Optional[str] # 图片类才有 raw: dict # 保留原始字段方便排查统一结构的好处是下游不管是落库还是拼 Prompt只认这一套字段就够了。source_type 用来区分结果类型raw 保留原始数据万一排查问题也不需要回去翻日志。4.2 翻页与结果数量控制max_results 不是精确翻页DuckDuckGo 没有真正意义的页码概念它的翻页是基于页面底部的 next 参数循环请求实现的。第三方库里max_results是一个努力达到的目标值不是精确的返回条数。我自己测试的结果是max_results 传 20 大概率能返回 18-20 条传 50 有时候 40 条就停了。另一个容易忽略的问题是max_results 传得越大库内部发起的请求次数越多。它的底层逻辑是一批拉 30 条不够再翻下一页。如果你传 200意味着至少发 7-8 次请求连续高频请求很容易触发限流。实践经验是单次请求控制在 20-50 条之间宁可分多次关键词变体去补也别一次拉到底。如果你需要大样本量采集我建议自己做循环每次 max_results 设 20在两次循环之间随机睡 2-5 秒。虽然慢但稳定。快 1 秒和稳 1 天后者永远是采集任务的第一原则。4.3 多类型并发执行同步库不够就上异步当你同时搜索网页、图片、新闻时一个很直观的优化就是并发。duckduckgo_search 提供了异步版本 AsyncDDGS用起来和同步版几乎一样import asyncio from duckduckgo_search import AsyncDDGS async def search_all(keyword: str): async with AsyncDDGS() as ddgs: text_results await ddgs.atext(keyword, max_results10) image_results await ddgs.aimages(keyword, max_results10) news_results await ddgs.anews(keyword, timelimitw, max_results10) return text_results, image_results, news_results if __name__ __main__: text, images, news asyncio.run(search_all(分布式系统)) print(len(text), len(images), len(news))异步接口的类型名就是在同步方法前面加个 aatext、aimages、anews。实测下来一次同时拉三种类型总耗时比串行少了差不多一半。但注意并发不要走极端。多类型之间并发没问题因为每个类型对应不同的内部端点但如果你同时对同一个类型发起几十个不同关键词的请求等于瞬间给同一个端点打了几十个请求触发限流的概率会指数级上升。我的策略是类型间并发关键词串行串行时每条之间随机延迟。5. 错误处理、限流与长期稳定性免费午餐的隐性成本5.1 常见异常处理别裸调接口要包一层重试只要是第三方免费接口稳定性就永远是个话题。duckduckgo_search 库里定义了几种常见异常包括 DuckDuckGoSearchException 基类、RatelimitException限流、TimeoutException超时等。我的统一处理模板是捕获异常 - 等待退避 - 重试import time import random from duckduckgo_search import DDGS from duckduckgo_search.exceptions import RatelimitException, TimeoutException def safe_search(search_func, *args, retries3, **kwargs): for attempt in range(retries): try: return search_func(*args, **kwargs) except RatelimitException: wait_time 2 ** attempt random.uniform(0, 1) time.sleep(wait_time) except TimeoutException: time.sleep(2) except Exception as e: print(funexpected error: {e}, retry {attempt 1}) time.sleep(3) return []指数退避是必须的不要用固定延时重试。我见过有人写time.sleep(1)重试三次结果三次全撞在同一个限流窗口里白等。随机抖动也很重要目的是避免多个任务进程同时重试造成二次冲击。5.2 被限流之前能做什么主动降频比被动重试更重要重试只是兜底真正有用的策略是主动控制频率。根据我长时间运行的经验以下几件事能显著降低限流概率第一请求间隔做随机化不要固定每 1 秒一次。固定间隔在服务端看来非常像脚本随机间隔更像真实用户。第二控制单关键词的请求次数。上面提过max_results 别贪大。第三批量任务尽量错峰。我发现整点或者整半点开始跑批撞限流的概率明显更高可能大家都在这个时间点刷任务。第四如果采集任务部署在多台机器上尽量让请求分散到不同出口避免单出口在短时间内集中请求。还有一条要记住DuckDuckGo 没有官方 SLA也没有正式承诺的商用授权边界。它适合做个人工具、内部系统、辅助数据源如果你要在商业化产品里把它当核心引擎得考虑合规风险和多源容灾。免费的午餐省的是钱省不了运维的心。5.3 页面改版怎么办业务代码和解析逻辑必须解耦第三方库最大的风险就是上游页面结构一旦变化库可能短时间内失效。应对方案是在工程结构上做隔离业务服务只依赖你自己的 SearchClient 接口不直接依赖 duckduckgo_search 的方法。class SearchClient: def search_text(self, keyword: str, max_results: int 10) - list[SearchResult]: ... def search_news(self, keyword: str, timelimit: str w) - list[SearchResult]: ...如果库需要升级或者你要换成自定义解析器只改 SearchClient 内部实现就行下游代码完全不用动。我经历过一次库大版本升级内部方法签名变化了因为业务层只调 SearchClient改完一个文件就全量恢复。另外锁版本是个好习惯。不要用最新的库版本裸裸地跑生产任务在 requirements 里把版本号写死升级前先在测试环境跑一遍回归。6. 实战做一个轻量多类型搜索小工具6.1 功能规划与目录结构这个实战案例来自我实际做过的一个内容采集小工具目标很简单输入一个关键词和几个搜索类型程序自动从 DuckDuckGo 抓回结果统一格式后输出成 JSON 文件。目录结构如下search_tool/ ├── main.py # 命令行入口 ├── schema.py # SearchResult 定义 ├── searcher.py # SearchClient 封装 └── output/ # 输出目录功能清单就三条支持 text、news、images 三种搜索类型的任意组合结果统一成 SearchResult 结构输出 JSON 文件并按类型分组。6.2 核心代码实现schema.py 里的 SearchResult 直接复用第 4 节的 dataclass 定义。searcher.py 里写一个精简版 SearchClientfrom datetime import datetime from duckduckgo_search import DDGS from schema import SearchResult class SearchClient: def __init__(self): self._ddgs DDGS() def search(self, keyword: str, types: list[str], max_results: int 10) - dict[str, list[SearchResult]]: outputs {} if text in types: outputs[text] self._search_text(keyword, max_results) if news in types: outputs[news] self._search_news(keyword, max_results) if images in types: outputs[images] self._search_images(keyword, max_results) return outputs def _search_text(self, keyword: str, max_results: int) - list[SearchResult]: raw self._ddgs.text(keyword, max_resultsmax_results) return [SearchResult( source_typetext, titler.get(title, ), urlr.get(href, ), excerptr.get(body, ), published_atNone, sourceNone, thumbnailNone, rawr, ) for r in raw] def _search_news(self, keyword: str, max_results: int) - list[SearchResult]: raw self._ddgs.news(keyword, max_resultsmax_results, timelimitw) return [SearchResult( source_typenews, titler.get(title, ), urlr.get(url, ), excerptr.get(body, ), published_atself._parse_date(r.get(date)), sourcer.get(source), thumbnailNone, rawr, ) for r in raw] def _search_images(self, keyword: str, max_results: int) - list[SearchResult]: raw self._ddgs.images(keyword, max_resultsmax_results) return [SearchResult( source_typeimage, titler.get(title, ), urlr.get(image, ), excerptr.get(title, ), published_atNone, sourcer.get(source), thumbnailr.get(thumbnail), rawr, ) for r in raw] staticmethod def _parse_date(date_str): try: return datetime.fromisoformat(date_str) except (ValueError, TypeError): return Nonemain.py 用 argparse 接收关键词和类型列表import argparse import json from pathlib import Path from searcher import SearchClient def main(): parser argparse.ArgumentParser(descriptionDuckDuckGo multi-type search tool) parser.add_argument(keyword, helpsearch keyword) parser.add_argument(--types, nargs, default[text, news, images], choices[text, news, images], helpsearch types) parser.add_argument(--max, typeint, default10) args parser.parse_args() client SearchClient() results client.search(args.keyword, args.types, args.max) output_dir Path(output) output_dir.mkdir(exist_okTrue) output_path output_dir / f{args.keyword.replace( , _)}.json output_path.write_text( json.dumps(results, ensure_asciiFalse, indent2, defaultstr), encodingutf-8, ) for stype, items in results.items(): print(f[{stype}] {len(items)} results) if __name__ __main__: main()跑一下试试python main.py 记忆化搜索 --types text news images --max 15这个工具的代码量不多但它的架构思路可以扩展到几百倍规模统一 schema、独立 SearchClient、与库版本解耦。6.3 实际运行效果与耗时观察我在模拟项目X里跑这个工具关键词选了一个中低频的技术词组三种类型各拉 15 条总耗时大约 6-8 秒比串行调用快了接近一半。新闻结果里 date 字段解析正常图片结果里有一部分缩略图为空需要在入库时做兼容。一个值得注意的现象是DuckDuckGo 的热门关键词冷门关键词返回质量差异很大。热门词的结果相关性高但同质化严重冷门词的结果有时候会漏掉一些精品来源。所以在设计检索策略时我一般会对同一个主题同时跑主关键词和两三个同义变体然后把结果合并去重覆盖度会明显提升。7. 常见问题速查与避坑心得7.1 异常现象速查表我把实际运行中遇到的高频问题整理成了这张表排查时对着看就行现象可能原因解决方案抛出 RatelimitException请求频率过高触发限流降频、指数退避重试、错峰运行返回空列表但手动搜索有结果region 设置太窄或 timelimit 过滤太狠放宽 region去掉 timelimit 再试图片结果里 thumbnail 为空部分图床不提供缩略图忽略 thumbnail 或生成默认占位图新闻 date 解析出错库版本旧或日期有多种格式变体升级库用 datetime.fromisoformat 容错解析pip 安装失败Python 版本偏低用 Python 3.8 环境结果突然大量重复可能是限流降级后返回了缓存数据降低频率稍后再试搜索结果里出现明显不相关内容关键词语义太泛用更精确的关键词组合或对 title 做相关性过滤7.2 零散经验几个文档里不会明说的细节第一个经验是搜索接口只负责发现不负责提取。很多人拿到搜索结果后想直接把 body 或者 excerpt 当正文用这个想法要尽早放弃。搜索摘要的定位是让用户判断这条结果值不值得点进去不是完整文章。我做知识库时是把搜索接口当候选发现层拿到 URL 后再单独走正文抽取流程。第二个经验是DuckDuckGo 的结果排序逻辑和主流搜索引擎不完全一样它的排序里隐私因素占比高个性化信号很少所以结果偶尔会显得不够懂你。在做 RAG 召回时不要直接把这些结果当高置信度上下文最好加一道相关性重排比如用向量相似度再筛一遍。第三个经验是关于许可证的。duckduckgo_search 库本身有开源许可证但 DuckDuckGo 的服务条款并没有为用户提供明确的数据使用授权。个人项目、内部工具随意但如果是商业产品最好咨询法律意见或者把它定位为辅助数据源而不是核心依赖。第四个经验可能最容易被忽略日志一定要把 raw 原始字段完整打出来。因为第三方接口的字段可能悄悄变化如果你只存提取后的字段等出问题时根本不知道原始数据长什么样。我在 searcher 的 output 里特意保留了 raw排查了不止一次因为字段名变化导致的静默失败。7.3 最后的一点个人心得说实话DuckDuckGo 的搜索接口不是一个全能搜索解决方案它更像一个便宜、灵活、够用的搜索供给侧补充。在我那个模拟项目X里它负责的是那 30% 不需要高精度的长尾需求把主搜索源的压力卸掉同时靠多类型结果让整个系统的覆盖范围从单一网页扩展到了新闻、图片和结构化答案。如果你也想在项目里用这套方案我建议从一开始就照着分层结构来做底层是 duckduckgo_search 或自研请求器中间是一层你自己的 SearchClient 适配器上层是业务逻辑。这样无论它怎么改版、你怎么换库都能把改动控制在一个文件以内。免费资源稳定性和可控性永远得靠自己的工程习惯去补。
返回列表