☰
Python3实战:明日方舟干员数据查询工具从采集到分析
2026/10/2 22:19:16 网站建设 项目流程

简介:这是一份面向明日方舟玩家与Python学习者的干员数据查询工具源码,基于Python3开发,通过Click模块构建命令行接口,解决日常查询干员信息、材料需求与练度追踪时反复切换网页的痛点。工具支持别名管理、与prts.wiki网站数据同步、精英等级与技能专精追踪、升级材料查询等功能,适合有一定Python基础、希望本地化管理游戏数据的用户。资源包共270个文件,以253个toml数据文件为主,配合13个py脚本模块及txt、md、license等说明文件,压缩包约475KB,结构紧凑、便于二次开发。目前已有73人学习下载。读者可获得完整的命令行工具源码,理解Click接口设计、数据同步与解析逻辑,并参考模块划分方式自行扩展查询功能,对Python命令行项目实践具有较高参考价值。

1. 从一份干员数据查询工具说起:Python3 怎么把明日方舟的干员档案跑通

很多做明日方舟辅助工具的同行,第一步都卡在同一个地方:干员数据从哪来、怎么存、怎么查。游戏本身不提供公开 API,Wiki 页面结构又经常调整,手动整理一份干员表,两三百个角色、每个角色十几个字段,维护成本高得离谱。这个基于 Python3 的明日方舟干员数据查询工具,解决的就是这件事——把干员基础档案、职业分支、技能标签、精英化材料这些结构化信息,用 Python3 做本地化存储和条件查询,让后续做配队推荐、材料统计、图鉴展示时不用每次重新爬。

它适合两类人:一是刚学完 Python3 基础、想找一个真实数据项目练手的开发者,二是已经在做明日方舟游戏助手、需要一套可维护数据层的从业者。核心思路不复杂:数据采集层负责把干员信息抓下来并清洗成统一结构,存储层用 SQLite 或 JSON 落地,查询层对外暴露按职业、星级、标签、技能类型过滤的接口。整条链路只依赖 Python3 标准库加少量第三方包,在 CentOS7 或本地 Windows 环境都能跑。下面按实际落地顺序拆开讲,从数据模型设计到查询接口封装,再到踩过的坑,一步步来。

2. 干员数据模型怎么定:字段、职业分支与技能标签的拆解

2.1 先想清楚查询需求,再反推表结构

很多人一上来就建一张大宽表,把所有字段塞进一个operators表,结果查询条件一复杂就得写一堆LIKE。我一般会先列清楚这个工具要支持哪些查询场景:按星级筛选(6 星、5 星)、按职业筛选(先锋、近卫、狙击、重装、医疗、辅助、术师、特种)、按分支筛选(比如近卫里的「领主」「剑豪」「无畏者」)、按标签筛选(「输出」「防护」「治疗」「支援」「费用回复」)、按技能类型筛选(自动回复、攻击回复、受击回复)。这些条件之间是 AND/OR 组合关系,如果全塞一张表,标签和技能这种一对多关系就得用逗号拼接字符串,查询时LIKE '%输出%'不仅慢,还容易误匹配。

所以我的做法是拆成三张核心表加两张关联表。operators存干员基础信息,skills存技能定义,tags存标签字典,operator_tags和operator_skills做多对多关联。这样查询「所有带输出标签的 6 星近卫」就是一次三表 JOIN,条件清晰,索引也好加。

-- 干员基础表:只存一对一字段 CREATE TABLE operators ( id INTEGER PRIMARY KEY, -- 干员唯一 ID,用 Wiki 的页面 ID 或自增 name TEXT NOT NULL UNIQUE, -- 干员名,如「银灰」 rarity INTEGER NOT NULL, -- 星级 1-6 class TEXT NOT NULL, -- 职业:先锋/近卫/狙击/重装/医疗/辅助/术师/特种 branch TEXT, -- 分支:如近卫下的「领主」 position TEXT, -- 站位:近战/远程 gender TEXT, -- 性别 place_of_birth TEXT, -- 出身地 birthday TEXT, -- 生日,格式 MM-DD recruitable INTEGER DEFAULT 1 -- 是否公开招募可出 ); -- 标签字典表 CREATE TABLE tags ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE -- 如「输出」「防护」「治疗」 ); -- 干员-标签关联表 CREATE TABLE operator_tags ( operator_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (operator_id, tag_id), FOREIGN KEY (operator_id) REFERENCES operators(id), FOREIGN KEY (tag_id) REFERENCES tags(id) ); -- 技能表 CREATE TABLE skills ( id INTEGER PRIMARY KEY, operator_id INTEGER NOT NULL, skill_index INTEGER NOT NULL, -- 第几个技能,1/2/3 name TEXT NOT NULL, -- 技能名 recovery_type TEXT, -- 回复类型:自动回复/攻击回复/受击回复 activation_type TEXT, -- 触发类型:手动触发/自动触发 description TEXT, -- 技能描述 FOREIGN KEY (operator_id) REFERENCES operators(id) );

建表时有两个参数值得注意。rarity用 INTEGER 而不是 TEXT,因为后面排序和范围查询(rarity >= 5)用整数更快。recruitable用 0/1 表示布尔值,SQLite 没有原生 BOOLEAN 类型,这是常见做法。operator_tags的主键用联合主键,天然防止重复插入同一条关联记录。

2.2 数据采集:从 Wiki 页面到结构化 JSON 的清洗流程

数据来源常见做法是抓明日方舟 Wiki 的干员列表页和详情页。这里不展开具体站点,只说清洗逻辑。Wiki 页面通常用表格或信息框展示干员属性,解析后得到的是嵌套 HTML 或半结构化文本,需要转成统一 JSON 再入库。

import re import json from typing import Optional def parse_rarity(raw: str) -> Optional[int]: """从 '6星' 或 '★★★★★★' 中提取星级数字""" if not raw: return None # 匹配中文数字或星号数量 match = re.search(r'(\d)', raw) if match: return int(match.group(1)) # 如果是纯星号,数星号个数 stars = raw.count('★') return stars if stars > 0 else None def parse_tags(raw: str) -> list: """标签字段通常是 '输出 防护 支援' 或 '输出、防护' 格式""" if not raw: return [] # 统一分隔符后切分 normalized = re.sub(r'[、,,/\s]+', ' ', raw.strip()) return [t for t in normalized.split(' ') if t] def parse_skills(raw_list: list) -> list: """技能列表清洗,每个技能提取名称、回复类型、触发类型""" skills = [] for idx, item in enumerate(raw_list, start=1): skill = { 'skill_index': idx, 'name': item.get('name', '').strip(), 'recovery_type': item.get('recovery', '').strip(), 'activation_type': item.get('activation', '').strip(), 'description': item.get('desc', '').strip() } # 过滤掉空技能占位 if skill['name']: skills.append(skill) return skills def clean_operator(raw: dict) -> dict: """把原始抓取字典转成入库结构""" return { 'name': raw.get('name', '').strip(), 'rarity': parse_rarity(raw.get('rarity', '')), 'class': raw.get('class', '').strip(), 'branch': raw.get('branch', '').strip() or None, 'position': raw.get('position', '').strip() or None, 'gender': raw.get('gender', '').strip() or None, 'place_of_birth': raw.get('birthplace', '').strip() or None, 'birthday': raw.get('birthday', '').strip() or None, 'recruitable': 1 if raw.get('recruitable', '是') == '是' else 0, 'tags': parse_tags(raw.get('tags', '')), 'skills': parse_skills(raw.get('skills', [])) }

这段清洗逻辑的关键点在于容错。Wiki 数据经常出现字段缺失、格式不统一的情况,比如星级有时写「6星」有时写「★★★★★★」,标签分隔符混用中英文逗号。parse_rarity先尝试匹配数字,匹配不到再数星号,保证两种格式都能处理。parse_tags用正则统一分隔符再切分,避免因为分隔符不一致导致标签粘连。parse_skills里过滤空技能名,是因为有些干员只有两个技能,第三个位置是占位空值。

入库时用INSERT OR IGNORE处理标签字典,避免重复标签报错;关联表用INSERT OR REPLACE保证重复运行时数据一致。

import sqlite3 def save_operator(conn: sqlite3.Connection, op: dict): cur = conn.cursor() # 插入干员基础信息 cur.execute(""" INSERT INTO operators (name, rarity, class, branch, position, gender, place_of_birth, birthday, recruitable) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(name) DO UPDATE SET rarity=excluded.rarity, class=excluded.class, branch=excluded.branch, position=excluded.position """, (op['name'], op['rarity'], op['class'], op['branch'], op['position'], op['gender'], op['place_of_birth'], op['birthday'], op['recruitable'])) op_id = cur.execute("SELECT id FROM operators WHERE name=?", (op['name'],)).fetchone()[0] # 处理标签 for tag_name in op['tags']: cur.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag_name,)) tag_id = cur.execute("SELECT id FROM tags WHERE name=?", (tag_name,)).fetchone()[0] cur.execute("INSERT OR IGNORE INTO operator_tags (operator_id, tag_id) VALUES (?, ?)", (op_id, tag_id)) # 处理技能:先删旧再插新,保证更新时不会残留 cur.execute("DELETE FROM skills WHERE operator_id=?", (op_id,)) for skill in op['skills']: cur.execute(""" INSERT INTO skills (operator_id, skill_index, name, recovery_type, activation_type, description) VALUES (?, ?, ?, ?, ?, ?) """, (op_id, skill['skill_index'], skill['name'], skill['recovery_type'], skill['activation_type'], skill['description'])) conn.commit()

ON CONFLICT(name) DO UPDATE是 SQLite 3.24 以上支持的 upsert 语法,保证重复采集时更新而不是报错。技能表先删后插,是因为技能数量可能变化,直接更新会残留旧数据。这些细节在批量采集几百个干员时能省很多手动修数据的时间。

3. 查询接口怎么封装:从 SQL 到 Python 函数的落地路径

3.1 基础查询:按星级、职业、标签组合过滤

数据入库后,查询层要解决的核心问题是:怎么把用户输入的条件组合成安全的 SQL。直接拼字符串会有注入风险,而且条件多了代码会乱。我一般用一个build_query函数,把可选条件收集到列表里,再用参数化查询执行。

def query_operators(conn, rarity=None, class_=None, branch=None, tags=None, tag_logic='AND'): """ 按条件查询干员。 rarity: int 或 None class_: str 或 None branch: str 或 None tags: list[str] 或 None,如 ['输出', '防护'] tag_logic: 'AND' 表示同时满足所有标签,'OR' 表示满足任一 """ sql = """ SELECT DISTINCT o.id, o.name, o.rarity, o.class, o.branch FROM operators o """ params = [] joins = [] wheres = [] if tags: joins.append("JOIN operator_tags ot ON ot.operator_id = o.id") joins.append("JOIN tags t ON t.id = ot.tag_id") placeholders = ','.join(['?'] * len(tags)) wheres.append(f"t.name IN ({placeholders})") params.extend(tags) if rarity is not None: wheres.append("o.rarity = ?") params.append(rarity) if class_: wheres.append("o.class = ?") params.append(class_) if branch: wheres.append("o.branch = ?") params.append(branch) sql += ' ' + ' '.join(joins) if wheres: sql += ' WHERE ' + ' AND '.join(wheres) if tags and tag_logic == 'AND': # AND 逻辑:分组后统计匹配标签数等于输入标签数 sql += f" GROUP BY o.id HAVING COUNT(DISTINCT t.name) = {len(tags)}" elif tags: sql += " GROUP BY o.id" sql += " ORDER BY o.rarity DESC, o.name ASC" return conn.execute(sql, params).fetchall()

这个函数有几个设计取舍。tags用IN加GROUP BY ... HAVING COUNT实现 AND 逻辑,比多次自连接更直观。DISTINCT防止 JOIN 产生重复行。排序按星级降序、名称升序,符合图鉴展示习惯。参数全部走占位符,杜绝注入。

调用示例:

# 查所有 6 星近卫,且带「输出」标签 rows = query_operators(conn, rarity=6, class_='近卫', tags=['输出']) for r in rows: print(r[1], r[2], r[3], r[4]) # 查带「治疗」或「支援」标签的干员 rows = query_operators(conn, tags=['治疗', '支援'], tag_logic='OR')

3.2 技能与标签的关联查询:一次拿全干员详情

图鉴详情页需要一次性拿到干员基础信息、所有标签、所有技能。如果分三次查询再在 Python 里拼装,代码量大且容易漏。我一般写一个get_operator_detail函数,用三次查询分别取基础、标签、技能,再组装成字典。

def get_operator_detail(conn, name): """按干员名查完整详情,返回 dict""" op = conn.execute(""" SELECT id, name, rarity, class, branch, position, gender, place_of_birth, birthday, recruitable FROM operators WHERE name = ? """, (name,)).fetchone() if not op: return None op_id = op[0] tags = [r[0] for r in conn.execute(""" SELECT t.name FROM tags t JOIN operator_tags ot ON ot.tag_id = t.id WHERE ot.operator_id = ? ORDER BY t.name """, (op_id,)).fetchall()] skills = conn.execute(""" SELECT skill_index, name, recovery_type, activation_type, description FROM skills WHERE operator_id = ? ORDER BY skill_index """, (op_id,)).fetchall() return { 'id': op[0], 'name': op[1], 'rarity': op[2], 'class': op[3], 'branch': op[4], 'position': op[5], 'gender': op[6], 'place_of_birth': op[7], 'birthday': op[8], 'recruitable': bool(op[9]), 'tags': tags, 'skills': [ {'index': s[0], 'name': s[1], 'recovery': s[2], 'activation': s[3], 'desc': s[4]} for s in skills ] }

这里recruitable从数据库的 0/1 转成 Python 的bool,方便上层判断。标签按名称排序,保证每次输出顺序一致。技能按skill_index排序,对应游戏里一技能、二技能、三技能的顺序。

3.3 命令行入口:用 argparse 做一个能直接跑的查询工具

有了查询函数,再包一层命令行入口,就能直接python query.py --rarity 6 --class 近卫 --tag 输出这样用。argparse是 Python3 标准库,不用额外装包。

import argparse import sqlite3 def main(): parser = argparse.ArgumentParser(description='明日方舟干员数据查询工具') parser.add_argument('--db', default='arknights.db', help='SQLite 数据库路径') parser.add_argument('--rarity', type=int, help='星级,如 6') parser.add_argument('--class', dest='class_', help='职业,如 近卫') parser.add_argument('--branch', help='分支,如 领主') parser.add_argument('--tag', action='append', dest='tags', help='标签,可多次指定') parser.add_argument('--tag-logic', choices=['AND', 'OR'], default='AND', help='多标签逻辑,默认 AND') parser.add_argument('--detail', help='查指定干员详情,传干员名') args = parser.parse_args() conn = sqlite3.connect(args.db) conn.row_factory = sqlite3.Row if args.detail: detail = get_operator_detail(conn, args.detail) if detail: print(f"{detail['name']} {detail['rarity']}星 {detail['class']}") print(f"标签: {', '.join(detail['tags'])}") for s in detail['skills']: print(f" 技能{s['index']}: {s['name']} ({s['recovery']}/{s['activation']})") else: print('未找到该干员') else: rows = query_operators(conn, rarity=args.rarity, class_=args.class_, branch=args.branch, tags=args.tags, tag_logic=args.tag_logic) for r in rows: print(f"{r[1]:<8} {r[2]}星 {r[3]:<4} {r[4] or ''}") conn.close() if __name__ == '__main__': main()

--tag用action='append',支持--tag 输出 --tag 防护这样传多个标签。--detail走详情分支,其他参数走列表查询。row_factory设成sqlite3.Row后可以用列名访问,但上面查询函数返回的是元组,所以打印时用索引。这个入口脚本在 CentOS7 自带的 Python3.6 上就能跑,不需要额外依赖。

4. 避坑与排查:干员数据查询工具最常见的 5 个翻车点

4.1 现象:标签 AND 查询返回空结果,但单独查每个标签都有数据

原因:GROUP BY ... HAVING COUNT(DISTINCT t.name) = N里,如果tags表里有同名标签但大小写或空格不一致,COUNT会算多或算少。比如「输出」和「输出 」被当成两个标签,IN匹配到两个,但COUNT(DISTINCT)只算一个,导致HAVING条件不满足。

解决:入库前统一strip()和全半角转换。在parse_tags里加一步tag.strip().replace(' ', ''),把全角空格和首尾空白去掉。如果已经入库了脏数据,跑一条UPDATE tags SET name = TRIM(name)再重建关联。

4.2 现象:批量采集时中途报UNIQUE constraint failed: operators.name

原因:INSERT INTO operators没有加ON CONFLICT处理,第二次跑采集脚本时同名干员触发唯一约束。或者同一批数据里有两个同名干员(比如不同形态),但表设计时name设了 UNIQUE。

解决:把插入改成INSERT ... ON CONFLICT(name) DO UPDATE SET ...,保证幂等。如果确实存在同名不同形态的干员,name不能做唯一键,改用 Wiki 页面 ID 做唯一键,name只做普通索引。

4.3 现象:查询结果里干员重复出现,同一个名字打印两次

原因:query_operators里 JOIN 了operator_tags和tags,一个干员有多个标签时会产生多行。虽然加了DISTINCT,但如果 SELECT 的列里有o.id之外的字段且这些字段在不同行里值相同,DISTINCT去重是基于整行的,可能去不干净。

解决:SELECT 只取o.id, o.name, o.rarity, o.class, o.branch,这些字段对一个干员是唯一的,DISTINCT能正确去重。如果还需要其他字段,用子查询或GROUP BY o.id代替DISTINCT。

4.4 现象:技能描述里的换行符导致命令行输出错乱

原因:Wiki 抓下来的技能描述里包含\n或<br>,直接打印时把一行拆成多行,表格对齐全乱。

解决:入库前把\n、<br>、<br/>统一替换成空格或分号。在parse_skills里加desc = re.sub(r'<br\s*/?>', ';', desc)和desc = desc.replace('\n', ' ')。如果已经入库,查询时用REPLACE(description, char(10), ' ')处理。

4.5 现象:在 CentOS7 上跑脚本报ModuleNotFoundError: No module named 'sqlite3'

原因:CentOS7 默认 Python2 的 sqlite3 模块可能没装,或者系统里 Python3 是编译安装的,编译时没带--enable-loadable-sqlite-extensions和 sqlite-devel 头文件。

解决:先yum install sqlite-devel,然后重新编译 Python3 时确保_sqlite3模块被编译进去。验证方法是python3 -c "import sqlite3; print(sqlite3.sqlite_version)"。如果不想重编译,用pymysql或psycopg2换 MySQL/PostgreSQL 也行,但 SQLite 的单文件优势就没了。这个坑在 CentOS7 安装 Python3 的教程里经常被忽略,血泪经验是编译前先装sqlite-devel。

5. 进阶技巧:用 pandas 做干员数据分析和导出

5.1 把查询结果转成 DataFrame 做统计

前面查询函数返回的是元组列表,做统计不方便。用 pandas 可以几行代码算出各职业干员数量、各星级分布、标签出现频率。pandas 是 Python3 数据分析的标配,装一次能用很久。

import pandas as pd import sqlite3 def load_operators_df(db_path='arknights.db'): """把干员基础信息和标签加载成 DataFrame""" conn = sqlite3.connect(db_path) # 基础信息 df = pd.read_sql_query(""" SELECT id, name, rarity, class, branch, position, recruitable FROM operators """, conn) # 标签聚合:每个干员的标签拼成字符串 tag_df = pd.read_sql_query(""" SELECT ot.operator_id, GROUP_CONCAT(t.name, ',') AS tags FROM operator_tags ot JOIN tags t ON t.id = ot.tag_id GROUP BY ot.operator_id """, conn) conn.close() df = df.merge(tag_df, left_on='id', right_on='operator_id', how='left') df['tags'] = df['tags'].fillna('') return df # 统计各职业 6 星干员数量 df = load_operators_df() six_star = df[df['rarity'] == 6] print(six_star.groupby('class')['name'].count().sort_values(ascending=False)) # 统计标签出现频率 from collections import Counter all_tags = [] for tags in df['tags']: all_tags.extend([t for t in tags.split(',') if t]) print(Counter(all_tags).most_common(10))

GROUP_CONCAT是 SQLite 的聚合函数,把多行标签拼成一个逗号分隔字符串,再在 pandas 里 split 展开。merge用how='left'保证没有标签的干员也保留。Counter统计标签频率,能快速看出哪些标签是热门。

5.2 导出 CSV 和 Excel 的注意事项

导出 CSV 时中文乱码是常见问题。Windows Excel 默认用 GBK 打开 CSV,而 pandas 默认写 UTF-8。解决办法是写文件时加encoding='utf-8-sig',带 BOM 头,Excel 就能正确识别。

# 导出全部干员到 CSV,utf-8-sig 解决 Excel 乱码 df.to_csv('operators_export.csv', index=False, encoding='utf-8-sig') # 导出 6 星干员到 Excel,指定 sheet 名 with pd.ExcelWriter('six_star.xlsx', engine='openpyxl') as writer: six_star.to_excel(writer, sheet_name='6星干员', index=False)

openpyxl需要单独pip install openpyxl。如果数据量超过几万行,Excel 写入会慢,建议只导出筛选后的子集。CSV 没有行数限制,适合全量导出。

5.3 一个我常用的验证习惯

每次改完采集或查询逻辑,我会跑一个「对账」脚本:从数据库里随机抽 10 个干员,打印它们的标签和技能,然后手动去 Wiki 对一眼。这个习惯帮我抓到过好几次标签漏抓、技能顺序错位的问题。数据类项目最怕的就是「看起来能跑,但数据是错的」,对账脚本就是后悔药。另外,sqlite3的.dump命令可以导出整个数据库的 SQL 文本,改表结构前先sqlite3 arknights.db .dump > backup.sql,出问题直接回滚。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询