Install
openclaw skills install @moxin1044/web-crawlerUse this skill whenever the user wants to create Python web crawlers/scrapers based on a given URL. Supports Vue, React, Next.js, Nuxt, Angular, Svelte and other frontend architectures by analyzing API calls, handling dynamic content with headless browsers. Main language is Python. Features: - Static & dynamic crawling (requests, httpx, playwright, selenium, drissionpage) - JS reverse engineering (webpack unpacking, AST analysis, hook injection) - Anti-bot bypass (Cloudflare, Akamai, reCAPTCHA, fingerprint spoofing) - Login & session management (cookie, JWT, OAuth, signature replay) - Encryption & signing (AES/RSA/MD5/SHA, custom signing algorithms) - Pagination strategies (page number, cursor, waterfall, infinite scroll) - Data export (CSV, XLSX, JSON, SQLite, MySQL, MongoDB, images, files) - Distributed crawling (Redis queue, Scrapy-Redis, task scheduling) - Anti-detection (UA rotation, proxy pool, fingerprint randomization, behavior simulation) - Compliance (robots.txt, rate limiting, terms of service, legal boundaries) Triggers: "写爬虫", "爬取", "抓取数据", "scraper", "crawler", "scrape", "spider", "Vue 爬虫", "React 爬虫", "动态网页", "反爬", "加密参数", "签名"
openclaw skills install @moxin1044/web-crawlerBefore writing any code, analyze the target:
Fetch/XHRapi., /api/, /graphql, .json, ajax, json patterns| Type | Indicators | Strategy |
|---|---|---|
| Static HTML | Server-rendered, full content in source | requests + BeautifulSoup |
| SPA (Vue/React) | Empty <div id="app">, JS bundles | Find API or use headless browser |
| SSR (Next.js/Nuxt) | Full HTML but hydrated | requests often works directly |
| API-driven | XHR returns JSON | Direct API requests (best case) |
| WebSocket | ws:// connections, real-time data | websockets / websocket-client |
| Mobile-only | Different UA serves different content | Spoof mobile UA or use app API |
cf-ray header, challenge page)_abck cookie)canvas, webgl, navigator properties)Is there a clean JSON API?
├── Yes → requests / httpx (fastest, simplest)
└── No → Is content server-rendered?
├── Yes → requests + BeautifulSoup/lxml
└── No → Need JS rendering?
├── Light JS → requests-html (pyppeteer backend)
├── Heavy JS / SPA → playwright (recommended) or selenium
└── Need to bypass detection → DrissionPage / undetected-chromedriver
| Library | Speed | JS Support | Anti-Detection | Use Case |
|---|---|---|---|---|
requests | ⚡⚡⚡ | ❌ | ❌ | Static sites, APIs |
httpx | ⚡⚡⚡ | ❌ | ❌ | Async API crawling |
requests-html | ⚡⚡ | ✅ | ❌ | Light JS rendering |
playwright | ⚡ | ✅✅ | ⚠️ | Modern SPAs, screenshots |
selenium | ⚡ | ✅✅ | ⚠️ | Legacy, wide support |
DrissionPage | ⚡⚡ | ✅✅ | ✅✅ | Anti-bot, Chinese sites |
undetected-chromedriver | ⚡ | ✅✅ | ✅✅ | Cloudflare bypass |
scrapy | ⚡⚡⚡ | ❌ | ❌ | Large-scale, pipelines |
pyppeteer | ⚡ | ✅✅ | ⚠️ | Puppeteer port |
import requests
import pandas as pd
import time
import random
# Config
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 {attempt+1} failed: {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 {page}: {len(data['items'])} items")
time.sleep(random.uniform(1, 3)) # Polite delay
return all_data
if __name__ == "__main__":
results = crawl_all()
pd.DataFrame(results).to_csv("output.csv", index=False, encoding="utf-8-sig")
print(f"Saved {len(results)} records to 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()
# Intercept API responses (preferred over parsing 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")
# Infinite scroll
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 bypasses many fingerprint checks automatically
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)
# Run
process = CrawlerProcess(settings={
"FEEDS": {"output.json": {"format": "json"}},
"USER_AGENT": "Mozilla/5.0",
"DOWNLOAD_DELAY": 2,
"AUTOTHROTTLE_ENABLED": True,
})
process.crawl(ExampleSpider)
process.start()
When parameters are encrypted/signed:
sign: md5(timestamp + secret + params)token: base64(aes_encrypt(data, key))execjs or node subprocess# Example: calling JS encryption from Python
import execjs
ctx = execjs.compile("""
function sign(params, timestamp, secret) {
// ... extracted from target site
return CryptoJS.MD5(timestamp + secret + JSON.stringify(params)).toString();
}
""")
signature = ctx.call("sign", {"page": 1}, "1700000000", "secret_key")
| Method | Implementation |
|---|---|
| Cookie | requests.Session(), persist cookies |
| JWT | Store token, add to Authorization: Bearer header |
| OAuth | Follow authorization code flow |
| Signed requests | Reproduce signature algorithm |
| QR Login | Poll scan status API |
| SMS/Email OTP | Manual input or OCR |
import requests
session = requests.Session()
# Login
login_resp = session.post("https://example.com/api/login",
json={"username": "user", "password": "pwd"})
# Session maintains cookies automatically
data = session.get("https://example.com/api/protected").json()
| Type | Detection | Implementation |
|---|---|---|
| Page number | ?page=1 | Loop incrementing page |
| Cursor/Offset | ?cursor=abc or ?offset=20 | Use returned cursor |
| Waterfall | POST with timestamp/last_id | Use last item's id |
| Infinite scroll | Scroll event triggers XHR | Playwright scroll loop |
| Next link | rel="next" or next_page field | Follow links |
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)
| Type | Solution |
|---|---|
| Image captcha | OCR (ddddocr, Tesseract) |
| Slider captcha | Track trajectory, simulate human movement |
| reCAPTCHA v2 | 2Captcha / AntiCaptcha API, or audio challenge |
| reCAPTCHA v3 | Need high trust score (aged account, good behavior) |
| hCaptcha | 2Captcha, or ML model |
| Cloudflare Turnstile | Use undetected-chromedriver or FlareSolverr |
| GeeTest | Analyze gap distance, simulate drag with acceleration |
| Format | Library | Best For |
|---|---|---|
| CSV | pandas / csv | Tabular data, Excel compat |
| XLSX | openpyxl / pandas | Multi-sheet, formatting |
| JSON | json / orjson | Nested/structured data |
| SQLite | sqlite3 | Local DB, querying |
| MySQL | pymysql / sqlalchemy | Production DB |
| MongoDB | pymongo | Unstructured, flexible schema |
| Images | requests + open() | Download to folder |
| Files | urllib / aiohttp | PDFs, docs, media |
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] # crude extension detection
filename = f"{folder}/img_{i:04d}.{ext}"
with open(filename, "wb") as f:
f.write(resp.content)
except Exception as e:
print(f"Failed {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 library)time.sleep(random.uniform(1, 5))Retry-After headerundetected-chromedriver or DrissionPage for fingerprint spoofingwebdriver flag: navigator.webdriver = undefinedcanvas/webgl fingerprintsrobots.txt — respect disallow rulesWhen user requests a crawler, follow this dialogue:
Ask the user (only if not provided):
pip install commandsfetch(), parse(), save(), main()pip install commands for all dependenciespython crawler.pyEvery generated script should follow this structure:
"""
Crawler: [Site Name]
Description: [What it crawls]
Author: Generated by Aura
Date: [auto]
Dependencies: pip install requests beautifulsoup4 pandas
"""
import os
import sys
import time
import random
import logging
from pathlib import Path
# ===== Configuration =====
TARGET_URL = "https://example.com"
OUTPUT_DIR = Path("./output")
OUTPUT_FORMAT = "csv" # csv / xlsx / json / sqlite
MAX_PAGES = 100
DELAY_RANGE = (1, 3) # random delay between requests
TIMEOUT = 15
MAX_RETRIES = 3
HEADERS = {
"User-Agent": "Mozilla/5.0 ...",
"Accept": "text/html,application/xhtml+xml,...",
}
# ===== Logging =====
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__)
# ===== Core Functions =====
def fetch(url, **kwargs):
"""Fetch a single URL with retry logic."""
...
def parse(html):
"""Parse HTML/JSON and extract target data."""
...
def save(data):
"""Save extracted data to configured output."""
...
def main():
"""Main entry point."""
OUTPUT_DIR.mkdir(exist_ok=True)
# ... crawl logic
logger.info(f"Crawling complete. {len(results)} items saved to {OUTPUT_DIR}")
if __name__ == "__main__":
main()
| Need | CSS Selector | XPath |
|---|---|---|
| Class | .classname | //*[@class="classname"] |
| ID | #idname | //*[@id="idname"] |
| Attribute | [href] | //*[@href] |
| Text contains | :contains("text") | //div[contains(text(), "text")] |
| Nth child | :nth-child(n) | //div[n] |
| Direct child | > .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",
}
# Basic
pip install requests beautifulsoup4 lxml pandas openpyxl
# Dynamic
pip install playwright
playwright install chromium
# Anti-detection
pip install DrissionPage undetected-chromedriver fake-useragent
# Framework
pip install scrapy scrapy-redis
# Async
pip install aiohttp httpx
# JS execution
pip install PyExecJS
# Node.js required for PyExecJS
# Image processing
pip install Pillow
# OCR
pip install ddddocr # Chinese captcha OCR
# Retry with exponential backoff
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"Rate limited, waiting {wait}s")
time.sleep(wait)
elif resp.status_code == 403:
logger.error("Forbidden — may need cookies/proxy")
break
else:
resp.raise_for_status()
except requests.RequestException as e:
wait = 2 ** attempt
logger.warning(f"Attempt {attempt+1} failed: {e}, retry in {wait}s")
time.sleep(wait)
return None
This skill ensures every generated crawler is robust, production-ready, anti-detection-aware, and tailored to modern web architectures (Vue/React/SPA/SSR/API).