这次我们来看一个很有意思的独立开发项目:作者用一个周末的时间、大约 10 美元的成本,搭建了一套覆盖 50 万条域名记录的搜索引警,面向的是 makers——独立开发者、创客、经常要起项目名和选域名的人。
这类工具解决的痛点非常具体:做产品前要起名字,起完名字要确认域名还在不在,还要看相近的域名是否被抢注。手动去注册商网站一个个查,速度慢、体验差、来回切换页面很折腾。于是就有了这种思路:把几十万条域名记录提前拉进本地数据库,用关键词搜索、后缀过滤、长度筛选来快速缩小候选范围。相比“打开注册商慢慢试”,效率不是一个量级。
值得关注的是这组数字背后的技术取舍:50 万条数据量不算大,但也不是 Excel 能处理的规模;10 美元预算意味着不能上重型基础设施;一个周末开发意味着技术栈必须足够简单、足够顺手。这三个条件组合下来,基本决定了它不会走“Elasticsearch 集群 + 多节点部署”的路线,而是轻量、单机、够用就行的方案。
这篇文章会把这类“小成本域名搜索引警”的完整搭建思路拆开讲一遍:数据从哪来、怎么清洗入库、用什么存储方案、搜索逻辑怎么写、如何把能力包装成 HTTP API,以及部署上线时怎么把成本控制在个位数美元。如果你正在考虑做类似的工具站、内部搜索服务,或者只是想把“搜索”这个能力加进自己的独立开发流程里,这篇文章可以直接收藏。
1. 核心能力速览
先给一张规格表,快速判断这个项目适不适合你。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 域名搜索引警 / 轻量级域名查询工具 |
| 数据规模 | 约 50 万条域名记录(项目描述) |
| 核心功能 | 关键词搜索、TLD 后缀过滤、域名长度筛选、状态标注 |
| 目标用户 | makers、独立开发者、创业者、SEO/品牌调研人员 |
| 开发成本 | 约 10 美元(项目描述,主要用于服务器和基础设施) |
| 开发周期 | 一个周末(项目描述) |
| 常见技术栈 | Python + FastAPI + SQLite/DuckDB + 静态前端 |
| 支持平台 | Linux 服务器;开发机 Windows/macOS 均可 |
| 是否支持 API | 支持,HTTP JSON 接口 |
| 是否支持批量任务 | 可通过批量脚本或接口循环实现 |
| 显存要求 | 不需要 GPU,纯 CPU 应用 |
注意一点:标题里的 10 美元和周末开发周期,决定了这类项目的核心原则是“轻”。轻量存储、轻量服务、轻量前端,任何需要单独维护集群的组件都不在考虑范围内。
2. 适用场景与使用边界
这个工具到底适合谁?从我的角度理解,它主要服务以下几类场景:
- 独立开发者在产品立项时快速生成域名候选,不用一个后缀一个后缀去注册商页面验证。
- 内容站或 SEO 团队批量筛选与品牌词相关的域名,做竞品词和长尾词调研。
- 域名投资收藏者建立自己的本地域名库,按关键词和长度检索稀有域名。
- 需要在离线环境批量检查域名记录的运维或运营人员。
它能解决的问题也很明确:一次性导入 50 万条记录,后续搜索都是在本地完成,响应速度远快于挨个查询注册商;把搜索封装成 API 后,可以进一步接到品牌命名工具、落地页生成器或者聊天机器人里;总体维护成本低,放在个人服务器上不会产生额外负担。
但使用边界同样要讲清楚:
- 它不是通用的互联网搜索引警,不能对标 Google 或 Bing,只能搜索你已经收集到的域名数据。
- 它不负责实时 whois 查询。数据库里的注册状态是历史的,不能替代注册商实时结果。
- 它不能作为域名交易、商标注册、法律纠纷的唯一判断依据,最终确认必须回到注册局或注册商页面。
- 如果数据集来源不合法,或者工具被用于批量抢注和恶意扫描,会带来明显的合规风险。
合规是这个项目最需要注意的部分,尤其是 whois 数据,里面可能包含注册人的个人信息。在多数地区,批量抓取和展示 whois 个人信息会触碰隐私保护规则。更稳妥的做法是只保留“该域名是否处于已注册状态”这样的结果,而不是把注册人、邮箱、电话等字段都存档展示。
3. 数据准备与合规采集
50 万条域名记录不是凭空长出来的,数据来源和清洗链路是整个项目的地基。选错数据源,后面搜索再好也没有用。
3.1 公开域名数据源
常见合法来源有这几种:
- 证书透明度日志(Certificate Transparency Logs):HTTPS 证书在签发时会写入公开日志,日志里包含大量域名字段。通过解析 CT 日志可以批量提取域名,再经过去重和字段清洗得到可用列表。
- Common Crawl 公开爬取数据:定期发布的互联网网页快照数据,里面包含海量 URL,提取域名后可以补充很多长尾站点记录。
- 注册局公开列表:部分 TLD 的注册局会提供 zone file 访问权限,但申请门槛因注册局而异,而且不是所有后缀都能拿到。能拿到的前提下,这是最权威的数据源之一。
这类外部数据源的共同问题是原始文件巨大,需要做大量过滤。比较好的处理流程是:先解析原始数据,去掉端口号、路径、协议前缀,再统一域名格式,最后按主域名维度去重。
3.2 自建爬虫的边界
如果不想完全依赖外部列表,也可以写受限爬虫从公开网页链接中提取域名。但这里有两个明确边界:一是遵守目标网站的 robots 协议和访问频率,不能对注册商、whois 服务做高强度请求;二是不要做大范围端口扫描和子域名爆破,这类行为容易引发安全和法律问题。一个周末时间要完成 50 万条数据收集,建议优先用公开数据集,爬虫只做补充。
3.3 数据清洗字段
拿到原始域名列表后,按以下规则做清洗:
- 去除重复项。
- 过滤空域名和含非法字符的记录。
- 决定是否要保留子域名。域名搜索工具一般只保留注册主域名,即
example.com,而不是sub.example.com,这样搜索“example”时结果更干净。 - 提取 TLD 后缀、域名裸名、域名长度、是否数字域名、是否含连字符等特征字段。
- 给每条记录标记数据来源和更新时间,便于后续增量更新。
3.4 存储字段设计
建议按这种结构建表:
| 字段 | 说明 |
|---|---|
| id | 主键 |
| domain | 完整域名,如 example.com |
| name | 裸域名,如 example |
| tld | 后缀,如 com、net、xyz |
| length | 域名长度,可用来快速找短域名 |
| has_hyphen | 是否含连字符 |
| has_digit | 是否含数字 |
| status | 注册状态备注,可留空 |
| source | 数据来源 |
| updated_at | 更新时间 |
这里再强调一次:不要保存不必要的 whois 个人字段。只存“域名是否存在”这样的布尔或枚举状态,就能满足大多数搜索场景。
4. 技术选型与系统设计
50 万条记录是个很微妙的规模。它不能用 Excel 直接打开,但也完全不需要分布式搜索引擎。从 10 美元预算和周末开发的约束来看,技术选型核心就三个字:不折腾。
4.1 可选存储方案对比
SQLite + FTS5 是一个很自然的选项。SQLite 自带全文搜索扩展 FTS5,50 万条文本数据对它来说压力不大,数据库就是一个单文件,备份、迁移、部署都非常简单。缺点是 FTS5 的分词和 MATCH 语法需要一些学习成本,而且不太适合复杂的分词场景,但对域名这种前缀/后缀特征明显的文本来说足够了。
DuckDB 适合做分析型查询,列式存储、导入 CSV 很快,适合一次性批量过滤和导出,比如“找出所有 .io 后缀且长度小于 6 的域名”。缺点是需要额外安装 Python 包,不适合高频点查场景。
PostgreSQL + pg_trgm 能做模糊匹配,功能很强,但对一台 512MB 内存的便宜 VPS 来说维护成本偏高,不建议作为首选。
综合来看,如果是自己搭这类工具,优先考虑 SQLite + FTS5,或者更朴素的 SQLite + 普通索引。FTS5 性能更好,普通索引更适合快速上线。
4.2 系统架构建议
一个最小可运行的架构分成四层:
data/source.csv 原始域名数据 data/domains.db SQLite 数据库 app/main.py FastAPI 服务入口 app/search.py 搜索逻辑 app/update.py 数据更新脚本 static/index.html 搜索页面服务层用 FastAPI,前端用纯静态 HTML,数据层用 SQLite。整条链路上没有消息队列、没有 Redis、没有容器编排。这套设计在 10 美元预算下是合理的。
5. 环境准备与开发环境搭建
先看环境要求。这个项目不依赖 GPU,纯 CPU 应用,服务器成本可以压得很低。
5.1 推荐环境
- 操作系统:Ubuntu 22.04 或 24.04,开发机用 macOS、Windows 都可以。
- Python:3.10 及以上。
- 磁盘:数据库文件加原始 CSV,预留 1GB 足够。
- 内存:512MB 到 1GB 的 VPS 可以跑,搜索是轻量操作。
- 数据库:SQLite 自带,不需要额外安装服务。
5.2 初始化项目
mkdir domain-search cd domain-search python3 -m venv venv source venv/bin/activate pip install fastapi uvicorn如果使用 DuckDB,再装一个依赖:
pip install duckdb5.3 目录结构
domain-search/ ├── data/ │ ├── domains.csv │ └── domains.db ├── app/ │ ├── __init__.py │ ├── main.py │ ├── search.py │ └── update.py ├── static/ │ └── index.html ├── requirements.txt └── README.md6. 核心功能实现:域名导入与搜索
这一章给出核心代码模板。实际项目可能有差异,但整体思路是通用的。
6.1 建表
打开 SQLite,创建域名表:
CREATE TABLE IF NOT EXISTS domains ( id INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT NOT NULL UNIQUE, name TEXT NOT NULL, tld TEXT NOT NULL, length INTEGER NOT NULL, has_hyphen INTEGER DEFAULT 0, has_digit INTEGER DEFAULT 0, status TEXT, source TEXT, updated_at TEXT ); CREATE INDEX IF NOT EXISTS idx_domains_tld ON domains(tld); CREATE INDEX IF NOT EXISTS idx_domains_length ON domains(length);6.2 导入 50 万条域名数据
假设数据文件是 CSV 格式,每行一个域名:
domain example.com notely.xyz导入脚本app/update.py:
import csv import sqlite3 from pathlib import Path DB_PATH = Path("data/domains.db") CSV_PATH = Path("data/domains.csv") def normalize_domain(line: str) -> str: line = line.strip().lower() if not line or line.startswith("#"): return None return line.rstrip(".") def main(): conn = sqlite3.connect(DB_PATH) cur = conn.cursor() with open(CSV_PATH, "r", encoding="utf-8") as f: reader = csv.DictReader(f) rows = [] for row in reader: domain = normalize_domain(row.get("domain", "")) if not domain or "." not in domain: continue parts = domain.split(".") name = ".".join(parts[:-1]) tld_part = parts[-1] rows.append(( domain, name, tld_part, len(name), 1 if "-" in name else 0, 1 if any(ch.isdigit() for ch in name) else 0, "unknown", "public-list", "2025-06-01" )) if len(rows) >= 5000: cur.executemany( "INSERT OR IGNORE INTO domains " "(domain, name, tld, length, has_hyphen, has_digit, status, source, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", rows ) conn.commit() rows = [] if rows: cur.executemany( "INSERT OR IGNORE INTO domains " "(domain, name, tld, length, has_hyphen, has_digit, status, source, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", rows ) conn.commit() cur.execute("SELECT COUNT(*) FROM domains") print("total:", cur.fetchone()[0]) conn.close() if __name__ == "__main__": main()批量写入 5000 条一次提交,避免 50 万行逐条 insert 导致速度过慢。INSERT OR IGNORE配合唯一约束可以天然去重,重复导入不会产生脏数据。
6.3 搜索逻辑
先实现最基础的两个搜索维度:
- 前缀搜索:输入
note,返回note.com、notely.xyz等结果。 - 包含搜索:输入
note,返回所有域名裸名里包含 note 的记录。 - TLD 过滤:只搜
.com或.io。 - 长度过滤:短域名优先。
搜索模块app/search.py:
import sqlite3 from pathlib import Path DB_PATH = Path("data/domains.db") def search_domains(keyword: str, tld: str = "", limit: int = 50): if not keyword: return [] conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cur = conn.cursor() sql = """ SELECT domain, tld, length, status FROM domains WHERE name = ? OR name LIKE ? """ params = [keyword, f"{keyword}%"] if tld: sql += " AND tld = ?" params.append(tld) sql += " ORDER BY length ASC, domain ASC LIMIT ?" params.append(limit) cur.execute(sql, params) rows = [dict(r) for r in cur.fetchall()] conn.close() return rows50 万条数据下,LIKE 'note%'不一定能走标准索引,实际响应时间需要在本机测试。如果单次查询能在几百毫秒内返回,对个人工具完全够用;如果等不了,就给name字段建 FTS5 全文索引。
FTS5 建表:
CREATE VIRTUAL TABLE domains_fts USING fts5( domain, name, tld, content='domains', content_rowid='id' ); INSERT INTO domains_fts(rowid, domain, name, tld) SELECT id, domain, name, tld FROM domains;搜索时改用:
SELECT domain, tld FROM domains WHERE id IN ( SELECT rowid FROM domains_fts WHERE name MATCH 'note*' )FTS5 的 MATCH 语法对点号等字符的处理比较特殊,正式使用前建议先在一千条小数据集上验证,避免匹配结果不符合预期。
7. 搜索引警接口 API 与批量查询
工具做成以后,最有扩展价值的一步是暴露 HTTP API。这样前端可以调用,脚本可以调用,后续接聊天机器人也可以调用。
7.1 FastAPI 接口
app/main.py:
from fastapi import FastAPI, Query from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from search import search_domains app = FastAPI(title="Domain Search API") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) class SearchRequest(BaseModel): keyword: str tld: str = "" limit: int = 50 @app.get("/api/search") def api_search( keyword: str = Query(..., description="搜索关键词"), tld: str = Query("", description="TLD 后缀,例如 com"), limit: int = Query(50, ge=1, le=100) ): results = search_domains(keyword, tld=tld, limit=limit) return { "code": 0, "message": "ok", "data": results } @app.post("/api/search") def api_search_post(req: SearchRequest): results = search_domains(req.keyword, tld=req.tld, limit=req.limit) return { "code": 0, "message": "ok", "data": results } @app.get("/health") def health(): return {"status": "up"}启动服务:
uvicorn app.main:app --host 127.0.0.1 --port 8000启动后,浏览器直接访问:
http://127.0.0.1:8000/api/search?keyword=note&tld=io7.2 curl 调用
curl "http://127.0.0.1:8000/api/search?keyword=note&tld=io&limit=10"返回结构:
{ "code": 0, "message": "ok", "data": [ { "domain": "note.io", "tld": "io", "length": 4, "status": "unknown" } ] }7.3 Python 批量查询脚本
如果要把域名搜索接到批量流程里,比如一次性跑几千个关键词,建议写一个脚本,控制并发和限速:
import requests import time import json BASE_URL = "http://127.0.0.1:8000" def batch_search(keywords, tld="", output="results.jsonl"): results = [] for kw in keywords: try: resp = requests.get( f"{BASE_URL}/api/search", params={"keyword": kw, "tld": tld, "limit": 20}, timeout=5 ) if resp.status_code == 200: data = resp.json().get("data", []) results.append({"keyword": kw, "results": data}) else: results.append({"keyword": kw, "error": resp.status_code}) except Exception as e: results.append({"keyword": kw, "error": str(e)}) time.sleep(0.2) # 限速,避免打满服务 with open(output, "w", encoding="utf-8") as f: for item in results: f.write(json.dumps(item, ensure_ascii=False) + "\n") print("done, total:", len(results)) if __name__ == "__main__": kws = ["note", "memo", "planner", "pixel", "drift"] batch_search(kws)批量任务里最容易被忽略的是失败重试。接口偶尔会因为网络抖动或资源占用返回 5xx,建议在脚本里加 retry 逻辑:遇到超时或 500,等 1 秒后再试一次,连续失败则把错误写入日志,而不是直接中断整个任务。
7.4 接口安全建议
如果服务要暴露到公网,至少做三件事:
- 加访问令牌,请求头里带
Authorization: Bearer <token>。 - 用限流中间件或 Nginx 层限制单 IP 请求频率。
- 只开放需要公开的接口,不需要的接口不要暴露。
8. 部署上线与成本控制
10 美元预算怎么分配?核心原则是:能白嫖就白嫖,能用免费开源就不用商业服务,能单文件就单文件。
8.1 服务器选择
这类项目对配置要求很低。常见选择是买一台便宜 Linux VPS,1 核、1GB 内存、20GB 存储,价格通常在每月几美元。如果你选择 1 核 512MB 的机器,跑 FastAPI + SQLite 也够用。前端部分可以用 Cloudflare Pages 或 GitHub Pages 免费托管,进一步节省 VPS 带宽。
8.2 使用 systemd 托管服务
写一个 systemd 服务文件/etc/systemd/system/domain-search.service:
[Unit] Description=Domain Search API After=network.target [Service] User=ubuntu WorkingDirectory=/home/ubuntu/domain-search ExecStart=/home/ubuntu/domain-search/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=always RestartSec=3 Environment="PYTHONUNBUFFERED=1" [Install] WantedBy=multi-user.target启用并启动:
sudo systemctl daemon-reload sudo systemctl enable domain-search sudo systemctl start domain-search查看日志:
sudo journalctl -u domain-search -f8.3 前端页面
前端可以直接写一个静态页面,用 Nginx 或 Caddy 反代到 API。简单示例static/index.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Domain Search</title> </head> <body> <h1>域名搜索</h1> <input type="text" id="kw" placeholder="输入关键词,如 note" /> <select id="tld"> <