缘由

无非就是闲着没事干又不想看书,给自己找点事做罢了.至于功能,无非就是文件读写+网络请求.

先说说原理吧!

获取数据:(PC端)读日志获取链接->参数解析->发起请求->参数解析->(判断尾页)->写入文本

分析数据:读文件->判断4/5星距离的保底次数->输出

很简单对吧?事实上只要文件写的好,分析起来很轻松的.但看了一下脚本,感觉很繁琐(毕竟用到了Excel,我也不会啊),但我还有csv可以用,那写入就简单了!问题也主要在获取数据这块,请求一共需要14+4个数据,并且token长度高达1Kb!这在url链接里面还是算很长的.所以我先将参数解析成字典并写入ini文件里分析

## 切割参数

res = urllib.parse.urlsplit(log_url).query
# 将字符串转为字典

query = dict(urllib.parse.parse_qsl(res))
for j in query:
    # 执行更新/写入根节点操作

    ini.set(ini_type[0], j, query[j])

然后在VS里面打开,这样就很清楚了.很多参数看名字就知道是什么,那些值奇奇怪怪的参数当然是不需要我们去动的,那剩下的参数也就是关于分页/卡池类型才是我们关注的重点.

2021-12-03T02:46:40.png
经过简单分析之后发现,卡池类型由两个参数决定,第一个决定的是在游戏中正常打开历史记录首次显示的祈愿池(如在常驻页面打开则初始显示常驻第一页内容),这个参数是和网页输出内容相关的,不需要动.而第二个则是和分页查询相关大的要来了,简单测试之后发现确实可以查询到分页内容,但当我去修改参数时发现输出变成了第一页的内容,当时是很懵逼的.要知道,能在浏览器打开你游祈愿记录,那就说明没有限制(比如某些网页只能在手机端甚至是微信内才能打开)

稍加思索(稍加死锁.jpg)之后,发现失败的原因是额外参数end_id不一样导致的,在第一页时这个参数值为0,在其他分页时则为一个奇奇怪怪的很长的值(如1636218360001429666)一看就是时间戳,但日期却很奇怪.测试了几个,发现基本都在整点过六分,而且是过去时间比如半个月前.

好的,上面的问题先让我们不管,因为找到了新的突破口,下面让我们继续.目前已经实现了分页的查询,我一次查询十条,这是两次查询.之后会继续完善

2021-12-03T15:34:11.png

因为我太菜了,在测试的时候也收获了不少错误信息,比如下面这条

{'data': None, 'message': 'visit too frequently', 'retcode': -110}

在正常情况下能获取4-8次数据也就是40-80条,这是很奇怪的,因为能获取第三页那就说明我代码逻辑没问题!

由于之前使用了try来处理,所以没报错,调试了一会感觉是因为速度太快了,输出json果然是这样.

v1.0 完成!

从一开始的写入 csv 到现在的写入 xlsx ,不得不说我实在是太菜了,这么一个玩意都快写了一个月( 3 号到 28 号).

这是成品效果图:

2021-12-28T06:19:38.png
2021-12-28T06:31:43.png
目前来说是能用了(只需要配合一下Excel自带的筛选功能),但果然还是需要加颜色来区分吧!目前还没了解怎么去给表格加上样式,但既然连图都能画,那应该也是很简单的(

改成 xlsx 之后代码足足少了三行(其实基本没变,主要是逻辑调整了很多)

其实重新看一遍代码逻辑也挺简单的,虽然我写了差不多两百行,但实际上有大约五十行是在引入模块和变量初始化,又有大约五十行是在处理 log 文件中的链接并写入 ini 配置文件,也就是说只有大约一百行代码是在进行查询以及写入.

但是前前后后拖了很久了,我昨天上手发现有很多地方我自己都看不懂了(这谁写的代码啊摔).于是又花了一段时间去理解代码逻辑,最后才有了这个成品.

就这个项目而言, csv 并不适合,因为全部卡池加起来足足有五个(新手池,角色池,角色池2,武器池,常驻池).而 csv 并没有工作表这个东西,本质上是用分割符来划分的 txt 文件.

补充

在项目v0.9快完工时,我去看了一下猫站上现成的也是Python写的导出抽卡记录的项目,代码量我还能接受,我承认那个项目比我的好太多了,代码也很规范,就是有些地方一言难尽,比如这里,就连我一个鶸都写不出这种代码:

from config import Config

f = open("verison.txt")
verison = f.read()
f.close()
f = open(".\\dist\\config.json", "w", encoding="utf-8")
f.write("{}")
f.close()
c = Config(".\\dist\\config.json")
c.setKey("verison", verison)
c.setKey("url","")
c.setKey("FLAG_MANUAL_INPUT_URL", False)
c.setKey("FLAG_CLEAN", False)
c.setKey("FLAG_SHOW_REPORT", True)
c.setKey("FLAG_WRITE_XLSX", True)
c.setKey("FLAG_USE_CONFIG_URL",True)
c.setKey("FLAG_USE_LOG_URL",True)
c.setKey("FLAG_USE_CAPTURE",True)

后记

抛开结果不谈,在这次实践中我还是收获了很多的,同时也深刻的认识到了自己的水平有多鶸(悲

如果有时间的话,我会重新调整代码,使其支持Excel和MarkDown的,或者套个现成模板输出成HTML

目前已经实现了 Excel 格式的写入,但还是不太美观,以后想起来的话搞个简单的模板吧!

代码

# URL_DICT模块

import urllib.parse

def url_join_args(api, query=None, **kwargs):
    result = api
    if '&' in result and '=' in result:
        result = api + '&'
    elif not result.endswith('?') and (query or kwargs):
        result = api + '?'
    if query:
        result = result + urllib.parse.urlencode(query)
    if kwargs:
        if query:
            result = result + '&' + urllib.parse.urlencode(kwargs)
        else:
            result = result + urllib.parse.urlencode(kwargs)
    return result
import xlsxwriter
import requests
import urllib.parse
import time
import sys
from configparser import ConfigParser, RawConfigParser
import os.path
from alive_progress import alive_bar
if(True):
    # Python犯病了

    sys.path.append(os.path.abspath(os.path.dirname(__file__)))
    from URL_DICT import url_join_args


# 原理:(PC端)读日志获取链接->参数解析->判断尾页->处理数据->写入文本


# 取win用户目录,外服替换成`Genshin Impact`

home = os.path.expanduser('~')

log_path = home+r'\AppData\LocalLow\miHoYo\原神\output_log.txt'


desktop = home+r'\Desktop\\'

mihoyo_api = 'https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog'

mihoyo_ua = {
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6,zh-TW;q=0.5',
    'Accept-Encoding': 'gzip, deflate, br',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Host': 'hk4e-api.mihoyo.com',
    'Origin': 'https://webstatic.mihoyo.com',
    'Pragma': 'no-cache',
    'Referer': 'https://webstatic.mihoyo.com/',
    'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-site',
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Unity 3D; ZFBrowser 2.1.0; ?? 2.3.0_4786731_4861639) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36'
}
# 根节点为日志中记录的,查询节点为三个祈愿池

ini_type = ['root', 'user']

gacha_type = {'常驻池': '200', '角色池': '301', '武器池': '302'}


language_list = ['de-de', 'en-us', 'es-es', 'fr-fr', 'id-id', 'ja-jp',
                 'ko-kr', 'pt-pt', 'ru-ru', 'th-th', 'vi-vn', 'zh-cn', 'zh-tw']

user_parameter = ['uid', 'url', 'lang', 'region']

item_list = ['uid', 'gacha_type', 'item_id', 'count',
             'time', 'name', 'lang', 'item_type', 'rank_type', 'id']
# 初始化

query_dict = {}
log_url = ''
page = 1
size = 10
end_id = '0'
# 初始化查询参数字典

added_dict = {'gacha_type': gacha_type['角色池'], 'page': page,

              'size': size, 'end_id': end_id}

ini = ConfigParser()
# RawConfigParser解决存在%时抛出异常

# ini = configparser.RawConfigParser()

# 不读直接写入会丢失未修改数据

ini.read(desktop+r'祈愿配置.ini', encoding='utf-8')


with open(log_path, mode='r', encoding='utf-8') as log, alive_bar() as bar:
    for line in log:
        bar()
        if not line.startswith('OnGetWebViewPageFinish'):
            continue
        if not '?' in line:
            continue
        else:
            log_url = line.replace('OnGetWebViewPageFinish:', '')
            # 切割参数

            res = urllib.parse.urlsplit(log_url).query
            # 将字符串转为字典

            query = dict(urllib.parse.parse_qsl(res))
            # 创建节点

            for i in ini_type:
                if not ini.has_section(i):
                    ini.add_section(i)
            for j in query:
                # 执行更新/写入根节点操作

                ini.set(ini_type[0], j, query[j])

            # 写入空值健,后期再写入值

            for j in user_parameter:
                if not ini.has_option(ini_type[1], j):
                    ini.set(ini_type[1], j, '')

            ini.write(open(desktop+r'祈愿配置.ini', mode='w', encoding='utf-8'))

        break

    print('读日志结束')

    if not log_url:
        print('目标文件不存在链接!\n应该先在游戏中打开祈愿[历史记录]以生成一个带token的链接')

        sys.exit(0)

print('获取参数')


# 不需要重新读取,ini即为更新后的数据

with alive_bar(len(ini.items(ini.sections()[0]))) as bar:
    for key, value in ini.items(ini_type[0]):
        # sys.stdout.flush()
        query_dict[key] = value
        bar()
        time.sleep(0.001)


def get_history_log(api, added_dict):
    '''封装网络请求

    :param api:带有14个固定参数的url

    :param added_dict:四个额外参数的字典

    :返回一个十组数据的列表

    '''
    items: list = []
    # 拼接需要的临时URL

    tmp_url = url_join_args(api, added_dict)
    response = requests.get(tmp_url, headers=mihoyo_ua).json()
    print(response)
    # items为单次查询的数据的祈愿详情部分

    items = response['data']['list']
    # 如果不足size条数据说明是最后一页了,返回false进入下一个池子查询

    if len(items) < size:
        return [items, False]
    return [items, True]


def write_xlsx(worksheet, items, row: int):
    # row:行;col:列;items:列表

    # 修改了逻辑,一次写入一整行,而不是一格

    t = [1, 4, 5, 7, 8]
    rs = []

    for item in items:
        for i in t:
            rs.append(item[item_list[i]])
        worksheet.write_row('A'+str(row), rs)
        row += 1
        rs = []
    return True


# 拼接核心常量请求参数

query_dict['lang'] = language_list[-2]
# query_dict作用完成

mihoyo_api_url = url_join_args(mihoyo_api, query_dict)
# 初始化row

row = 2

# 打开XLSX

self_path = os.path.dirname(os.path.realpath(sys.argv[0]))
workbook = xlsxwriter.Workbook(F"{self_path}\\gachaExport.xlsx")


# 遍历三个祈愿池

for i in gacha_type:
    # 设置变量部分查询字典的祈愿池参数

    added_dict['gacha_type'] = gacha_type[i]
    # 切换工作表并写入表头

    worksheet = workbook.add_worksheet(str(i))
    header = ['祈愿池', '时间', '名称', '类型', '星级']

    worksheet.write_row('A1', header)

    while True:
        # 通过end_id来判断是否结束一个祈愿池的查询

        data = get_history_log(mihoyo_api_url, added_dict)
        if write_xlsx(worksheet, data[0], row):
            row += 10
        added_dict['page'] += 1
        added_dict['end_id'] = data[0][-1][item_list[9]] if data[1] else '0'

        if not data[1]:
            # 查询完毕,重置查询参数开始下一个

            added_dict['page'] = 1
            row = 2
            break

        # 测试用

        # if added_dict['page'] > 5:
        #     break
        time.sleep(0.2)
    break
# 在close之前数据都不会写入

workbook.close()