天凤牌谱采集及分析

麻将sitp  天凤  2020年07月28日

采集牌谱 #

采集牌谱流程如下:

下载年度记录 #

前往天凤-记录的过去记录部分, 下载名字形如scraw[year].zip的文件并解压.

找到凤凰桌的牌谱链接 #

2013年以前的压缩包解压后直接得到一系列scc[yymmdd].html文件, 2013年及以后的压缩包解压后得到scc[yymmdd].html.gz文件, 再次解压后得到html文件. 每个html文件存储着当天全部的凤凰桌对局信息, 用文本文档打开html文件, 其中<a href="[url]">中的[url]就是牌谱链接, 点击即可选择用天凤网页版或者客户端查看牌谱.

转换牌谱链接得到牌谱文本 #

当然我们需要的不是在线查看牌谱, 我们需要下载牌谱文本. 在线查看牌谱的链接形如tenhou.net/0/?log=[id], 牌谱文本的链接形如tenhou.net/0/log/?[id]. 因此将牌谱链接中的?log=替换成log/?, 输入至浏览器即可看到牌谱文本, 复制粘贴即可保存到本地.

自动爬取牌谱文本 #

如果愿意手动重复以上步骤, 那么可以跳过这一部分.

自动化爬取牌谱文本的关键是向牌谱文本链接发送请求, 并保存接收到的回复. 在python中通常情况下这是由urllib.request.urlopen函数做到的, 但是天凤似乎对于程序爬取有一定的限制, 直接urlopen会返回404错误, 所以我们需要加一个请求头伪装成来自浏览器的请求, 同时使用Request对象和Opener对象发起请求. KStarXin提供的一个示例如下:

import urllib.request

HEADER = {
    'Host': 'e.mjv.jp',
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.5',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive'
}
url = 'http://e3.mjv.jp/0/log/?2013010100gm-00e1-0000-e5440138' # for example

req = urllib.request.Request(url=url, headers=HEADER)
opener = urllib.request.build_opener()
response = opener.open(req)
response = gzip.decompress(response.read()).decode('utf-8')

除此之外还需注意牌谱实际不是储存在tenhou.net域名下的, 而是分别储存在*.mjv.jp域名下. 经初步整理, 从2013年到2018年的牌谱文件域名有:

所以对于每一个牌谱id, 我们分别尝试从各个域名进行下载.

参考资料 #

  1. 天鳳の牌譜ログ取得方法: https://mj-data.net/天鳳の牌譜ログ取得方法 (失效链接)

解析牌谱 #

牌谱文件是xml格式的, 可以通过python标准库中xml.etree.ElementTree类来方便地解析xml文件.

接下来我们将介绍xml文件中的各个元素及其属性的含义. 需要注意的是数数一般是从0开始数的(除了立直步骤之外).

mjloggm #

根元素.

ver #

牌谱格式的版本, 2018年1月1日的牌谱版本为2.3. 我们也将只对版本2.3的牌谱进行解析.

SHUFFLE
乱数的信息, 牌山是由这个乱数所决定的. 我们不是很关心这个乱数, 因为其余的牌谱信息足以再现对局.
seed
乱数的种子.
ref
为空.

GO #

牌桌规则和等级等信息.

type
化为十进制表示的8位二进制数.
0x01如果是PVP对战则为1
0x02如果没有赤宝牌则为1
0x04如果无食断则为1
0x08如果是半庄则为1
0x10如果是三人麻将则为1
0x20如果是特上卓或凤凰卓则为1
0x40如果是速卓则为1
0x80如果是上级卓则为1
lobby
大会lobby的ID.

UN #

天凤名, 段位, 等级分等的对局者信息.

n0 ~ n3
天凤名. URL格式UTF编码. 对局者的编号是0, 编号按逆时针顺序增加.
dan
段位. 以逗号分隔的四个数. 0: 新人 ~ 20: 天凤.
rate
等级分. 以逗号分隔的四个数, 有两位小数.
sx
性别.

TAIKYOKU #

从这个元素开始都是用来记录实际对局的.

oya
起家. 通常是从0开始.

INIT #

每局的初始信息.

seed
以逗号分隔的六个数, 分别是局顺, 本场, 供托, 开门的两个骰子值(0~5), DORA表示牌的编号.
ten
每局开始时候各玩家的点数除以100. 以逗号分隔的四个数.
oya
庄家的编号.
hai0 ~ hai3
起手牌. 以逗号分隔的13个数. 没有理牌.

(T|U|V|W)(0~135) #

摸牌的信息.

T ~ W
玩家编号. T表示编号为0的玩家摸牌, W表示编号为3的玩家摸牌.
0 ~ 135
摸进的牌的编号.

(D|E|F|G)(0~135) #

打牌的信息.

D ~ G
玩家编号. D表示编号为0的玩家打牌, G表示编号为3的玩家打牌.
0 ~ 135
打出的牌的编号.

N #

副露的信息.

who
开副露的玩家的编号.
面子的编号.

DORA #

新的DORA牌的信息(在开杠之后).

hai
新的DORA表示牌的编号.

REACH #

立直的信息. 在立直宣言和立直成立时出现. 在立直宣言牌放铳的情况下, 立直成立的REACH元素不出现.

who
立直玩家的编号
step
立直的阶段. 1表示立直宣言, 2表示立直成立.
ten
立直成立后各玩家的持有点数除以100

AGARI #

和牌的信息. 一炮双响的情况下AGARI元素会出现两次.

ba
2个数. 积棒个数和供托的立直棒个数
hai
除了副露以外的手牌(兵牌), 包含和的牌. 用逗号分隔, 牌编号格式.
m
包含暗杠的副露牌, 用逗号分隔, 面子编号格式.
machi
和的牌, 牌编号格式.
ten
3个数. 符, 和的打点(不除以100), 满贯情报(0: 满贯未满, 1: 满贯, 2: 跳满, 3: 倍满, 4: 三倍满, 5: 役满).
yaku
非役满和牌的役的信息. 每个役编号和番数交替以逗号分隔.
yakuman
役满和牌的役的信息. 只有役编号, 用逗号分隔.
doraHai
DORA表示牌的牌编号, 用逗号分隔.
doraHaiUra
里DORA表示牌的牌编号, 用逗号分隔.
who
和牌玩家的编号.
fromWho
放铳玩家的编号.
sc
和牌之后点数的移动情况. 8个数. 每个玩家的持有点数/100和收支点数/100交替以逗号分隔.
owari
终局的信息. 8个数, 每个玩家的持有点数/100以及包含uma的point数交替以逗号分隔.

RYUUKYOKU #

流局的信息.

ba
同AGARI元素中的ba.
sc
同AGARI元素中的sc.
type
流局的理由.
意思
nm流局满贯
yao9九种九牌
kaze4四风连打
reach4四家立直
ron3三家和了
kan4四杠散了
owari
同AGARI元素中的owari.