海风域名查询工具v1.0:批量域名信息查询与归一化实战
简介海风域名查询工具v1.0是一套面向Linux主机环境的PHP源码类域名查询程序适合站长、运维人员或有一定PHP部署基础的学习者用于在自有服务器上部署轻量化的域名信息检索服务减少对第三方接口的依赖满足日常批量查询与记录需求。压缩包为rar格式程序主体为PHP源码整体仅372KB结构紧凑、部署成本低适合快速安装到Linux主机使用。目前已有118人学习/下载可作为同类自建工具的参考。包内提供可运行的完整源码并配套安装初始化所需的目录、配置及数据文件用户能够按自身环境完成后台设置与初始导入从而搭建可独立维护的域名查询服务同时也可借此学习PHP程序在Linux环境下的安装路径规划、配置项写法与数据表初始化逻辑对想动手自建域名工具的中级开发者有实际参考价值。1. 海风域名查询工具 v1.0 到底查什么不是又一个 whois 客户端如果你手里管着几十上百个域名每隔几个月就得手动确认一遍“还活着吗、注册商有没有被改、到期时间还剩多久”那你大概率被网页版 whois 折磨过每个注册商一个页面验证码盘踞在中间复制下来的时间格式七零八落。海风域名查询工具 v1.0 就是为这个批量盘点场景写的。它的定位不是单条 whois 咨询而是一次输入一份域名清单把每个域名的注册商、到期时间、NS 记录、解析 IP、备案状态统一拉平输出既能落地 SQLite 做增量更新也能直接给运营一版 Excel。适合运维、网站运营和做域名资产交接的人也适合排查“为什么这个域名解析不对”时快速看清全貌。这事难的不是查一个域名而是把几十种 whois 服务器的长短文本和 RDAP 的 JSON 归到一个能横向排序的格式。2. 拆开海风域名查询工具 v1.0 的架构域名规范化、查询后端、结果存储三层各管什么2.1 第一层域名输入与规范化为什么不能直接拿字符串去查whois 查询对输入格式极其挑剔。最初我把域名清单里的行直接交给查询函数翻车翻在几个没想到的地方清单里混着https://www.example.com/path?x1、*.example.com、例子.中国、EXAMPLE.COM.这四种字符串。第一种是浏览器地址栏直接复制出来的 URL第二种是证书 SAN 里抄出的通配符第三种是中文域名第四种结尾多一个点。用这些原始串去请求 whois 服务端前三种普遍失败第四种不报错但结果对不上。所以海风 v1.0 把输入规范化单独拆成一层而不是在查询函数里顺手做——顺手做容易在一个分支抛异常时影响整批任务拆开后排查方便也能独立测试。一个可行的清洗函数是这样的import re from urllib.parse import urlparse def normalize_domain(raw: str) - str | None: raw raw.strip().lower() if not raw: return None # 带协议的输入先用 urlparse 取出 hostname if :// in raw: raw urlparse(raw).hostname or # 去掉路径、端口和结尾的根域点 raw raw.split(/)[0].split(:)[0].rstrip(.) # 中文域名转 ASCII 的 punycode if re.search(r[\u4e00-\u9fa5], raw): raw raw.encode(idna).decode(ascii) # 去掉通配符前缀注意不能用 lstrip(*.)。 if raw.startswith(*.): raw raw[2:] parts raw.split(.) if len(parts) 2 or any(p for p in parts): return None return raw这段代码的核心逻辑是五件事。第一统一小写域名解析大小写不敏感whois 返回文本却经常夹大小写入库前不统一后续 group 和去重都会多花成本。第二用urlparse(raw).hostname处理带协议头的输入比正则提取稳它已经剥掉了协议、认证信息和端口。第三rstrip(.)去掉根域残留的点。第四中文域名必须 IDNA 编码成xn--开头数据库和协议层只认这种写法。第五剥通配符用显式前缀判断加切片别用lstrip(*.)。原因很隐蔽lstrip的参数是字符集合*.表示剥掉所有开头的*和.如果域名开头恰好是.也会被一起剥掉不是按字面量剥。这一层还要负责过滤非法输入。比如用户把 IP 地址当域名传进来海风 v1.0 的选择是交给 DNS 解析层处理whois 部分直接标为不适用避免拿 IP 去查域名 whois 得到一串意义不明的数据。规范化层的原则是宁可多几行判断也不让特殊字符寄生在域名里进入下游。2.2 第二层查询后端选型——WHOIS 协议、RDAP、DNS 解析的职责分工第二层是查询后端海风 v1.0 对三个数据维度走三条不同通道。注册信息走 whois 协议或 RDAP解析信息走 DNS国内域名的备案状态走单独的 HTTP 接口。三件事一开始被写进一个query_domain()函数后来发现不拆不行whois 超时会拖住 DNS 结果返回RDAP 报错会中断后续解析一批域名里只要有一个异常的整个批次就不干净。拆成三个独立模块之后每个模块的返回结构统一成字典并发调度时也少了很多隐性问题。注册信息查询的通道选择核心逻辑是这样的def query_registration(domain: str, cfg): try: rdap_data query_rdap(domain, timeoutcfg.rdap_timeout) return parse_rdap(rdap_data) except RDAPNotFound: return {status: available, source: rdap} except (RDAPTimeout, RDAPBackendError): # 降级到 whois而不是整体失败 pass whois_text query_whois_tcp(domain, port43, timeoutcfg.whois_timeout) return parse_whois(whois_text)为什么优先 RDAP 而不是 whoisRDAP 走 HTTPS 443 端口在绝大多数服务器和办公网络里不会被防火墙拦它返回的是 JSON 结构化数据字段稳定解析工作量比 whois 纯文本小得多。但 RDAP 不是所有后缀都有部分小国别后缀至今只提供传统 whois 服务所以策略是优先 RDAP、失败后降级 whoisDNS 全程独立。这段代码里有一个参数值得细看超时时间。rdap_timeout和whois_timeout在海风 v1.0 里都是可配置项默认 8 秒。短了在跨洋链路上会频繁误报超时长了拖慢整个批次的节奏。实际跑批时见过 15 秒的配置一次查 200 个域名光超时等待就占掉大半时间收益却几乎为零。DNS 解析模块相对独立用dns.resolver.resolve()分别取 A/AAAA 和 CNAME。解析失败不等于域名不存在可能是 NXDOMAIN也可能是本地 resolver 超时或劫持。海风 v1.0 对 DNS 结果只记录不判断因为这些数据的消费方是上层脚本工具层硬套“存在/不存在”会造成误判。whois 协议本身值得一提它是 TCP 43 端口上的明文协议发送查询命令后逐行读取文本响应。各大注册商的返回模板这些年在持续改版与其依赖一个把模板写死的第三方库不如自己维护一份高频注册商的字段映射表低频后缀用通用正则兜底。这也是海风 v1.0 的查询层没有依赖第三方 whois 库的核心理由。2.3 第三层结果归一化入库SQLite 还是 CSV 得看用途第三层做两件事把不同渠道的字段统一格式再把统一后的记录持久化。字段统一最典型的就是到期时间。.com的 RDAP 返回2031-08-14T07:00:00Z.cn的 whois 返回2031-08-14 12:00:00有的注册商只给2031-08-14这种纯日期。如果只做到字符串比对这一层数据永远没法横向排序。归一化函数长这样from datetime import datetime, timezone def normalize_expiry(raw: str, assume_utc: bool True) - str: raw raw.strip() for fmt in (%Y-%m-%dT%H:%M:%SZ, %Y-%m-%d %H:%M:%S, %Y-%m-%d): try: dt datetime.strptime(raw, fmt) if dt.tzinfo is None and assume_utc: dt dt.replace(tzinfotimezone.utc) return dt.isoformat() except ValueError: continue return raw格式匹配顺序是有讲究的先匹配最严格的 ISO 8601再匹配带空格和秒的最后匹配纯日期。assume_utc照顾不带时区标记的文本默认按 UTC 处理。这样不同批次的数据才能稳定排序所谓“时间对不上”的玄学问题基本都能从这一步的输出开始追溯。持久化方面v1.0 默认写 SQLite建表如下CREATE TABLE IF NOT EXISTS domain_records ( domain TEXT PRIMARY KEY, registrar TEXT, expiry_date TEXT, dns_servers TEXT, resolved_ip TEXT, icp_status TEXT, source TEXT, checked_at TEXT, raw_snippet TEXT ); CREATE INDEX IF NOT EXISTS idx_expiry ON domain_records(expiry_date);几个字段设计值得解释。domain作为自然主键因为这张表的增删改查全都按域名展开不需要自增 idwhois 返回文本大小写混乱入库前必须用规范化后的字符串做主键否则同一域名会变成多条记录。dns_servers和resolved_ip是变长数组SQLite 没有原生数组统一 JSON 序列化后存 TEXT查询时用json_extract取回。raw_snippet保留原始响应前 500 字符平时查询不涉及它真到了比对两批数据字段差异时它是救命的东西。写入逻辑用INSERT OR REPLACE天然按域名覆盖INSERT OR REPLACE INTO domain_records (domain, registrar, expiry_date, dns_servers, resolved_ip, icp_status, source, checked_at, raw_snippet) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);这个语句的风险在于它会先删旧行再插新行如果表上有外键引用就会出错。海风 v1.0 没建外键所以安全。往后要是想扩展历史版本表就得改成INSERT ... ON CONFLICT(domain) DO UPDATE把变更前的raw_snippet留档否则两周前谁改了注册商都查不出来。3. 用海风域名查询工具 v1.0 跑通一次批量查询配置与参数逐项说透3.1 环境准备与依赖为什么从 Python 3.10 起步海风域名查询工具 v1.0 是一个 Python 命令行工具运行环境建议 Python 3.10 以上。这不是版本洁癖是因为代码里用了 3.10 引入的type | None联合类型标注和结构化模式匹配语法上能少写不少条件判断。版本低于 3.10 也能改但改完后代码可读性会明显变差。依赖安装三步python3 --version python3 -m venv .venv source .venv/bin/activate pip install requests dnspython tldextract运行时只依赖三个库。requests负责 HTTP 请求包括 RDAP 查询和备案接口dnspython负责 DNS 解析因为系统的getaddrinfo不支持自定义 resolver 和超时控制批量场景必须用它tldextract用于从完整域名中提取注册域名公共后缀列表更新频繁自己维护一份顶级域表过两年就落后于现实。没有引入第三方 whois 库理由在上一章提过注册商返回模板差异太大库作者维护不过来用正则加字段映射表反而直观可控。3.2 最小跑通命令单域名查询的参数拆解环境就绪后先跑单域名验证整条链路。海风 v1.0 的命令行入口是seawind.pypython seawind.py example.com --output-format json这条命令里没配数据库文件也没配并发工具以默认参数运行RDAP 优先、超时 8 秒、结果输出到终端 JSON。这是刻意的设计——单域名场景要的是快速看清全貌不能逼用户先建一个数据库才能用。输出样子{ domain: example.com, registrar: IANA, expiry_date: 2031-08-14T07:00:00Z, dns_servers: [a.iana-servers.net, b.iana-servers.net], resolved_ip: [93.184.216.34], source: rdap }注意expiry_date是标准 ISO 格式resolved_ip是数组形式给多 IP 的场景留了余地。如果域名未注册status字段会变成available其余字段留空不抛异常。这个行为在批量跑的时候很重要因为一个批次的整体流程不会因为个别域名未注册而中断。3.3 批量模式与并发参数并发数不是越大越好单域名跑通后批量模式才是主要战场。假设手头有个 200 行的domains.txt# 主站 example.com www.example.com # 活动域名 promo.example.net以#开头的注释行会被自动忽略空行也是。批量命令python seawind.py --input domains.txt --concurrency 8 --db records.db批量跑和单域名跑的最大区别是并发模型。海风 v1.0 内部用asyncio.Semaphore控制活跃查询协程数量把每个域名拆成独立任务全部结束后统一写库。实现骨架import asyncio async def query_batch(domains: list[str], concurrency: int): sem asyncio.Semaphore(concurrency) async def one(domain: str): async with sem: return await query_one(domain) return await asyncio.gather(*(one(d) for d in domains))Semaphore(concurrency)保证同时只有concurrency个查询在运行其余任务在async with sem处排队等待。这个参数是海风 v1.0 最容易调错的地方以踩坑经验看--concurrency 8是绝大多数环境下的可靠起步值。.com这类接受度高的后缀可以试着提到 16.cn、.de这类风格严格的后缀建议压低到 4。并发过大的信号有三个前 20 个域名成功率 100%到第 50 个开始整批出现failed错误信息不是 timeout 而是连接重置或 429把并发降到 1 后失败的域名又全部成功。出现任何一个先把并发减半重跑一轮。不要为了追求速度调到 32 以上大多数查询端都有限流硬顶的结果是成功率不升反降整体耗时更长。注意跑第二轮时如果failed数量显著增多第一件事不是查网络而是把并发降到 4再用第一轮失败的清单复查。这个血泪经验能省下一整天的排查时间。3.4 四种输出出口的取舍终端表格、JSON 文件、CSV 和 SQLite不同人消费查询结果的方式完全不一样。海风 v1.0 的四种输出出口覆盖了四种典型场景出口命令写法典型使用场景终端表格--output-format table临时查几个域名肉眼确认JSON 文件--output-format json --output out.json程序继续消费做增量比对CSV 文件--output-format csv --output out.csv运营用 Excel 打开做资产盘点SQLite--db records.db每周巡检做长期趋势CSV 输出里有一个很容易被忽略的编码细节import csv with open(out.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[domain, registrar, expiry_date]) writer.writeheader() for row in rows: writer.writerow(row)encodingutf-8-sig会在文件头写入 BOMWindows 下的 Excel 靠这个标识才能正确识别 UTF-8 中文。很多人上线前直接在 Linux 上生成 CSV默认utf-8编码发给运营后中文全变乱码回来问是不是工具坏了。这不是 bug是历史编码兼容性问题选择在工具层直接规避省得每次跑批前都叮嘱一句。JSON 输出则要保证字段顺序固定海风 v1.0 在写 JSON 时固定了键的顺序让不同批次的输出可以直接 diff这对后续写巡检脚本判断字段漂移非常有用。4. 海风域名查询工具 v1.0 使用避坑与常见问题排查4.1 现象一批量跑大面积超时单查却不超时现象批量执行进行到一半时大量查询失败日志里timed out刷屏同一个域名手动改成并发 1 去查又能正常出结果。看起来像网络故障本质却是两件不同的事叠加。原因出口网络对 43 号端口的限制加上查询端的速率限制。云服务器和办公网络默认对非标准端口出站策略严格whois 的 TCP 43 端口经常被直接丢包或者时通时不通即使端口可达并发一高也会触发注册局的限流现象同样是超时。解决先用nc -vz whois.verisign-grs.com 43测试 43 端口连通性确认端口可达后再谈并发问题。然后把并发降到 4用失败的域名清单重跑。如果降并发后成功率恢复锁定为限流处理策略是失败三次的域名丢进retry.txt错峰重跑不要硬顶。提示排查超时问题时先用curl -m 8 https://rdap.org/domain/example.com验证 RDAP 链路是否正常能快速区分是 43 端口被限制还是查询端整体不可达。4.2 现象二中文域名显示成 xn-- 开头的乱码还查不到结果现象查询结果里域名列显示的是一长串xn--开头的字符串完全看不出原始中文一部分中文域名的 whois 返回为空。原因IDNA 编码是协议层正常行为数据库和 whois 协议只认 punycode。但 whois 服务端对 IDN 的支持参差不齐有的注册局只接受 punycode有的只接受 Unicode 原文显示层也不做回显转换直接把 punycode 扔给用户看。解决库内存储用 punycode展示时还原成中文查询时优先用 punycode。回显转换一行代码def to_unicode(domain: str) - str: return domain.encode(ascii).decode(idna)再补一个排查技巧如果 punycode 查不到用原始中文再试一次两边都失败才考虑是注册局没开放对应 whois 服务别急着认为工具坏了。4.3 现象三到期时间排序错乱三个月后到期的排到了一年后现象把一批结果按到期时间排序发现几个域名的顺序明显不对。翻原始 whois 才看到有的是本地时间有的是 UTC格式五花八门。原因whois 文本的日期格式没有统一规范2031-08-14 08:00:00可能是 UTC 可能是北京时间字符串直接排序必出错。解决海风 v1.0 统一了归一规则优先识别 RFC 3339无时区标记按 UTC 处理入库前全转 ISO 格式。这里有一个关键参数assume_utc默认开启只适用于数据源本身不带时区的情况。如果你查询的注册局明确以北京时间返回文本应该调整偏移而不是盲开assume_utc否则所有记录都会差 8 个小时。4.4 现象四RDAP 和 whois 返回的到期时间不一致现象同一个域名RDAP 返回2031-08-14T04:00:00Zwhois 返回2031-08-14 04:00:00两套数据日期相差一两天。同一域名两批入库后结果冲突。原因RDAP 数据由注册局维护whois 文本由注册商生成两条管道同步存在延迟个别注册局还会出现数据回滚。这不是查询逻辑写错是上游数据源本身就不同步。解决海风 v1.0 按三条策略处理。RDAP 有值则优先采用whois 只负责兜底缺失字段两者冲突时保留source字段记录数据来源同时保存checked_at时间戳。后续比对看到同一域名不同时间的两份记录时就能自动感知同步滞后问题而不是稀里糊涂覆盖掉。4.5 现象五CSV 在 Excel 里中文全乱码现象同事反馈工具导出的 CSV 在 Windows 办公机上打开是乱码英文正常中文必乱。原因生成 CSV 的默认编码是 UTF-8Windows 版 Excel 只认带 BOM 的 UTF-8 和 ANSI。文件本身没问题是编码历史兼容性的锅。解决生成时写作utf-8-sig代码在 3.4 节已经给出。这个坑最好在工具层解决而不是靠下游“另存为 UTF-8”转一次——每次跑批都人工干预一次成本比改工具高得多。5. 海风域名查询工具 v1.0 的进阶验证一份已知样本一条巡检命令5.1 用已知状态的域名做交叉验证验证工具不是看跑出来有没有输出而是看输出和真实状态对不对得上。做法是维护一份已知样本集覆盖五种典型状态正常注册且解析正常的域名、已过期被停用的域名、未注册的域名、中文域名、带子域名的输入。验证命令python seawind.py --input known_samples.txt --output-format json --output verify.json手工抽查verify.json里的标志字段。正常注册的域名status应为activeexpiry_date与 whois 原始输出一致未注册域名应返回available如果返回的不是available说明查到了脏数据。把这套样本集在升级工具、换服务器或改并发参数后重跑能快速判断新环境连接查询端是否正常。每次跑批后把实际结果和样本预期做一次比对并记录这是保证工具长期可用性的最低成本手段。5.2 把查询排进周期巡检工具真正值钱的地方是可重复。把域名资产清单固定下来在 Linux 的 crontab 或 Windows 计划任务里挂一行python seawind.py --input assets.txt \ --concurrency 8 \ --db records.db \ --output-format csv \ --output assets_$(date %Y%m%d).csv数据库保留所有批次的结果CSV 快照按日期存档。一个月后把两次快照做一次 diffexpiry_date变化超过 3 天的域名挑出来二次确认基本能覆盖域名被转让、注册商被改、解析被指到陌生 IP 这些高危场景。长期维护这套工具后我养成的习惯是每次跑批前先对原始输入做重复项和长度统计脏输入往往藏在几十行都不看一眼的名单里。整个工具做到“能跑”不难做到“每次跑出的结果都敢拿去做决策”才算合格。希望帮到你。本文还有配套的精品资源点击获取