ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python3实战:明日方舟干员数据查询工具从采集到分析

Python3实战:明日方舟干员数据查询工具从采集到分析 简介这是一份面向明日方舟玩家与Python学习者的干员数据查询工具源码基于Python3开发通过Click模块构建命令行接口解决日常查询干员信息、材料需求与练度追踪时反复切换网页的痛点。工具支持别名管理、与prts.wiki网站数据同步、精英等级与技能专精追踪、升级材料查询等功能适合有一定Python基础、希望本地化管理游戏数据的用户。资源包共270个文件以253个toml数据文件为主配合13个py脚本模块及txt、md、license等说明文件压缩包约475KB结构紧凑、便于二次开发。目前已有73人学习下载。读者可获得完整的命令行工具源码理解Click接口设计、数据同步与解析逻辑并参考模块划分方式自行扩展查询功能对Python命令行项目实践具有较高参考价值。1. 从一份干员数据查询工具说起Python3 怎么把明日方舟的干员档案跑通很多做明日方舟辅助工具的同行第一步都卡在同一个地方干员数据从哪来、怎么存、怎么查。游戏本身不提供公开 APIWiki 页面结构又经常调整手动整理一份干员表两三百个角色、每个角色十几个字段维护成本高得离谱。这个基于 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, start1): 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 rarityexcluded.rarity, classexcluded.class, branchexcluded.branch, positionexcluded.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, rarityNone, class_None, branchNone, tagsNone, tag_logicAND): 按条件查询干员。 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(ft.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, rarity6, class_近卫, tags[输出]) for r in rows: print(r[1], r[2], r[3], r[4]) # 查带「治疗」或「支援」标签的干员 rows query_operators(conn, tags[治疗, 支援], tag_logicOR)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, defaultarknights.db, helpSQLite 数据库路径) parser.add_argument(--rarity, typeint, help星级如 6) parser.add_argument(--class, destclass_, help职业如 近卫) parser.add_argument(--branch, help分支如 领主) parser.add_argument(--tag, actionappend, desttags, help标签可多次指定) parser.add_argument(--tag-logic, choices[AND, OR], defaultAND, 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, rarityargs.rarity, class_args.class_, branchargs.branch, tagsargs.tags, tag_logicargs.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用actionappend支持--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(rbr\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_patharknights.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_onid, right_onoperator_id, howleft) 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(ascendingFalse)) # 统计标签出现频率 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用howleft保证没有标签的干员也保留。Counter统计标签频率能快速看出哪些标签是热门。5.2 导出 CSV 和 Excel 的注意事项导出 CSV 时中文乱码是常见问题。Windows Excel 默认用 GBK 打开 CSV而 pandas 默认写 UTF-8。解决办法是写文件时加encodingutf-8-sig带 BOM 头Excel 就能正确识别。# 导出全部干员到 CSVutf-8-sig 解决 Excel 乱码 df.to_csv(operators_export.csv, indexFalse, encodingutf-8-sig) # 导出 6 星干员到 Excel指定 sheet 名 with pd.ExcelWriter(six_star.xlsx, engineopenpyxl) as writer: six_star.to_excel(writer, sheet_name6星干员, indexFalse)openpyxl需要单独pip install openpyxl。如果数据量超过几万行Excel 写入会慢建议只导出筛选后的子集。CSV 没有行数限制适合全量导出。5.3 一个我常用的验证习惯每次改完采集或查询逻辑我会跑一个「对账」脚本从数据库里随机抽 10 个干员打印它们的标签和技能然后手动去 Wiki 对一眼。这个习惯帮我抓到过好几次标签漏抓、技能顺序错位的问题。数据类项目最怕的就是「看起来能跑但数据是错的」对账脚本就是后悔药。另外sqlite3的.dump命令可以导出整个数据库的 SQL 文本改表结构前先sqlite3 arknights.db .dump backup.sql出问题直接回滚。希望帮到你。本文还有配套的精品资源点击获取
返回列表