10美元两天搭建域名搜索服务:数据源、索引与API全解析

发布时间:2026/8/31 20:21:35
10美元两天搭建域名搜索服务:数据源、索引与API全解析 上周末我用 10 美元和两天时间做完了别人可能要卖几千块钱才能定制的数据产品一个覆盖 50 万条域名记录的搜索服务。这不是一个炫技故事。它想说明的其实是三个很现实的判断第一对独立开发者和做“小众工具”的团队来说技术不是最贵的资源数据源的合法性、稳定性和成本结构才是真正的门槛。第二50 万条域名在数据库里根本算不上“大数据”只要设计得当一台普通云服务器甚至一个 Serverless 函数都能扛住。第三市面上很多看似复杂的域名搜索工具核心功能没有你想象的那么难做难的是把数据源、更新策略、搜索体验和成本控制绑在一起。这篇文章不打算只讲“我做到了”。我会把从数据获取、清洗、存储、查询到上线排查的完整思路拆开给出能直接复用的代码和成本模型。如果你正准备给自己的产品加一个域名发现模块或者想做一个面向站长和创作者的垂直搜索工具这篇文章值得收藏。1. 这篇文章真正要解决的问题先代入一个真实场景。你在做一项独立产品用户需要一个“帮我找域名”的工具或者你自己要给项目起名想批量筛选一批还没被注册、拼写顺眼的域名。常规做法是打开一个域名交易平台输入关键词然后把几千条结果翻到底。问题是这类工具背后的数据往往不透明它有没有覆盖过期列表有没有整合不同注册局的公开数据搜索结果页是不是故意混进了广告和溢价域名对于普通用户来说这些不透明意味着你可能选了一个中间商盘里最贵的那个域名。再往上一档是专业域名搜索服务。这些工具确实专业但收费模式通常按 API 调用次数或商业授权计算一个月几十到几百美元。对很多独立开发者来说前期产品根本没有验证完需求就要花这笔钱成本并不低。我做的这个搜索工具本质上是给“域名搜索引擎”这件事做了一个最基础、最可控的实现数据来源全部来自公开渠道合规风险可控数据量级是 50 万条不是几千万条所以单机就能跑查询方式是关键词搜索 条件筛选足够覆盖 80% 的找域名需求成本控制在 10 美元级别后续如果数据量涨十倍也只需要升一档配置。这篇文章最有价值的读者不是想拿 50 万条域名做什么学术研究的人而是那些正在做独立产品、想低成本验证“域名工具”需求的人。它不是让你直接照抄代码上线赚钱而是让你理解这套东西的设计决策少走弯路。2. 域名搜索的核心概念与设计边界在写代码之前先把概念边界理清楚。网上聊域名搜索经常把几件事混在一起导致需求跑偏。2.1 域名搜索不等于 Whois 查询很多人以为域名搜索就是反复调用 Whois 协议去查“某个域名注册了没有”。这是最小可用但最不划算的方案。Whois 查询有几个问题单个 Whois 服务有频率限制连续查询容易触发封禁每次查询都是网络请求50 万条记录不可能一条一条实时查Whois 结果格式不统一不同注册局字段差别很大解析成本高很多注册商返回的数据并不包含“是否可注册”需要结合注册局规则去推测。所以实际工程上没有人会用实时 Whois 循环查 50 万条数据。更常见的方案是维护一张离线域名快照表用批处理定期更新搜索走本地索引需要精确确认时再对少量结果做实时 Whois 校验。2.2 域名搜索的三层信息一个实用的域名搜索工具信息可以分成三层层级信息类型示例更新频率基础层域名本身example.com低登记层注册信息注册日期、过期日期、注册商、状态中信号层业务/语义信号域名长度、是否含数字、是否字典词、历史热度低很多工具只做“基础层 登记层”通过关键词和过期时间就能筛出不少有价值的数据。信号层是加分项但如果一开始就把词典匹配、品牌商标过滤、历史热度全部塞进来工程量会迅速失控。2.3 搜索的边界问题我还要提前说明一个边界在纯离线数据里你没有办法准确判断“这个域名现在能不能注册”。原因很简单注册状态是一个实时状态。今天凌晨刚掉出来的域名可能下一秒就被抢注了。离线快照只能反映数据源抓取那一刻的状态。你的搜索语义应该是“这个域名曾经出现在过期列表/公开数据里大概率有捡漏机会”而不是“这个域名现在一定可以注册”。把期望管理好工具才不会变成信任危机产品。实际产品里通常是在搜索结果旁边放一个“点击检测实时可用性”按钮把离线搜索和实时校验分开。2.4 为什么 50 万条数据不需要分布式再解决一个常见误解50 万条记录需要什么技术方案先说结论不需要 Hadoop不需要 Spark甚至不一定需要数据库集群。50 万条 JSON 行压缩后可能就 20 到 50 MB展开到普通关系型数据库里也就几个 GB 级别。一台 2 核 4G 的云服务器或者一个带 SQLite 的轻量服务完全扛得住。这个设计最核心的点在于数据量小决定了你可以选择最简单、最便宜、最容易维护的架构。不要为了看起来高级而把系统搞复杂。3. 数据从哪里来合法且低成本的公开数据源这是整篇文章最关键的部分。域名搜索服务有没有价值七成取决于数据源三成取决于搜索体验。数据源如果选错了后面所有的代码都是在给垃圾数据做包装。3.1 公开数据源的四种选择我整理了几种适合个人开发者起步的公开数据源类型数据源类型说明适合做什么域名注册局公开 Zone 文件部分注册局会开放顶级域名的完整 DNS 记录列表例如通过授权申请获取构建全量域名列表分析存量域名特征过期域名列表注册局或第三方站点会定期发布即将过期/刚过期的域名名单抢注监控、买卖机会发现证书透明度日志每次签发 SSL 证书都会公开记录域名信息可以从 CT 日志中解析发现活跃业务域名、判断建站情况社区整理的开源数据集网上的开源项目会整理部分域名列表、热门域名抽样快速起步补充数据量从成本、合规和稳定性的角度看我更推荐先用“过期域名列表 开源域名数据集 CT 日志抽样”的组合。三者分别对应了“可以抢的”“现有存量的”“真实建站的”三个维度信息互补性很强。3.2 如何拼出 50 万条记录假设你拿到的基础数据集是 40 万条过期域名记录加上 CT 日志解析出的 10 万条活跃建站域名就构成了 50 万条记录的主表。字段可以这样设计字段名类型说明idstring唯一主键可以是域名本身domainstring完整域名例如example.comtldstring顶级域名例如comrootstring主域名主体例如examplelengthint域名主体长度has_digitint是否包含数字0/1hyphen_countint连字符数量sourcestring数据来源标记例如 expired/ct_logfirst_seendate第一次出现日期last_seendate最近一次出现日期statusstring数据标记例如 pending_delete/active这里真正的工程要点是不要只存“域名”这一个字符串。搜索产品是需要“筛选”的而筛选必须有结构化字段支撑。没有结构化字段你只能在字符串里做模糊匹配玩法非常受限。3.3 合规与授权提醒这里必须多说一句。任何从第三方站点抓取数据的行为都建议先阅读对方的服务条款和 robots 文件。特别要注意不要绕过登录、验证码、IP 限制机制不要高频抓取同一站点尽量采用官方 API 或授权下载通道如果数据来自某个特定注册商注意商标词和隐私政策不要拿数据去做骚扰性营销或精准批量推销。合规问题不是套话它决定你的工具能不能长期活下来。域名数据本身是公开信息但“公开”不等于“可以任意使用”。我把合规列成第一优先级是因为这个环节出问题项目就没有“后续”可言。4. 架构设计与成本模型很多人的第一反应是50 万条数据需要一台像样的服务器吧其实不一定。我把这套工具拆成了三个部分对象存储 数据库 轻量查询服务。4.1 整体架构数据源 - 抓取/解析脚本 - 清洗脚本 - 对象存储(原始文件) | v 数据库(结构化索引) | v 轻量搜索 API / 网页端数据链路是单向的抓取、清洗、入库、检索分成四步。这样做的目的是每一步都是可重试、可重建的。哪怕数据库跑坏了只要重新从对象存储导入一次就能恢复不需要重新抓取全网数据。4.2 为什么选择这种架构先说对象存储。原始数据文件放进对象存储好处是便宜、持久、便于版本化。每天生成一个新的数据快照文件覆盖之前的版本或者保留最近 7 天版本都非常方便。再说数据库。50 万条记录放在 SQLite 里完全没问题。SQLite 的全文搜索模块 FTS5 支持中文分词的扩展同时也支持英文域名场景的 token 化搜索。如果后续数据量涨到 500 万条SQLite 依然可以扛住只是需要更合理的索引设计。如果涨到 5000 万条才需要考虑升级为 Postgres 或 ClickHouse但那是后面的事。最后是查询服务。查询接口用一个轻量 API 包一层就行不需要重型 Web 框架。你可以选择 Serverless 函数也可以选择部署在一台低配云服务器上。考虑到查询频率不高用按量付费的 Serverless 服务可能比包月的服务器更省钱。4.3 10 美元成本预算到底怎么分配根据运营方式不同成本分配有两种典型路径。路径一完全免费层 短任务。对象存储免费额度够用Serverless 免费额度够用唯一花钱的地方是自定义域名和少量 API 调用10 美元以内可以覆盖。路径二低配云服务器 自动化更新。一台 2 核 2G 的云服务器按 10 美元/月计算已经可以支撑数据自动抓取、数据库存储和网页端展示。缺点是前期要压测一下不要同时开太多慢查询。10 美元的本质不是“预算充足”而是“用量边界非常清晰”。因为 50 万条数据的数据量级太小服务极少会出现并发爆炸真正要控制的是抓取频率和查询超时。5. 环境准备与前置条件下面进入实操环节。先说环境。我这套示例采用的是 Python 3.10数据库用 SQLite 内置模块搜索用 SQLite FTS5接口用 FastAPI 演示。你不需要完全复刻这套技术栈只要明白核心逻辑换成本地电脑、换成 Java 或 Node.js都可以套用。5.1 依赖清单依赖用途Python 3.10主开发语言requests下载数据源文件pandas数据清洗和抽样可选FastAPI uvicorn提供查询 APISQLite3内置数据库无需额外安装python-whois需要实时校验时使用安装命令如下pip install requests pandas fastapi uvicorn python-whois5.2 数据准备在动手之前你需要先准备好一份域名数据集。假设你已经从授权渠道下载了一份domains.jsonl每行是一个 JSON 对象基本格式如下{domain: example.com, tld: com, root: example, source: expired, first_seen: 2025-01-01, last_seen: 2025-02-01}真实数据源里字段可能比这复杂也可能存在脏数据。第 6 步会给出完整的清洗和导入流程。先把数据准备到位后面的步骤才会顺利。6. 核心流程拆解与完整代码实现6.1 数据清洗与归一化第一步把原始数据里的域名拆出主体、后缀、长度、连字符数等结构化字段同时过滤掉明显无效的记录。# 文件路径scripts/clean_domains.py import json import re VALID_TLDS {com, net, org, io, dev, xyz, cn, top} def normalize_domain(raw: str): raw raw.strip().lower().rstrip(.) pattern re.compile(r^([a-z0-9-])\.([a-z]{2,63})$) m pattern.match(raw) if not m: return None root, tld m.groups() if tld not in VALID_TLDS: return None if root.startswith(-) or root.endswith(-): return None if -- in root: return None return root, tld def clean_line(line: str): line line.strip() if not line: return None try: item json.loads(line) except json.JSONDecodeError: return None domain (item.get(domain) or ).strip() normalized normalize_domain(domain) if not normalized: return None root, tld normalized return { domain: f{root}.{tld}, tld: tld, root: root, length: len(root), has_digit: int(any(ch.isdigit() for ch in root)), hyphen_count: root.count(-), source: item.get(source, unknown), first_seen: item.get(first_seen, ), last_seen: item.get(last_seen, ), } def clean_file(input_path: str, output_path: str): clean_count 0 with open(input_path, r, encodingutf-8) as f_in, \ open(output_path, w, encodingutf-8) as f_out: for line in f_in: result clean_line(line) if result: f_out.write(json.dumps(result, ensure_asciiFalse) \n) clean_count 1 print(f清洗完成有效记录数: {clean_count}) if __name__ __main__: clean_file(data/raw_domains.jsonl, data/clean_domains.jsonl)这段代码做的事情很朴素转小写、去空格、去尾部点是域名规范化的基础操作正则限制掉非法字符格式把root、tld、length、has_digit、hyphen_count拆出来是为了后续 SQL 筛选过滤掉包含连续连字符的域名因为这类域名大多不可读且容易跟钓鱼域名沾边。6.2 导入 SQLite 并建立 FTS5 搜索索引清洗完成后接下来的核心任务是把 JSONL 文件导入 SQLite并建立全文搜索索引。# 文件路径scripts/build_db.py import json import sqlite3 DB_PATH data/domains.db CLEAN_FILE data/clean_domains.jsonl CREATE_TABLE_SQL CREATE TABLE IF NOT EXISTS domains ( id INTEGER PRIMARY KEY, domain TEXT UNIQUE, tld TEXT, root TEXT, length INTEGER, has_digit INTEGER, hyphen_count INTEGER, source TEXT, first_seen TEXT, last_seen TEXT, quality_score REAL DEFAULT 0 ); CREATE_FTS_SQL CREATE VIRTUAL TABLE IF NOT EXISTS domains_fts USING fts5(domain, root, contentdomains, content_rowidid); TRIGGER_INSERT_SQL CREATE TRIGGER IF NOT EXISTS domains_ai AFTER INSERT ON domains BEGIN INSERT INTO domains_fts(rowid, domain, root) VALUES (new.id, new.domain, new.root); END; def build_db(): conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute(CREATE_TABLE_SQL) cur.execute(CREATE_FTS_SQL) cur.execute(TRIGGER_INSERT_SQL) with open(CLEAN_FILE, r, encodingutf-8) as f: rows [] for line in f: item json.loads(line) rows.append(( item[domain], item[tld], item[root], item[length], item[has_digit], item[hyphen_count], item[source], item[first_seen], item[last_seen], )) cur.executemany( INSERT OR IGNORE INTO domains (domain, tld, root, length, has_digit, hyphen_count, source, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), rows, ) conn.commit() cur.execute(SELECT COUNT(*) FROM domains) total cur.fetchone()[0] print(f数据库构建完成总记录数: {total}) conn.close() if __name__ __main__: build_db()这里用到了两个关键设计主表domains负责存储结构化字段方便做筛选、排序、去重FTS5 虚拟表domains_fts负责关键词搜索支持快速搜索example这样的模糊输入触发器保证每次插入主表时自动同步一条 FTS 索引记录避免应用层忘记同步。这个设计在 50 万条数据量级下查询速度通常是毫秒级完全够用。6.3 提供搜索 API数据入库之后下一步是写一个轻量接口让网页端或客户端可以调用。# 文件路径app/main.py from fastapi import FastAPI, Query import sqlite3 DB_PATH data/domains.db app FastAPI(titleDomain Search API) def search_domains(keyword: str, tld: str None, max_length: int None, limit: int 20): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row cur conn.cursor() sql SELECT d.domain, d.tld, d.length, d.has_digit, d.hyphen_count, d.source, d.last_seen FROM domains_fts f JOIN domains d ON d.id f.rowid WHERE domains_fts MATCH ? params [keyword] if tld: sql AND d.tld ? params.append(tld) if max_length is not None: sql AND d.length ? params.append(max_length) sql ORDER BY d.length DESC LIMIT ? params.append(limit) rows cur.execute(sql, params).fetchall() result [dict(row) for row in rows] conn.close() return result app.get(/search) def search( q: str Query(..., description搜索关键词), tld: str Query(None, description按顶级域名过滤例如 com), max_length: int Query(None, description域名主体最大长度), limit: int Query(20, description返回条数默认 20), ): if not q: return {error: q is required} return {data: search_domains(q, tld, max_length, limit)}启动方式uvicorn app.main:app --reload --port 8000访问示例curl http://127.0.0.1:8000/search?qdevtldiomax_length8这个接口就是最简可用的搜索引擎雏形输入关键词、选择后缀、限制长度返回一组域名。6.4 实时状态校验扩展如果搜索结果需要展示“当前是否可注册”可以加一个实时校验收尾。因为上面已经说了离线数据的边界这里补充一个最小实现思路# 文件路径app/checker.py import whois def check_domain(domain: str): try: w whois.whois(domain) status w.status if not status or status []: return {domain: domain, available: False, reason: whois_status_empty} # 不同注册局返回的状态字段不同这里只给一种通用判断 return {domain: domain, status: str(status)} except Exception as e: return {domain: domain, available: True, reason: str(e)}注意python-whois的解析在不同 TLD 上表现差异很大。这段代码不能当严谨商业产品用但用来做原型验证已经足够。生产环境更推荐接入注册商提供的官方域名可用性 API。7. 运行结果与效果验证数据库构建完成后你会看到类似下面的输出清洗完成有效记录数: 500000 数据库构建完成总记录数: 500000启动 API 服务后请求一个搜索示例curl http://127.0.0.1:8000/search?qshoptldcommax_length10limit5预期返回类似结构{ data: [ {domain: shoppingzone.com, tld: com, length: 11, has_digit: 0, hyphen_count: 0, source: expired}, {domain: bestshop.com, tld: com, length: 8, has_digit: 0, hyphen_count: 0, source: expired} ] }验证是否成功的标准有三条能返回包含关键词的域名结果筛选条件后缀、长度确实生效查询速度在本地毫秒级在远程服务器也应在 1 秒内除非数据量大幅增长。如果失败第一步看什么没有任何结果检查domains_fts是否为空执行SELECT COUNT(*) FROM domains_fts;SQL 报错检查 SQLite 版本是否支持 FTS5SELECT SQLITE_VERSION();较老版本需要重新编译启用 FTS5API 请求 500看 uvicorn 的报错栈最常见的是 db 路径写错或主表字段不匹配。8. 常见问题与排查思路实际开发中容易踩的坑我整理成了一组表格。问题现象可能原因排查方式解决方案搜索dev返回空结果FTS5 默认分词把短关键词忽略或者索引未生效查看domains_fts表是否有数据重新执行索引构建或用prefix配置让短词可搜索查询admin返回大量无关结果关键词拼写归一化不足比如admin.或admin-混入索引检查清洗阶段的正则过滤加强 normalize 逻辑对前缀和后缀多重限制域名列表包含中文域名乱码编码处理不一致检查源文件编码和 Python open 编码参数统一使用utf-8或punycode数据库体积超过预期原始 JSON 中字段未裁剪导致大量冗余字段入库查看数据库各表占用空间只保留搜索和展示需要的字段API 同时请求多线程时报锁错误SQLite 默认串行写读多写少时容易出现锁冲突检查 SQLitebusy timeout配置连接字符串加timeout30或读操作走只读连接搜索速度突然变慢FTS 索引未更新或者查询条件里出现非索引字段排序查看EXPLAIN QUERY PLAN执行计划确认为 FTS 条件加上MATCH后走索引这些错误里最常见的是 FTS5 索引失效和字符编码问题。如果你在自己电脑上跑通了但服务器上运行异常优先检查两边的 SQLite 版本和 Python 版本。9. 最佳实践与工程建议把工具做出来很简单但要做到“长期可用”“成本稳定”“数据可信”还需要一些工程上的收敛。9.1 数据更新策略域名数据不是静态的过期域名列表每天都在变。建议采用“全量快照 每日增量”策略。每周从数据源拉取一次全量文件导出为新的 JSONL 快照每天跑一次增量脚本把新增的域名追加进来超过 90 天没有变化的记录可以标记为“过期”而不是直接删除方便用户参考历史热度。对象存储里建议保留最近 7 个全量快照数据库损坏时可以快速回滚。9.2 字段命名与版本管理字段名一旦上线就不要频繁变更。后面接入的用户脚本、API 输出、报表系统都可能依赖字段名。如果要调整一定要在接口里做兼容映射。建议在设计阶段就定下一套 schema 版本号例如schema_version1.0每次改动升级一个副版本并保留旧字段到新版数据里。9.3 搜索安全与限流如果你的搜索 API 会公网暴露至少要做到接口加 API Key 或签名鉴权不要裸奔单 IP 限制 QPS防止被脚本刷爆对高度恶意的关键词做拦截比如包含明显广告词或违法内容的词日志不要记录完整查询参数中的敏感信息域名搜索涉及商业意图注意脱敏。9.4 性能优化方向50 万条数据基础性能是没问题的但要注意这几个优化点在tld、length、has_digit字段上建普通索引搜索主键强制走 FTS5 的MATCH不要用LIKE %keyword%去替代搜索结果分页不要用OFFSET拖太深可以直接限制最大返回条数如果有大并发需求给 SQLite 开 WAL 模式提升读写并行能力PRAGMA journal_modeWAL; PRAGMA busy_timeout5000;9.5 产品层建议不要让用户做数据科学家最后说一个偏产品的问题。搜索工具给用户展示什么直接决定了用户愿不愿意用。一个搜索结果页不能只给用户一行域名和一段过期时间。建议至少展示域名主体长度方便快速判断品牌感是否含数字因为很多人明确拒绝数字域名数据来源区分“过期列表”和“证书日志”体现信息可信度一个“实时检测”按钮触发在线核验。这些字段都不难加但会让工具从“数据表格”变成一个真正能帮用户做决策的产品。10. 后续可做的扩展方向当你跑通了这个最小版本后面有几条很自然的演进路线。第一数据源扩容。50 万条只是起点。通过接入更多注册局公开文件、历史 Whois 快照和 CT 日志定期解析数据量可以从 50 万涨到 500 万再到 5000 万。每涨一个数量级对存储和查询的优化要求都会上一个台阶但依然是单机可以应对的。第二信号层的自动化。给每个域名打上“是否是英文词典词”的标签计算两个常见单词组合的词频或者接入自然语言模型做品牌可读性打分。这一步会让搜索结果的排序质量明显提升。第三监控与告警。对重点关键词加监控例如“包含ai且长度为 5 以内的域名”一旦新的过期列表中出现了符合条件的域名自动推送通知。这本质上是把一个搜索工具升级成抢注监测工具商业价值会更高。第四面向创作者的变现场景。可以在搜索结果里加入“该域名是否在主流社交平台有同名账号”的交叉信息。这会让你的工具在域名买卖之外多一层品牌命名工具的价值。这些扩展方向并不需要推翻当前架构只需要在“数据源、字段、计算层、输出层”四个位置做增量。这也是为什么一开始就要把数据存储和搜索索引设计得干净因为你后面会经常在数据管道上做手脚。域名搜索工具这个东西看起来像是一个很小的需求但真正上手之后会发现数据源、清洗、索引、查询、校验、成本控制每个环节都有值得打磨的细节。10 美元不是一个炫耀的资本它代表的是“小成本项目的边界”。50 万条域名也不是一个很大的成绩它代表的是“用最简单架构完成一个真实需求”的工程判断力。如果你正在做独立产品或想在域名/品牌命名这个细分领域找一个突破口我建议你先复制这份最小实现然后把你自己的数据源和筛选逻辑换上去。跑了真实数据之后你会比我这篇文章里说的“没问题”更清楚哪里有问题而那个问题很有可能才是你产品真正的机会所在。