抢注域名这事儿,就是跟注册局那帮策略工程师掰手腕。

多数注册局把 Whois 查询视为流量来源,做了三层闸门:一分钟只能查30次,每次请求必须间隔1–2秒,出现异常流量就抛滑块或直接封IP。去年上线的 eu.org 优先池还加了行为指纹,单纯多线程跑脚本十分钟就会被拉黑。

速率限制最恶心。同一IP在60秒内超过阈值即拒绝,低于800 ms的连续查询会被标记。验证码与IP封锁更狠——滑动拼图或JS挑战,一旦失败整段ASN进灰名单。

Cloudflare 接管后的注册局后台喜欢用 bot detection,Requests + WhoisXML API 方案看似优雅,实际上返回429比成功响应还多。后来换成 Splinter 跑无头 Chrome,把查询按钮当成普通用户去点,拦截率立刻掉到2%。代价是单核CPU每秒只能处理7个并发,但这已经够用了。

from splinter import Browser
with Browser('chrome', headless=True) as b:
    b.visit('https://register.example/search?q={}'.format(domain))
    b.find_by_id('check').click()
    sleep(0.35) # 人为制造间隔,避开检测

即使脚本跑得快,网络延迟也会吃掉优势。机房时钟和本地NTP差15 ms,就可能错过释放窗口。解决办法是在宿主开 chrony,容器里再放一个 Python 时钟校正服务,统一把误差压到 ±2 ms 以内。至于并发冲突,最简单也最粗暴:给每条线程绑独立住宅代理,让注册局以为来自不同家庭宽带。

把 Selenium 脚本伪装成真人,比写业务逻辑还费劲

武器架稳点。Python 3.10+ 是基准,别用3.7了——urllib3 和 ssl 的老版本在TLS指纹上太明显,注册局那边一看连接握手就能嗅出你是爬虫框架。直接换成 undetected-chromedriver,这货帮你打了CDP补丁,Chrome DevTools Protocol 里那些 webdriver 标志位它自动置为 false。

关键一步在 ChromeOptions。别只设 --headless 就完事,那样你会被封得亲妈都不认识。必须要带上 --disable-blink-features=AutomationControlled,这个参数砍掉了 Blink 渲染引擎里暴露给 JS 的 webdriver 运行时指纹。再加一条 --user-data-dir=/tmp/chrome_profile_{random},每次跑脚本前生成临时用户目录,保持 Cookie 和 LocalStorage 干净——注册局的验证码逻辑经常读 localStorage 里的 _cf_uv 字段,第一次访问没有这个值,直接弹滑块。

from selenium.webdriver import Chrome, ChromeOptions
from fake_useragent import UserAgent

opts = ChromeOptions()
opts.add_argument('--disable-blink-features=AutomationControlled')
opts.add_argument(f'--user-data-dir=/tmp/chrome_{int(time.time())}')
opts.add_argument(f'--window-size={random.randint(1200,1400)},{random.randint(800,900)}')
ua = UserAgent(browsers=['chrome'], os=['windows', 'macos'])
opts.add_argument(f'--user-agent={ua.random}')
# 别开 headless 跑抢注,无头模式下的 Canvas 指纹和正常浏览器差了3个像素
driver = Chrome(options=opts)

UA 别用同一个。注册局那边会收集 User-Agent 跟屏幕分辨率做关联——你一个1920x1080的 Windows Chrome 101 连续查50个域名,傻子都看得出是脚本。用 fake_useragent 的轮询模式,每次请求前随机切一个UA版本,同时窗口尺寸也要跟着变。我踩过坑:UA是Chrome 120,窗口却只有1024x768,验证码服务商那边直接标了 low trust score。

代理池不是锦上添花,是保命符

注册局后端通常会配 nginx + lua-resty-limit-traffic,按IP做滑动窗口限流。单住宅IP在60秒内超过15次查询就进观察期,超30次直接403。我搭了个轻量代理池,买一批数据中心IP混10%住宅IP,用 requests 挂 proxies 参数,Selenium 那边通过 --proxy-server 传进去。但注意:住宅IP只在需要查询 Whois 的环节用,竞标提交那一步必须走低延迟机房IP——住宅延迟80 ms基本告别毫秒级抢注。

PROXY_POOL = ['http://user:pass@dc01:8080', 'http://user:pass@res01:3128']
session = requests.Session()
session.proxies = {'http': random.choice(PROXY_POOL), 'https': random.choice(PROXY_POOL)}
# 每轮请求间隔800–1200 ms,带随机抖动
sleep(random.uniform(0.8, 1.2))

还有一个细节很多人忽略:DNS 解析泄漏。你用代理,但 Python 的 socket 模块默认不走代理去解析域名,注册局那边看到DNS请求源IP跟HTTP请求源IP不同,直接标记可疑。解决方案是在ChromeOptions里配 --host-resolver-rules="MAP * 0.0.0.0" 强制浏览器走代理解析,或者用 requests 时设置 session.trust_env = False 并手动指定DNS-over-HTTPS。

脚本写好了,丢到一台2核4G的VPS上跑。别用Windows自带定时任务,systemd timer + 日志轮转更稳。每跑完一批域名查询,写一条带时间戳的JSON到本地,注册局那边就算查日志也看不出规律——因为你每次请求的间隔、UA、代理IP都是乱序的。这才是伪装成真人的核心:不是动作像,而是统计特征像。

Selenium stealth configuration undetected chromedriver proxy

域名批量查询系统,多线程扫描与可用性判断

代理池和限频伪装搞定后,真正的瓶颈变成了查询速度。单线程跑 Whois,一个域名平均1.2秒,一万个就是三个多小时——等结果出来,好域名早被人抢了。我最初用 threading 开了30个线程,结果发现注册局那边的 Whois 服务根本扛不住,直接给我返回 connection reset。后来查了RFC 3912,才知道 Whois 协议本身是明文、无连接状态的,很多注册局对每个IP的并发连接数有限制。

解决方案其实很粗暴:把查询拆成多轮,每轮控制并发数,中间加随机休眠。我写了个 WhoisScanner 类,核心逻辑是这样的——

class WhoisScanner:
    def __init__(self, pool_size=8, query_delay=(1.0, 2.5)):
        self.semaphore = threading.Semaphore(pool_size)
        self.min_delay, self.max_delay = query_delay
        self.session = requests.Session()
        self.session.headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'})
        self.cache = {}

    def query(self, domain):
        if domain in self.cache:
            return self.cache[domain]
        with self.semaphore:
            sleep(random.uniform(self.min_delay, self.max_delay))
            try:
                resp = self.session.get(f'https://whois.example.com/api/{domain}', timeout=5)
                status = 'registered' if resp.json().get('exists') else 'available'
                self.cache[domain] = status
                return status
            except:
                return 'unknown'

这里有个坑:信号量 Semaphore 控制的是并发数,但如果某次查询超时,后面的请求会被阻塞更久。我后来加了个 asyncio 版,用 aiohttp 配合 asyncio.Semaphore,同样8并发,吞吐量翻了将近3倍。原因很简单——线程切换有开销,而异步在等待 I/O 时几乎不占CPU。

域名生成器不只是穷举

光查询快没用,你得知道查什么。拼音首字母组合、数字吉祥序列、短字符暴力枚举……这些生成逻辑看着简单,但有个细节容易漏:过滤保留字和无效字符。比如纯数字域名超过5位基本没人抢,但4位数字 8888.com 早被注册了。我写了个 DomainGenerator 类,支持拼音+数字+连字符组合,默认过滤掉注册局保留的短字符串(比如 www、ftp、mail、smtp、imap、admin)和长度小于2的字符。

class DomainGenerator:
    RESERVED = {'www', 'ftp', 'mail', 'smtp', 'imap', 'admin'}

    def __init__(self, tlds=['.com', '.cn', '.net']):
        self.tlds = tlds

    def from_pinyin(self, words, max_len=8):
        for w in words:
            if len(w) > max_len:
                continue
            for tld in self.tlds:
                yield f'{w}{tld}'

    def from_numbers(self, length=4):
        from itertools import product
        for combo in product('0123456789', repeat=length):
            domain = ''.join(combo)
            if domain not in self.RESERVED:
                for tld in self.tlds:
                    yield f'{domain}{tld}'

拼音生成器的命中率比随机字符串高得多。我扫了一晚上 .cn 后缀的双拼组合,找到了3个还没注册的行业词——虽然都是冷门词,但至少不是垃圾域名。字符集过滤这块,我用 re.match(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$') 做合法性校验,连字符不能在头尾,长度不能超过63,这些规则RFC 1035写得很清楚,但很多抢注工具直接忽略了。

结果缓存与过期策略

每次查询都走 Whois 是浪费。我把结果存到 SQLite 里,键是域名,值是 (status, timestamp)。但 Whois 信息是有 TTL 的,一般24小时后才可能变化。我设置缓存有效期为6小时,过期后重新查询。如果某个域名状态是 registered,我会再查一次它的到期时间——如果是 pendingDelete 状态,就加入待释放队列,等30天删除期结束再扫一次。

整个流程跑下来,从生成候选列表到输出可用域名,平均每个域名耗时从1.2秒降到了0.3秒左右——因为缓存命中率大概在40%。这0.3秒里还包括了写入数据库的I/O。说到底,抢域名拼的不是单次查询快,而是在别人还没想到要查的时候,你已经查完了。

Multithreaded domain batch query system WHOIS checker

毫秒级竞标引擎,从监控到下单的全自动化流程

候选列表有了、Whois 缓存也跑通了,但真正让竞标变得刺激的,是注册局那边的一道硬门槛:域名释放的时间窗口。WHOIS 显示的删除时间只是一个近似值,不同注册局对 pendingDelete 阶段的处理可能差出12个小时。我吃过这个亏——盯着一个四字母 .com,Whois 显示明天凌晨4点释放,结果凌晨2点就被抢走了。不是 Whois 不准,而是注册商内部的删除策略和注册局的实际释放时间根本不同步。

所以第一步是时间同步。我用了 ntplib 直接向阿里云 NTP 服务器(ntp.aliyun.com)同步,本地机器跑 ntpdate -q 校准偏差。代码里写了个 sync_ntp() 函数,返回当前时间戳和与系统时间的偏移毫秒数——偏差超过50ms就直接用NTP时间覆盖本地时钟。抢注这行,50ms的偏差足以让你在结果页看到“已被注册”。

import ntplib, time

def sync_ntp(server='ntp.aliyun.com'):
    client = ntplib.NTPClient()
    response = client.request(server, version=3)
    offset = response.offset
    local_now = time.time()
    ntp_now = local_now + offset
    return ntp_now, offset

然后才是真正的轮询。我写了一个 DomainMonitor 类,参数是一个域名列表+目标注册局API端点。监控逻辑不依赖 Whois,而是直接调用注册商提供的域名状态查询接口(比如 Namecheap 的 API 或 GoDaddy 的 REST 接口)。每5秒轮询一次,如果返回状态码变成 AVAILABLE 或者 PRE_RELEASE,立刻触发下单流程。这里有个坑:很多注册商的 API 有频率限制,Namecheap 是每秒最多1次查询,超了直接封IP半小时。

我没用简单的 time.sleep(5),而是用 asyncio.sleep(5) + 随机抖动——5秒的基础间隔上±1秒,模拟人工浏览行为。代码里还加了个 backoff 策略:如果连续3次返回 RATE_LIMIT,就把间隔拉到10秒,并换一个代理IP。

监控到域名可用之后,下一步是自动提交订单。这一步我用的是 Selenium + undetected-chromedriver,因为常规 ChromeDriver 很容易被注册商的反爬机制识别。登录环节走 Cookie 池:预先手动登录一次,导出 Cookies 存成 JSON,程序启动时直接注入。这样省掉了每次都要处理验证码的痛苦——但抢注时往往需要填表单,验证码还是绕不过去。

验证码这一块,我试过两套方案。第一套是 OCR:pytesseract + 二值化预处理,对于数字+字母的简单验证码(没扭曲那种)命中率能到70%。但注册商一旦换成极验滑块或者 ReCaptcha,OCR 直接废掉。第二套是接打码平台(我用的是 2Captcha),API 返回 token 后塞进 Selenium 的 execute_script() 里触发验证回调。响应时间在3~8秒之间——对于毫秒级竞标来说,这8秒足够让另一个机器人把域名拍走。

下单流程我拆成了三步:

1. fill_form(domain, duration=1, privacy=True)——用 Selenium 的 find_element(By.NAME, 'domainName') 填前缀,注意注册商表单里后缀通常是下拉框,得先模拟点击展开再选。

2. solve_captcha()——截图验证码区域,送打码平台,等回调,填入。

3. submit_order()——点击“添加到购物车”,然后“结账”。这里有一处坑:很多注册商的购物车页面会弹窗推荐附加服务(比如隐私保护、DNS托管),弹窗的DOM是动态加载的,必须 WebDriverWait 等到元素出现再点“跳过”。

def submit_order(driver, domain):
    driver.get('https://www.namecheap.com/domains/registration/results/?domain=' + domain)
    wait = WebDriverWait(driver, 10)
    add_btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button.add-to-cart')))
    add_btn.click()
    time.sleep(0.5)
    skip = driver.find_elements(By.XPATH, "//button[text()='No Thanks']")
    if skip:
        skip[0].click()
    driver.get('https://www.namecheap.com/cart/')
    checkout = wait.until(EC.element_to_be_clickable((By.ID, 'checkoutButton')))
    checkout.click()

重试机制我用了 tenacity 库的 @retry(stop=stop_after_attempt(3), wait=wait_fixed(0.5))。如果提交订单时网络抖动或者注册商返回500,自动重试。但有一个限制:同一域名在30秒内不能重复下单,否则注册商系统会标记为重复订单并封账号。我加了一个 order_lock 字典,以domain为key记录最后一次下单时间戳,30秒内跳过二次下单。

整个竞标引擎跑完,从监控到下单的平均耗时在1.2秒左右——其中验证码占了0.5~0.8秒,Selenium 的页面加载占了0.3秒,真正填表单和点按钮只用了0.1秒。如果验证码能换成打码平台预先储备 token,或者直接注册商 API 免验证码(像 GoDaddy 的 Reseller API 支持预授权支付),这个时间能压到400ms以内。不过那需要注册商合作伙伴资质,个人玩家搞不到。

抢注机器人的本质就是在别人还没反应过来的时候,把重复劳动压缩到极致——时间同步、轮询、填表、验证码、重试,每一步省下的几十毫秒,在竞标那天可能就是成败的分水岭。

应对反抢注机制,IP轮换、行为模拟与异常处理

1.2秒看起来快,但注册局那边不是傻子。你盯着 .com 的删除列表,他们也盯着你的请求模式——同一IP每秒10次查询、Selenium 的 navigator.webdriver 特征没清、点击之间间隔完全均匀,这些在注册商的WAF眼里就像黑夜里的萤火虫。我第一版脚本跑测试的时候,Namecheap 直接给我弹了Cloudflare的验证码页面,连续三次就封了账号24小时。

IP 轮换不能只靠代理列表随机切

很多人买一批代理,requests.get 前 random.choice 一下就觉得完事了。但注册商那边会记录每个IP的请求序列,如果一个代理刚查了50个域名下一秒又查另外50个,时间戳间隔完全一致,照样触发频率限制。

我的做法是:维护一个代理池字典,每个代理绑定一个请求计数器和最后请求时间。从池里取代理时,优先选那些5秒内没用过、且累计请求数小于30的。

proxy_pool = {
    'http://user:pass@12.34.56.78:8080': {'count': 0, 'last_used': 0},
    'http://user:pass@23.45.67.89:8080': {'count': 0, 'last_used': 0},
}

def get_available_proxy(pool, cooldown=5, max_requests=30):
    now = time.time()
    candidates = [
        (proxy, info) for proxy, info in pool.items()
        if now - info['last_used'] > cooldown and info['count'] < max_requests
    ]
    if not candidates:
        oldest = min(pool.items(), key=lambda x: x[1]['last_used'])
        sleep(oldest[1]['last_used'] + cooldown - now)
        return oldest[0]
    chosen = random.choice(candidates)
    chosen[1]['count'] += 1
    chosen[1]['last_used'] = now
    return chosen[0]

而且每个代理用完30次就扔回池底休息60秒。Selenium 那边更麻烦——WebDriver 每次启动都带一个唯一的session ID,注册商如果检测到同一个session ID在多个IP之间跳变,直接判定为自动化脚本。我后来把Chrome的用户数据目录独立出来,每个IP绑定一个单独的profile目录,这样cookie、localStorage和session都跟着IP走。

行为模拟不是加个 random.uniform 就够

人操作浏览器的轨迹,不是简单的等0.5秒→点一下→再等0.5秒。实测发现,真人填表时鼠标会有小幅抖动、两次点击之间的间隔服从正态分布而非均匀分布,而且滚动页面通常发生在输入框切换之后,不是之前。

我用 ActionChains 模拟鼠标移动路径,中间插几个随机拐点:

def human_move_to(element):
    action = ActionChains(driver)
    start_x = random.randint(100, 500)
    start_y = random.randint(100, 500)
    target_x = element.location['x'] + random.randint(-5, 5)
    target_y = element.location['y'] + random.randint(-5, 5)
    mid_x = (start_x + target_x) / 2 + random.randint(-30, 30)
    mid_y = (start_y + target_y) / 2 + random.randint(-30, 30)
    action.move_by_offset(mid_x - start_x, mid_y - start_y)
    action.move_by_offset(target_x - mid_x, target_y - mid_y)
    action.perform()
    time.sleep(random.uniform(0.1, 0.3))
    element.click()

除了鼠标轨迹,滚动行为也得配上。我注册了一个 MutationObserver 的监听,每次DOM结构变化时做一个随机短滚动,让页面加载过程看起来更接近人类浏览的节奏。

整套体系磨合了两个月,从最初被封号到后来稳定跑通,中间踩的坑都能写本小册子了。但你要问我值不值——当那些你看中的四字母 .com 被你用一个自动化脚本拿下,而其他人还在手动刷新页面的时候,那种感觉还是挺爽的。

最后提醒一句:所有操作必须在注册商服务条款允许的范围内进行,批量查询和自动化下单可能违反某些注册商的使用协议,封号是轻的,重则域名被收回。我知道有人因为用脚本抢注被 Namecheap 永久拉黑,连带着 PayPal 账号都受牵连。技术是工具,怎么用你自己掂量。