网易藏宝阁梦幻西游账号爬虫项目 — 完整交付文档
版本: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 备份原始数据 |
| 法律合规 |
中 |
个人使用、不用于商业竞争、控制频率 |