PyDevDeep/async-domain-analyzer

GitHub: PyDevDeep/async-domain-analyzer

基于异步并发和双层抓取的域名批量分类系统,通过多维度评分快速识别活跃商业站点与停放域名。

Stars: 0 | Forks: 0

# Async Domain Analyzer 🚀 [![CI Pipeline](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/7cb7fb044c193258.svg)](https://github.com/PyDevDeep/async-domain-analyzer/actions) [![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen.svg)](https://github.com/PyDevDeep/async-domain-analyzer) [![Python](https://img.shields.io/badge/python-3.13-blue.svg)](https://www.python.org/downloads/) [![Poetry](https://img.shields.io/badge/poetry-1.8+-purple.svg)](https://python-poetry.org/) ## ⚡ 快速入门 (60 秒) ``` # 1. 克隆 repository git clone https://github.com/PyDevDeep/async-domain-analyzer.git cd async-domain-analyzer # 2. 安装 Poetry(如果尚未安装) curl -sSL https://install.python-poetry.org | python3 - # 3. 安装依赖 poetry install # 4. (可选)配置 Serper.dev API cp _env.example .env # 编辑 .env:SERPER_API_KEY=your_key_here # 5. 运行 triaging poetry run python -m src.main --input data/seeds.csv --workers 5 # 6. 运行 triaging 以重新验证失败的项 poetry run python -m src.main --input data/seeds.csv --rerun-failed ``` **完成!** 结果已保存至 `data/output_YYYYMMDD_HHMMSS.csv` + `summary.md` ## 📋 目录 - [核心功能](#-key-features) - [检查内容及原因](#-what-was-checked-and-why) - [未检查内容及原因](#-what-was-not-checked-and-why) - [排序逻辑(评分 0–100)](#-sorting-logic-scoring-0100) - [如果有两天时间我会增加什么](#-what-i-would-add-in-2-days) - [代码在 5000 个域名时会在哪里崩溃](#-where-the-code-will-break-at-5000-domains) - [因需求不明确而做出的假设](#-assumptions-due-to-ambiguous-requirements) - [CLI 命令与参数](#-cli-commands-and-parameters) - [技术栈](#-tech-stack) ## ✨ 核心功能 ### 🔄 优雅降级(工业级弹性) - **Serper.dev 是可选的:** 系统无需 API 密钥即可工作,仅使用 Pass 1 (BeautifulSoup) - **自动回退:** 如果 Pass 1 失败(403、超时、重度 JS 渲染) → Pass 2 (Serper.dev),前提是有可用的 API 密钥 - **不会崩溃:** 失败的域名在 CSV 中标记为 `status=error`,其余域名继续处理 ### 🔁 通过 Cache.db 重新运行失败项(智能恢复) - **独立于 CSV:** `--rerun-failed` 从 SQLite 缓存读取状态,而不是从输入文件 - **适用于任何输入:** 纯域名列表或复杂的 CSV —— 系统通过数据库查找失败的域名 - **节省时间:** 仅重新抓取状态为 `status=error` 的域名;成功的数据直接从缓存拉取 ### 📊 按相关度导出排序(智能导出顺序) - **可配置排序:** `.env` 参数 `EXPORT_SORT_BY_RELEVANCE=true` 用于按分数对 CSV 进行排序 - **二级排序:** 首先按分数排序(100→0),然后按字母顺序处理同分情况 - **保留原始顺序:** 默认为 `false` —— CSV 中的域名顺序与输入文件中的顺序保持一致 - **NULL 安全:** 没有分数(抓取失败)的域名会自动移至列表末尾 **`.env` 配置示例:** ``` # 按相关性对 CSV 进行排序(High Priority → Low Priority) EXPORT_SORT_BY_RELEVANCE=true # 或保留原始顺序(默认) EXPORT_SORT_BY_RELEVANCE=false ``` **当 `EXPORT_SORT_BY_RELEVANCE=true` 时的输出:** ``` domain,score,priority apple.com,100,High ← highest score wikipedia.org,100,High ← same score → alphabetical order amazon.com,85,High httpbin.org,40,Low fake-domain.com,0,Low ← failed domains at the end ``` ### ⚡ 异步 I/O + 连接池 - **5 个工作线程在大约 20 秒内处理 100 个域名**(相比之下,同步版本需要 100 秒) - **可配置的并行度:** `--workers 10` 适用于快速的 VPS,或 `--workers 2` 适用于资源受限的环境 ### 📊 自动化报告 - **CSV:** 兼容 Google Sheets,包含 19 列(分数、SSL、年龄、内容、错误) - **Markdown 摘要:** 包含高/中/低优先级细分的执行摘要 - **结构化日志:** 通过 structlog 输出 JSON 日志,便于集成 ELK/Splunk ### 🧪 93% 测试覆盖率 + CI/CD - **50 个单元/集成测试** (pytest + pytest-asyncio) - **GitHub Actions CI:** 每次推送时运行 Ruff、Pyright、Coverage - **Pre-commit hooks:** 提交前自动格式化 ## 🔍 检查内容及原因 ### 1. SSL 证书 (check_ssl_certificate) **内容:** 连接到 443 端口,解析颁发者、到期日期、有效性 **原因:** 真实的商业网站几乎总有有效的 SSL 证书。停放域名或诈骗网站很少正确配置 HTTPS。这是一个快速(< 2 秒)且可靠的“活跃度”指标。 **实现:** `ssl.create_connection()` → `wrap_socket()` → `getpeercert()` **评分权重:** +20 分(占总分的 20%) ### 2. 通过 WHOIS 获取域名年龄 (get_domain_age) **内容:** WHOIS 查询获取 creation_date,并转换为天数 **原因:** 老域名(> 1 年)是稳定性的象征。新域名(< 30 天)通常是垃圾邮件或测试域名。1-2 年的域名属于中等优先级。 **实现:** `whois.whois(domain)` → 解析 `creation_date`(列表或 datetime 对象) **评分权重:** 超过 1 年得 +20 分,少于 30 天得 0 分,其间按线性比例计算 ### 3. 实时页面内容 (analyze_html_content) **内容:** 使用 BeautifulSoup 解析,以检测表单、图像和字数 **原因:** 停放域名 = 10 个单词 + 没有表单。活跃网站 = 100+ 个单词 + 表单/图像。这是区分“真实商业网站”与“停放域名”的最准确指标。 **实现:** - `soup.find_all("form")` → has_forms (布尔值) - `soup.find_all("img")` → has_images (布尔值) - `soup.get_text()` → word_count (整数) - `has_live_content = (word_count > 100) AND (has_forms OR has_images)` **评分权重:** +40 分(最高权重,因为这是主要标准) ### 4. 文本密度 (word_count) **内容:** 计算 HTML body 中的单词数量 **原因:** 即使没有表单,高字数(> 500 字)也表明这是一个内容丰富的网站(博客、新闻、文档)。低字数(< 50)表示空白页面,或者是 Pass 1 无法读取的 JS 渲染内容。 **评分权重:** 超过 500 字得 +20 分,100-500 字按 0-20 的线性比例计算 ### 5. 内容类型和响应代码 (scraper_pass1.py → fetch_url) **内容:** HTTP HEAD 请求检查可用性 + GET 请求获取 HTML **原因:** - `status_code = 200` → 网站活跃 - `status_code = 403/404` → 网站受保护或不存在 → 触发 Pass 2 - `Content-Type != text/html` → 非 HTML(PDF、图片) → 跳过解析 **实现:** `aiohttp.ClientSession.get()` → 在使用 BeautifulSoup 前检查 headers ### 6. 重定向后的最终 URL (get_final_url) **内容:** HEAD 请求设置 `allow_redirects=True` 以获取最终 URL **原因:** 许多域名会重定向到 www 或其他子域名。最终 URL 揭示了域名是正在积极提供服务(重定向到 CDN、其他 TLD),还是仅向停放服务返回 301。 **权重:** 不直接影响分数,但会存储在 CSV 中以提供上下文 ## ❌ 未检查内容及原因 ### 1. Pass 1 中的 JavaScript 执行 **内容:** Pass 1 未使用无头浏览器(Playwright、Selenium) **原因:** - **速度:** BeautifulSoup 处理一个域名需 0.5–1 秒。Playwright 需要 3–5 秒。 - **资源:** 一个无头浏览器每个实例需要 100–200 MB 内存。5 个工作线程就是 1 GB 内存。 - **权衡:** 重度依赖 JS 的网站(React、Vue)在 Pass 1 中会失败 → 回退到 Pass 2(Serper.dev 抓取 API 支持 JS)。 - **经济学:** 混合架构允许完全免费地处理 1000 个域名中的 700 个,仅在遇到复杂的 JS 网站或具有反机器人保护的网站时才使用付费资源。 ### 2. 反向链接配置或域名权重 (DA/DR) **内容:** 未检查 Moz DA、Ahrefs DR 或反向链接数量 **原因:** - **API 成本:** Moz API 处理 2.5 万次请求需 99 美元/月。Ahrefs API 需 500 美元/月。 - **速度:** 反向链接 API 通常很慢(2–5 秒/请求)。 - **分类相关性:** 需求重点是“活跃与停放”,而不是 SEO 指标。DA/DR 对 SEO 审计很重要,但对初始分类意义不大。 - **替代方案:** 域名年龄 + SSL + 活跃内容在没有额外 API 的情况下提供了足够的质量相关性。 ### 3. DNS 记录 (MX、TXT、SPF) **内容:** 未使用 `dig` 或 `dnspython` 进行 DNS 记录解析 **原因:** - **速度:** DNS 查找会增加每个域名 0.5–1 秒的时间。 - **弱信号:** MX 记录的存在仅表明配置了电子邮件,并不代表业务“活跃”。许多停放域名也有 MX 记录。 - **专注内容:** HTML 内容 + SSL 在相同时间内提供了更强的信号。 ### 4. 社交媒体存在 **内容:** 未检查 Facebook 像素、Twitter meta 标签或 LinkedIn 信息 **原因:** - **解析复杂性:** Meta 标签通常受抓取保护或需要身份验证。 - **弱指标:** 大量垃圾网站使用伪造的社交 meta 标签进行 SEO。 - **时间:** 每个域名会增加 1–2 秒,但评分准确度没有相应提升。 ### 5. 流量估算 (SimilarWeb、Alexa) **内容:** 未通过外部 API 估算流量 **原因:** - **API 不可用:** Alexa API 已于 2022 年关闭。SimilarWeb API 费用超过 300 美元/月。 - **准确性:** 公共 API 流量估算对中大型网站(占输入列表的 90%)极不准确。 - **替代方案:** 域名年龄 + SSL + 内容与流量相关联,无需直接测量。 ### 6. 内容语言检测 **内容:** 未通过 langdetect 或 HTML lang 属性进行语言检测 **原因:** - **速度:** langdetect 库会增加每个域名 0.2–0.5 秒的时间。 - **低相关性:** 需求中没有要求按语言过滤。如果语言重要,最好在 Google Sheets 中添加后处理过滤器。 - **准确性:** HTML lang 属性经常缺失或不正确。langdetect 仅在超过 50 个字符的文本上才能可靠工作。 ## 🛠 混合架构与经济学 该系统利用两层数据收集模型(混合抓取)来确保性能、可靠性和成本效益之间的完美平衡。 ### 1. 两层逻辑 (Pass 1 -> Pass 2) 1. **Pass 1:原生爬虫(免费)** - 由异步 `aiohttp` 请求 + `BeautifulSoup4` 驱动。 - **效率:** 成功处理约 70% 的网站(静态内容)。 - **成本:** $0.00。 2. **Pass 2:Serper.dev 回退(付费)** - 仅在遇到阻止(403)、超时或 Pass 1 无法检测内容的重度 JS 网站(SPA)时触发。 - **效率:** 绕过 Cloudflare 保护并通过 Google 搜索片段解析数据。 - **成本:** 每个域名约 10 个积分($0.01)。 ### 2. 成本分析 对于一批 1000 个域名: - **700 个域名 (Pass 1):** 免费处理。 - **300 个域名 (Pass 2):** 3000 Serper 积分 = **$3.00**(基于 50k 积分 50 美元计算)。 - **平均成本:** **每个域名 $0.003**,比使用高级无头浏览器服务便宜 10 倍。 ### 3. 智能缓存与重新运行 - **SQLite 缓存:** 结果存储在本地。对成功的域名重新运行该工具是即时的,且无需额外成本。 - **智能重新运行:** `--rerun-failed` 标志自动识别数据库中的错误条目,清除它们,并仅重试失败的域名。这使您可以达到 100% 的结果,而无需为成功的结果支付两次费用。 ### 4. 性能(基于日志) - **Pass 1 速度:** < 1 秒。 - **Pass 2 速度:** 1.5 - 3 秒。 - **可扩展性:** 支持 1 到 50+ 个并发工作线程。 ## 🧮 排逻辑(评分 0–100) 系统采用 **100 分制** 而不是简单的 1–10 分制,以获得更好的粒度,并便于与下游 ML 模型或加权排名集成。 ### 评分公式 ``` Score = SSL_Score + Age_Score + Content_Score + Volume_Score ``` ### 组成部分 | 组件 | 最高分 | 评分逻辑 | |-----------|------------|---------------| | **SSL 有效性** | 20 | 如果 SSL 有效,则 `+20`;如果过期不超过 90 天,则 `+10`;如果无效/不存在,则为 `0` | | **域名年龄** | 20 | 超过 730 天(2 年)得 `+20`,180–730 天得 `+10`,少于 30 天得 `0`,期间按线性比例计算 | | **活跃内容** | 40 | 如果 `has_live_content = True` (word_count > 100 且有 forms 或 images),则 `+40`,否则为 `0` | | **内容量** | 20 | 超过 500 字得 `+20`,100–500 字按 0–20 线性比例计算,少于 100 字得 `0` | ### 实现细节 (scorer.py) #### 1. SSL 评分(函数 `calculate_ssl_score`) ``` Input: ssl_data (dict with keys: valid, days_until_expiry, issuer) Logic: - If ssl_data["valid"] == True → +20 points - If valid == False but days_until_expiry > -90 (expired < 3 months ago) → +10 (the domain may have been live recently but the SSL renewal was missed) - Otherwise → 0 Output: Integer 0–20 ``` #### 2. 年龄评分(函数 `calculate_age_score`) ``` Input: domain_age_days (Integer or None) Logic: - If domain_age_days == None → 0 (WHOIS failure, conservative approach) - If age < 30 days → 0 (newly registered domain, low priority) - If age >= 730 days (2 years) → 20 - If 30 <= age < 730 → linear interpolation: score = ((age - 30) / (730 - 30)) * 20 Example: 365 days (1 year) → ((365-30)/(730-30)) * 20 = 9.57 ≈ 10 points Output: Integer 0–20 ``` #### 3. 内容评分(函数 `calculate_content_score`) ``` Input: has_live_content (Boolean) Logic: - If has_live_content == True → +40 (check: word_count > 100 AND (has_forms OR has_images)) - Otherwise → 0 Output: Integer 0 or 40 ``` #### 4. 容量评分(函数 `calculate_volume_score`) ``` Input: word_count (Integer) Logic: - If word_count >= 500 → +20 - If 100 <= word_count < 500 → linear interpolation: score = ((word_count - 100) / (500 - 100)) * 20 Example: 300 words → ((300-100)/400) * 20 = 10 points - If word_count < 100 → 0 Output: Integer 0–20 ``` ### 优先级表 | 分数 | 优先级 | 下一步行动 | 解释 | |-------|----------|-------------|----------------| | **80–100** | **高** | 人工审查 | 真实商业网站,具有有效的 SSL、老域名和丰富的内容。转化概率 > 70%。 | | **50–79** | **中** | 监控 | 网站活跃,但域名较新、内容较少或 SSL 已过期。需要进一步澄清。 | | **0–49** | **低** | 丢弃 | 停放域名、无效 SSL 或无内容。不值得花时间进行人工审查。 | ### 计算示例 #### 示例 1:理想的商业网站 ``` Domain: example-store.com SSL: Valid (Let's Encrypt, expires in 60 days) → +20 Age: 1825 days (5 years) → +20 Content: word_count=1200, has_forms=True, has_images=True → +40 Volume: 1200 words → +20 ----- Total Score: 100 Priority: High (Manual Review) ``` #### 示例 2:新成立的初创公司 ``` Domain: new-startup.io SSL: Valid (Cloudflare, expires in 89 days) → +20 Age: 45 days → ((45-30)/(730-30)) * 20 = 0.43 ≈ 1 Content: word_count=350, has_forms=True, has_images=False → +40 Volume: 350 words → ((350-100)/400) * 20 = 12.5 ≈ 13 ----- Total Score: 74 Priority: Medium (Monitor) Reason: Domain is fresh, but content is live → worth revisiting in a month ``` #### 示例 3:停放域名 ``` Domain: parked-example.net SSL: Invalid (no HTTPS) → 0 Age: 3650 days (10 years) → +20 Content: word_count=15, has_forms=False, has_images=False → 0 Volume: 15 words → 0 ----- Total Score: 20 Priority: Low (Discard) Reason: Old but dead — a typical parked domain ``` ## 🚀 如果有两天时间我会增加什么 ### 第 1 天:高级过滤与丰富功能 #### 1. Google Sheets API 集成 **内容:** 自动将结果同步到 Google Sheets **原因:** 目前输出的是本地 CSV。为了协作,实时 Google Sheets 更可取。 **实现:** - 添加依赖:`poetry add gspread google-auth` - 创建 `src/sheets_exporter.py`: - 通过服务账号 JSON 进行 `authenticate_gsheets()` 函数验证 - 函数 `export_to_sheet(dataframe, sheet_id, worksheet_name)` - 通过 `worksheet.append_rows(values)` 追加新行 - CLI 参数:`--export-sheets --sheet-id=YOUR_SHEET_ID` - 验收标准:在执行 `poetry run python src/main.py --export-sheets` 后,结果在 30 秒内出现在 Google Sheets 中 #### 2. 邮件域名提取 + MX 记录检查 **内容:** 从联系表单中提取电子邮件域名并检查 MX 记录 **原因:** 存在有效的 MX 记录增加了公司处于活跃状态的可能性。 **实现:** - 在 `analyze_html_content` 中,添加对 `` 的解析: - 邮箱正则表达式:`r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'` - 通过 `email.split('@')[1]` 从电子邮件中提取域名 - 添加函数 `check_mx_records(email_domain)`: - 通过 dnspython 实现 `dns.resolver.resolve(email_domain, 'MX')` - 如果存在 MX 记录 → 评分加 5 分 - 验收标准:对于在联系方式中包含电子邮件的域名,分数增加 5 分 #### 3. 通过 Claude API 实现的 AI 驱动细分领域检测 **内容:** 自动分类网站细分领域(电子商务、SaaS、博客、作品集) **原因:** 允许按行业过滤域名,而无需人工审查。 **实现:** - 添加 `poetry add anthropic` - 创建 `src/niche_classifier.py`: - 函数 `classify_niche(title, meta_description, snippet_text)` - Claude 的提示词:“根据标题、描述和片段识别该网站的细分领域。返回一个类别:[ecommerce|saas|blog|portfolio|corporate|other]” - 频率限制:每天 1000 次请求(Anthropic 免费套餐) - 触发条件:仅对分数 > 70 的域名调用(以节省 API 调用) - 输出:CSV 中新增 `niche` 列 - 验收标准:高优先级的域名已识别出细分领域 ### 第 2 天:可扩展性与监控 #### 4. 使用 Redis 替代 SQLite 缓存 **内容:** 从 SQLite 迁移到 Redis 进行分布式缓存 **原因:** SQLite 在并行工作线程下存在写锁争用。Redis 支持原子操作 + TTL。 **实现:** - 添加 `poetry add redis aioredis` - 创建 `src/redis_cache.py`: - 包含以下方法的 `RedisCacheManager` 类: - `async def get(domain: str) -> dict | None` - `async def set(domain: str, data: dict, ttl: int = 604800)` (7 天) - 使用 `aioredis.Redis.set(key, json.dumps(data), ex=ttl)` - 在 `config.py` 中添加 `REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")` - 在 `main.py` 中切换:`--cache-backend=redis` 或 `--cache-backend=sqlite` - 验收标准:使用 Redis 缓存运行时,不会出现 sqlite3.OperationalError #### 5. Prometheus 指标导出器 **内容:** 实时抓取过程指标(吞吐量、错误率、平均响应时间) **原因:** 用于生产监控和调试瓶颈。 **实现:** - 添加 `poetry add prometheus-client` - 创建包含以下内容的 `src/metrics.py`: - `Counter("domains_processed_total")` - `Counter("domains_failed_total")` - `Histogram("domain_processing_duration_seconds")` - `Gauge("serper_credits_remaining")` - 在 `main.py` 末尾,启动 `prometheus_client.start_http_server(8000)` - 验收标准:Grafana 仪表板在端口 8000 上显示实时指标 #### 6. 针对高优先级域名的 Slack/电子邮件通知 **内容:** 当发现分数 > 90 的域名时实时告警 **原因:** 对顶级线索的快速响应可提高转化率。 **实现:** - 添加 `poetry add slack-sdk aiosmtplib` - 创建 `src/notifier.py`: - 函数 `async def send_slack_alert(domain, score, reason, webhook_url)` - 负载:`{"text": f"🔥 发现高优先级域名:{domain} (分数: {score})"}` - 在评分后的 `process_single_domain` 中: - 如果分数 >= 90 → `await send_slack_alert(...)` - CLI 参数:`--notify-slack --slack-webhook=YOUR_WEBHOOK` - 验收标准:测试运行在检测到高优先级域名后 5 秒内发送 Slack 消息 #### 7. Playwright 混合爬虫(防阻塞) * **问题:** Serper 和标准的 `aiohttp` 请求通常会被 Cloudflare、Akamai 或非标准渲染 (SPA) 阻止。 * **解决方案:** 使用 **Playwright** 实现第三阶段分析 (Pass 3)。 - **无头浏览:** 为返回 403/401 的站点模拟真实用户的标准请求。 - **Stealth 插件:** 使用 `playwright-stealth` 隐藏自动化特征。 - **动态渲染:** 等待 JS 内容加载,从而提取更多用于评分的数据。 - **智能回退:** 仅在轻量级 `HTTP GET` 失败时才触发 Playwright,从而节省资源。 #### 8. 带有指数退避的自动重试逻辑 **内容:** 扩展 `@async_retry` 装饰器以实现智能退避 **原因:** 当前的重试是固定的(1s → 2s → 4s)。对于速率限制,指数 + 抖动是更好的选择。 **实现:** - 向 `src/retry.py` 添加参数: - `jitter=True` → 向延迟添加 0–0.5 秒的随机时间 - `max_delay=60` → 最大延迟上限 - 公式:`delay = min(base_delay * (2 ** attempt) + random(0, 0.5), max_delay)` - 验收标准:在遇到 WHOIS 速率限制时,重试不会超过 60 秒 ## 💥 代码在 5000 个域名时会在哪里崩溃 ### 1. SQLite 写锁争用(严重) **问题:** SQLite 使用文件级锁定。在并行工作线程下,多个进程尝试同时写入 → `sqlite3.OperationalError: database is locked`。 **阈值:** 使用 5 个工作线程时约为 500 个域名。使用 10+ 个工作线程时,早在 100 个域名时就会出现失败。 **症状:** - 日志:`WARNING: SQLite lock timeout, retrying...` - 由于重试开销,吞吐量从 5 个域名/秒降至 0.5 个域名/秒 - 上下文切换导致 CPU 使用率增加 **短期修复:** ``` # 已在 cache.py 中实现: conn = sqlite3.connect(db_path, timeout=30.0) cursor.execute("PRAGMA journal_mode=WAL;") ``` WAL 模式允许并发读取,但写入仍然会被阻塞。 **长期修复:** - **迁移到 Redis:** - 通过 Redis 管道进行原子 SET/GET - 基于 TTL 的过期,而不是手动清理 - 通过 SETNX 对关键部分使用分布式锁 - 基准测试:Redis 在普通硬件上可处理 10k SET/GET 操作/秒 - **替代方案:** 使用通过 SQLAlchemy AsyncSession 进行连接池管理的 PostgreSQL **针对 5k 域名的临时解决方法:** ``` # 在 batch_processor.py 中,更改 strategy: # 替代在每个 domain 后立即执行 cache.set() 的做法: results = await process_domains_batch(domains) # 在一个 transaction 中批量写入所有结果: cache_manager.bulk_set(results) # executemany() instead of individual INSERTs ``` ### 2. 被反机器人保护封锁 IP(高风险) **问题:** 在短时间内(1-2 小时)抓取 5000 个域名时,CDN 提供商(Cloudflare、Akamai、Fastly)会检测到该模式并封锁 IP。 **阈值:** 每小时从单个 IP 发出约 300–500 次请求会触发受保护网站的速率限制。 **症状:** - HTTP 403 Forbidden 并附带 Cloudflare 验证页面 - HTTP 429 Too Many Requests - 日志显示:针对 70% 的域名 `Pass 1 failed → fallback to Pass 2` → Serper API 成本增加 3-4 倍 **解决方案:** 1. **住宅代理轮换:** - 与 Bright Data 或 Smartproxy API 集成 - 每 10–20 个请求轮换一次 IP - 成本:每月 500 美元可获得 40 GB 住宅流量(足够处理 5 万个域名) 2. **客户端速率限制:** # 添加到 config.py: MAX_REQUESTS_PER_MINUTE = 60 # 限制吞吐量 # 在 batch_processor.py 中: async with aiohttp.ClientSession() as session: rate_limiter = AsyncLimiter(MAX_REQUESTS_PER_MINUTE, 60) async with rate_limiter: await fetch_url(session, url) 3. **User-Agent 轮换:** # 目前在 scraper_pass1.py 中是硬编码的: headers = {"User-Agent": "Mozilla/5.0 ..."} # 添加轮换: from fake_useragent import UserAgent ua = UserAgent() headers = {"User-Agent": ua.random} ### 3. Pandas DataFrame 导致的内存耗尽(中等风险) **问题:** `exporter.py` 在导出前将所有结果加载到单个 DataFrame 中: ``` df = pd.DataFrame(results) # results = list of 5000 dict objects df.to_csv(output_path) ``` 每个域名结果约占用 2 KB(元数据、HTML 片段、URL)。5000 个域名 = 内存中 10 MB。在 5 万个域名时 = 100 MB → 可接受。在 50 万个域名时 → 1 GB → 可能导致低内存 VPS 发生交换。 **阈值:** 在 RAM < 4 GB 的机器上处理 50,000+ 个域名 **解决方案:** ``` # 流式 CSV 写入替代批量 DataFrame: import csv with open(output_path, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=COLUMN_NAMES) writer.writeheader() # Process in chunks of 1000 domains: for chunk in chunked(domains, 1000): chunk_results = await process_domains_batch(chunk) writer.writerows(chunk_results) f.flush() # Force write to disk ``` ### 4. Serper.dev API 成本激增(业务风险) **问题:** Pass 1 败的每个域名都会回退到 Pass 2 (Serper.dev)。如果 70% 的域名因 IP 阻塞而失败 → 70% 的调用将发送到 Serper API。 **阈值:** 5000 个域名 × 70% 失败率 = 3500 次 Serper 调用 × 5 积分 = 17,500 积分 **月度配额:** 2500 积分 → 超出 15,000 积分 → 超额费用 $15(Serper 定价:$0.001/积分) **症状:** - 日志:`Serper budget limit reached, skipping remaining domains` - CSV 包含大量 `status=error, reason=Budget limit reached` **解决方案:** 1. **通过 DNS 预过滤:** # 在抓取前检查 DNS 解析: async def is_resolvable(domain): try: await asyncio.get_event_loop().getaddrinfo(domain, None) return True except: return False # 跳过 NXDOMAIN 域名 → 节省 20–30% 的 Serper 调用 2. **本地 Playwright 回退:** - 针对受保护的网站,使用本地无头浏览器代替 Serper.dev - 成本:0 次 API 调用,但每个域名增加 3 秒耗时,每个工作线程增加 200 MB 内存 - 权衡:速度较慢,但免费 3. **预算熔断器(已实现):** # 在 rate_limiter.py 中: if self.credits_used >= self.max_credits: logger.error("Serper budget exhausted") return False # 阻止所有后续 Serper 调用 ### 5. WHOIS 速率限制(中等风险) **问题:** 公共 WHOIS 服务器具有速率限制(通常为每个 IP 每小时 100–200 次请求)。对于 5000 个域名 = 5000 次 WHOIS 请求 → 在 200 次后会被阻止。 **阈值:** 每小时约 200 个域名 **症状:** - 日志:`WHOIS lookup failed: Connection refused` - 大多数域名的 domain_age 保持为 None → 评分下降 20 分 **解决方案:** 1. **具有延长 TTL 的 WHOIS 缓存:** # domain_age 很少更改(仅在转移时): cache_manager.set(domain, result, ttl=30*86400) # 30 天而不是 7 天 2. **批量 WHOIS API:** - WhoisXML API:$0.004/请求 - 批量查询:5000 个域名 = $20 - 权衡:付费,但保证正常运行时间 3. **Wayback Machine 回退:** async def get_domain_age_wayback(domain): url = f"https://archive.org/wayback/available?url={domain}" data = await fetch_json(url) first_snapshot = data['archived_snapshots']['closest']['timestamp'] return parse_date(first_snapshot) ### 6. 超时级联故障(高风险) **问题:** 如果许多域名响应缓慢(> 10 秒),工作线程会阻塞在 fetch_url 中 → 吞吐量下降。 **阈值:** 超过 20% 的域名超时 → 处理时间从 1 小时增长到 4-5 小时(对于 5000 个域名) **解决方案(已实现):** ``` # 在 batch_processor.py 中: result = await asyncio.wait_for( analyze_domain(session, domain, config), timeout=30.0 # Hard deadline per domain ) ``` **额外改进:** ``` # 基于先前结果的自适应 timeout: avg_response_time = calculate_average(recent_results) if avg_response_time > 5000: # 5 seconds max_workers = 3 # Reduce parallelism timeout = 15 # Shorten timeout for slow domains ``` ## 🤔 因需求不明确而做出的假设 ### 1. “真实商业网站”的定义 **需求中的歧义:** “确定哪些域名是活跃的真实商业网站,哪些是停放域名” **假设:** - **活跃 = 存在内容 + 交互性:** - word_count > 100(有意义文本的最低阈值) - has_forms OR has_images(功能指示器) - **不被视为活跃:** - 静态占位符页面(10–50 字) - “即将推出”或“正在建设中”页面(即使有图像) - 带有广告/链接的停放域名(图像多但独特内容不足 50 字) **理由:** - 表单 = 联系方式 (CTA) → 业务指标 - 没有表单的图像可能是停放域名上的广告 - 100 字 —— 经验确定的阈值:菜单 + 2-3 段落 = 最小的商业网站 **替代解释(未采用):** - 活跃 = 网站以 HTTP 200 响应(太宽泛) - 活跃 = 具有有效的 SSL(许多停放域名也有 SSL) ### 2. 评分权重 (20-20-40-20) **需求中的歧义:** “优先考虑需人工审查的域名” **假设:** SSL (20) + 年龄 (20) + 内容 (40) + 容量 (20) = 100 **理由:** - **内容 = 最高权重 (40):** 区分活跃与停放的主要标准 - **SSL + 年龄 = 各 20 分:** 稳定性和可信度的补充指标 - **容量 = 20 分:** 区分浅层与深层内容网站 **替代方案(未采用):** - SSL (10) + 年龄 (30) + 内容 (60) —— 更加强调内容,较少关注安全性 - SSL (30) + 年龄 (10) + 内容 (60) —— 电子商务的安全优先级 **选择该方案的理由:** - 大多数商业网站都有 SSL(已通过 Let's Encrypt 普及) - 域名年龄很重要,但即使是新域名的初创公司也可能是有价值的线索 - 内容是最可靠的指标:停放网站几乎没有超过 100 字的 ### 3. 域名年龄阈值(30 天 = 0 分,730 天 = 20 分) **需求中的歧义:** “优先考虑较旧的域名” **假设:** - < 30 天 = 刚注册,通常是垃圾邮件或测试 → 0 分 - - 30–730 天 = 线性插值 **为什么是 30 和 730:** - **30 天:** Google 沙盒期在 1-2 个月后结束。在 30 天之前,许多域名仍然没有流量。 - **730 天(2 年):** 经验统计:50% 的初创公司在 2 年内失败。存活了 2 年以上的域名 = 稳定性信号。 **替代方案(未采用):** - 90 天 / 1 年(粒度较小) - 1 个月 / 5 年(对新域名过于宽松) ### 4. 字数阈值(100 字用于判定 has_live_content) **需求中的歧义:** 没有明确说明多少文本构成“活跃内容” **假设:** 100 字 —— 有意义页面的最低标准 **理由:** - 典型的停放域名:“此域名待售。请联系我们。” = 6–20 字 - 最小着陆页:标题(10 字) + 主视觉区(30 字) + 特点(60 字) = 约 100 字 - 少于 100 字 → 极有可能是占位符或广告 **经验验证:** - 手动验证了 50 个域名: - < 50 字 → 90% 为停放域名 - 50–100 字 → 70% 为停放域名(薄弱的着陆页) - 100+ 字 → 80% 为活跃网站 **替代方案:** - 50 字(太低,存在大量误报) - 200 字(太高,会遗漏最小着陆页) ### 5. Pass 2 回退触发条件 **需求中的歧义:** “确保受保护网站的数据质量” **假设:** 在以下情况下触发 Serper.dev 回退: 1. Pass 1 返回 HTTP 403/404/503 2. Pass 1 超时超过 10 秒 3. Pass 1 返回少于 100 字(可能表示 JS 渲染) **为什么是这些条件:** - **403/404:** 明显的失败;BeautifulSoup 无法提取任何内容 - **超时:** 服务器响应慢或被防火墙阻止 → 最好通过 Serper 检查 - **< 100 字:** 可能是 React SPA,其中所有内容都在 JS 中 → Serper 能看到渲染后的 HTML **什么不会触发回退:** - 带有任何大于 100 字 word_count 的 HTTP 200(认为 Pass 1 成功) - SSL 错误(即使没有 SSL 也可以提取 HTML) **权衡:** - 积极回退 → API 成本较高,但准确性更好 - 保守回退 → 成本较低,但会遗漏重度 JS 网站 **所选策略:** 适度积极(在 < 100 字时触发),因为这平衡了成本与覆盖率。 ### 6. 缓存 TTL(7 天) **需求中的歧义:** 没有明确说明缓存结果的时间长度 **假设:** 7 天 = 平衡新鲜度与效率 **理由:** - **为什么不是 1 天:** 如果由于错误而重新运行 —— 缓存仍然有效,从而节省 API 调用 - **为什么不是 30 天:** 网站可能会发生变化(新内容、SSL 续期) → 7 天可保持相关性 **例外情况:** - domain_age 缓存 30 天(WHOIS 数据很少更改) - SSL 证书到期日缓存至到期日当天(续订前的静态值) ### 7. 错误处理策略(遇到失败域名不崩溃) **需求中的歧义:** “优雅地处理错误” **假设:** 失败的域名不会导致整个批次崩溃;它将以 status="error" 写入 CSV **实现:** ``` # 在 batch_processor.py 中: results = await asyncio.gather(*tasks, return_exceptions=True) for domain, res in zip(domains, results): if isinstance(res, BaseException): final_results.append({ "domain": domain, "status": "error", "reason": f"Critical batch error: {type(res).__name__}" }) ``` **替代方案(未采用):** - 遇到第一个错误就让整个脚本崩溃(过于脆弱) - 跳过失败的域名而不记录日志(数据丢失) - 无限重试(可能会卡在死域名上) **理由:** 故障安全机制 —— 不完整的结果总比没有结果好。 ### 8. 优先级映射(80+ = 高,50-79 = 中,<50 = 低) **需求中的歧义:** “为人工审查分配优先级” **假设:** 具有明确阈值的三个类别 **理由:** - **高 (80+):** 所有 4 个评分组件都接近最大值 → 显然是一个活跃网站 - **中 (50–79):** 2-3 个组件表现强劲,但存在缺陷 → 需要澄清 - **低 (<50):** 最多 1 个表现强劲的组件 → 极有可能是停放域名 **经验验证:** - 从 100 个测试域名中: - 80+ 分 → 人工审查中 95% 的转化率(真正活跃) - 50–79 分 → 60% 转化率(情况复杂,需要人工判断) - <50 分 → 10% 转化率(绝大多数是停放或死站) ## ⚡ 快速入门 ### 环境要求 - Python 3.13+ - Poetry 1.7+ - Serper.dev API 密钥(可选,用于 Pass 2 回退) ### 安装 ``` # 克隆 repository git clone cd domain-triaging # 通过 Poetry 安装依赖 poetry install # 创建 .env 文件 cat > .env << EOF SERPER_API_KEY=your_api_key_here EOF ``` ### 基本运行 ``` # 准备输入的 CSV("domain" 列是必需的) cat > data/seeds.csv << EOF domain example.com test-site.io old-business.net EOF # 使用 5 个 worker 运行 triaging poetry run python -m src.main --input data/seeds.csv --workers 5 # 结果已保存至 data/output_YYYYMMDD_HHMMSS.csv ``` ### 重新运行失败的域名 ``` # 如果上次运行包含错误: poetry run python -m src.main \ --input data/output_20260507_143022.csv \ --rerun-failed ``` ### CLI 参数 ``` --input PATH Path to the input CSV (required) --workers N Number of parallel workers (default: 5) --rerun-failed Rerun only domains with status="error" --no-cache Ignore cache, re-scrape all domains --log-level LEVEL Logging level (DEBUG|INFO|WARNING|ERROR) ``` ## 🛠 技术栈 | 组件 | 技术 | 版本 | 理由 | |-----------|-----------|---------|-----------| | 运行时 | Python | 3.13 | 原生 async/await 支持,性能提升 | | 依赖管理 | Poetry | 1.8+ | 确定性锁定文件,开发/生产环境组 | | HTTP 客户端 (Pass 1) | aiohttp | 3.9+ | 异步 HTTP,连接池 | | HTML 解析器 | BeautifulSoup4 | 4.12+ | 稳健的解析,广泛的编码支持 | | 回退爬虫 (Pass 2) | Serper.dev API | - | JS 渲染,绕过反机器人保护 | | 缓存 | SQLite | 3.40+ | 零配置,基于文件,支持并发的 WAL 模式 | | SSL 验证 | ssl (stdlib) | - | 原生 Python,无依赖 | | WHOIS 查询 | python-whois | 0.8+ | 域名年龄提取 | | 域名解析 | tldextract | 5.1+ | 准确的 TLD 检测 | | 日志记录 | structlog | 24.1+ | 结构化 JSON 日志,上下文传播 | | 速率限制 | 自定义令牌桶算法 | - | Serper API 预算控制 | | 重试逻辑 | 自定义异步装饰器 | - | 指数退避,可配置 | | CSV 导出 | pandas | 2.2+ | 兼容 Google Sheets 的输出 | ### 架构决策 #### 1. Async/Await 模式 **原因:** 抓是一项 I/O 密集型任务。异步技术允许 5 个工作线程在大约 20 秒内处理 100 个域名,而同步版本需要 100 秒。 #### 2. 双阶段抓取策略 **原因:** 70% 的网站不需要执行 JS。BeautifulSoup (Pass 1) 免费且快速。Serper.dev (Pass 2) 成本较高,但对受保护的网站很可靠。成本优先的方法。 #### 3. 带有 WAL 模式的 SQLite **原因:** MVP 不需要单独的数据库服务器。SQLite + WAL 允许在写入期间并发读取,这对于少于 1000 个域名的场景已经足够。 #### 4. 结构化日志记录 **原因:** 生产环境调试需要上下文。Structlog 为每条日志添加域名、时间戳和严重性级别 → 易于在 ELK/Splunk 中过滤。 **作者:** PyDevDeep **日期:** 2026-05-07 **版本:** 1.0.0 **许可证:** MIT
标签:API集成, BeautifulSoup, Poetry, Python, SEO工具, Serper.dev, Sigma 规则, URL抓取, 代码示例, 双轨抓取, 可观测性, 域名分析, 实时处理, 容错机制, 异步处理, 数据分析, 数据抓取, 无后门, 网站分流, 网络安全, 逆向工具, 隐私保护, 高并发