agentsclimarketplace

Web scraper

Skill aiskillstore/marketplace/skills/zl2023github/web-scraper

Security-audited skills for Claude, Codex & Claude Code. One-click install, quality verified.

Install
npx -y skills add aiskillstore/marketplace --skill web-scraper

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

What its author says it does

Copied from the file, not written here

爬虫/数据采集工程师 Agent — 覆盖网页数据采集、API抓取、动态内容渲染、反爬对抗、数据清洗存储、分布式爬虫架构、App抓包逆向等全领域数据采集工作。能动手搭建完整爬虫系统,不只是出方案。

SKILL.md

23.1 KB, as published. Nobody here has run it

爬虫/数据采集工程师 Agent

适用场景

当用户需要:

  • 从网站/API/App采集结构化或非结构化数据
  • 搭建可维护的爬虫系统(单机或分布式)
  • 解决反爬问题(IP封禁、验证码、参数加密、字体反爬等)
  • 抓取动态渲染页面(SPA/JS渲染/WebSocket)
  • 逆向App请求协议(Protobuf/加密参数)
  • 数据清洗、去重、存储到数据库/文件
  • 爬虫监控与运维

工作流程

阶段一:需求分析与可行性评估

  1. 明确目标:需要采集什么数据?字段列表?数据量级?更新频率?
  2. 来源分析
    • 网页 → 分析URL结构、分页方式、数据是否在HTML中
    • API → 抓包分析请求/响应格式、认证方式、参数加密
    • App → 确定抓包方案(mitmproxy/Charles)、协议类型(HTTP/WebSocket/gRPC)
  3. 可行性评估
    • 是否有robots.txt限制
    • 反爬强度(验证码、频率限制、WAF、指纹检测)
    • 法律合规性(数据使用边界、个人信息保护)
  4. 方案选择
    • 简单静态页面 → requests + BeautifulSoup/lxml
    • 动态渲染页面 → Playwright/Selenium/DrissionPage
    • 大规模采集 → Scrapy + 分布式方案
    • App数据 → mitmproxy + 逆向分析

阶段二:技术方案设计

根据目标选择技术栈:

场景推荐方案
简单静态页面(少量)requests + BeautifulSoup + lxml
简单静态页面(大量)Scrapy + parsel
动态渲染页面Playwright / DrissionPage
大规模分布式Scrapy-Redis + Celery + Kafka
App抓包mitmproxy + Frida + jadx
高性能异步aiohttp + BeautifulSoup + uvloop
反爬对抗curl_cffi + 代理池 + 浏览器指纹伪装

阶段二:环境搭建

# 基础环境
pip install requests beautifulsoup4 lxml parsel httpx

# 爬虫框架
pip install scrapy scrapy-redis scrapy-splash

# 浏览器自动化
pip install playwright selenium
playwright install chromium

# 异步
pip install aiohttp aiofiles

# 数据存储
pip install pymongo redis pymysql psycopg2-binary

# 反爬工具
pip install curl_cffi fake-useragent

# App抓包与逆向
pip install mitmproxy
# Frida: pip install frida-tools (需配合手机端frida-server)

阶段三:实施步骤

3.1 静态页面采集

import requests
from bs4 import BeautifulSoup
import json

headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...',
    'Accept': 'text/html,application/xhtml+xml,...',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}

# 基础请求
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding  # 自动检测编码

# 解析
soup = BeautifulSoup(resp.text, 'lxml')
# 或使用 parsel (Scrapy核心解析器)
from parsel import Selector
sel = Selector(text=resp.text)

3.2 动态页面采集 (Playwright)

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    context = browser.new_context(
        user_agent='Mozilla/5.0 ...',
        viewport={'width': 1920, 'height': 1080},
        locale='zh-CN'
    )
    page = context.new_page()
    page.goto(url, wait_until='networkidle')
    
    # 等待元素出现
    page.wait_for_selector('.content-item', timeout=10000)
    
    # 滚动加载
    for _ in range(5):
        page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
        page.wait_for_timeout(2000)
    
    # 提取数据
    items = page.evaluate('''() => {
        return Array.from(document.querySelectorAll('.item')).map(el => ({
            title: el.querySelector('.title')?.innerText,
            url: el.querySelector('a')?.href
        }))
    }''')
    
    browser.close()

3.3 使用 Scrapy 框架

# 创建项目
scrapy startproject myproject
cd myproject
scrapy genspider example example.com
# spiders/example.py
import scrapy
from scrapy.http import HtmlResponse

class ExampleSpider(scrapy.Spider):
    name = 'example'
    allowed_domains = ['example.com']
    start_urls = ['https://example.com/page/1']
    
    custom_settings = {
        'DOWNLOAD_DELAY': 1.5,
        'RANDOMIZE_DOWNLOAD_DELAY': True,
        'CONCURRENT_REQUESTS': 8,
        'DOWNLOADER_MIDDLEWARES': {
            'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
            'scrapy_user_agents.middlewares.RandomUserAgentMiddleware': 400,
        },
        'ITEM_PIPELINES': {
            'myproject.pipelines.DuplicatesPipeline': 100,
            'myproject.pipelines.DatabasePipeline': 300,
        }
    }
    
    def parse(self, response: HtmlResponse):
        # 提取数据
        for item in response.css('.item'):
            yield {
                'title': item.css('.title::text').get(),
                'url': item.css('a::attr(href)').get(),
                'price': item.css('.price::text').re_first(r'[\d.]+'),
            }
        
        # 翻页
        next_page = response.css('.next::attr(href)').get()
        if next_page:
            yield scrapy.Request(url=response.urljoin(next_page))

3.4 动态页面 (Playwright 完整示例)

from playwright.sync_api import sync_playwright
import json

def scrape_dynamic_page(url: str) -> list:
    """采集动态渲染页面,支持滚动加载和等待条件"""
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=['--disable-blink-features=AutomationControlled']
        )
        context = browser.new_context(
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
            viewport={'width': 1920, 'height': 1080},
            locale='zh-CN',
            # 注入反检测脚本
            extra_http_headers={'Accept-Language': 'zh-CN,zh;q=0.9'}
        )
        
        # 注入反自动化检测
        context.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
            Object.defineProperty(navigator, 'plugins', {get: () => [1,2,3,4,5]});
        """)
        
        page = context.new_page()
        page.goto(url, wait_until='networkidle')
        
        # 等待数据加载
        page.wait_for_selector('.data-item', timeout=15000)
        
        # 滚动加载
        for _ in range(3):
            page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
            page.wait_for_timeout(2000)
        
        # 提取数据
        data = page.evaluate('''() => {
            return Array.from(document.querySelectorAll('.item')).map(el => ({
                title: el.querySelector('.title')?.innerText?.trim(),
                price: el.querySelector('.price')?.innerText?.trim(),
                link: el.querySelector('a')?.href
            }))
        }''')
        
        browser.close()
        return data

3.3 API 抓取

import requests
import time
import hashlib
import json

def fetch_api_data(base_url: str, params: dict, api_key: str = None):
    """通用API数据采集,支持分页和认证"""
    headers = {
        'User-Agent': 'Mozilla/5.0 ...',
        'Accept': 'application/json',
    }
    if api_key:
        headers['Authorization'] = f'Bearer {api_key}'
    
    all_data = []
    page = 1
    while True:
        params['page'] = page
        resp = requests.get(base_url, params=params, headers=headers, timeout=15)
        resp.raise_for_status()
        data = resp.json()
        
        # 提取数据
        items = data.get('data', data.get('items', data.get('results', [])))
        if not items:
            break
        all_data.extend(items)
        
        # 分页判断
        total = data.get('total', data.get('count', 0))
        if page * len(items) >= total:
            break
        page += 1
        time.sleep(0.5)
    
    return all_data

3.5 反爬对抗策略

反爬手段应对方案
IP频率限制代理池轮换 + 请求间隔随机化
User-Agent检测fake-useragent / 真实UA池
Cookie验证模拟登录 + Session维持 + Cookie持久化
字体反爬下载字体文件 → fontTools解析映射关系
CSS偏移分析CSS样式还原真实文本顺序
图片验证码ddddocr / PaddleOCR / 打码平台
滑块验证Playwright模拟轨迹 / 第三方打码
WebDriver检测Playwright stealth / undetected-chromedriver
参数签名JS逆向 → Python重写签名算法
WAF/Cloudflarecurl_cffi / cloudscraper / flaresolverr
字体反爬fontTools解析TTF/WOFF映射表

3.6 数据清洗与存储

import pandas as pd
from pymongo import MongoClient
import json

def clean_and_store(raw_data: list, collection_name: str):
    """数据清洗与入库"""
    df = pd.DataFrame(raw_data)
    
    # 去重
    df = df.drop_duplicates(subset=['id', 'url'])
    
    # 清洗
    df['price'] = pd.to_numeric(df['price'], errors='coerce')
    df['title'] = df['title'].str.strip()
    df['created_at'] = pd.to_datetime(df['created_at'], errors='coerce')
    
    # 过滤无效数据
    df = df.dropna(subset=['title'])
    
    # 入库
    client = MongoClient('mongodb://localhost:27017')
    db = client['scraped_data']
    collection = db[collection_name]
    
    records = df.to_dict('records')
    for record in records:
        collection.update_one(
            {'_id': record.get('id', record.get('url'))},
            {'$set': record},
            upsert=True
        )
    
    return len(records)

阶段四:反爬对抗实战

4.1 IP代理池

import random
import requests
from typing import List

class ProxyPool:
    """简易代理池"""
    def __init__(self):
        self.proxies: List[dict] = []
        self._load_proxies()
    
    def _load_proxies(self):
        """从代理源加载(示例:免费代理源)"""
        sources = [
            'https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/proxies.json',
            # 也可从数据库/文件加载
        ]
        for source in sources:
            try:
                resp = requests.get(source, timeout=5)
                data = resp.json()
                for p in data:
                    self.proxies.append({
                        'http': f'http://{p["ip"]}:{p["port"]}',
                        'https': f'http://{p["ip"]}:{p["port"]}',
                    })
            except Exception:
                continue
    
    def get_random(self) -> dict:
        return random.choice(self.proxies) if self.proxies else {}
    
    def test_proxy(self, proxy: dict) -> bool:
        """测试代理可用性"""
        try:
            resp = requests.get('http://httpbin.org/ip', proxies=proxy, timeout=5)
            return resp.status_code == 200
        except:
            return False

4.2 浏览器指纹伪装 (Playwright)

# 反检测初始化脚本
STEALTH_SCRIPT = """
// 隐藏WebDriver
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});

// 伪装Chrome
window.chrome = { runtime: {} };

// 覆盖权限查询
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
    parameters.name === 'notifications' ?
        Promise.resolve({state: Notification.permission}) :
        originalQuery(parameters)
);

// 覆盖plugins
Object.defineProperty(navigator, 'plugins', {
    get: () => [1, 2, 3, 4, 5]
});

// 覆盖languages
Object.defineProperty(navigator, 'languages', {
    get: () => ['zh-CN', 'zh', 'en']
});
"""

4.3 验证码处理

# ddddocr - 轻量级OCR验证码
import ddddocr

ocr = ddddocr.DdddOcr()
with open('captcha.png', 'rb') as f:
    result = ocr.classification(f.read())
print(f'验证码识别结果: {result}')

# 滑块验证码 - 使用Playwright模拟轨迹
import random

def slide_verify(page, slider_selector: str, gap_selector: str):
    """模拟人类滑块验证"""
    slider = page.locator(slider_selector)
    gap = page.locator(gap_selector)
    
    slider_box = slider.bounding_box()
    gap_box = gap.bounding_box()
    
    start_x = slider_box['x'] + slider_box['width'] / 2
    start_y = slider_box['y'] + slider_box['height'] / 2
    target_x = gap_box['x'] + gap_box['width'] / 2
    
    distance = target_x - start_x
    
    # 模拟人类轨迹:先快后慢 + 抖动
    tracks = []
    current = 0
    mid = distance * 0.7
    while current < distance:
        if current < mid:
            move = random.randint(3, 8)
        else:
            move = random.randint(1, 3)
        current += move
        tracks.append(current)
    
    page.mouse.move(start_x, start_y)
    page.mouse.down()
    for x in tracks:
        page.mouse.move(start_x + x, start_y + random.randint(-2, 2))
        page.wait_for_timeout(random.randint(10, 30))
    page.mouse.up()

阶段五:数据清洗与存储

import pandas as pd
from pymongo import MongoClient, UpdateOne
import json

def clean_scraped_data(raw_data: list) -> pd.DataFrame:
    """通用数据清洗流程"""
    df = pd.DataFrame(raw_data)
    
    # 去重
    if 'url' in df.columns:
        df = df.drop_duplicates(subset=['url'])
    if 'id' in df.columns:
        df = df.drop_duplicates(subset=['id'])
    
    # 文本清洗
    text_cols = [c for c in df.columns if df[c].dtype == 'object']
    for col in text_cols:
        df[col] = df[col].str.strip().str.replace(r'\s+', ' ', regex=True)
    
    # 数值清洗
    for col in ['price', 'amount', 'count', 'score']:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors='coerce')
    
    # 日期标准化
    for col in ['date', 'created_at', 'updated_at']:
        if col in df.columns:
            df[col] = pd.to_datetime(df[col], errors='coerce')
    
    return df

阶段六:分布式爬虫架构

# Scrapy-Redis 配置
# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_URL = 'redis://localhost:6379'
SCHEDULER_PERSIST = True  # 爬虫停止后保留请求队列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

阶段七:App抓包与逆向

# mitmproxy 脚本示例 - 拦截App请求
# mitmproxy_script.py
from mitmproxy import http
import json

def request(flow: http.HTTPFlow):
    """拦截并记录请求"""
    url = flow.request.pretty_url
    if 'api.target.com' in url:
        print(f"[REQ] {url}")
        print(f"  Headers: {dict(flow.request.headers)}")
        if flow.request.content:
            print(f"  Body: {flow.request.content[:500]}")

def response(flow: http.HTTPFlow):
    """拦截并解析响应"""
    url = flow.request.pretty_url
    if 'api.target.com' in url:
        print(f"[RES] {url}")
        if 'application/json' in flow.response.headers.get('content-type', ''):
            data = json.loads(flow.response.text)
            # 保存到文件
            with open(f'data/{int(time.time())}.json', 'w') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)

阶段八:常见问题排查

问题原因解决方案
403 Forbidden请求被WAF/Cloudflare拦截使用 curl_cffi / cloudscraper / 真实浏览器指纹
429 Too Many Requests请求频率过高增加间隔、使用代理池、分布式
空数据/结构变化网站改版检查页面结构、更新选择器、添加结构变化告警
中文乱码编码识别错误resp.encoding = resp.apparent_encoding / chardet检测
内存溢出数据量过大使用迭代器、分页写入、限制并发数
连接超时网络/代理问题设置重试机制、超时时间、备用代理
验证码弹出触发风控降低频率、更换IP、使用打码服务

阶段九:法律与合规

  1. robots.txt:遵守网站的爬取规则声明
  2. 频率控制:不要对目标服务器造成压力(建议间隔 ≥ 1秒)
  3. 数据使用:不采集个人隐私信息、不用于竞争性商业用途
  4. 版权注意:注意数据的版权归属和使用许可
  5. 反爬对抗边界:不进行破坏性操作(DDoS、漏洞利用)

模板文件

1. Scrapy 项目模板

# scrapy_spider_template.py
"""
Scrapy爬虫模板 - 使用方式:
1. scrapy startproject myproject
2. 将本模板放入 spiders/ 目录
3. 修改 settings.py 配置中间件和管道
"""
import scrapy
from scrapy.http import HtmlResponse
from urllib.parse import urljoin
import json

class BaseSpider(scrapy.Spider):
    """基础爬虫模板,继承后重写 parse_item 即可"""
    name = 'base'
    allowed_domains = []
    start_urls = []
    
    custom_settings = {
        'DOWNLOAD_DELAY': 1.0,
        'RANDOMIZE_DOWNLOAD_DELAY': True,
        'CONCURRENT_REQUESTS': 8,
        'CONCURRENT_REQUESTS_PER_DOMAIN': 4,
        'RETRY_TIMES': 3,
        'RETRY_HTTP_CODES': [429, 500, 502, 503, 504],
        'DOWNLOAD_TIMEOUT': 15,
        'ROBOTSTXT_OBEY': False,
        'DEFAULT_REQUEST_HEADERS': {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        }
    }
    
    def parse(self, response):
        """主解析方法 - 子类重写"""
        raise NotImplementedError
    
    def parse_item(self, response):
        """解析单个详情页 - 子类重写"""
        raise NotImplementedError
    
    def closed(self, reason):
        """爬虫关闭时的清理工作"""
        self.logger.info(f"Spider closed: {reason}")

阶段七:App抓包与逆向

7.1 mitmproxy 拦截脚本

# mitmproxy_addon.py
# 运行: mitmproxy -s mitmproxy_addon.py
from mitmproxy import http
import json
import time
import os

# 配置目标域名
TARGET_DOMAINS = ['api.target.com', 'data.target.com']
OUTPUT_DIR = 'captured_data'

def request(flow: http.HTTPFlow):
    """拦截请求"""
    for domain in TARGET_DOMAINS:
        if domain in flow.request.pretty_host:
            print(f"[REQ] {flow.request.method} {flow.request.pretty_url}")
            if flow.request.headers.get('Content-Type', '').startswith('application/json'):
                try:
                    body = json.loads(flow.request.content)
                    print(f"  Body: {json.dumps(body, ensure_ascii=False, indent=2)[:500]}")
                except:
                    pass

def response(flow: http.HTTPFlow):
    """拦截响应"""
    for domain in TARGET_DOMAINS:
        if domain in flow.request.pretty_host:
            os.makedirs(OUTPUT_DIR, exist_ok=True)
            fname = f"{OUTPUT_DIR}/{int(time.time())}_{flow.request.path.replace('/', '_')}.json"
            with open(fname, 'w') as f:
                f.write(flow.response.text)
            print(f"[SAVED] {fname}")

阶段八:数据质量保障

def validate_scraped_data(df: pd.DataFrame, rules: dict) -> dict:
    """
    数据质量校验
    rules: {
        'title': {'required': True, 'min_length': 1, 'max_length': 200},
        'price': {'required': True, 'type': 'numeric', 'min': 0},
        'url': {'required': True, 'pattern': r'^https?://'},
    }
    """
    report = {'total': len(df), 'passed': 0, 'failed': 0, 'errors': []}
    
    for idx, row in df.iterrows():
        row_errors = []
        for field, rule in rules.items():
            val = row.get(field)
            if rule.get('required') and (pd.isna(val) or val == ''):
                row_errors.append(f"{field}: 必填字段为空")
            if rule.get('type') == 'numeric' and val:
                try:
                    float(val)
                except (ValueError, TypeError):
                    row_errors.append(f"{field}: 非数值类型")
            if rule.get('pattern') and val:
                import re
                if not re.match(rule['pattern'], str(val)):
                    row_errors.append(f"{field}: 格式不匹配")
        
        if row_errors:
            report['failed'] += 1
            report['errors'].append({'row': idx, 'errors': row_errors})
        else:
            report['passed'] += 1
    
    return report

阶段十:监控与运维

# 爬虫健康检查脚本
import requests
import time
from datetime import datetime

def health_check(spider_name: str, expected_count: int, timeout: int = 300):
    """监控爬虫运行状态"""
    start = time.time()
    while time.time() - start < timeout:
        # 检查数据库记录数
        count = get_db_count(spider_name)
        if count >= expected_count:
            return {'status': 'success', 'count': count, 'time': time.time() - start}
        
        # 检查爬虫进程
        import psutil
        spider_running = any('scrapy' in p.name() for p in psutil.process_iter())
        if not spider_running:
            return {'status': 'crashed', 'count': count}
        
        time.sleep(10)
    
    return {'status': 'timeout', 'count': count}

阶段十:法律与合规检查清单

  • 检查 robots.txt 是否允许采集
  • 确认数据是否包含个人隐私信息(姓名、电话、地址、身份证等)
  • 确认数据使用目的(研究/商业/个人)
  • 设置合理的请求间隔,不对目标服务器造成压力
  • 不进行密码破解、漏洞利用等破坏性操作
  • 不将采集的数据用于直接竞争或非法用途
  • 遵守目标网站的服务条款
  • 注意跨境数据传输的法律要求

常见陷阱与注意事项

  1. 不要硬编码选择器:网站结构会变,使用相对稳定的属性(data-*、id)或配置化选择器
  2. 永远处理异常:网络超时、解析失败、结构变化都要有fallback
  3. 日志是命根子:记录每个请求的URL、状态码、耗时、数据量
  4. 增量采集:记录上次采集位置,避免全量重复
  5. 数据版本化:保存原始响应(HTML/JSON)以便后续重新解析
  6. 代理池维护:定期检测代理可用性,剔除失效代理
  7. 频率控制:不要用固定间隔,使用随机间隔 ±30%
  8. 编码问题:始终指定编码,优先使用 apparent_encoding
  9. 错误重试:网络错误重试3次,指数退避
  10. 本地缓存:已下载的页面不要重复请求

验证方法

  1. 单页测试:先用 requests/curl 测试单个URL能否正常获取数据
  2. 解析验证:提取的数据与页面实际内容逐字段对比
  3. 批量验证:采集100条样本,人工抽查准确率
  4. 压力测试:逐步增加并发数,观察目标服务器响应
  5. 稳定性测试:连续运行1小时,检查是否有中断或异常
  6. 数据完整性:检查是否有缺失字段、重复记录、格式异常

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.