☰
WHOIS查询源码实战:从端口43直连到RDAP与批量监控
2026/9/28 12:50:44 网站建设 项目流程

简介:域名信息查询同款WHOIS源码.zip 是一套面向网站开发者与站长的域名WHOIS查询功能实现,基于PHP后端查询逻辑与前端页面搭配,可用于解析域名注册信息、名字服务器等详细数据。资源包共19个文件,压缩后仅290KB,包含4个PHP处理脚本、3个HTML页面模板与3个SVG图形资源,另有CSS样式、JS交互脚本、WOFF/TTF/EOT字体及PNG/ICO图标等,覆盖查询、展示、美化各环节,结构简洁,适合快速部署或二次开发。目前已有278人学习/下载,适合需要搭建自有域名查询工具或学习WHOIS协议应用的入门到中级开发者。通过源码可掌握域名信息查询的前后端交互流程,包括查询参数接收、WHOIS数据解析、结果展示与页面美化;PHP脚本与页面文件分离,便于按需替换查询源或增加批量查询等扩展功能。整体体积小、依赖简单,是一份实用的域名信息查询参考样例。

1. 域名信息查询同款WHOIS源码.zip:为什么你绕不开这套经典方案

做域名监控、到期提醒、批量选品留档的人,几乎都会在某天下载一份“域名信息查询同款WHOIS源码.zip”。这套源码包的价值在于,它把端口43上的纯文本应答、多个注册局的解析差异、过期时间计算这些没人愿意反复踩的坑,一次性打包成了可运行的本地服务。对开发者、运维和域名投资者来说,搭起它就能脱离对第三方付费接口的依赖,自己控制缓存和限流,把查询从“每次等对方返回”变成“本地一份干净数据”。这篇笔记会从协议差异讲起,带你走完解压、启动、封装、避坑和批量查询的完整流程,新手能照做,熟手能直接拿走参数。

2. 先立住查询原理:端口43直连、RDAP与新顶级域的响应差异

WHOIS 之所以“看起来简单,做起来翻车”,是因为同一个查询动作背后有三套数据通道,老源码用得最多的是 TCP 43 直连,最近几年才是 RDAP。如果一开始不把这层关系理顺,后面解析代码怎么写都会觉得像在猜谜。

2.1 端口43直连:免费、直接,但响应是给人读的文本

传统 WHOIS 协议非常简单:客户端通过 TCP 连上注册局服务器的 43 端口,发送一行域名加回车换行,服务器就用纯文本把注册信息回给你。没有 HTTP 状态码,没有 JSON,没有分页,一套响应里既有注册商、注册日期、到期日期,也有状态、Name Server,甚至还有注册局的免责声明。

很多源码包里的核心逻辑就是一层这么薄的 socket 封装。常见做法是先用whois.iana.org确认域名所属注册局,再跳到具体的 WHOIS 服务器问一次。最小实现可以这样写:

import socket def whois_raw_query(domain: str, server: str, port: int = 43, timeout: float = 6.0) -> str: """向指定 WHOIS 服务器发送一条查询,返回原始文本。""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect((server, port)) sock.send(f"{domain}\r\n".encode("ascii")) chunks = [] while True: data = sock.recv(4096) if not data: break chunks.append(data.decode("utf-8", errors="ignore")) return "".join(chunks) finally: sock.close() # .com/.net 可以直连 VeriSign 的 GR Server print(whois_raw_query("qq.com", "whois.verisign-grs.com"))

这段代码的核心在于 socket 读取是阻塞式的,服务器断开连接才算响应结束,所以timeout参数很关键。设太小,跨国链路稍微抖动就报超时;设太大,服务线程容易被慢连接拖死。我一般会取 5 到 6 秒,配合重试控制,既不显得急躁也不会把线程池占满。

还要注意编码。老注册局的响应可能是 ASCII,也可能是 GBK 或 UTF-8,直接decode("utf-8", errors="ignore")会把中文注册商截掉。实际工程中,应当先做编码探测,拿不到明确编码时用 UTF-8 失败回退 GB18030,这到解析章节再展开。

2.2 RDAP 是正规军,为什么老源码还坚持 43 端口

RDAP 是近几年注册数据访问的标准方向,走 HTTPS 443,响应是标准 JSON 结构,字段名统一,还带 HTTP 限流头。比如查询 .com 域名可以请求https://rdap.verisign.com/com/v1/domain/qq.com,返回里的events数组会明确标出注册时间和过期时间,几乎不用写正则。

既然 RDAP 这么规范,为什么标题里的这套源码还要坚持走 43 端口?原因很现实:第一,老代码发布时 RDAP 还没普及到所有注册局,很多新顶级域只能靠 WHOIS 跳转;第二,RDAP 端点分散在 IANA 的 bootstrap 文件里,调用方要先拉取数据再决定请求哪个端点,这套逻辑比“查一次 IANA 再查一次注册局”更绕;第三,大量 PHP 源码跑在共享虚拟主机上,出站 443 随时可用,但编辑和扩展一套 socket 脚本比引入 RDAP 客户端库更省事。

我的判断是:新项目应优先考虑 RDAP,老源码改造则不必推翻重来。在 43 端口实现外面包一层适配器,优先走 RDAP,失败再回退到 socket 查询,这样兼容性和可维护性都拿得住,也是目前最主流的渐进方案。

2.3 动手前先建一张 TLD 映射表

不论用 43 端口还是 RDAP,第一步都是确定“这个域名该问谁”。不同后缀的查询服务器差别很大,不能硬编码一套。这里整理了平时最常用的一批:

后缀WHOIS 服务器响应格式备注
com/netwhois.verisign-grs.comkey: value字段最规整,适合先拿来调试解析器
cnwhois.cnnic.cn中文字段 + 英文混排编码常是 GBK,字段名与 com 差异大
orgwhois.pir.orgkey: value状态字段多,需要按多值处理
iowhois.nic.iokey: value字段较少,但存在服务器跳动
xyz/top 等新顶级域需先查 IANA常见“refer”跳转很可能第一层响应只是指引,需二次查询

把这五类跑通,基本就能覆盖绝大多数查询需求。注意表格里的映射关系会随注册局运营变更,建议在服务里留一个可热更新的配置文件,而不是把服务器地址写死在代码里。等源码落地之后,这张表就是你要维护的第一个基础设施。

3. 从zip包到本地服务:解压、识别项目类型、最小启动命令

拿到“域名信息查询同款WHOIS源码.zip”之后,很多人习惯性双击解压,然后对着目录发呆。这里最容易踩的第一个坑,不是代码跑不起来,而是包根本就没解干净。先花两分钟检查包内格式,能省下后面一整段排错时间。

3.1 解压前先处理伪加密:这一步能省一下午

网上流传的源码 zip 包经常带一个“有密码”的假象:解压时提示 password required,但发布者从头到尾没说过有密码。这多半是 zip 伪加密——打包工具把加密标志位置了 1,实际文件内容并没有加密,只是部分解压工具看到标志位就要密码。

判断方法很简单:用 Python 读 zip 的中央目录,直接看 General Purpose Bit Flag 的最低 bit。伪加密的标志能被无损剥离,处理脚本如下:

import struct def strip_fake_encryption(src: str, dst: str) -> None: """把 zip 文件里的加密标志位强制清零,处理伪加密后另存。""" data = bytearray(open(src, "rb").read()) for signature in (b"PK\x03\x04", b"PK\x01\x02"): offset = 0 while True: pos = data.find(signature, offset) if pos == -1: break flags_pos = pos + 6 flags = struct.unpack_from("<H", data, flags_pos)[0] if flags & 0x1: struct.pack_into("<H", data, flags_pos, flags & 0xFFFE) offset = pos + 1 with open(dst, "wb") as f: f.write(data) stripped = strip_fake_encryption("WHOIS源码.zip", "whois_stripped.zip")

代码把本地文件头和中央目录里的加密标志一并清零,避免有些工具读取中央目录后仍然报错。参数上只需要注意路径别覆盖原文件,处理完先unzip -l whois_stripped.zip看一眼文件列表。

如果清零后仍要密码,那就是真加密,这种只能联系发布者要原始密码,别浪费时间在字典爆破上。这也是 zip 源码包最典型的“虚晃一枪”,解决掉它,后续流程才走得下去。

3.2 识别包内技术栈:用启动方式反过来认项目

解压后先不要急着找 README,直接看目录结构。源码包的技术栈通常从文件特征一眼就能认出,不同结构对应完全不同的启动方式:

目录特征技术栈最小启动命令
app.py + requirements.txt + templates/Python Flaskpython app.py
index.php + config.php + sql/原生 PHPphp -S 0.0.0.0:8080 -t public
vendor/ + composer.jsonPHP 全家桶项目composer install 后 php artisan serve
package.json + server.jsNode.jsnpm install && npm start
docker-compose.yml容器化docker compose up -d

标题里这类“同款源码”最常碰到的是 Python Flask 和原生 PHP 两种。PHP 版本的解析逻辑一般集中在whois.php或function.php,用fsockopen连 43 端口;Python 版本则通常拆成了whois_client.py、parser.py和app.py。不管哪一类,先跑ls -R大概浏览一遍,找到入口文件,再决定用哪条启动命令。

3.3 最小启动命令与本地自测

以最常见的情况为例:解压出来是 Flask 项目。先建虚拟环境,再装依赖,最后启动:

python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python app.py

参数说明:端口默认在 Flask 代码里通常是 5000,如果被占用,改成 5001;host建议写成127.0.0.1先本地验证,不要一上来就绑0.0.0.0。requirements.txt里如果带了mysqlclient或lxml,在 Windows 上经常要编译,装不动就先把那几行注释掉,等主流程跑通再补。

启动成功后的自测命令:

curl -s "http://127.0.0.1:5000/api/whois?domain=qq.com" | head -c 800

能返回 JSON 或一段注册信息文本,说明整条链路已经通了。不同源码的响应字段命名可能不同,但这一步只要能拿到数据,就证明 socket 查询、解析、HTTP 层三个环节都工作正常。接下来要做的,就是把这些数据变成“敢直接拿去用”的服务。

4. 把查询做成服务:响应解析、缓存策略与并发限流

源码能查出原始文本和能提供稳定服务之间,隔着解析、缓存、限流三座山。很多免费的“同款源码”只做到前两步中的一半,所以跑起来总给人一种“能用但不敢用”的感觉。这一章把这三个模块逐个补齐。

4.1 注册局文本响应差异:用“TLD 分支”解析而不是一个正则打天下

把原始文本转成结构化字典,是所有 WHOIS 服务里最容易翻车的一段。com 的字段是英文Creation Date:,cn 的字段却可能是中文“注册日期:”加 GBK 编码,org 的 Status 还会出现多行重复。同一个域名在不同注册局解析出来的字段名、取值格式完全不同,所以解析器第一条原则就是按 TLD 分支处理。

import re def parse_whois(raw: bytes, tld: str) -> dict: """按 TLD 分支解析注册局原始响应,返回结构化字段。""" try: text = raw.decode("utf-8") except UnicodeDecodeError: text = raw.decode("gb18030", errors="replace") out = {"registrar": "", "creation": "", "expiration": "", "status": [], "ns": []} if tld in ("com", "net"): patterns = { "registrar": r"Registrar:\s*(.+)", "creation": r"Creation Date:\s*([0-9T:.\-Z]+)", "expiration": r"Registry Expiry Date:\s*([0-9T:.\-Z]+)", "status": r"Status:\s*(.+)", "ns": r"Name Server:\s*(\S+)", } elif tld == "cn": patterns = { "registrar": r"注册商:\s*(.+)", "creation": r"注册日期:\s*(.+)", "expiration": r"到期日期:\s*(.+)", "status": r"状态:\s*(.+)", "ns": r"Name Server:\s*(\S+)", } else: patterns = { "registrar": r"Registrar:\s*(.+)", "creation": r"Creation Date:\s*(.+)", "expiration": r"Expiry Date:\s*(.+)", "status": r"Status:\s*(.+)", "ns": r"Name Server:\s*(\S+)", } for key, pattern in patterns.items(): matches = re.findall(pattern, text) if key in ("status", "ns"): out[key] = matches elif matches: out[key] = matches[0].rstrip() return out

这段代码的关键在两点:status和ns是典型的多值字段,必须用findall收集完整列表,否则后续做状态判断时只能看到最后一条;编码回退放在解析前,先试 UTF-8,失败转 GB18030,能同时覆盖中英文响应。字段取值上,注册商、日期类的单值字段处理成空字符串而不是None,这样下游做告警判断时不会因为类型不统一而报错。

这套解析器还不够完美,真实响应里偶尔会出现 “Creation Date” 值被截断的情况,建议在返回前补一层字段清洗:把日期统一转成YYYY-MM-DD,无法识别的日期宁可留空,也不要硬算一个错误值进数据库。

4.2 缓存参数:为什么 WHOIS 服务不能每次都打注册局

很多人抱怨免费源码查询慢,剖开看多半是没有缓存。WHOIS 数据的本质是低频变化信息,绝大多数域名的注册商、创建日期、到期日期不会在几小时内改变。每一条查询都向 43 端口打一次,等于把简单请求压在一个极易受限流的通道上。

内存缓存是最简单的方案,不必一上来就上 Redis:

import time CACHE = {} def get_whois_cached(domain: str, ttl: int = 86400) -> dict: """带 TTL 的内存缓存查询结果,默认缓存一天。""" now = time.time() cached = CACHE.get(domain) if cached and now - cached[1] < ttl: return cached[0] data = query_and_parse(domain) CACHE[domain] = (data, now) return data

参数说明:ttl默认给 86400 秒,适合域名到期监控、资产盘点这类批量场景。如果服务是给注册商内部用的,刚过户、刚注册的域名查询频率高,可以把 TTL 压到 300 到 600 秒,再配合更新时间字段手动失效。内存缓存只要维护一份字典加时间戳就足够,单机部署完全不需要引入外部组件。

4.3 并发与限流:线程数、间隔、退避三个参数

WHOIS 服务器的限流策略通常不公开,但行为很直白:单位时间内查询过多,连接直接被重置。源码包里常见的错误是用了无限制线程池,50 个域名同时发出去,前几条成功,后面全被 RST。

常见做法是用信号量把并发压到 1 到 2,并在每次查询后强制间隔:

import threading import time semaphore = threading.Semaphore(2) def limited_query(domain: str) -> dict: """限制并发查数,每次查询后强制等待再释放。""" with semaphore: try: data = get_whois_cached(domain) return data finally: time.sleep(1.0)

并发参数不是拍脑袋定的:Semaphore(2)表示同时最多两个 socket 连接,避免大量连接堆积;time.sleep(1.0)放在锁内保证同一时刻全局只有一个请求在间隔窗口外执行。批量场景实测下来,每秒 1 到 2 个请求是大多数注册局能接受的安全水位,比这个高就容易触发封禁。

配合重试退避时要注意,失败后第一次等 1 秒,第二次 2 秒,第三次 4 秒,最多重试三次,不要一失败就无限重试把限流放大。这些参数在源码里写成常量,统一可调。

5. WHOIS查询服务避坑:zip伪加密、端口超时与IP被限

前面把服务和参数都讲透了,接下来是实际运行中一定会遇到的几个硬坑。每一条都是“现象 → 原因 → 解决”的完整套路,遇到同款问题时直接照着排查。

5.1 现象:解压报 password required,但发布者从未设密码

解压工具弹出要密码,源码包里又没附带任何密码说明,第一反应通常是怀疑包损坏,或者去想“发布者是不是忘了给密码”。实际上这叫 zip 伪加密,打包工具或者修改工具把加密标志位置 1,文件内容本身没有加密。

原因:部分老的压缩工具在生成 zip 时会把 General Purpose Bit Flag 的 bit 0 误置为 1,Windows 资源管理器和部分解压软件看到这个标志就要求输入密码。

解决:按 3.1 节的方法,把本地文件头和中央目录里的加密标志清零后重新另存,再用unzip -l验证。如果清零后仍然提示密码,说明是真加密,仔细看发布页的说明文件,一般不提供密码的包不值得花工夫去爆破。

5.2 现象:本地 curl 正常,程序里 connect 43 端口超时

很多源码在本机能跑通 HTTP 自测,部署到云服务器后却卡在 connect 超时。表现是socket.connect阶段就卡满 timeout,查询完全无法返回。

原因有几种:目标 WHOIS 服务器拒绝该网段访问;云厂商安全组或宿主网络对出站 43 端口做限制;还有少数注册局对某些机房 IP 段有成规模的封禁记录。

解决:先换一个注册局服务器测试,比如 .com 的whois.verisign-grs.com换成whois.crsnic.net;再不行把超时降到 5 秒以内,配合重试;最稳妥的后备方案是给查询模块加一层 RDAP 回退,443 端口出站几乎不会遇到这类限制。查 .cn 域名走whois.cnnic.cn一般不会碰到网络限制问题,优先排查你的部署位置到目标机房的路由。

5.3 现象:同一套解析代码查 .com 正常,查 .cn 丢字段、乱码

源码自带的解析器可能只针对 .com 调过,跑 .cn 时注册商变成空值,日期字段解析失败,甚至出现中文乱码。这不是解析器 bug,是数据和协议的差异。

原因:.cn 由 CNNIC 托管,字段名是“注册商”“注册日期”“到期日期”这类中文,响应编码多为 GBK;而已有的解析器写死了英文正则,也没做编码回退。

解决:把解析逻辑显式按 TLD 分支,.cn 分支用 4.1 节的中文正则,不可能同时兼容所有英文格式。编码处理上,先尝试 UTF-8 解码,捕获异常后转 GB18030。注意有的中文注册局返回里会混入 UTF-8 英文段落,建议在字段清洗环节统一strip(),避免明明有值却因为换行符没匹配上。

5.4 现象:查询结果只有 refer 跳转提示,没有实际域名信息

某些新顶级域,比如 xxx 和部分 ccTLD,第一层查询拿到的不再是注册信息,而是一小段跳转提示,类似“Whois Server: whois.nic.abc”,更老的响应里甚至只有一行 URL。

原因:IANA 层面的 WHOIS 服务器只负责告诉你这个 TLD 的注册数据在哪,不直接存域名记录。源码如果没有实现跳转跟随,自然拿不到真正数据。

解决:实现两层查询。先向whois.iana.org发起查询,从响应里提取refer行,再向目标服务器发第二层请求。更省事的方案是维护一张 TLD 映射表,把 .com、.cn 这类常见后缀直接写死到对应服务器,新顶级域走动态查表逻辑。两年以上的老源码大多没有这层逻辑,需要自己补上。

5.5 现象:批量查询二三十条后 connect 被直接断开

批量脚本跑到一半,连接开始被对方瞬间 RST,连不上也读不到数据,等几分钟又恢复一些。这是最典型注册局限流行为。

原因:对方没有返回任何限流提示,直接断开连接,从客户端看像是网络故障。本质上是对单 IP 的查询速率或总量做限制,超过阈值就拉黑一段时间。

解决:把并发降到 1,查询间隔加到 1 秒以上,重试退避按 1s/2s/4s 推进,并且不做无限重试。必要时把部分高频公共后缀切换到 RDAP,RDAP 端点的限流策略更透明,HTTP 头会直接告诉你何时可以重试。不要想着绕过限流,注册局封禁是持续性的,正确做法是让批量的速率曲线保持平稳。

6. 批量查询与结果对账:把单点工具变成长效监控

服务稳定之后,最常用的动作是把一批域名丢进去集中查询,然后把结果落盘留档。这里给一个通用的批量脚本骨架,按行读取域名列表,逐个调本地接口,间隔参数可根据注册局反馈调整。

import json import time import urllib.request def batch_check(domain_file: str, endpoint: str = "http://127.0.0.1:5000/api/whois") -> None: with open(domain_file, encoding="utf-8") as f: domains = [line.strip() for line in f if line.strip()] with open("result.jsonl", "a", encoding="utf-8") as out: for i, domain in enumerate(domains): req = urllib.request.Request( endpoint, data=f"domain={domain}".encode(), method="POST", ) with urllib.request.urlopen(req, timeout=10) as resp: record = json.load(resp) record["checked_at"] = time.time() out.write(json.dumps(record, ensure_ascii=False) + "\n") if i % 20 == 0: print(f"progress {i}/{len(domains)}") time.sleep(1.2)

脚本里最关键的两个参数是timeout=10和sleep(1.2)。前者防止单个域名卡住整个队列,后者保证整体请求密度保持在安全水位;jsonl格式比 CSV 更灵活,每行一条完整记录,字段变化不需要改表结构。

对账技巧在于:每周把result.jsonl里expiration距当前时间少于 30 天的记录筛出来,单独导成告警清单;同时随机抽几条原始响应手工比对,验证解析器没被注册局改版带偏。如果发现某一天开始批量结果里过期时间普遍异常,优先怀疑解析正则,而不是域名数据真的变了。

我吃过一次并发过大被临时限流的亏之后,养成的习惯是任何 WHOIS 批量脚本都默认带 1.2 秒间隔,并把每条查询的原始报文和解析结果一起落盘留档。这样出了问题,至少能分清是代码改坏了还是注册局接口变了,而不是对着黑匣子猜。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询