天凤麻将牌谱解析:从XML日志到Python数据分析

发布时间:2026/9/8 8:21:54
天凤麻将牌谱解析:从XML日志到Python数据分析 天凤麻将的牌谱数据解析是我近一年来最上头的业余项目之一。每一场天凤对局都会生成一份XML格式的牌谱从配牌到和了从一次副露到一次放铳全都被事无巨细地记录下来。很多人打完就关掉牌谱地址但实际上这里面埋着大量可挖掘的信息你的立直率、副露率、平均打点甚至一局牌里哪一步失误最频繁都能通过解析算出来。这篇文章就从我一个数据爱好者的角度讲清楚天凤牌谱数据解析的完整过程包括获取牌谱、读懂XML、用Python解析以及最后能拿这些数据做什么。无论你是日麻玩家、数据分析新手还是做麻将AI方向的研究者都能从这里找到切入点。1. 为什么天凤的牌谱是一份被低估的数据资产1.1 牌谱里到底藏了哪些信息先明确一件事天凤的牌谱不是简单的“录像回放”而是一份结构化的对局日志。只要你有某一局的对局ID就能拿到一份完整的XML文件里面记录的信息包括但不限于这几类对局设定东风战还是半庄战单间还是般场规则版本牌谱对应的房间类型。玩家信息四个玩家的名字、段位、点数基准等这些通常都在UN节点里。整局的牌山顺序相当于上帝视角能知道接下来谁会摸到什么牌。每一巡的动作序列谁摸牌、谁切牌、谁吃碰杠、谁立直、谁和了精确到每一张牌的编号。宝牌信息表宝牌指示牌、里宝牌指示牌在DORA节点里逐一标注。和了与流局结果荣和、自摸、流局、错和、不听完流等连罚符点数都算得清清楚楚。这意味着什么意味着你的每一次“凭感觉”的判断都可以被拉出来量化。比如你觉得自己防守很好但数据可能告诉你你在对方立直后放铳的概率远高于同段位平均水平。这种反馈是录像复盘给不了的。1.2 拿到这些数据能解决什么问题牌谱数据解析最常见的用途有四类。第一类是个人复盘。我自己一开始就是想搞清楚“为什么总是在关键时刻放铳”。通过解析牌谱中的REACH和AGARI节点能精确还原每一次立直被追、立直放铳的场景再配合当时的牌河内容去做针对性修正。第二类是水平对比。天凤每个段位都有公开的大数据统计比如和了率、放铳率、平均打点、立直率这些指标。你把解析结果和自己的对局数一算就能看到自己处在同类玩家的哪个位置。很多上分卡住的人问题往往不是某一项技术不行而是某项指标明显偏离了安全区间。第三类是策略研究。比如你想研究“三色同顺”这个役种在低段位和高段位之间的出现频率差异或者研究“副露率与最终顺位”的关联这些都需要大量牌谱作为样本。手工看牌谱看到猴年马月解析脚本跑一晚上就能出几百局数据。第四类是AI与机器学习。天凤牌谱是目前比较干净、规整的麻将数据源之一很多麻将AI项目都基于它做预训练或行为克隆。解析出来的数据可以作为监督学习的训练集也可以用来做强化学习里的对手建模。说白了牌谱数据解析就是把“经验判断”变成“数据判断”的一把钥匙。2. 获取牌谱数据从日志到本地文件2.1 通过天凤日志页找到牌谱地址获取天凤牌谱最常用的入口是天凤客户端的“日志”功能或者对局结束后结果页里附带的牌谱链接。链接长这样http://tenhou.net/0/log/?log2024052523gm-00e1-0000-xxxxxxxx注意这里的核心是log参数它相当于一份牌谱的身份证。拿到这个链接后通常的做法是直接在浏览器里打开页面会显示牌谱回放。但如果想看原始XML有几个办法在链接后面加上tw1或ts0这类参数会改变呈现形式不一定能直接拿到XML。更稳妥的做法是用浏览器“另存为”保存网页或者直接用脚本请求原始地址。天凤的服务器对普通HTTP请求也返回数据但响应体是gzip压缩过的XML流不是HTML页面。这就是为什么有人用requests抓下来发现是乱码——其实那不是乱码是gzip压缩数据。我自己的习惯是先通过天凤的日志检索页面按玩家ID拉取最近对局列表得到一串log参数再逐个拼接成下载地址。批量拉取时给每个地址加一点延时几十个文件慢慢下一天下来能攒不少样本。2.2 本地文件格式与常见坑保存下来的牌谱文件可能是这些形式保存方式得到的内容处理方式浏览器直接另存为压缩后的XML二进制用gzip解压脚本请求requestsgzip流用gzip.decompress()处理手动复制日志页内容纯XML文本直接解析第三方牌谱工具导出通常为XML或JSON看工具说明这里最大的坑是文件扩展名。很多人把gzip流直接存成.xml结果用文本编辑器打开全是乱码。我的做法是统一保存成.xml.gz交给解析脚本时先判断后缀再解压。另一个坑是牌谱的时效性。天凤会不定期清理旧牌谱尤其是段位场的日志可能几个月后就访问不到了。所以如果你是认真想做数据分析建议当天打完就立刻把牌谱文件归档下来不要等到需要的时候才想起来去抓。3. 用Python解析牌谱从XML到结构化数据3.1 牌谱XML的关键节点结构天凤牌谱的XML结构并不复杂但初次打开的人容易懵因为节点属性很多。我带你过一遍最核心的骨架。根节点是mjloggm然后向下依次是TAIKYOKU对局规则信息比如规则版本、大厅ID。UN四个玩家的名字通常以n0、n1、n2、n3这样的属性形式存在。TEN初始点数相关信息四人麻将一般是25000点或30000点。KYOKU一局牌例如东1局0本场、东3局1本场、南2局0本场等。半庄战通常有8个或更多KYOKU节点流局和局续行会产生额外节点。在KYOKU内部HAI表示配牌T表示摸牌或切牌N表示吃碰杠DORA表示宝牌指示牌REACH表示立直声明AGARI表示和了RYUUKYOKU表示流局。我整理成一个表方便你对照着看牌谱节点含义解出来有什么用UN玩家列表记录对局对阵信息T n.../摸牌或切牌摸切统计、牌山复用N who0 m.../副露动作吃碰杠统计DORA hai.../宝牌指示分析宝牌对决策影响REACH who2 step1/立直声明立直时机、进攻性分析AGARI who0 .../荣和或自摸和了率、打点、放铳分析RYUUKYOKU .../流局流局率、听牌率你不需要一开始就把所有节点都吃透先抓住T、N、REACH、AGARI这四个就能覆盖绝大多数基础统计需求。3.2 牌ID编码规则摸牌、切牌和赤宝牌很多人在解析时卡在最基础的一步T n37/和T n173/到底代表什么牌天凤牌谱里有自己的一套牌ID编码规则理解它以后解析就顺了。牌的“牌型ID”沿用日麻常规编号万子1到9是0到8筒子1到9是9到17索子1到9是18到26东南西北是27到30白发中是31到33。这是34种牌型的基础编号。但实战里每种牌有4张所以天凤牌谱用的是“实体牌编号”也就是把整副136张牌各自编号。因此T节点里的数字范围不是0到33而是更大的范围。更关键的是摸与切的区分。我手头解析过的牌谱里T n.../的n值有一个比较通用的约定n小于136时表示“摸牌”n本身代表摸到的实体牌编号。n大于等于136时表示“切牌”用n - 136得到被切出的实体牌编号。这是一套很自然的编码思路因为天凤在对局内部本身就能确定每一张具体牌的唯一身份不只是“5万”这个种类而是“5万中的第2张”。这样做的好处是解析时不需要额外去猜是不是赤宝牌因为赤宝牌也占用独立的实体牌编号。需要特别注意的是不同解析资料对赤宝牌的处理略有区别。有的把赤5万、赤5筒、赤5索单独编成34、35、36有的在实体牌编号体系里额外扩展。所以我建议你拿到一份牌谱后先做一个小样本对照打开一局牌谱的回放页面记下几张关键牌型再去解析结果里比对确认自己的映射表没有偏差。3.3 实际解析代码框架我的解析脚本通常分三步走读取XML、解析基础信息、提取动作序列。下面给出一份可以直接运行的框架代码以我手头某份典型牌谱为例。import gzip import xml.etree.ElementTree as ET from collections import defaultdict def load_mjlog(path): 读取天凤牌谱文件支持直接解压后的xml和xml.gz if path.endswith(.gz): with gzip.open(path, rb) as f: content f.read() else: with open(path, rb) as f: content f.read() return ET.fromstring(content) def get_players(root): 提取四个玩家名字 un root.find(.//UN) if un is None: return [] names [] for i in range(4): name un.get(fn{i}) names.append(name if name else fplayer{i}) return names def parse_game_events(root): 遍历所有局提取关键动作 games [] for kyoku in root.findall(KYOKU): kyoku_info {type: kyoku.get(type), actions: [], agari: None, ryuukyoku: None} for node in kyoku.iter(): tag node.tag if tag T: n int(node.get(n)) # 按牌谱编码规则区分摸牌和切牌 if n 136: kyoku_info[actions].append((draw, n)) else: kyoku_info[actions].append((discard, n - 136)) elif tag N: # 副露动作who表示玩家m编码了副露内容 kyoku_info[actions].append((naki, node.attrib)) elif tag REACH: kyoku_info[actions].append((reach, node.attrib)) elif tag AGARI: kyoku_info[agari] node.attrib elif tag RYUUKYOKU: kyoku_info[ryuukyoku] node.attrib games.append(kyoku_info) return games if __name__ __main__: root load_mjlog(sample.xml.gz) players get_players(root) games parse_game_events(root) print(玩家列表:, players) print(总局数:, len(games)) for g in games[:2]: print(局类型:, g[type], 动作数:, len(g[actions]))这段代码里有几个地方值得展开说。第一load_mjlog里的.gz判断很关键。天凤服务器返回的数据本身就是gzip压缩如果你用requests直接抓取响应体就是压缩过的需要gzip.decompress(resp.content)但如果你已经保存成.xml.gz就用上面的方式处理。第二get_players里天凤牌谱的UN节点在不同版本里字段格式可能略有差异。有的版本用n0到n3有的版本是子标签。我建议先打印一下un.attrib看看实际结构再决定用什么字段。第三parse_game_events里我用kyoku.iter()遍历所有后代节点而不是只遍历直接子节点。因为HAI等节点内部也可能有子动作节点直接迭代所有节点更省心。第四T节点的解析规则也就是摸牌和切牌的编号约定需要你拿自己手上的牌谱验证。不同牌谱版本可能有微小差异但大方向是一致的。3.4 进阶还原每一巡的摸切顺序上面框架代码已经能统计出一些基础信息但如果你想回答“某玩家在第几巡切了什么牌”这种问题还得再往前一步把每个动作归属到具体玩家。这里有个坑T节点本身并不直接标注是谁摸牌、谁切牌。你看到一长串T节点它们只是按时间顺序排列的事件流。想还原归属必须建一个简单的游戏状态机知道当前是东家先动知道副露之后会跳过摸牌知道立直后的摸切顺序等。我自己第一次实现状态机时踩了不少坑。比如杠之后需要补牌N节点之后往往紧跟一个T补牌节点但这个补牌是“摸牌”还是“切牌”在编号上并没有特别明显的标记得靠手牌数量变化去推断。再比如“九种九牌流局”和“错和”这些情况事件流会插入额外节点增加判断复杂度。我的建议是如果你不是非要对每个动作做精细化还原优先做一些不依赖状态机的统计比如用N节点统计副露率用REACH节点统计立直率用AGARI节点统计和了率和放铳率。这些已经足够回答很多策略问题了。真到需要精细化摸切追踪时可以去看一些开源的天凤牌谱解析器在它们的基础上改比自己从零写要快得多。4. 解析结果能用来做什么4.1 生存指标统计和了率、放铳率、平均打点先聊最基础的生存指标。只要你有几十局牌谱就能算出这几个数字和了率 自己和了次数 / 参与对局总数放铳率 自己放铳次数 / 参与对局总数立直率 立直次数 / 参与对局总数平均打点 自己和了时的总得分 / 和了次数平均顺位 所有对局最终顺位的平均值这些指标的含义不用我多解释。关键是不同段位的人群分布差异很大。拿放铳率来说高段位玩家的放铳率普遍控制得比低段位低不少而和了率并不是越高越好因为很多时候过度追求和了会牺牲防守。所以单纯看一个指标没用把多个指标放在一起看才有意义。我一般会把解析结果按每50局切一段画出趋势线。这样能明显看出自己最近是变激进了还是变保守了。曾经有段时间我的放铳率直线上升排除了对局风格变化后发现是那段时间经常在“立直后无脑跟打现物”这一个环节上偷懒导致被对手的跨张碰听打出防守漏洞。这种复盘结论不看数据很难定位。4.2 副露与立直行为画像副露和立直能反映一个人的打法风格而天凤牌谱里正好有N和REACH节点可以用来统计。我给两位玩家做过一次对比数据很有说服力玩家立直率副露率和了率平均打点A32%8%0.246200B17%38%0.275200A明显是门清重视型喜欢靠立直和一番手役来搏高打点B是鸣牌速攻型通过副露加快听牌速度和了率高但打点偏低。这两种风格没有绝对优劣但放到不同规则场里胜率表现会有明显差异。解析出这类画像后你再回头去看自己的牌谱就能发现自己偏好的打法到底有没有被数据验证。比如你一直觉得自己是防守型但统计显示你的副露率高达35%那说明你的自我认知和实际打法并不一致。这种“认知偏差”用数据矫正特别有效。4.3 从牌谱到AI与策略研究如果你愿意往更深一层走天凤牌谱还是一座很好的数据矿山。麻将AI领域的研究者经常用天凤的高段位对局数据来做模仿学习具体做法就是先把牌谱解析成“状态-动作”对状态是当前的手牌、牌河、对手动作动作是这一次的切牌或副露选择。解析成“状态-动作”对的关键难点在于天凤的XML没有直接给你“当前手牌是什么”这样的现成数据你需要根据牌山信息和摸切顺序重新构造手牌状态。这件事的工程量比基础统计大不少但自己动手做一遍对麻将规则的理解会加深很多。我第一次把解析出的“状态-动作”对凑够一万条时突然理解了为什么有些AI在防守时的切牌选择这么保守——因为这些数据里高段位玩家在面对立直时确实更倾向于打安全牌而不是继续进攻。5. 常见问题与排查技巧5.1 文件读取与编码问题这里我见过最多的坑就是gzip。直接拿requests去请求天凤牌谱地址返回的响应体是gzip压缩过的不是明文XML。如果打印出来看到一串二进制不要慌先检查响应头里的Content-Encoding通常是gzip用gzip.decompress处理即可。另一个是文件名乱码。天凤牌谱链接里含时间戳和随机字符串直接用log参数做文件名最稳妥。比如2024052523gm-00e1-0000-xxxxxxxx.xml.gz我见过有人用“东1局翻盘那局.xml”这种命名短期没问题但攒多了就知道后悔了。用可排序的ID命名后续批量处理才省心。5.2 批量解析性能问题当你积累到几百上千份牌谱时一次性用ET.fromstring读进内存会开始变慢。XML文件本身不算大但架不住数量多。我的建议是使用xml.etree.ElementTree.iterparse做流式解析按需处理每个KYOKU节点。解析结果不要再存回XML直接转成pandas的DataFrame落盘成csv或parquet。批量处理时手动控制并发数别开太多线程去请求远端牌谱容易被限流。我自己攒到3000份牌谱时全量解析一次大概需要几分钟后来用了iterparse优化后时间缩短到一分钟左右。如果你不是为了做大规模AI训练这套方案完全够用。5.3 牌谱失效与网络问题天凤清理旧牌谱是真实存在的。越老的牌谱越容易出现404。所以我特别建议养成“打完就备份”的习惯。甚至可以说牌谱数据解析里最稀缺的资产不是解析代码而是你自己保存下来的那堆XML文件。另外批量下载时不要高频请求。我一般会在每两次请求之间至少隔一两秒把单日下载量控制在一个合理范围。如果被限制就等一段时间再继续。做数据项目长期稳定比短期拉满重要得多。5.4 数据准确性验证最后分享一个我踩过坑后养成的习惯解析完一份牌谱先不要直接上统计抽几个关键节点和回放页面做一个交叉验证。具体做法是随便拿一局牌谱打开天凤的牌谱回放翻到东1局第1巡看庄家摸到什么牌、切了什么牌然后在解析结果里找到相同节点比对编号。再翻到某一次副露检查N节点的who字段是不是和回放一致。这样连续验证三四个节点就能确定解析规则没写错。这一步看似多花五分钟能省下后面所有统计分析的返工时间。我在实际项目中还有一个体会牌谱解析这件事最怕的不是技术难题而是数据源不干净。只要你保证自己归档的XML是完整、未篡改的解析结果基本就是可靠的。这份踏实感是后面所有分析工作的地基。