Observability
系统化梳理日志、指标与追踪的协同机制及成本控制策略。
下载 29
根据URL自动生成Python爬虫脚本,支持动态页面与反爬绕过。
openclaw skills install @moxin1044/web-crawler命令、参数、文件名以原文为准
在编写任何代码前,先对目标站点进行分析:
Fetch/XHR 进行过滤api.、/api/、/graphql、.json、ajax、json| 类型 | 特征 | 应对策略 |
|---|---|---|
| 静态 HTML | 服务端渲染,源码中包含完整内容 | 使用 requests + BeautifulSoup |
| SPA(Vue/React) | <div id="app"> 为空,存在 JS 打包文件 | 寻找 API 接口或使用无头浏览器 |
| SSR(Next.js/Nuxt) | HTML 完整但经过客户端激活 | requests 通常可直接使用 |
| API 驱动 | XHR 返回 JSON | 直接调用 API 接口(最佳情况) |
| WebSocket | 存在 ws:// 连接,实时数据传输 | 使用 websockets / websocket-client |
| 移动端专用 | 不同 User-Agent 返回不同内容 | 模拟移动端 UA 或使用 App API |
cf-ray 请求头、挑战页面)_abck Cookie)canvas、webgl、navigator 属性)是否存在清晰的 JSON API?
├── 是 → requests / httpx(最快最简单)
└── 否 → 内容是否由服务器渲染?
├── 是 → requests + BeautifulSoup/lxml
└── 否 → 是否需要 JavaScript 渲染?
├── 轻量级 JS → requests-html(基于 pyppeteer)
├── 重度 JS / SPA → playwright(推荐)或 selenium
└── 需要绕过检测 → DrissionPage / undetected-chromedriver| 库 | 速度 | JS 支持 | 反检测能力 | 使用场景 |
|---|---|---|---|---|
requests | ⚡⚡⚡ | ❌ | ❌ | 静态页面、API |
httpx | ⚡⚡⚡ | ❌ | ❌ | 异步 API 爬取 |
requests-html | ⚡⚡ | ✅ | ❌ | 轻量级 JS 渲染 |
playwright | ⚡ | ✅✅ | ⚠️ | 现代 SPA、截图 |
selenium | ⚡ | ✅✅ | ⚠️ | 旧系统、广泛支持 |
DrissionPage | ⚡⚡ | ✅✅ | ✅✅ | 反机器人、中文站点 |
undetected-chromedriver | ⚡ | ✅✅ | ✅✅ | 绕过 Cloudflare |
scrapy | ⚡⚡⚡ | ❌ | ❌ | 大规模、流水线式爬取 |
pyppeteer | ⚡ | ✅✅ | ⚠️ | Puppeteer 的 Python 版本 |
import requests
import pandas as pd
import time
import random
# 配置
BASE_URL = "https://api.example.com/data"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json",
"Referer": "https://example.com/",
}
COOKIES = {"session": "xxx"}
PROXY = {"http": "http://proxy:8080", "https": "http://proxy:8080"}
def fetch_page(page=1):
params = {"page": page, "size": 20}
for attempt in range(3):
try:
resp = requests.get(BASE_URL, headers=HEADERS, params=params,
cookies=COOKIES, proxies=PROXY, timeout=15)
resp.raise_for_status()
return resp.json()
except requests.RequestException as e:
print(f"第 {attempt+1} 次尝试失败: {e}")
time.sleep(2 ** attempt)
return None
def crawl_all(max_pages=100):
all_data = []
for page in range(1, max_pages + 1):
data = fetch_page(page)
if not data or not data.get("items"):
break
all_data.extend(data["items"])
print(f"第 {page} 页: {len(data['items'])} 条数据")
time.sleep(random.uniform(1, 3)) # 友好延迟
return all_data
if __name__ == "__main__":
results = crawl_all()
pd.DataFrame(results).to_csv("output.csv", index=False, encoding="utf-8-sig")
print(f"已保存 {len(results)} 条记录至 output.csv")from playwright.sync_api import sync_playwright
import pandas as pd
import time
def crawl_spa():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
viewport={"width": 1920, "height": 1080},
locale="zh-CN",
)
page = context.new_page()
# 拦截 API 响应(优于解析 DOM)
api_data = []
def handle_response(response):
if "/api/list" in response.url and response.status == 200:
try:
api_data.append(response.json())
except:
pass
page.on("response", handle_response)
page.goto("https://example.com/list", wait_until="networkidle")
# 无限滚动处理
last_height = page.evaluate("document.body.scrollHeight")
while True:
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
page.wait_for_timeout(2000)
new_height = page.evaluate("document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
browser.close()
return api_data
if __name__ == "__main__":
data = crawl_spa()
pd.DataFrame(data).to_excel("output.xlsx", index=False)from DrissionPage import ChromiumPage, ChromiumOptions
import time
def crawl_stealth():
co = ChromiumOptions()
co.set_argument("--no-sandbox")
co.set_argument("--disable-blink-features=AutomationControlled")
co.set_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
page = ChromiumPage(co)
page.get("https://example.com")
# DrissionPage 可自动绕过多数指纹检测
items = page.eles('css:.item-class')
results = []
for item in items:
results.append({
"title": item.ele('css:.title').text,
"link": item.ele('css:a').attr('href'),
"price": item.ele('css:.price').text,
})
page.quit()
return results# spider.py
import scrapy
from scrapy.crawler import CrawlerProcess
class ExampleSpider(scrapy.Spider):
name = "example"
start_urls = ["https://example.com/page/1"]
def parse(self, response):
for item in response.css(".item"):
yield {
"title": item.css(".title::text").get(),
"url": item.css("a::attr(href)").get(),
}
next_page = response.css(".next a::attr(href)").get()
if next_page:
yield response.follow(next_page, self.parse)
# 运行
process = CrawlerProcess(settings={
"FEEDS": {"output.json": {"format": "json"}},
"USER_AGENT": "Mozilla/5.0",
"DOWNLOAD_DELAY": 2,
"AUTOTHROTTLE_ENABLED": True,
})
process.crawl(ExampleSpider)
process.start()当参数被加密或签名时:
- sign: md5(timestamp + secret + params)
- token: base64(aes_encrypt(data, key))
- 自定义 webpack 模块
- 重放:提取算法并在 Python 中重新实现
- ExecJS:通过 execjs 或 node 子进程直接运行混淆的 JS
- 钩子注入:覆盖函数以导出中间值
# 示例:从 Python 调用 JS 加密逻辑
import execjs
ctx = execjs.compile("""
function sign(params, timestamp, secret) {
// ... 从目标网站提取的代码
return CryptoJS.MD5(timestamp + secret + JSON.stringify(params)).toString();
}
""")
signature = ctx.call("sign", {"page": 1}, "1700000000", "secret_key")| 方法 | 实现方式 |
|---|---|
| Cookie | 使用 requests.Session(),持久化保存 Cookie |
| JWT | 保存 token,添加至 Authorization: Bearer 请求头 |
| OAuth | 按授权码流程执行 |
| 签名请求 | 重现签名算法 |
| 二维码登录 | 轮询扫描状态接口 |
| 短信/邮箱验证码 | 手动输入或使用 OCR 识别 |
import requests
session = requests.Session()
# 登录
login_resp = session.post("https://example.com/api/login",
json={"username": "user", "password": "pwd"})
# 会话自动维护 Cookie
data = session.get("https://example.com/api/protected").json()| 类型 | 检测方式 | 实现方式 |
|---|---|---|
| 页面编号 | ?page=1 | 循环递增页面参数 |
| 游标/偏移量 | ?cursor=abc 或 ?offset=20 | 使用返回的游标值 |
| 流水线分页 | POST 请求携带时间戳或 last_id | 使用最后一条数据的 ID |
| 无限滚动 | 滚动事件触发 XHR 请求 | 使用 Playwright 模拟滚动循环 |
| 下一页链接 | rel="next" 或 next_page 字段 | 跟随链接跳转 |
import random
import requests
from fake_useragent import UserAgent
ua = UserAgent()
PROXIES = [
"http://user:pass@proxy1:8080",
"http://user:pass@proxy2:8080",
]
def fetch(url):
headers = {"User-Agent": ua.random}
proxy = {"http": random.choice(PROXIES), "https": random.choice(PROXIES)}
return requests.get(url, headers=headers, proxies=proxy, timeout=10)| 类型 | 解决方案 |
|---|---|
| 图片验证码 | OCR(ddddocr、Tesseract) |
| 滑块验证码 | 跟踪轨迹,模拟人类移动行为 |
| reCAPTCHA v2 | 使用 2Captcha / AntiCaptcha API,或音频挑战 |
| reCAPTCHA v3 | 需要高信任分(老账号、良好行为) |
| hCaptcha | 使用 2Captcha,或训练机器学习模型 |
| Cloudflare Turnstile | 使用 undetected-chromedriver 或 FlareSolverr |
| GeeTest | 分析间隙距离,模拟带加速度的拖拽操作 |
| 格式 | 库 | 适用场景 |
|---|---|---|
| CSV | pandas / csv | 表格数据,兼容 Excel |
| XLSX | openpyxl / pandas | 多工作表、格式化需求 |
| JSON | json / orjson | 嵌套结构化数据 |
| SQLite | sqlite3 | 本地数据库,支持查询 |
| MySQL | pymysql / sqlalchemy | 生产环境数据库 |
| MongoDB | pymongo | 非结构化数据,灵活字段 |
| 图片 | requests + open() | 下载到本地文件夹 |
| 文件 | urllib / aiohttp | PDF、文档、媒体文件 |
import os
import requests
from pathlib import Path
def download_images(urls, folder="images"):
Path(folder).mkdir(exist_ok=True)
for i, url in enumerate(urls):
try:
resp = requests.get(url, timeout=10)
ext = url.split(".")[-1][:4] # 简单的扩展名判断
filename = f"{folder}/img_{i:04d}.{ext}"
with open(filename, "wb") as f:
f.write(resp.content)
except Exception as e:
print(f"下载失败 {url}: {e}")import sqlite3
conn = sqlite3.connect("data.db")
conn.execute("""CREATE TABLE IF NOT EXISTS items
(id INTEGER PRIMARY KEY, title TEXT, url TEXT, price REAL)""")
def save(item):
conn.execute("INSERT OR IGNORE INTO items (title, url, price) VALUES (?,?,?)",
(item["title"], item["url"], item.get("price")))
conn.commit()# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_URL = "redis://localhost:6379/0"import redis
import json
r = redis.Redis()
QUEUE = "crawl:urls"
def push_urls(urls):
for url in urls:
r.lpush(QUEUE, json.dumps({"url": url, "retry": 0}))
def pop_url():
return json.loads(r.brpop(QUEUE)[1])import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.json()
async def crawl(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
results = asyncio.run(crawl(url_list))fake-useragent 库)time.sleep(random.uniform(1, 5))Retry-After 响应头undetected-chromedriver 或 DrissionPage 进行指纹伪装webdriver 标志:navigator.webdriver = undefinedcanvas/webgl 指纹robots.txt** — 尊重禁止规则当用户请求生成爬虫时,请遵循以下对话流程:
若未提供信息,请向用户询问:
pip install 命令fetch()、parse()、save()、main()pip install 命令python crawler.py所有生成的脚本应遵循以下结构:
"""
爬虫:[站点名称]
描述:[爬取内容说明]
作者:由 Aura 生成
日期:[自动生成]
依赖项:pip install requests beautifulsoup4 pandas
"""
import os
import sys
import time
import random
import logging
from pathlib import Path
# ===== 配置 =====
TARGET_URL = "https://example.com"
OUTPUT_DIR = Path("./output")
OUTPUT_FORMAT = "csv" # csv / xlsx / json / sqlite
MAX_PAGES = 100
DELAY_RANGE = (1, 3) # 请求间随机延迟
TIMEOUT = 15
MAX_RETRIES = 3
HEADERS = {
"User-Agent": "Mozilla/5.0 ...",
"Accept": "text/html,application/xhtml+xml,...",
}
# ===== 日志配置 =====
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(OUTPUT_DIR / "crawler.log", encoding="utf-8"),
logging.StreamHandler(),
]
)
logger = logging.getLogger(__name__)
# ===== 核心函数 =====
def fetch(url, **kwargs):
"""获取单个 URL 内容,包含重试逻辑。"""
...
def parse(html):
"""解析 HTML/JSON 并提取目标数据。"""
...
def save(data):
"""将提取的数据保存至指定输出格式。"""
...
def main():
"""主入口函数。"""
OUTPUT_DIR.mkdir(exist_ok=True)
# ... 爬取逻辑
logger.info(f"爬取完成。共保存 {len(results)} 条数据至 {OUTPUT_DIR}")
if __name__ == "__main__":
main()| 需求 | CSS 选择器 | XPath |
|---|---|---|
| 类名 | .classname | //*[@class="classname"] |
| ID | #idname | //*[@id="idname"] |
| 属性 | [href] | //*[@href] |
| 文本包含 | :contains("text") | //div[contains(text(), "text")] |
| 第 N 个子元素 | :nth-child(n) | //div[n] |
| 直接子元素 | > .child | /div/a |
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Cache-Control": "max-age=0",
}# 基础依赖
pip install requests beautifulsoup4 lxml pandas openpyxl
# 动态页面支持
pip install playwright
playwright install chromium
# 反检测支持
pip install DrissionPage undetected-chromedriver fake-useragent
# 框架支持
pip install scrapy scrapy-redis
# 异步支持
pip install aiohttp httpx
# JavaScript 执行支持
pip install PyExecJS
# 使用 PyExecJS 需要安装 Node.js
# 图像处理
pip install Pillow
# OCR 支持(中文验证码识别)
pip install ddddocr# 带指数退避重试机制
import time
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
return resp
elif resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 60))
logger.warning(f"请求过于频繁,等待 {wait} 秒")
time.sleep(wait)
elif resp.status_code == 403:
logger.error("禁止访问 — 可能需要 Cookie 或代理")
break
else:
resp.raise_for_status()
except requests.RequestException as e:
wait = 2 ** attempt
logger.warning(f"第 {attempt+1} 次尝试失败: {e},将在 {wait} 秒后重试")
time.sleep(wait)
return None此技能确保生成的爬虫具备高稳定性、生产就绪能力、反检测意识,并适配现代网页架构(Vue/React/SPA/SSR/API)。
已收录 2 个 Skill