PythonWHOIS域名查询
Python WHOIS 域名查询工具开发笔记
一、项目背景与需求
1.1 核心需求
开发一个 Python 脚本,实现以下功能:
- 查询域名的 WHOIS 信息
- 判断域名是否可注册
- 如果已被注册,显示注册日期、过期日期、域名状态、注册商等信息
- 估算域名何时会被释放(如果所有者不续费)
1.2 域名生命周期
一个 .com 域名过期后,会经历以下阶段才能被重新注册:
| 阶段 | 持续时间 | 说明 |
|---|---|---|
| 续费宽限期 | 30 天 | 原所有者可按正常价格续费 |
| 赎回期 | 30 天 | 原所有者需支付高额费用赎回 |
| 等待删除期 | 5 天 | 域名无法操作,等待注册局删除 |
| 开放注册 | - | 域名被释放,可供公众注册 |
总时长约 65 天(30 + 30 + 5)。
二、技术选型与依赖
2.1 Python 库
| 库名 | 用途 |
|---|---|
| python-whois | WHOIS 协议查询 |
| requests | RDAP 协议查询(HTTPS) |
| pytz | 时区处理 |
| re | 状态码正则提取 |
2.2 安装命令
pip install python-whois pytz requests三、WHOIS 与 RDAP 协议说明
3.1 WHOIS 协议
- 传统域名查询协议,基于 TCP 端口 43
- 返回纯文本格式数据
- 不同后缀使用不同的 WHOIS 服务器
3.2 RDAP 协议
- WHOIS 的现代替代协议
- 基于 HTTPS,端口 443
- 返回 JSON 格式结构化数据
- 国内访问更稳定
3.3 公共 WHOIS 服务器
| 域名后缀 | 公共 WHOIS 服务器 |
|---|---|
| .com / .net | whois.verisign-grs.com |
| .org | whois.pir.org |
| .cn | whois.cnnic.cn |
| .cc / .tv | whois.verisign-grs.com |
| .info | whois.afilias.net |
| .biz | whois.neulevel.biz |
3.4 公共 RDAP 服务地址
四、域名状态码详解
4.1 常见状态码
| 状态码 | 含义 | 说明 |
|---|---|---|
| clientDeleteProhibited | 注册商禁止删除 | 防止域名被误删 |
| clientTransferProhibited | 注册商禁止转移 | 防止域名被转移到其他注册商 |
| clientUpdateProhibited | 注册商禁止更新 | 防止域名信息被修改 |
| serverDeleteProhibited | 注册局禁止删除 | 注册局级别的删除保护 |
| serverTransferProhibited | 注册局禁止转移 | 注册局级别的转移保护 |
| serverUpdateProhibited | 注册局禁止更新 | 注册局级别的信息保护 |
| ok / active | 正常状态 | 域名处于正常使用状态 |
| pendingDelete | 等待删除 | 域名即将被释放 |
| redemptionPeriod | 赎回期 | 域名处于赎回期 |
4.2 client 与 server 的区别
- client 开头:由注册商设置,联系注册商可解除
- server 开头:由注册局设置,权限级别更高,通常涉及争议或特殊保护
五、开发过程中的关键问题与解决
5.1 问题:python-whois 不支持 server 参数
错误信息:
whois() got an unexpected keyword argument 'server'原因:旧版本 python-whois 的 whois() 函数不支持指定 WHOIS 服务器。
解决方案:
升级 python-whois 到最新版:
pip install --upgrade python-whois5.2 问题:RDAP 库 API 不存在
错误信息:
module 'rdap' has no attribute 'RDAPClient'原因:rdap 库的 API 用法不正确。
解决方案:
弃用 rdap 库,改用 requests 直接发送 HTTPS 请求。
5.3 问题:.xyz 等新后缀状态解析乱码
现象:
🔍 域名状态: ['g', 'T', 'r', 't', '.', 'o', 'h', 'n', 'e', 'f', 'i', 'P', 's', 'c', 'a', 'b', 'd', 'p', '/', ':', 'l']原因:python-whois 库对 .xyz 等新顶级域名的 WHOIS 响应格式解析不完善。
解决方案:
使用正则表达式从原始响应中提取标准状态码:
pattern = r'(client|server)[A-Za-z]+(?:Prohibited|Hold|Update|Delete|Transfer|Renew)'
matches = re.findall(pattern, raw_str, re.IGNORECASE)5.4 问题:已注册域名被误判为可注册
现象:775776.xyz 已被注册,但 yuming2.py 显示"域名可注册"。
原因:判断逻辑不严谨,没有同时检查 text 中的 "No match" 和 domain_name 字段。
解决方案:
def is_domain_available(whois_result):
# 方法1:检查是否有 No match
if hasattr(whois_result, 'text') and whois_result.text:
if 'no match' in whois_result.text.lower():
return True
# 方法2:检查是否有域名名称
if hasattr(whois_result, 'domain_name') and whois_result.domain_name:
return False
# 方法3:如果没有任何数据
if whois_result is None:
return False
# 方法4:注册日期和过期日期都是 None
if (not hasattr(whois_result, 'creation_date') or whois_result.creation_date is None) and \
(not hasattr(whois_result, 'expiration_date') or whois_result.expiration_date is None):
return True
return False六、代码版本
6.1 yuming1.py - 详细版
yuming1.py 输出完整的域名生命周期分析,适合需要详细了解域名状态和释放流程的场景。
import sys
from datetime import datetime, timedelta
import pytz
import requests
import whois
import re
def clean_status_string(raw_status):
"""
清理 WHOIS 返回的原始状态字符串
从乱码中提取真正的状态码,如 clientTransferProhibited
"""
if not raw_status:
return []
# 如果已经是列表,遍历处理
if isinstance(raw_status, list):
cleaned = []
for item in raw_status:
if isinstance(item, str):
# 尝试从字符串中提取标准状态码
# 匹配类似 clientTransferProhibited, clientDeleteProhibited, ok 等
pattern = r'(client|server)[A-Za-z]+(?:Prohibited|Hold|Update|Delete|Transfer|Renew)'
matches = re.findall(pattern, item, re.IGNORECASE)
if matches:
cleaned.extend(matches)
else:
# 如果没匹配到,尝试检查是否包含已知状态关键词
known_status = ['ok', 'active', 'inactive', 'pending', 'redemption', 'hold']
for ks in known_status:
if ks.lower() in item.lower():
cleaned.append(ks)
break
# 去重并过滤空值
return list(set([s for s in cleaned if s]))
# 如果是字符串,直接处理
if isinstance(raw_status, str):
pattern = r'(client|server)[A-Za-z]+(?:Prohibited|Hold|Update|Delete|Transfer|Renew)'
matches = re.findall(pattern, raw_status, re.IGNORECASE)
if matches:
return list(set(matches))
return []
def query_domain_rdap(domain):
"""使用 RDAP 查询域名信息(基于 HTTPS,国内访问更稳定)"""
try:
# 根据域名后缀选择对应的 RDAP 服务地址
rdap_servers = {
'.com': f'https://rdap.verisign.com/com/v1/domain/{domain}',
'.net': f'https://rdap.verisign.com/net/v1/domain/{domain}',
'.org': f'https://rdap.pir.org/domain/{domain}',
'.cn': f'https://rdap.cnnic.cn/domain/{domain}',
'.cc': f'https://rdap.verisign.com/cc/v1/domain/{domain}',
'.tv': f'https://rdap.verisign.com/tv/v1/domain/{domain}',
'.info': f'https://rdap.afilias.net/info/v1/domain/{domain}',
'.biz': f'https://rdap.afilias.net/biz/v1/domain/{domain}',
}
suffix = '.' + domain.split('.')[-1]
url = rdap_servers.get(suffix)
if not url:
# 对于 .xyz 等新后缀,尝试使用通用的 RDAP 服务
# 很多新后缀使用统一的 RDAP 服务
print(f"⚠️ 未找到 {suffix} 的 RDAP 服务,尝试使用通用 RDAP")
# 尝试使用 IANA 的 RDAP 服务
url = f'https://rdap.verisign.com/com/v1/domain/{domain}'
print(f"🌐 RDAP 请求: {url}")
response = requests.get(url, timeout=10, headers={'Accept': 'application/json'})
if response.status_code != 200:
print(f"⚠️ RDAP 请求失败,状态码: {response.status_code}")
return None
data = response.json()
print("✅ RDAP 查询成功")
# 解析 RDAP 响应
info = {}
# 1. 解析过期日期和注册日期
if 'events' in data:
for event in data['events']:
action = event.get('eventAction', '')
date_str = event.get('eventDate', '')
if date_str:
try:
if date_str.endswith('Z'):
date_str = date_str.replace('Z', '+00:00')
dt = datetime.fromisoformat(date_str)
if action == 'expiration':
info['expiration_date'] = dt
elif action == 'registration':
info['creation_date'] = dt
except:
pass
# 2. 解析域名状态
if 'status' in data:
status_list = []
for s in data['status']:
if isinstance(s, str):
if '#' in s:
core = s.split('#')[0].strip()
else:
core = s.strip()
if core:
status_list.append(core)
elif isinstance(s, dict) and 'status' in s:
status_list.append(s['status'])
info['status'] = status_list
# 3. 解析注册商信息
if 'registrar' in data and isinstance(data['registrar'], dict):
info['registrar'] = data['registrar'].get('name', '')
elif 'entities' in data:
for entity in data['entities']:
if entity.get('roles') and 'registrar' in entity.get('roles', []):
info['registrar'] = entity.get('vcardArray', [{}])[1].get('fn', [''])[0]
break
# 4. 解析名称服务器
if 'nameservers' in data:
ns_list = []
for ns in data['nameservers']:
if 'ldhName' in ns:
ns_list.append(ns['ldhName'])
elif 'unicodeName' in ns:
ns_list.append(ns['unicodeName'])
info['name_servers'] = ns_list
return info
except requests.exceptions.Timeout:
print("⚠️ RDAP 请求超时")
return None
except requests.exceptions.ConnectionError:
print("⚠️ RDAP 网络连接失败")
return None
except Exception as e:
print(f"⚠️ RDAP 解析失败: {e}")
return None
def query_domain_whois(domain):
"""使用 WHOIS 查询(备选方案)"""
try:
servers = [
None, # 自动检测
'whois.verisign-grs.com',
'whois.crsnic.net',
'whois.nic.xyz', # .xyz 专用 WHOIS 服务器
]
for server in servers:
try:
if server:
w = whois.whois(domain, server=server)
else:
w = whois.whois(domain)
# 如果查询结果中有 status,尝试清理
if hasattr(w, 'status') and w.status:
if isinstance(w.status, list):
# 检查是否解析异常(出现单个字符的情况)
cleaned = clean_status_string(w.status)
if cleaned:
w.status = cleaned
else:
# 如果清理后为空,保留原始数据但标记
pass
return w
except TypeError:
w = whois.whois(domain)
return w
except Exception:
continue
return None
except Exception as e:
print(f"⚠️ WHOIS 查询失败: {e}")
return None
def parse_date(date_val):
if isinstance(date_val, list):
date_val = date_val[0]
if date_val:
if isinstance(date_val, datetime):
if date_val.tzinfo is None:
date_val = pytz.UTC.localize(date_val)
return date_val
return None
def get_domain_lifecycle(domain):
print("=" * 60)
print(f"域名: {domain}")
print("=" * 60)
info = None
# 优先使用 RDAP 查询
print("\n🌐 尝试使用 RDAP 查询...")
info = query_domain_rdap(domain)
# RDAP 失败则降级到 WHOIS
if info is None:
print("\n🌐 RDAP 失败,尝试 WHOIS 查询...")
w = query_domain_whois(domain)
if w is None:
print("❌ 所有查询方式均失败")
return
# 获取域名状态并清理
raw_status = w.status
cleaned_status = []
if raw_status:
# 如果是列表且包含单个字符(乱码),尝试清理
if isinstance(raw_status, list):
# 检查是否有单个字符的项
has_single_chars = any(isinstance(s, str) and len(s) == 1 for s in raw_status)
if has_single_chars:
# 尝试从原始响应中重新提取状态
# 使用正则从字符串中提取
raw_str = str(raw_status)
pattern = r'(client|server)[A-Za-z]+(?:Prohibited|Hold|Update|Delete|Transfer|Renew)'
matches = re.findall(pattern, raw_str, re.IGNORECASE)
if matches:
cleaned_status = list(set([m for m in matches if m]))
else:
# 检查是否包含常见的状态词
known_status = ['ok', 'active', 'inactive', 'pending', 'redemption']
for ks in known_status:
if ks.lower() in raw_str.lower():
cleaned_status.append(ks)
else:
# 正常情况,直接清理
cleaned_status = clean_status_string(raw_status)
# 如果没有清理出有效状态,尝试从其他字段推断
if not cleaned_status:
# 根据是否过期推断状态
exp_date = parse_date(w.expiration_date)
if exp_date:
now = datetime.now(pytz.UTC)
if exp_date > now:
cleaned_status = ['active']
else:
cleaned_status = ['expired']
info = {
'creation_date': parse_date(w.creation_date),
'expiration_date': parse_date(w.expiration_date),
'status': cleaned_status,
'registrar': w.registrar,
'name_servers': w.name_servers,
}
print("✅ 使用 WHOIS 查询成功")
# 解析关键日期
creation_date = info.get('creation_date')
expiration_date = info.get('expiration_date')
# 分析状态
status_list = []
if info.get('status'):
for s in info['status']:
if isinstance(s, str):
if '#' in s:
core = s.split('#')[0].strip()
else:
core = s.strip()
if core and len(core) > 1: # 过滤掉单个字符
status_list.append(core)
unique_status = list(set(status_list))
# 如果状态列表为空或只有单个字符,显示为 "正常(状态解析中)"
if not unique_status:
unique_status = ['active (正常注册)']
print(f"\n🔍 域名状态: {unique_status}")
now = datetime.now(pytz.UTC)
if expiration_date is None:
print("⚠️ 无法获取过期日期")
return
days_until_expire = (expiration_date - now).days
# 生命周期常量
RENEW_GRACE = 30
REDEMPTION = 30
PENDING_DELETE = 5
TOTAL_DAYS = RENEW_GRACE + REDEMPTION + PENDING_DELETE
print("\n" + "-" * 60)
print("📋 域名生命周期分析")
print("-" * 60)
if days_until_expire > 0:
print(f"✅ 域名状态: **正常注册中**")
print(f"📅 注册日期: {creation_date.strftime('%Y-%m-%d') if creation_date else '未知'}")
print(f"📅 过期日期: {expiration_date.strftime('%Y-%m-%d')}")
print(f"⏳ 距离过期: **{days_until_expire} 天** (约 {days_until_expire//365} 年)")
has_transfer_prohibited = any('transferprohibited' in s.lower() for s in status_list)
has_delete_prohibited = any('deleteprohibited' in s.lower() for s in status_list)
has_ok = any(s.lower() == 'ok' for s in status_list)
has_active = any(s.lower() == 'active' for s in status_list)
if has_transfer_prohibited:
print("🔒 已开启转移保护锁")
if has_delete_prohibited:
print("🔒 已开启删除保护锁")
if has_ok or has_active:
print("✅ 域名状态正常 (OK/Active)")
print(f"\n📌 预计释放时间(如果所有者不续费):")
release_date = expiration_date + timedelta(days=TOTAL_DAYS)
print(f" {release_date.strftime('%Y-%m-%d')} 左右")
print(f"\n📊 后续流程时间线:")
print(f" 📍 当前: 正常使用期")
print(f" 📍 过期后第 0-{RENEW_GRACE} 天: 续费宽限期 (原价续费)")
print(f" 📍 过期后第 {RENEW_GRACE+1}-{RENEW_GRACE+REDEMPTION} 天: 赎回期 (高价赎回)")
print(f" 📍 过期后第 {RENEW_GRACE+REDEMPTION+1}-{TOTAL_DAYS} 天: 等待删除期 (不可操作)")
print(f" 📍 过期后第 {TOTAL_DAYS+1} 天: **开放注册** 🎯")
else:
days_expired = abs(days_until_expire)
print(f"⚠️ 域名状态: **已过期 {days_expired} 天**")
print(f"📅 过期日期: {expiration_date.strftime('%Y-%m-%d')}")
is_pending_delete = any('pendingdelete' in s.lower() for s in status_list)
is_redemption = any('redemption' in s.lower() for s in status_list)
if is_pending_delete:
days_left = PENDING_DELETE - (days_expired - RENEW_GRACE - REDEMPTION)
if days_left <= 0:
days_left = 0
print(f"📌 当前阶段: **等待删除期**")
print(f"⏳ 预计 {days_left} 天后释放")
if days_left > 0:
release_date = now + timedelta(days=days_left)
print(f"📅 预计释放日期: {release_date.strftime('%Y-%m-%d')}")
else:
print("🔥 域名可能即将被释放,请密切关注!")
elif is_redemption or days_expired > RENEW_GRACE:
days_left = RENEW_GRACE + REDEMPTION - days_expired
if days_left < 0:
days_left = 0
print(f"📌 当前阶段: **赎回期** (约 {days_left} 天后进入等待删除期)")
print(f"💸 原所有者需要支付高额费用才能赎回")
if days_left > 0:
release_date = now + timedelta(days=days_left + PENDING_DELETE)
print(f"📅 预计释放日期: {release_date.strftime('%Y-%m-%d')}")
elif days_expired <= RENEW_GRACE:
days_left = RENEW_GRACE - days_expired
print(f"📌 当前阶段: **续费宽限期** (还剩 {days_left} 天)")
print(f"💰 原所有者仍可以正常价格续费")
if days_left > 0:
release_date = now + timedelta(days=days_left + REDEMPTION + PENDING_DELETE)
print(f"📅 预计释放日期: {release_date.strftime('%Y-%m-%d')}")
print(f"\n❌ 当前 **不能注册**,需要等待释放流程完成")
# 其他信息
print("\n" + "-" * 60)
print("📋 其他信息")
print("-" * 60)
if info.get('registrar'):
print(f"注册商: {info['registrar']}")
if info.get('name_servers'):
ns = info['name_servers']
if isinstance(ns, list) and ns:
print(f"DNS: {', '.join(ns[:3])}{'...' if len(ns)>3 else ''}")
elif ns:
print(f"DNS: {ns}")
print("=" * 60)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python yuming.py <域名>")
print("示例: python yuming.py baidaoya.com")
print("示例: python yuming.py 775776.xyz")
sys.exit(1)
domain = sys.argv[1]
get_domain_lifecycle(domain)6.2 yuming2.py - 简洁版
yuming2.py 输出简化版结果,适合快速查看域名注册状态。使用颜色标识结果,输出更简洁。
import sys
from datetime import datetime, timedelta
import pytz
import requests
import whois
import re
# ============ 颜色配置 ============
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
CYAN = '\033[96m'
BOLD = '\033[1m'
END = '\033[0m'
def clean_status_string(raw_status):
"""从 WHOIS 原始状态中提取有效状态码"""
if not raw_status:
return []
if isinstance(raw_status, list):
cleaned = []
for item in raw_status:
if isinstance(item, str):
pattern = r'(client|server)[A-Za-z]+(?:Prohibited|Hold|Update|Delete|Transfer|Renew)'
matches = re.findall(pattern, item, re.IGNORECASE)
if matches:
cleaned.extend(matches)
else:
known_status = ['ok', 'active', 'inactive', 'pending', 'redemption', 'hold']
for ks in known_status:
if ks.lower() in item.lower():
cleaned.append(ks)
break
return list(set([s for s in cleaned if s and len(s) > 1]))
if isinstance(raw_status, str):
pattern = r'(client|server)[A-Za-z]+(?:Prohibited|Hold|Update|Delete|Transfer|Renew)'
matches = re.findall(pattern, raw_status, re.IGNORECASE)
if matches:
return list(set(matches))
if 'no match' in raw_status.lower():
return ['available']
return []
def is_domain_available(whois_result):
"""判断域名是否可注册"""
if hasattr(whois_result, 'text') and whois_result.text:
if 'no match' in whois_result.text.lower():
return True
if hasattr(whois_result, 'domain_name') and whois_result.domain_name:
return False
if whois_result is None:
return False
if (not hasattr(whois_result, 'creation_date') or whois_result.creation_date is None) and \
(not hasattr(whois_result, 'expiration_date') or whois_result.expiration_date is None):
return True
return False
def query_domain_rdap(domain):
"""使用 RDAP 查询"""
try:
suffix = '.' + domain.split('.')[-1]
rdap_servers = {
'.com': f'https://rdap.verisign.com/com/v1/domain/{domain}',
'.net': f'https://rdap.verisign.com/net/v1/domain/{domain}',
'.org': f'https://rdap.pir.org/domain/{domain}',
'.cn': f'https://rdap.cnnic.cn/domain/{domain}',
'.cc': f'https://rdap.verisign.com/cc/v1/domain/{domain}',
'.tv': f'https://rdap.verisign.com/tv/v1/domain/{domain}',
'.info': f'https://rdap.afilias.net/info/v1/domain/{domain}',
'.biz': f'https://rdap.afilias.net/biz/v1/domain/{domain}',
}
url = rdap_servers.get(suffix)
if not url:
return None
response = requests.get(url, timeout=10, headers={'Accept': 'application/json'})
if response.status_code == 404:
return {'available': True}
if response.status_code != 200:
return None
data = response.json()
info = {'available': False}
if 'events' in data:
for event in data['events']:
action = event.get('eventAction', '')
date_str = event.get('eventDate', '')
if date_str:
try:
if date_str.endswith('Z'):
date_str = date_str.replace('Z', '+00:00')
dt = datetime.fromisoformat(date_str)
if action == 'expiration':
info['expiration_date'] = dt
elif action == 'registration':
info['creation_date'] = dt
except:
pass
if 'status' in data:
status_list = []
for s in data['status']:
if isinstance(s, str):
if '#' in s:
core = s.split('#')[0].strip()
else:
core = s.strip()
if core:
status_list.append(core)
info['status'] = status_list
if 'registrar' in data and isinstance(data['registrar'], dict):
info['registrar'] = data['registrar'].get('name', '')
if 'nameservers' in data:
ns_list = []
for ns in data['nameservers']:
if 'ldhName' in ns:
ns_list.append(ns['ldhName'])
elif 'unicodeName' in ns:
ns_list.append(ns['unicodeName'])
info['name_servers'] = ns_list
return info
except Exception:
return None
def query_domain_whois(domain):
"""使用 WHOIS 查询"""
try:
servers = [
None,
'whois.verisign-grs.com',
'whois.crsnic.net',
]
for server in servers:
try:
if server:
w = whois.whois(domain, server=server)
else:
w = whois.whois(domain)
return w
except TypeError:
w = whois.whois(domain)
return w
except Exception:
continue
return None
except Exception:
return None
def parse_date(date_val):
if isinstance(date_val, list):
date_val = date_val[0]
if date_val:
if isinstance(date_val, datetime):
if date_val.tzinfo is None:
date_val = pytz.UTC.localize(date_val)
return date_val
return None
def format_status(status_list):
"""格式化状态显示"""
if not status_list:
return "未获取到状态"
short = []
for s in status_list:
if isinstance(s, str):
s_lower = s.lower()
if 'deleteprohibited' in s_lower or 'delete prohibited' in s_lower:
short.append('🔒删除锁')
elif 'transferprohibited' in s_lower or 'transfer prohibited' in s_lower:
short.append('🔒转移锁')
elif 'updateprohibited' in s_lower or 'update prohibited' in s_lower:
short.append('🔒更新锁')
elif 'ok' in s_lower:
short.append('✅正常')
elif 'active' in s_lower:
short.append('✅活跃')
elif 'pendingdelete' in s_lower:
short.append('⏳等待删除')
elif 'redemption' in s_lower:
short.append('💰赎回期')
elif 'hold' in s_lower:
short.append('⏸️暂停')
return ', '.join(short) if short else "正常"
def simplify_registrar(registrar):
"""简化注册商名称"""
if not registrar:
return "未知"
registrar_lower = registrar.lower()
if 'squarespace' in registrar_lower:
return 'Squarespace'
elif 'godaddy' in registrar_lower:
return 'GoDaddy'
elif 'alibaba' in registrar_lower or 'hichina' in registrar_lower:
return '阿里云'
elif 'spaceship' in registrar_lower:
return 'Spaceship'
elif 'namecheap' in registrar_lower:
return 'Namecheap'
elif 'cloudflare' in registrar_lower:
return 'Cloudflare'
elif 'ename' in registrar_lower:
return '易名'
elif 'west' in registrar_lower:
return '西部数码'
elif 'xinnet' in registrar_lower:
return '新网'
else:
return registrar
def check_domain(domain):
"""域名查询主函数"""
print("=" * 50)
print(f"🔍 域名: {domain}")
print("=" * 50)
# ===== 第一步:尝试 RDAP =====
info = query_domain_rdap(domain)
if info and info.get('available'):
print(f"{Colors.GREEN}✅ 域名可注册!{Colors.END}")
print("=" * 50)
return
# ===== 第二步:尝试 WHOIS =====
w = query_domain_whois(domain)
if w is None:
print(f"{Colors.RED}❌ 查询失败,请检查网络{Colors.END}")
print("=" * 50)
return
# ===== 检查是否可注册 =====
is_available = False
if hasattr(w, 'text') and w.text:
if 'no match' in w.text.lower() or 'not found' in w.text.lower():
is_available = True
if not is_available and (not hasattr(w, 'domain_name') or not w.domain_name):
is_available = True
if is_available:
print(f"{Colors.GREEN}✅ 域名可注册!{Colors.END}")
print("=" * 50)
return
# ===== 已注册域名:解析信息 =====
expiration_date = parse_date(w.expiration_date)
creation_date = parse_date(w.creation_date) # 🆕 获取注册日期
if expiration_date is None:
print(f"{Colors.RED}❌ 无法获取域名过期日期{Colors.END}")
print("=" * 50)
return
# 解析状态
raw_status = w.status if hasattr(w, 'status') else []
status_list = clean_status_string(raw_status)
now = datetime.now(pytz.UTC)
days_until_expire = (expiration_date - now).days
# ===== 计算域名已存在多少年 =====
if creation_date:
years_old = (now - creation_date).days // 365
age_display = f"(已注册 {years_old} 年)"
else:
age_display = ""
# ===== 简洁输出 =====
if days_until_expire > 0:
print(f"{Colors.RED}❌ 已被注册{Colors.END}")
# 🆕 显示注册日期
if creation_date:
print(f"📅 注册: {creation_date.strftime('%Y-%m-%d')} {age_display}")
else:
print(f"📅 注册: 未知")
print(f"📅 过期: {expiration_date.strftime('%Y-%m-%d')}(还有 {days_until_expire} 天)")
# 显示状态
status_display = format_status(status_list)
print(f"🔐 状态: {status_display}")
# 显示注册商
registrar = ''
if hasattr(w, 'registrar') and w.registrar:
registrar = w.registrar
if isinstance(registrar, list):
registrar = registrar[0]
registrar = simplify_registrar(registrar)
print(f"🏢 注册商: {registrar}")
# 计算释放时间
RENEW_GRACE = 30
REDEMPTION = 30
PENDING_DELETE = 5
TOTAL_DAYS = RENEW_GRACE + REDEMPTION + PENDING_DELETE
release_date = expiration_date + timedelta(days=TOTAL_DAYS)
print(f"⏳ 如不续费,约 {release_date.strftime('%Y-%m-%d')} 释放")
else:
# 域名已过期
days_expired = abs(days_until_expire)
print(f"{Colors.YELLOW}⚠️ 已过期 {days_expired} 天{Colors.END}")
if creation_date:
print(f"📅 注册: {creation_date.strftime('%Y-%m-%d')} {age_display}")
print(f"📅 过期日期: {expiration_date.strftime('%Y-%m-%d')}")
RENEW_GRACE = 30
REDEMPTION = 30
PENDING_DELETE = 5
if days_expired <= RENEW_GRACE:
phase = f"续费宽限期(剩 {RENEW_GRACE - days_expired} 天)"
elif days_expired <= RENEW_GRACE + REDEMPTION:
phase = f"赎回期(剩 {RENEW_GRACE + REDEMPTION - days_expired} 天)"
else:
phase = "等待删除期"
print(f"📌 阶段: {phase}")
release_date = expiration_date + timedelta(days=RENEW_GRACE + REDEMPTION + PENDING_DELETE)
print(f"⏳ 预计释放: {release_date.strftime('%Y-%m-%d')}")
print("=" * 50)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python yuming.py <域名>")
print("示例: python yuming.py liuqingdong.com")
sys.exit(1)
domain = sys.argv[1].lower()
check_domain(domain)七、使用方法
7.1 详细版查询
python yuming1.py baidaoya.com输出示例(已注册域名):
============================================================
域名: baidaoya.com
============================================================
尝试使用 RDAP 查询...
RDAP 请求: https://rdap.verisign.com/com/v1/domain/baidaoya.com
RDAP 查询成功
域名状态: ['clientDeleteProhibited', 'clientTransferProhibited']
------------------------------------------------------------
域名生命周期分析
------------------------------------------------------------
域名状态: 正常注册中
注册日期: 2026-07-27
过期日期: 2027-07-27
距离过期: 330 天 (约 0 年)
已开启转移保护锁
已开启删除保护锁
域名状态正常 (OK/Active)
预计释放时间(如果所有者不续费):
2027-09-30 左右
后续流程时间线:
当前: 正常使用期
过期后第 0-30 天: 续费宽限期 (原价续费)
过期后第 31-60 天: 赎回期 (高价赎回)
过期后第 61-65 天: 等待删除期 (不可操作)
过期后第 66 天: 开放注册
------------------------------------------------------------
其他信息
------------------------------------------------------------
注册商: Squarespace Domains II LLC
DNS: DANIELLA.NS.CLOUDFLARE.COM, VIN.NS.CLOUDFLARE.COM
============================================================7.2 简洁版查询
python yuming2.py baidaoya.com输出示例(已注册域名):
==================================================
域名: baidaoya.com
==================================================
已被注册
注册: 2026-07-27(已注册 0 年)
过期: 2027-07-27(还有 330 天)
状态: 删除锁, 转移锁
注册商: Squarespace
如不续费,约 2027-09-30 释放
==================================================输出示例(可注册域名):
==================================================
域名: liuqingdong.com
==================================================
域名可注册
==================================================八、安全性说明
8.1 查询路径
yuming.py 使用公共 WHOIS/RDAP 服务器进行查询:
| 查询方式 | 查询目标 | 截胡风险 |
|---|---|---|
| RDAP(优先) | VeriSign 官方 RDAP 服务 | 低 |
| WHOIS(备选) | VeriSign 公共 WHOIS 服务器 | 低 |
查询请求直接发送到注册局的公共服务器,不经过当前注册商的网站。
8.2 降低风险的建议
- 不要频繁查询同一个域名(一周 1-2 次足够)
- 使用公共 WHOIS 服务器而非注册商网站
- 不要在注册商网站登录账号后查询
- 可考虑使用代理或 VPN 查询
九、常见问题
9.1 查询失败怎么办?
- 检查网络连接
- 确认 python-whois 已升级到最新版本
- 尝试切换网络环境(如使用代理)
9.2 状态显示乱码怎么办?
脚本已内置正则提取功能,会自动从原始响应中提取标准状态码。
9.3 域名可注册的判断依据是什么?
- RDAP 返回 404
- WHOIS 返回 "No match"
- 无 domain_name 字段
- 无注册日期和过期日期
9.4 如何提高抢注成功率?
- 在域名进入"等待删除期"时使用抢注平台预订
- 知名抢注平台:DropCatch、SnapNames
- 不要只依赖手动注册
十、更新日志
| 日期 | 版本 | 更新内容 |
|---|---|---|
| 2026-08-31 | v1.0 | 初始版本,支持 RDAP 和 WHOIS 双协议查询 |
| 2026-08-31 | v1.1 | 修复 python-whois server 参数兼容性问题 |
| 2026-08-31 | v1.2 | 修复 .xyz 等新后缀状态解析乱码问题 |
| 2026-08-31 | v1.3 | 修复已注册域名被误判为可注册的 bug |
| 2026-08-31 | v1.4 | 增加注册日期显示和域名年龄计算 |
| 2026-08-31 | v1.5 | 拆分为 yuming1.py(详细版)和 yuming2.py(简洁版) |