Skip to content

网易藏宝阁梦幻西游账号爬虫项目 — 完整交付文档

版本:V2.0
日期:2025-07-12
状态:终稿
用途:个人使用


目录


1. 项目概述

1.1 项目目标

使用 Python 开发爬虫系统,抓取网易藏宝阁(梦幻西游)全服务器在售角色/账号信息,数据存储到本地 SQLite 数据库。

1.2 目标网站

  • 网站:网易藏宝阁(https://cg.163.com)
  • 游戏:梦幻西游电脑版(product=xyqs)

1.3 技术选型

组件 技术 说明
语言 Python 3.11+
浏览器引擎 Playwright 无头浏览器
反检测 playwright-stealth 指纹伪装
代理 IP 站大爷纯生版(住宅IP) 真人率高
数据库 SQLite 零配置,个人用
日志 loguru

1.4 系统架构

┌─────────────────────────────────────┐
│            调度器 (Scheduler)         │
│  ┌──────────┐  ┌──────────┐         │
│  │ 定时任务  │  │ 任务队列  │         │
│  └──────────┘  └──────────┘         │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│          爬虫引擎 (Crawler)          │
│  ┌───────────┐  ┌────────────────┐  │
│  │ Playwright │  │ 反检测模块      │  │
│  │ 浏览器池   │  │ ├─ 指纹伪装     │  │
│  │           │  │ ├─ UA 轮换      │  │
│  │           │  │ ├─ 行为模拟     │  │
│  │           │  │ └─ 节奏控制     │  │
│  └───────────┘  └────────────────┘  │
│  ┌───────────┐  ┌────────────────┐  │
│  │ 代理 IP 池│  │ Cookie 管理    │  │
│  └───────────┘  └────────────────┘  │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│        数据处理 (Parser)             │
│  ┌──────────┐  ┌──────────┐         │
│  │ JSON解析  │  │ 数据清洗  │         │
│  └──────────┘  └──────────┘         │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│        数据存储 (SQLite)             │
└─────────────────────────────────────┘

2. 技术方案

2.1 方案选型

推荐方案:Playwright + stealth + 住宅代理

方案 优点 缺点 选择
requests + API 逆向 易被封、维护高
Selenium 生态成熟 指纹特征明显
Playwright + stealth 指纹好、稳定 较慢

3. 反检测方案

3.1 多层防护体系

第1层:浏览器指纹伪装(playwright-stealth)
    ↓
第2层:住宅代理 IP 轮换(站大爷纯生版)
    ↓
第3层:请求节奏模拟(随机间隔 3-15 秒)
    ↓
第4层:访问路径模拟(首页→列表→详情)
    ↓
第5层:验证码处理(自动降速 + 打码平台兜底)
    ↓
第6层:异常恢复(自动换 IP/重试)

3.2 浏览器指纹伪装

from playwright_stealth import stealth_async

async def create_stealth_browser():
    browser = await playwright.chromium.launch(
        headless=True,
        args=[
            '--disable-blink-features=AutomationControlled',
            '--disable-dev-shm-usage',
            '--no-sandbox',
        ]
    )
    context = await browser.new_context(
        user_agent=get_random_ua(),
        viewport={'width': 1920, 'height': 1080},
        locale='zh-CN',
        timezone_id='Asia/Shanghai',
    )
    page = await context.new_page()
    await stealth_async(page)

    await page.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
        window.chrome = { runtime: {} };
    """)

    return browser, context, page

3.3 代理 IP 池

class ProxyPool:
    """站大爷纯生版代理池管理"""

    API_URL = "https://api.zdaye.com/getip"

    def __init__(self, api_key):
        self.api_key = api_key
        self.proxies = []
        self.blacklist = set()

    async def fetch_proxies(self, count=10):
        """从站大爷 API 提取代理 IP"""
        params = {
            'key': self.api_key,
            'num': count,
            'format': 'json',
            'sep': 1,
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(self.API_URL, params=params) as resp:
                data = await resp.json()
                for item in data.get('data', []):
                    proxy = f"http://{item['ip']}:{item['port']}"
                    if proxy not in self.blacklist:
                        self.proxies.append(proxy)

    def get_proxy(self):
        """获取一个可用代理"""
        available = [p for p in self.proxies if p not in self.blacklist]
        if not available:
            return None
        return random.choice(available)

    def mark_failed(self, proxy):
        """标记失败"""
        self.blacklist.add(proxy)

3.4 请求节奏控制

class RequestThrottler:
    INTERVALS = {
        'page_turn': (3, 8),      # 翻页间隔
        'detail_view': (5, 15),   # 查看详情间隔
        'search': (8, 20),        # 搜索间隔
        'section_switch': (2, 5), # 切换区服间隔
    }

    async def wait(self, action_type='page_turn'):
        min_sec, max_sec = self.INTERVALS[action_type]
        wait_time = random.uniform(min_sec, max_sec)
        wait_time += random.gauss(0, 1)
        wait_time = max(1, wait_time)
        await asyncio.sleep(wait_time)

    async def simulate_browsing(self, page):
        """模拟真人浏览行为"""
        await page.mouse.wheel(0, random.randint(100, 500))
        await asyncio.sleep(random.uniform(0.5, 2))
        await page.mouse.move(
            random.randint(100, 800),
            random.randint(100, 600)
        )

3.5 验证码处理

class CaptchaHandler:
    async def handle_slider(self, page):
        """处理滑块验证码 - 模拟真人拖动"""
        slider = await page.query_selector('.slider-btn')
        box = await slider.bounding_box()

        x_start = box['x'] + box['width'] / 2
        y_start = box['y'] + box['height'] / 2
        distance = await self.calculate_slider_distance(page)

        await page.mouse.move(x_start, y_start)
        await page.mouse.down()

        # 分段移动,模拟加速减速
        steps = random.randint(20, 40)
        for i in range(steps):
            progress = i / steps
            offset = distance * self.ease_out_quad(progress)
            await page.mouse.move(
                x_start + offset + random.uniform(-2, 2),
                y_start + random.uniform(-3, 3)
            )
            await asyncio.sleep(random.uniform(0.01, 0.05))

        await page.mouse.up()

3.6 异常恢复

错误等级 场景 处理方式
Level 1 网络超时 切换代理,重试
Level 2 验证码触发 处理验证码 + 自动降速
Level 3 IP 被封 换 IP + 加入黑名单
Level 4 账号异常 告警

4. API 字段解析

4.1 列表页接口字段说明

接口返回的是 JSON 数据,藏宝阁统一用 equip 前缀,实际是角色/账号信息。

基本信息

字段 示例值 含义
server_name 纵横天下 服务器名称
serverid 11 服务器 ID
server_sn 90004 服务器编号
area_name 畅玩服 区服类型(畅玩服/点卡服/经典服)
areaid 2 区服 ID
is_classic_server false 是否经典服
product xyqs 游戏标识(西游Q传=梦幻西游电脑版)

角色信息

字段 示例值 含义
equip_name 15399512 角色 ID
seller_roleid 15399512 卖家角色 ID
format_equip_name 五庄观 展示名称(门派名)
equip_type_name 五庄观 门派名称
school 8 门派 ID
level 109 角色等级
level_desc 109级 等级描述
detail_equip_level 109级 详情页等级展示
icon gif 链接 角色头像
equip_face_img 329_3747.gif 角色展示图片标识

价格与交易

字段 示例值 含义
price 520000 价格(单位:分,520000 = ¥5200)
price_desc ¥5200.00 价格展示文本
price_down_amount 0 降价金额
price_down_percent 0 降价百分比
time_left 13天23时 剩余上架时间
onsale_expire_time_desc 13天23时 上架到期时间
collect_num 51 收藏数
views 0 浏览量
order_cnt 0 下单数
can_cross_buy true 是否支持跨服购买
allow_multi_order true 是否允许多人下单
allow_urs_bargain false 是否允许还价
has_unpaid_order false 是否有未支付订单
min_buyer_level 0级 买家最低等级限制

角色修炼

字段 示例值 含义
expt_gongji 17 攻击修炼等级
expt_fangyu 17 防御修炼等级
expt_fashu 17 法术修炼等级
expt_kangfa 17 抗法修炼等级
max_expt_gongji 20 攻击修炼上限
max_expt_fangyu 20 防御修炼上限
max_expt_fashu 20 法术修炼上限
max_expt_kangfa 20 抗法修炼上限

宝宝(召唤兽)修炼

字段 示例值 含义
bb_expt_gongji 14 宝宝攻击修炼等级
bb_expt_fangyu 1 宝宝防御修炼等级
bb_expt_fashu 17 宝宝法术修炼等级
bb_expt_kangfa 0 宝宝抗法修炼等级

角色属性(列表页通常为 0,详情页才有实际值)

字段 含义
qixue 气血
mofa 魔法
shanghai 伤害
mingzhong 命中
fangyu 防御
speed 速度
lingli 灵力
zongshang 总伤(综合伤害)
damage 伤害
magic_damage 法术伤害
magic_defense 法术防御
hp 气血

附加属性(加点相关)

字段 含义
addon_liliang 附加力量
addon_tizhi 附加体质
addon_minjie 附加敏捷
addon_naili 附加耐力
addon_fali 附加魔力
addon_lingli 附加灵力
addon_total 附加总计
addon_skill_chance 附加技能概率
addon_effect_chance 附加特效概率

装备/宝石相关

字段 含义
equip_count 装备数量
gem_level 宝石等级
gem_value 宝石价值
jinglian_level 精炼等级
xiang_qian_level 镶嵌等级
suit_effect 套装效果
suit_skill 套装技能
special_effect 特效
special_skill 特技
fengyin 封印
anti_fengyin 抗封

系统/平台字段

字段 含义
game_ordersn 订单编号
eid 商品唯一标识
equipid 角色 ID
equip_status 商品状态(2=在售)
equip_onsale_version 上架版本号
pass_fair_show 通过公平展示
storage_type 存储类型(4=角色)
kindid 角色分类子类 ID
equip_type 角色类型编码

4.2 门派 ID 映射

| 1 | 大唐官府 | 人族 | | 2 | 化生寺 | 人族 | | 3 | 女儿村 | 人族 | | 4 | 方寸山 | 人族 | | 5 | 天宫 | 仙族 | | 6 | 普陀山 | 仙族 | | 7 | 龙宫 | 仙族 | | 8 | 五庄观 | 仙族 | | 9 | 狮驼岭 | 魔族 | | 10 | 魔王寨 | 魔族 | | 11 | 阴曹地府 | 魔族 | | 12 | 盘丝洞 | 魔族 | | 13 | 神木林 | 人族 | | 14 | 凌波城 | 人族 | | 15 | 无底洞 | 魔族 | | 16 | 女魃墓 | 魔族 | | 17 | 天机城 | 人族 | | 18 | 花果山 | 仙族 | | 19 | 东海渊 | 人族 | | 20 | 九黎城 | 魔族 | | 101 | 斧头帮 | — | | 102 | 钓鱼台 | — | | 103 | 国子监 | — |


5. 数据采集设计

5.1 采集流程

启动程序
    │
    ↓
获取服务器列表(全量)
    │
    ↓
按服务器逐个爬取:
    │
    ├── 请求列表页 API
    │   ├── 获取角色摘要信息(JSON)
    │   ├── 解析 JSON 入库
    │   ├── 控制翻页节奏(3-8秒)
    │   └── 记录已采集页码(支持断点续爬)
    │
    ├── 逐个请求详情页 API(可选)
    │   ├── 获取完整属性
    │   ├── 获取装备/宠物/技能
    │   ├── 控制节奏(5-15秒)
    │   └── 更新数据库
    │
    └── 切换下一个服务器
        ├── 换代理 IP
        ├── 随机等待
        └── 可选换浏览器实例

5.2 采集策略

策略 说明
列表页优先 先抓列表页获取基础信息,速度快
详情页按需 需要详细属性时再抓详情页
断点续爬 记录每个服务器已爬页码,中断后继续
增量更新 记录上次爬取时间,只抓新增/变更
去重处理 按 eid 去重,避免重复入库

6. 数据库设计

6.1 服务器表 (t_server)

CREATE TABLE t_server (
    id INTEGER PRIMARY KEY,
    server_id INTEGER UNIQUE NOT NULL,    -- serverid
    server_sn TEXT NOT NULL,              -- server_sn
    server_name TEXT NOT NULL,            -- server_name
    area_id INTEGER,                      -- areaid
    area_name TEXT,                       -- area_name
    is_classic INTEGER DEFAULT 0,         -- is_classic_server
    last_crawl_time DATETIME,             -- 最后爬取时间
    last_crawl_page INTEGER DEFAULT 0,    -- 最后爬取页码(断点续爬)
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

6.2 角色表 (t_role)

CREATE TABLE t_role (
    id INTEGER PRIMARY KEY AUTOINCREMENT,

    -- 唯一标识
    eid TEXT UNIQUE NOT NULL,             -- 商品唯一标识

    -- 基本信息
    server_id INTEGER NOT NULL,           -- serverid
    server_name TEXT NOT NULL,            -- server_name
    server_sn TEXT,                       -- server_sn
    area_id INTEGER,                      -- areaid
    area_name TEXT,                       -- area_name
    is_classic INTEGER DEFAULT 0,         -- is_classic_server

    -- 角色信息
    seller_roleid TEXT,                   -- 卖家角色ID
    school INTEGER,                       -- 门派 ID
    school_name TEXT,                     -- 门派名称
    level INTEGER,                        -- 角色等级
    format_name TEXT,                     -- 展示名称
    icon TEXT,                            -- 头像链接

    -- 价格
    price INTEGER NOT NULL,               -- 价格(分)
    price_yuan REAL,                      -- 价格(元)= price/100
    price_down_amount INTEGER DEFAULT 0,  -- 降价金额
    price_down_percent REAL DEFAULT 0,    -- 降价百分比

    -- 交易信息
    time_left TEXT,                       -- 剩余时间
    collect_num INTEGER DEFAULT 0,        -- 收藏数
    views INTEGER DEFAULT 0,              -- 浏览量
    order_cnt INTEGER DEFAULT 0,          -- 下单数
    can_cross_buy INTEGER DEFAULT 0,      -- 是否可跨服
    allow_bargain INTEGER DEFAULT 0,      -- 是否可还价
    equip_status INTEGER DEFAULT 2,       -- 状态 2=在售
    storage_type INTEGER,                 -- 存储类型

    -- 角色修炼
    expt_gongji INTEGER DEFAULT 0,        -- 攻击修炼
    expt_fangyu INTEGER DEFAULT 0,        -- 防御修炼
    expt_fashu INTEGER DEFAULT 0,         -- 法术修炼
    expt_kangfa INTEGER DEFAULT 0,        -- 抗法修炼
    max_expt_gongji INTEGER DEFAULT 0,    -- 攻击修炼上限
    max_expt_fangyu INTEGER DEFAULT 0,    -- 防御修炼上限
    max_expt_fashu INTEGER DEFAULT 0,     -- 法术修炼上限
    max_expt_kangfa INTEGER DEFAULT 0,    -- 抗法修炼上限

    -- 宝宝修炼
    bb_expt_gongji INTEGER DEFAULT 0,     -- 宝宝攻击修炼
    bb_expt_fangyu INTEGER DEFAULT 0,     -- 宝宝防御修炼
    bb_expt_fashu INTEGER DEFAULT 0,      -- 宝宝法术修炼
    bb_expt_kangfa INTEGER DEFAULT 0,     -- 宝宝抗法修炼

    -- 角色属性(详情页填充)
    qixue INTEGER DEFAULT 0,              -- 气血
    mofa INTEGER DEFAULT 0,               -- 魔法
    shanghai INTEGER DEFAULT 0,           -- 伤害
    mingzhong INTEGER DEFAULT 0,          -- 命中
    fangyu INTEGER DEFAULT 0,             -- 防御
    speed INTEGER DEFAULT 0,              -- 速度
    lingli INTEGER DEFAULT 0,             -- 灵力
    zongshang INTEGER DEFAULT 0,          -- 总伤

    -- 附加属性
    addon_liliang INTEGER DEFAULT 0,      -- 附加力量
    addon_tizhi INTEGER DEFAULT 0,        -- 附加体质
    addon_minjie INTEGER DEFAULT 0,       -- 附加敏捷
    addon_naili INTEGER DEFAULT 0,        -- 附加耐力
    addon_fali INTEGER DEFAULT 0,         -- 附加魔力
    addon_total INTEGER DEFAULT 0,        -- 附加总计

    -- 系统字段
    game_ordersn TEXT,                    -- 订单编号
    equipid INTEGER,                      -- 角色 ID
    kindid INTEGER,                       -- 分类 ID
    equip_type TEXT,                      -- 类型编码

    -- 原始数据
    raw_json TEXT,                        -- 原始 JSON(备份)
    has_detail INTEGER DEFAULT 0,         -- 是否已获取详情

    -- 时间
    first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
    is_active INTEGER DEFAULT 1           -- 是否在售
);

CREATE INDEX idx_role_server ON t_role(server_id);
CREATE INDEX idx_role_level ON t_role(level);
CREATE INDEX idx_role_school ON t_role(school);
CREATE INDEX idx_role_price ON t_role(price);
CREATE INDEX idx_role_active ON t_role(is_active);
CREATE INDEX idx_role_eid ON t_role(eid);

6.3 详情表 (t_role_detail)

CREATE TABLE t_role_detail (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    eid TEXT UNIQUE NOT NULL,             -- 商品唯一标识

    -- 装备信息
    equip_list TEXT,                      -- 装备列表 JSON
    equip_count INTEGER DEFAULT 0,        -- 装备数量
    gem_level INTEGER DEFAULT 0,          -- 宝石等级
    gem_value INTEGER DEFAULT 0,          -- 宝石价值

    -- 召唤兽
    pet_list TEXT,                        -- 召唤兽列表 JSON

    -- 技能
    skill_list TEXT,                      -- 技能列表 JSON

    -- 其他
    achievements INTEGER DEFAULT 0,       -- 成就点
    silver BIGINT DEFAULT 0,              -- 银子
    description TEXT,                     -- 描述

    -- 原始数据
    raw_json TEXT,                        -- 详情页原始 JSON

    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,

    FOREIGN KEY (eid) REFERENCES t_role(eid)
);

6.4 价格历史表 (t_price_history)

CREATE TABLE t_price_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    eid TEXT NOT NULL,
    price INTEGER NOT NULL,               -- 价格(分)
    price_yuan REAL,                      -- 价格(元)
    change_time DATETIME DEFAULT CURRENT_TIMESTAMP,

    FOREIGN KEY (eid) REFERENCES t_role(eid)
);

CREATE INDEX idx_price_eid ON t_price_history(eid);
CREATE INDEX idx_price_time ON t_price_history(change_time);

6.5 爬取日志表 (t_crawl_log)

CREATE TABLE t_crawl_log (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    server_id INTEGER,
    server_name TEXT,
    crawl_type TEXT,                      -- list / detail
    status TEXT,                          -- success / failed / partial
    pages_crawled INTEGER DEFAULT 0,
    roles_found INTEGER DEFAULT 0,
    errors INTEGER DEFAULT 0,
    proxy_used TEXT,
    duration_seconds INTEGER,
    error_msg TEXT,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

7. 项目结构

cbg-spider/
├── README.md
├── requirements.txt
├── config/
│   ├── settings.py            # 全局配置
│   └── school_map.py          # 门派 ID 映射
├── src/
│   ├── __init__.py
│   ├── main.py                # 入口
│   ├── scheduler/
│   │   ├── __init__.py
│   │   └── task_scheduler.py  # 任务调度
│   ├── crawler/
│   │   ├── __init__.py
│   │   ├── browser.py         # 浏览器管理
│   │   ├── list_crawler.py    # 列表页爬取
│   │   └── detail_crawler.py  # 详情页爬取
│   ├── anti_detect/
│   │   ├── __init__.py
│   │   ├── stealth.py         # 指纹伪装
│   │   ├── throttle.py        # 节奏控制
│   │   └── captcha.py         # 验证码处理
│   ├── proxy/
│   │   ├── __init__.py
│   │   └── pool.py            # 站大爷代理池
│   ├── parser/
│   │   ├── __init__.py
│   │   ├── list_parser.py     # 列表页解析
│   │   └── detail_parser.py   # 详情页解析
│   ├── storage/
│   │   ├── __init__.py
│   │   ├── database.py        # SQLite 操作
│   │   └── exporter.py        # CSV 导出
│   └── utils/
│       ├── __init__.py
│       └── logger.py          # 日志
├── data/                      # 数据目录
│   └── cbg.db                 # SQLite 数据库
└── logs/                      # 日志目录

7.1 requirements.txt

playwright>=1.40.0
playwright-stealth>=1.0.6
aiohttp>=3.9.0
aiosqlite>=0.19.0
loguru>=0.7.0
pandas>=2.1.0

8. 核心代码

8.1 入口 (main.py)

import asyncio
from src.scheduler.task_scheduler import TaskScheduler
from src.storage.database import Database
from src.utils.logger import logger


async def main():
    db = Database('data/cbg.db')
    await db.init_tables()

    scheduler = TaskScheduler(db)
    await scheduler.run()


if __name__ == '__main__':
    asyncio.run(main())

8.2 数据库操作 (database.py)

import aiosqlite
from pathlib import Path


class Database:
    def __init__(self, db_path):
        self.db_path = db_path
        Path(db_path).parent.mkdir(parents=True, exist_ok=True)

    async def init_tables(self):
        async with aiosqlite.connect(self.db_path) as db:
            await db.executescript(TABLES_SQL)
            await db.commit()

    async def upsert_role(self, role_data: dict):
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute('''
                INSERT OR REPLACE INTO t_role (
                    eid, server_id, server_name, server_sn, area_id, area_name,
                    is_classic, seller_roleid, school, school_name, level,
                    format_name, icon, price, price_yuan, price_down_amount,
                    price_down_percent, time_left, collect_num, views, order_cnt,
                    can_cross_buy, allow_bargain, equip_status, storage_type,
                    expt_gongji, expt_fangyu, expt_fashu, expt_kangfa,
                    max_expt_gongji, max_expt_fangyu, max_expt_fashu, max_expt_kangfa,
                    bb_expt_gongji, bb_expt_fangyu, bb_expt_fashu, bb_expt_kangfa,
                    qixue, mofa, shanghai, mingzhong, fangyu, speed, lingli, zongshang,
                    addon_liliang, addon_tizhi, addon_minjie, addon_naili,
                    addon_fali, addon_total,
                    game_ordersn, equipid, kindid, equip_type,
                    raw_json, last_updated, is_active
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
                          ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
                          ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                role_data.get('eid'),
                role_data.get('serverid'),
                role_data.get('server_name'),
                role_data.get('server_sn'),
                role_data.get('areaid'),
                role_data.get('area_name'),
                1 if role_data.get('is_classic_server') else 0,
                role_data.get('seller_roleid'),
                role_data.get('school'),
                role_data.get('equip_type_name'),
                role_data.get('level'),
                role_data.get('format_equip_name'),
                role_data.get('icon'),
                role_data.get('price'),
                role_data.get('price', 0) / 100,
                role_data.get('price_down_amount'),
                role_data.get('price_down_percent'),
                role_data.get('time_left'),
                role_data.get('collect_num'),
                role_data.get('views'),
                role_data.get('order_cnt'),
                1 if role_data.get('can_cross_buy') else 0,
                1 if role_data.get('allow_urs_bargain') else 0,
                role_data.get('equip_status'),
                role_data.get('storage_type'),
                role_data.get('expt_gongji'),
                role_data.get('expt_fangyu'),
                role_data.get('expt_fashu'),
                role_data.get('expt_kangfa'),
                role_data.get('max_expt_gongji'),
                role_data.get('max_expt_fangyu'),
                role_data.get('max_expt_fashu'),
                role_data.get('max_expt_kangfa'),
                role_data.get('bb_expt_gongji'),
                role_data.get('bb_expt_fangyu'),
                role_data.get('bb_expt_fashu'),
                role_data.get('bb_expt_kangfa'),
                role_data.get('qixue'),
                role_data.get('mofa'),
                role_data.get('shanghai'),
                role_data.get('mingzhong'),
                role_data.get('fangyu'),
                role_data.get('speed'),
                role_data.get('lingli'),
                role_data.get('zongshang'),
                role_data.get('addon_liliang'),
                role_data.get('addon_tizhi'),
                role_data.get('addon_minjie'),
                role_data.get('addon_naili'),
                role_data.get('addon_fali'),
                role_data.get('addon_total'),
                role_data.get('game_ordersn'),
                role_data.get('equipid'),
                role_data.get('kindid'),
                role_data.get('equip_type'),
                json.dumps(role_data, ensure_ascii=False),
                datetime.now().isoformat(),
                1,
            ))
            await db.commit()

    async def log_crawl(self, log_data: dict):
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute('''
                INSERT INTO t_crawl_log (
                    server_id, server_name, crawl_type, status,
                    pages_crawled, roles_found, errors, proxy_used,
                    duration_seconds, error_msg
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                log_data.get('server_id'),
                log_data.get('server_name'),
                log_data.get('crawl_type'),
                log_data.get('status'),
                log_data.get('pages_crawled', 0),
                log_data.get('roles_found', 0),
                log_data.get('errors', 0),
                log_data.get('proxy_used'),
                log_data.get('duration_seconds'),
                log_data.get('error_msg'),
            ))
            await db.commit()

8.3 列表页解析 (list_parser.py)

import json
from src.config.school_map import SCHOOL_MAP


class ListParser:

    def parse(self, raw_data: dict) -> list:
        """解析列表页 API 返回的 JSON"""
        roles = []

        items = raw_data.get('equip_list', [])
        if not items:
            # 尝试其他可能的 key
            items = raw_data.get('data', [])
            if not items:
                items = raw_data.get('list', [])

        for item in items:
            role = {
                'eid': item.get('eid'),
                'server_id': item.get('serverid'),
                'server_name': item.get('server_name'),
                'server_sn': item.get('server_sn'),
                'area_id': item.get('areaid'),
                'area_name': item.get('area_name'),
                'is_classic': item.get('is_classic_server', False),
                'seller_roleid': item.get('seller_roleid'),
                'school': item.get('school'),
                'school_name': item.get('equip_type_name') or SCHOOL_MAP.get(item.get('school'), ''),
                'level': item.get('level'),
                'format_name': item.get('format_equip_name'),
                'price': item.get('price', 0),
                'price_yuan': item.get('price', 0) / 100,
                'time_left': item.get('time_left'),
                'collect_num': item.get('collect_num', 0),
                'views': item.get('views', 0),
                'can_cross_buy': item.get('can_cross_buy', False),
                # 修炼
                'expt_gongji': item.get('expt_gongji', 0),
                'expt_fangyu': item.get('expt_fangyu', 0),
                'expt_fashu': item.get('expt_fashu', 0),
                'expt_kangfa': item.get('expt_kangfa', 0),
                'bb_expt_gongji': item.get('bb_expt_gongji', 0),
                'bb_expt_fangyu': item.get('bb_expt_fangyu', 0),
                'bb_expt_fashu': item.get('bb_expt_fashu', 0),
                'bb_expt_kangfa': item.get('bb_expt_kangfa', 0),
                # 原始数据
                'raw_json': json.dumps(item, ensure_ascii=False),
            }
            roles.append(role)

        return roles

9. 配置说明

9.1 全局配置 (config/settings.py)

class Settings:
    # 爬虫配置
    CONCURRENT_BROWSERS = 2          # 同时运行的浏览器数
    MAX_PAGES_PER_SERVER = 50        # 每个服务器最多爬取页数
    RETRY_MAX = 3                    # 最大重试次数

    # 反检测配置
    MIN_REQUEST_INTERVAL = 3         # 最小请求间隔(秒)
    MAX_REQUEST_INTERVAL = 15        # 最大请求间隔(秒)
    BROWSER_RESTART_AFTER = 100      # 每爬N页重启浏览器

    # 站大爷代理配置
    PROXY_ENABLED = True
    PROXY_API_KEY = ''               # 站大爷 API Key
    PROXY_FETCH_COUNT = 10           # 每次提取代理数量
    PROXY_MAX_REUSE = 5              # 单个代理最大使用次数

    # 数据库配置
    DB_PATH = 'data/cbg.db'

    # 日志配置
    LOG_LEVEL = 'INFO'
    LOG_DIR = 'logs'

9.2 门派映射 (config/school_map.py)

SCHOOL_MAP = {
    1: '大唐官府',
    2: '化生寺',
    3: '女儿村',
    4: '方寸山',
    5: '天宫',
    6: '普陀山',
    7: '龙宫',
    8: '五庄观',
    9: '狮驼岭',
    10: '魔王寨',
    11: '阴曹地府',
    12: '盘丝洞',
    13: '神木林',
    14: '凌波城',
    15: '无底洞',
    16: '女魃墓',
    17: '天机城',
    18: '花果山',
    19: '东海渊',
    20: '九黎城',
    101: '斧头帮',
    102: '钓鱼台',
    103: '国子监',
}

10. 风险评估

风险 等级 应对措施
IP 被封 站大爷住宅代理轮换 + 自动降速
验证码频繁 降低频率 + 滑块自动识别 + 打码平台兜底
页面结构变更 模块化解析器便于维护
API 接口变更 raw_json 备份原始数据
法律合规 个人使用、不用于商业竞争、控制频率