Python WHOIS 域名查询工具开发笔记

一、项目背景与需求

1.1 核心需求

开发一个 Python 脚本,实现以下功能:

  • 查询域名的 WHOIS 信息
  • 判断域名是否可注册
  • 如果已被注册,显示注册日期、过期日期、域名状态、注册商等信息
  • 估算域名何时会被释放(如果所有者不续费)

1.2 域名生命周期

一个 .com 域名过期后,会经历以下阶段才能被重新注册:

阶段持续时间说明
续费宽限期30 天原所有者可按正常价格续费
赎回期30 天原所有者需支付高额费用赎回
等待删除期5 天域名无法操作,等待注册局删除
开放注册-域名被释放,可供公众注册

总时长约 65 天(30 + 30 + 5)。

二、技术选型与依赖

2.1 Python 库

库名用途
python-whoisWHOIS 协议查询
requestsRDAP 协议查询(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 / .netwhois.verisign-grs.com
.orgwhois.pir.org
.cnwhois.cnnic.cn
.cc / .tvwhois.verisign-grs.com
.infowhois.afilias.net
.bizwhois.neulevel.biz

3.4 公共 RDAP 服务地址

域名后缀RDAP 服务地址
.comhttps://rdap.verisign.com/com/v1/domain/
.nethttps://rdap.verisign.com/net/v1/domain/
.orghttps://rdap.pir.org/domain/
.cnhttps://rdap.cnnic.cn/domain/
.cchttps://rdap.verisign.com/cc/v1/domain/
.tvhttps://rdap.verisign.com/tv/v1/domain/

四、域名状态码详解

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-whois

5.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. 不要频繁查询同一个域名(一周 1-2 次足够)
  2. 使用公共 WHOIS 服务器而非注册商网站
  3. 不要在注册商网站登录账号后查询
  4. 可考虑使用代理或 VPN 查询

九、常见问题

9.1 查询失败怎么办?

  • 检查网络连接
  • 确认 python-whois 已升级到最新版本
  • 尝试切换网络环境(如使用代理)

9.2 状态显示乱码怎么办?

脚本已内置正则提取功能,会自动从原始响应中提取标准状态码。

9.3 域名可注册的判断依据是什么?

  • RDAP 返回 404
  • WHOIS 返回 "No match"
  • 无 domain_name 字段
  • 无注册日期和过期日期

9.4 如何提高抢注成功率?

  • 在域名进入"等待删除期"时使用抢注平台预订
  • 知名抢注平台:DropCatch、SnapNames
  • 不要只依赖手动注册

十、更新日志

日期版本更新内容
2026-08-31v1.0初始版本,支持 RDAP 和 WHOIS 双协议查询
2026-08-31v1.1修复 python-whois server 参数兼容性问题
2026-08-31v1.2修复 .xyz 等新后缀状态解析乱码问题
2026-08-31v1.3修复已注册域名被误判为可注册的 bug
2026-08-31v1.4增加注册日期显示和域名年龄计算
2026-08-31v1.5拆分为 yuming1.py(详细版)和 yuming2.py(简洁版)

标签: none

添加新评论