Troubleshooting Computer Issues
用于系统性诊断和解决软件配置、安装错误及运行异常等问题。
下载 123
提供合法合规的网页数据抓取全流程方法,涵盖法律合规、架构设计、反检测策略与数据管道。
openclaw skills install @1kalin/afrexai-web-scraping-engine命令、参数、文件名以原文为准
为您的抓取操作评分(每项2分):
| 信号 | 健康状态 | 不健康状态 |
|---|---|---|
| 合法合规性 | 检查过 robots.txt,已审阅服务条款 | 盲目抓取 |
| 架构设计 | 工具与网站复杂度匹配 | 对静态 HTML 使用 Puppeteer |
| 反检测机制 | IP 轮换、延迟策略、指纹多样性 | 单一 IP,无延迟 |
| 数据质量 | 具备验证与去重流程 | 原始数据 dump,无清洗 |
| 错误处理 | 具备重试逻辑和熔断机制 | 遇到首个 403 错误即崩溃 |
| 监控能力 | 成功率追踪,已设置告警 | 无可见性 |
| 存储方式 | 结构化存储、去重、版本控制 | 平面文件、存在重复 |
| 调度策略 | 频率适当,避开高峰时段 | 在业务高峰期频繁请求 |
得分:/16 → 12+:可投入生产 | 8-11:需优化 | <8:停止并重新设计
compliance_brief:
target_domain: ""
date_assessed: ""
robots_txt:
checked: false
target_paths_allowed: false
crawl_delay_specified: ""
ai_bot_rules: "" # 许多网站现在明确禁止 AI 爬虫访问
terms_of_service:
reviewed: false
scraping_mentioned: false
scraping_prohibited: false
api_available: false
api_sufficient: false
data_classification:
type: "" # public-factual | public-personal | behind-auth | copyrighted
contains_pii: false
pii_types: [] # name, email, phone, address, photo
gdpr_applies: false # 涉及欧盟居民数据
ccpa_applies: false # 涉及加州居民数据
legal_risk: "" # low | medium | high | do-not-scrape
decision: "" # proceed | use-api | request-permission | abandon
justification: ""| 场景 | 风险等级 | 关键判例 |
|---|---|---|
| 公开数据,无需登录,robots.txt 允许 | 低 | hiQ v. LinkedIn (2022) |
| 公开数据,robots.txt 禁止 | 中 | Meta v. Bright Data (2024) |
| 需要认证才能访问的数据 | 高 | Van Buren v. US (2021),CFAA |
| 未经同意收集个人数据 | 高 | GDPR 第6条,CCPA §1798.100 |
| 复制发布受版权保护的内容 | 高 | 版权法 §106 |
| 价格/产品对比 | 低 | eBay v. Bidder's Edge(合理使用) |
| 学术或研究用途 | 低-中 | 各司法管辖区差异较大 |
| 绕过反机器人措施 | 高 | CFAA “超出授权访问” |
许多网站现已专门屏蔽 AI 相关爬虫:
# robots.txt 中常见的 AI 爬虫屏蔽规则
User-agent: GPTBot
User-agent: ChatGPT-User
User-agent: Google-Extended
User-agent: CCBot
User-agent: anthropic-ai
User-agent: ClaudeBot
User-agent: Bytespider
User-agent: PerplexityBot规则:若用于训练 AI 模型,请特别检查上述用户代理是否被屏蔽。
| 工具/方法 | 适用场景 | 速度 | JS 支持 | 复杂度 | 成本 |
|---|---|---|---|---|---|
| HTTP 客户端(requests/axios) | 静态 HTML、API | ⚡⚡⚡ | ❌ | 低 | 免费 |
| Beautiful Soup / Cheerio | 静态 HTML 解析 | ⚡⚡⚡ | ❌ | 低 | 免费 |
| Scrapy | 大规模结构化抓取 | ⚡⚡⚡ | 插件支持 | 中等 | 免费 |
| Playwright / Puppeteer | JS 渲染页面、SPA、交互操作 | ⚡ | ✅ | 中等 | 免费 |
| Selenium | 旧版浏览器自动化 | ⚡ | ✅ | 高 | 免费 |
| Crawlee | 混合模式(HTTP + 浏览器降级) | ⚡⚡ | ✅ | 中等 | 免费 |
| Firecrawl / ScrapingBee | 管理式服务,反反爬绕过 | ⚡⚡ | ✅ | 低 | 付费 |
| Bright Data / Oxylabs | 企业级,代理 + 浏览器支持 | ⚡⚡ | ✅ | 低 | 付费 |
内容是否在初始 HTML 源码中?
├── 是 → 网站结构是否一致?
│ ├── 是 → 使用静态抓取工具(requests + BeautifulSoup/Cheerio)
│ └── 否 → 使用 Scrapy 配合自定义解析器
└── 否 → 页面是否需要用户交互?
├── 是 → 使用 Playwright/Puppeteer 编写交互脚本
└── 否 → 使用 Playwright 非交互模式
└── 规模较大(>1万页)? → 使用 Crawlee(混合模式)
└── 反反爬强度高? → 使用管理式服务(Firecrawl/ScrapingBee)scraping_project:
name: ""
objective: "" # 需要什么数据,为何目的,更新频率
targets:
- domain: ""
pages_estimated: 0
rendering: "static" | "javascript" | "spa"
anti_bot: "none" | "basic" | "cloudflare" | "advanced"
rate_limit: "" # 安全的请求速率(每秒请求数)
tool_selected: ""
justification: ""
data_schema:
fields: []
output_format: "" # json | csv | database
schedule:
frequency: "" # once | hourly | daily | weekly
preferred_time: "" # 目标时区的非高峰时段
infrastructure:
proxy_needed: false
proxy_type: "" # residential | datacenter | mobile
storage: ""
monitoring: ""# Python example — production request pattern
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
# 重试策略
retry = Retry(
total=3,
backoff_factor=1, # 1秒, 2秒, 4秒
status_forcelist=[429, 500, 502, 503, 504],
respect_retry_after_header=True
)
session.mount("https://", HTTPAdapter(max_retries=retry))
# 真实的请求头
session.headers.update({
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Cache-Control": "no-cache",
})为避免指纹识别,请轮换以下请求头:
| 请求头 | 轮换池大小 | 说明 |
|---|---|---|
| User-Agent | 20-50 个真实浏览器 UA | 匹配操作系统分布 |
| Accept-Language | 5-10 种语言组合 | 匹配代理地理位置 |
| Sec-Ch-Ua | 与 User-Agent 匹配 | Chrome/Edge/Brave |
| Referer | 每次请求变化 | 上一页或搜索引擎 |
| 站点类型 | 安全延迟 | 激进策略(高风险) |
|---|---|---|
| 小型企业网站 | 5-10 秒 | 2-3 秒 |
| 中等规模网站 | 2-5 秒 | 1-2 秒 |
| 大型平台(如亚马逊等) | 3-5 秒 | 1 秒 |
| API 接口 | 依据 API 文档 | 绝不超出 |
| robots.txt 中的 crawl-delay | 严格遵守 | 永远不低于 |
规则:
Crawl-delay 在 robots.txt 中的设置Retry-After 响应头 —— 它意味着必须等待[data-product-id], [data-price](最稳定)#product-title, #price(稳定但可能变更)[aria-label="Price"](无障碍设计,较稳定)article, main, nav(结构稳定).product-card(可能随改版而变化)//div[3]/span[2](脆弱 —— 仅作最后手段)优先检查结构化数据 —— 在编写 CSS 选择器前先检查:
# 1. 检查 JSON-LD(最佳来源 —— 结构清晰、干净)
import json
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
for script in soup.find_all('script', type='application/ld+json'):
data = json.loads(script.string)
# 通常包含:Product, Article, Organization 等信息
# 2. 检查 Open Graph 元标签
og_title = soup.find('meta', property='og:title')
og_price = soup.find('meta', property='product:price:amount')
# 3. 检查 microdata
items = soup.find_all(itemtype=True)
# 4. 若以上为空,再回退到使用 CSS 选择器表格提取模式:
import pandas as pd
# 快速表格提取
tables = pd.read_html(html) # 返回 DataFrame 列表
# 处理复杂表格(含合并单元格)
def extract_table(soup, selector):
table = soup.select_one(selector)
headers = [th.get_text(strip=True) for th in table.select('thead th')]
rows = []
for tr in table.select('tbody tr'):
cells = [td.get_text(strip=True) for td in tr.select('td')]
rows.append(dict(zip(headers, cells)))
return rows分页处理:
# 模式 1:下一页按钮
while True:
# ... 抓取当前页面 ...
next_link = soup.select_one('a.next-page, [rel="next"], .pagination .next a')
if not next_link or not next_link.get('href'):
break
url = urljoin(base_url, next_link['href'])
# 模式 2:API 分页(无限滚动类网站)
page = 1
while True:
resp = session.get(f"{api_url}?page={page}&limit=50")
data = resp.json()
if not data.get('results'):
break
# ... 处理结果 ...
page += 1
# 模式 3:游标分页
cursor = None
while True:
params = {"limit": 50}
if cursor:
params["cursor"] = cursor
resp = session.get(api_url, params=params)
data = resp.json()
# ... 处理 ...
cursor = data.get('next_cursor')
if not cursor:
break# Playwright 模式处理 JS 渲染页面
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 ...",
)
page = context.new_page()
# 阻止不必要的资源加载(提升速度 + 隐蔽性)
page.route("**/*.{png,jpg,jpeg,gif,svg,woff,woff2}",
lambda route: route.abort())
page.goto(url, wait_until="networkidle")
# 等待特定内容出现(优于固定延时)
page.wait_for_selector('[data-product-id]', timeout=10000)
# JS 渲染完成后提取内容
content = page.content()
# ... 使用 BeautifulSoup/Cheerio 解析 ...
browser.close()| 信号 | 检测方式 | 应对措施 |
|---|---|---|
| IP信誉 | IP黑名单、数据中心IP段 | 使用住宅代理 |
| 请求频率 | 同一IP每分钟请求次数 | 限速 + 随机延迟(jitter) |
| TLS指纹 | JA3/JA4哈希匹配 | 使用真实浏览器或 curl-impersonate |
| 浏览器指纹 | Canvas、WebGL、字体特征 | 使用 Playwright 配合 stealth 插件 |
| JavaScript挑战 | Cloudflare Turnstile、hCaptcha | 使用托管浏览器服务 |
| Cookie/会话行为 | 缺失Cookie、无浏览历史 | 完整的会话管理 |
| 导航模式 | 直接访问URL、无来源信息 | 模拟自然浏览行为 |
| 鼠标/键盘事件 | 无交互数据 | 事件模拟(Playwright) |
| 头部一致性 | 请求头与User-Agent不匹配 | 使用匹配的头部组合 |
proxy_strategy:
# 第一级:免费/数据中心代理(适用于无反爬保护的网站)
basic:
type: "datacenter"
cost: "$1-5/GB"
success_rate: "60-80%"
use_for: "API接口、小型网站、无反爬场景"
# 第二级:住宅代理(适用于大多数受保护网站)
standard:
type: "residential"
cost: "$5-15/GB"
success_rate: "90-95%"
use_for: "Cloudflare、主流平台"
rotation: "每请求轮换或粘性会话(10分钟)"
# 第三级:移动/ISP代理(实现最高隐蔽性)
premium:
type: "mobile"
cost: "$15-30/GB"
success_rate: "95-99%"
use_for: "高强度反爬场景、社交媒体"
rules:
- 优先使用成本最低的层级,仅在被封时升级
- 代理地理位置应匹配目标受众地区
- 在出现 403 或 429 错误时才进行轮换,而非每次请求都轮换
- 多页面抓取时使用粘性会话
- 监控代理健康状态——移除慢速或被封的IP# Playwright 必备隐蔽设置
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
args=[
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
]
)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
locale="en-US",
timezone_id="America/New_York",
geolocation={"latitude": 40.7128, "longitude": -74.0060},
permissions=["geolocation"],
)
# 移除自动化标识
page = context.new_page()
page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]});
""")检测到 Cloudflare?
├── 仅存在 JavaScript 挑战 → 使用带 stealth 的 Playwright + 住宅代理
├── 出现 Turnstile 验证码 → 使用托管服务(ScrapingBee/Bright Data)
├── 处于攻击模式(Under Attack Mode) → 等待后重试,或使用托管服务
└── WAF 阻断 → 需采用其他方案
├── 检查是否存在 API 接口(网络标签页)
├── 检查是否有移动端 API
└── 考虑数据是否可通过其他途径获取# 验证逻辑 —— 存储前先验证
from dataclasses import dataclass, field
from typing import Optional
import re
from datetime import datetime
@dataclass
class ScrapedProduct:
url: str
title: str
price: Optional[float]
currency: str = "USD"
scraped_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
def validate(self) -> list[str]:
errors = []
if not self.url.startswith('http'):
errors.append("无效的URL")
if not self.title or len(self.title) < 3:
errors.append("标题过短或缺失")
if self.price is not None and self.price < 0:
errors.append("价格为负数")
if self.price is not None and self.price > 1_000_000:
errors.append("价格异常高 —— 请核实")
if self.currency not in ("USD", "EUR", "GBP", "BTC"):
errors.append(f"未知币种:{self.currency}")
return errors| 方法 | 使用场景 | 实现方式 |
|---|---|---|
| URL去重 | 页面有唯一URL | 对规范URL进行哈希 |
| 内容哈希 | 同一URL但内容变化 | 对关键字段使用 MD5/SHA256 哈希 |
| 模糊匹配 | 近似重复检测 | Jaccard相似度 > 0.85 |
| 复合键 | 多字段唯一性判断 | 哈希 (domain + product_id + variant) |
import hashlib
def dedup_key(item: dict, fields: list[str]) -> str:
"""从指定字段生成去重键。"""
values = "|".join(str(item.get(f, "")) for f in fields)
return hashlib.sha256(values.encode()).hexdigest()
# 使用示例
seen = set()
for item in scraped_items:
key = dedup_key(item, ["url", "product_id"])
if key not in seen:
seen.add(key)
clean_items.append(item)原始HTML → 解析 → 提取 → 验证 → 清洗 → 去重 → 存储
↓
隔离区(验证失败项)常见清洗操作:
| 问题 | 解决方案 |
|---|---|
HTML实体(如 &) | 使用 html.unescape() |
| 多余空白字符 | 使用 " ".join(text.split()) |
| Unicode编码问题 | 使用 unicodedata.normalize('NFKD', text) |
| 文本中的价格(如 "$49.99") | 正则表达式:r'[\$£€]?([\d,]+\.?\d*)' |
| 日期格式不统一 | 使用 dateutil.parser.parse() 并设置 dayfirst 参数 |
| 相对链接 | 使用 urllib.parse.urljoin(base, relative) |
| 编码问题 | 先用 chardet.detect() 判断编码,再解码 |
| 体积 | 频率 | 查询需求 | 推荐方案 |
|---|---|---|---|
| 小于 10K 条记录 | 一次性 | 无 | JSON/CSV 文件 |
| 小于 10K 条记录 | 定期执行 | 简单查询 | SQLite |
| 10K - 1M 条记录 | 定期执行 | 复杂查询 | PostgreSQL |
| 100 万条以上记录 | 持续运行 | 分析需求 | PostgreSQL + 分区 |
| 只追加的日志 | 持续运行 | 时序数据 | ClickHouse / TimescaleDB |
import sqlite3
import json
from datetime import datetime
def init_db(path="scraper_data.db"):
conn = sqlite3.connect(path)
conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY,
url TEXT UNIQUE,
data JSON NOT NULL,
scraped_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
checksum TEXT
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_url ON items(url)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_scraped ON items(scraped_at)")
return conn
def upsert(conn, url, data, checksum):
conn.execute("""
INSERT INTO items (url, data, checksum) VALUES (?, ?, ?)
ON CONFLICT(url) DO UPDATE SET
data = excluded.data,
updated_at = datetime('now'),
checksum = excluded.checksum
WHERE items.checksum != excluded.checksum
""", (url, json.dumps(data), checksum))
conn.commit()# CSV 导出
import csv
def to_csv(items, path, fields):
with open(path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(items)
# JSON Lines(适合大数据集——支持流式处理)
def to_jsonl(items, path):
with open(path, 'w') as f:
for item in items:
f.write(json.dumps(item) + '\n')
# 增量导出(仅导出自上次导出以来新增或更新的数据)
def export_since(conn, last_export_time):
cursor = conn.execute(
"SELECT data FROM items WHERE scraped_at > ? OR updated_at > ?",
(last_export_time, last_export_time)
)
return [json.loads(row[0]) for row in cursor]| HTTP 状态码 | 含义 | 处理方式 |
|---|---|---|
| 200 | 成功 | 正常处理 |
| 301/302 | 重定向 | 跟随跳转(最多 5 层) |
| 403 | 禁止访问/被屏蔽 | 切换代理,降低请求频率 |
| 404 | 找不到页面 | 记录日志,跳过,标记为失效链接 |
| 429 | 请求过于频繁 | 遵守 Retry-After,退避策略加倍 |
| 500-504 | 服务器错误 | 最多重试 3 次,使用退避机制 |
| 连接超时 | 网络问题 | 使用不同代理重试 |
| SSL 错误 | 证书问题 | 记录日志,排查原因,跳过 |
class CircuitBreaker:
def __init__(self, failure_threshold=5, reset_timeout=300):
self.failures = 0
self.threshold = failure_threshold
self.reset_timeout = reset_timeout
self.last_failure = 0
self.state = "closed" # closed | open | half-open
def record_failure(self):
self.failures += 1
self.last_failure = time.time()
if self.failures >= self.threshold:
self.state = "open"
# 提醒: "电路已打开 —— 失败次数过多"
def record_success(self):
self.failures = 0
self.state = "closed"
def can_proceed(self):
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure > self.reset_timeout:
self.state = "half-open"
return True # 允许尝试一次请求
return False
return True # half-open:允许尝试import json
from pathlib import Path
class Checkpointer:
def __init__(self, path="checkpoint.json"):
self.path = Path(path)
self.state = self._load()
def _load(self):
if self.path.exists():
return json.loads(self.path.read_text())
return {"completed_urls": [], "last_page": 0, "cursor": None}
def save(self):
self.path.write_text(json.dumps(self.state))
def is_done(self, url):
return url in self.state["completed_urls"]
def mark_done(self, url):
self.state["completed_urls"].append(url)
if len(self.state["completed_urls"]) % 50 == 0:
self.save() # 定期保存dashboard:
real_time:
- metric: "requests_per_minute"
alert_if: "> 60 for small sites"
- metric: "success_rate"
alert_if: "< 90%"
- metric: "avg_response_time_ms"
alert_if: "> 5000"
- metric: "blocked_rate"
alert_if: "> 10%"
per_run:
- metric: "pages_scraped"
- metric: "items_extracted"
- metric: "items_validated"
- metric: "items_deduplicated"
- metric: "new_items"
- metric: "updated_items"
- metric: "errors_by_type"
- metric: "run_duration"
- metric: "proxy_cost"
weekly:
- metric: "data_freshness"
description: "% of records updated in last 7 days"
- metric: "site_structure_changes"
description: "选择器不再匹配的页面"
- metric: "total_cost"
description: "代理费用 + 计算资源 + 存储成本"网站改版。选择器失效。尽早发现:
def health_check(results: list[dict], expected_fields: list[str]) -> dict:
"""检查爬虫是否仍在正确提取数据。"""
total = len(results)
if total == 0:
return {"status": "CRITICAL", "message": "无结果 — 可能已失效"}
field_coverage = {}
for field in expected_fields:
filled = sum(1 for r in results if r.get(field))
coverage = filled / total
field_coverage[field] = coverage
issues = []
for field, coverage in field_coverage.items():
if coverage < 0.5:
issues.append(f"{field}: {coverage:.0%} 填充率(期望 >50%)")
if issues:
return {"status": "WARNING", "issues": issues}
return {"status": "OK", "field_coverage": field_coverage}每日:
每周:
每月:
use_case: "每日跟踪竞争对手价格"
tool: "requests + BeautifulSoup"
schedule: "每日 03:00 UTC(非高峰时段)"
targets: ["competitor-a.com/products", "competitor-b.com/api"]
data:
- product_id
- product_name
- price
- currency
- in_stock
- scraped_at
storage: "SQLite 存储价格历史"
alerts: "价格变动超过 10% → 发送通知"use_case: "从多个招聘平台聚合职位列表"
tool: "Scrapy,每个站点独立编写爬虫"
schedule: "每 6 小时一次"
targets: ["board-a.com", "board-b.com", "board-c.com"]
data:
- title
- company
- location
- salary_range
- posted_date
- url
- source
dedup: "哈希(title + company + location)"
storage: "PostgreSQL"use_case: "监控行业相关新闻提及"
tool: "requests + RSS 订阅源(优先)+ 网页回退"
schedule: "每 30 分钟一次"
approach:
1: "RSS/Atom 订阅源(最快、最干净)"
2: "通过 Google News RSS 获取特定主题"
3: "若无订阅源,则进行直接网页抓取"
data:
- headline
- source
- url
- published_at
- snippet
- sentiment
alerts: "关键词匹配 → 立即通知"use_case: "追踪品牌提及与情感倾向"
tool: "官方 API(始终优先)+ 网络搜索回退"
rules:
- 绝不直接爬取社交平台 —— 必须使用官方 API
- Twitter/X:使用官方 API(基础版约 $100/月)
- Reddit:使用官方 API(提供免费额度)
- LinkedIn:禁止爬取(存在强烈法律追责风险)
- Instagram:仅允许通过官方 API(Meta Business 账号)
fallback: "使用 Brave 或 Google 搜索公开提及内容"use_case: "跟踪房产房源及价格变化"
tool: "Playwright(多数房源网站为 JS 驱动)"
schedule: "每日一次"
challenges:
- 大量 JavaScript 渲染
- 反爬机制(Cloudflare 常见)
- 页面结构频繁变更
- 地图驱动的结果展示
approach: "优先通过浏览器开发者工具中的 Network 标签发现 API 接口"单机部署(小规模):
├── asyncio + aiohttp(Python)→ 支持 50-200 并发请求
├── 工作线程池(ThreadPoolExecutor)→ 10-50 个线程
└── Scrapy reactor → 内置并发支持
多机部署(大规模):
├── URL 队列:Redis / RabbitMQ / SQS
├── 工作节点:多个 Scrapy 或自定义工作进程
├── 结果存储:共享 PostgreSQL / S3
└── 协调器:Celery / 自定义调度系统| 优化手段 | 影响程度 | 实现方式 |
|---|---|---|
| 静态页面 > 浏览器渲染 | 10-50 倍节省 | 优先尝试 HTTP 请求 |
| 阻止图片/CSS/字体/媒体资源 | 60-80% 带宽节省 | 请求路由过滤 |
| 缓存 DNS 查询 | 微小但累积效果 | 使用本地 DNS 缓存 |
| 响应压缩 | 50-70% 带宽节省 | 启用 Accept-Encoding: gzip, br |
| 智能调度 | 避免重复抓取 | 变化检测后再执行完整抓取 |
| 代理层级匹配 | 成本差异 3-10 倍 | 不对简单站点使用住宅代理 |
在构建爬虫前,先检查网站是否存在隐藏的 API 接口:
常见的隐藏 API 模式:
/api/v1/products?page=1&limit=20/graphql 附带查询参数/_next/data/...(Next.js 数据路由)/wp-json/wp/v2/posts(WordPress)# 最小化浏览器资源占用
context = browser.new_context(
viewport={"width": 1280, "height": 720},
java_script_enabled=True, # 仅在必要时启用
has_touch=False,
is_mobile=False,
)
# 阻止不需要的资源类型
page.route("**/*", lambda route: (
route.abort() if route.request.resource_type in
["image", "stylesheet", "font", "media"]
else route.continue_()
))# 当有权限在登录后爬取数据时
# 始终使用会话认证,绝不将密码硬编码在代码中
# 模式:登录一次,复用会话
session = requests.Session()
login_resp = session.post("https://example.com/login", data={
"username": os.environ["SCRAPE_USER"],
"password": os.environ["SCRAPE_PASS"],
})
assert login_resp.ok, "登录失败"
# 会话 Cookie 已自动保存 —— 后续请求可直接使用
data_resp = session.get("https://example.com/api/data")markdown
import hashlib
def has_changed(url, session, last_etag=None, last_modified=None):
"""检查页面是否已更改,无需下载完整内容。"""
headers = {}
if last_etag:
headers["If-None-Match"] = last_etag
if last_modified:
headers["If-Modified-Since"] = last_modified
resp = session.head(url, headers=headers)
if resp.status_code == 304:
return False, resp.headers.get("ETag"), resp.headers.get("Last-Modified")
return True, resp.headers.get("ETag"), resp.headers.get("Last-Modified")| 维度 | 权重 | 评估要点 |
|---|---|---|
| 合法合规性 | 20% | robots.txt、使用条款、个人身份信息处理、审计日志 |
| 数据质量 | 20% | 验证机制、准确性、完整性、数据新鲜度 |
| 系统韧性 | 15% | 错误处理、重试机制、熔断保护、断点续传 |
| 反检测能力 | 15% | 代理轮换、指纹多样性、速率限制控制 |
| 架构设计 | 10% | 工具选型合理、代码清晰、模块化程度 |
| 监控能力 | 10% | 成功率、异常检测、告警机制 |
| 性能表现 | 5% | 执行速度、成本效率、资源占用 |
| 文档完善度 | 5% | 操作手册、数据结构文档、法律合规评估 |
评分等级: 90+ 优秀 | 75-89 良好 | 60-74 需改进 | <60 需重构
| # | 错误 | 修正建议 |
|---|---|---|
| 1 | 未检查 robots.txt | 始终优先检查 —— 这是你的合法依据 |
| 2 | 固定延迟(无随机抖动) | 所有延迟增加 ±30% 的随机抖动 |
| 3 | 无数据验证 | 存储前对每个字段进行验证 |
| 4 | 使用浏览器抓取静态 HTML | HTTP 客户端速度更快、成本更低,快 10-50 倍 |
| 5 | 单一 IP 且无轮换 | 任何正式抓取任务都需代理轮换 |
| 6 | 无异常检测机制 | 监控提取数量和字段填充率 |
| 7 | 仅存储原始 HTML | 提取后立即结构化处理 |
| 8 | 无断点续传功能 | 长时间抓取必须支持中断恢复 |
| 9 | 忽略结构化数据 | 优先使用 JSON-LD/microdata,比 CSS 选择器更稳定 |
| 10 | API 存在却仍用网页抓取 | 始终先检查是否存在 API 接口 |
page 或 offset 参数。data 属性、ARIA 标签或文本内容选择器。例如 [data-testid="price"] 能抵御页面重构;而 .sc-bdVTJa 则无法保证稳定性。html[lang] 属性识别语言。设置 Accept-Language 头部以获取目标语言版本。注意不同语言可能对应不同 URL 结构(如 /en/、/de/、子域名等)。已收录 16 个 Skill