Commit 985e5882 authored by 陈俊羽's avatar 陈俊羽
Browse files

initial v0.1.0

parent 4ceba24a
/tmp/
/.idea/
# KnightReport
坎公骑冠剑会战统计工具
rm -r -Force ./tmp
mkdir tmp
cd tmp
pyinstaller -Fw ../src/main.py ../src/ctrl.py ../src/constants.py ../src/utils.py -n KnightReport
cd ..
\ No newline at end of file
# 坎公骑冠剑——超级骑士报表
* 想知道自己总共出了多少刀?
* 想知道自己是哪天忘记了出刀?
* 想看看是哪个群友天天在抢将军?
* 会长想鲨人却不知道该鲨谁?
为了解决bigfun接口缺乏统计功能的问题,特推出开箱即用的会战报表系统!
只需要双击一下,就可以生成详细的会战报表,方便大家详细了解自己的总出刀数、对各个boss出刀数、缺刀日期等数据。
[TOC]
## 准备工作
* 依赖Windows系统和Edge浏览器(macOS/Linux上的Edge理论上也行,但咱懒得测试了)
* 如果需要从源码运行,除上述需求以外,需要准备`Python`环境并安装`browser-cookies3`
* 如果需要从源码打包,除上述需求以外,还需要安装`pyinstaller`
## 如何运行
有三种方法可以使用到本程序功能:
### 直接运行
从release中下载可执行文件`KnightReport.exe`,双击运行
### 从源码运行
执行以下命令
```
mkdir tmp
cd tmp
python ../src/main.py
```
### 从源码打包
下载源码后,运行`build.ps1`进行打包,可执行文件将会在当前目录下的`tmp/dist/KnightReport.exe`
## 使用方法
1. 程序会启动bigfun的登录界面,如果是未登录状态,点击**bilibili账号登录**开始正常登录流程
2. 确认登陆完成后,**可以关闭网页**,等待程序结束
3. 如果已经是登陆状态,**可以直接关闭网页**
日志将会生成在当前目录下的`KnightReport.log`
错误信息将会生成在当前目录下的`KnightReport.err`
会战报表将会生成在当前目录下的`report.csv`
## 数据描述
报表中有以下数据:
| 题头 | 描述 |
| -------------- | ------------------------------------------------------------ |
| uid | 玩家uid |
| 玩家 | 玩家用户名 |
| 出刀 | 本次会战期间玩家总出刀次数 |
| 伤害 | 本次会战期间玩家总造成伤害 |
| XXX出刀 | 本次会战期间玩家对XXX(boss名)出刀次数 |
| XXX伤害 | 本次会战期间玩家对XXX(boss名)造成伤害 |
| YYYY-MM-DD漏刀 | 玩家在YYYY-MM-DD(日期)漏刀次数(-1为本日只出了2刀,-2为本日只出了一刀,-3为本日未出刀,无漏刀不显示) |
## 使用到的接口
为方便二次开发,特提供可用的WEB API
定义在`constants.py`中:
```python
# 用于人工介入的用户登录
# web API for user login
LoginURL = 'https://www.bigfun.cn/tools/gt/'
# 用于读取公会成员数据和会战日期区间
# web API for guild status
GuildStatusURL = "https://www.bigfun.cn/api/feweb?target=kan-gong-guild-log-filter%2Fa"
# 用于读取每日会战数据(需要日期参数)
# web API for combat status(need argument date)
DateStatusURL = "https://www.bigfun.cn/api/feweb?target=kan-gong-guild-report%2Fa&date={:s}"
```
## 平台测试
已经在以下平台上测试通过
| program | version |
| --------------- | -------------- |
| Windows | 10(19042.1110) |
| Python | 3.9.4 |
| browser-cookies | 0.12.1 |
| pyinstaller | 4.5.1 |
## 特别鸣谢
[bigfun](https://www.bigfun.cn/)[坎公百宝袋](https://www.bigfun.cn/tools/gt/)提供了全部API支持
[browser-cookies](https://github.com/borisbabic/browser_cookie3)提供了获得本地浏览器cookies的方案
import webbrowser
# initial edge hook for webbrowser
edge_path = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
webbrowser.register('edge', None, webbrowser.BackgroundBrowser(edge_path))
Headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67'
}
# web API for user login
LoginURL = 'https://www.bigfun.cn/tools/gt/'
# web API for guild status
GuildStatusURL = "https://www.bigfun.cn/api/feweb?target=kan-gong-guild-log-filter%2Fa"
# web API for combat status(need argument date)
DateStatusURL = "https://www.bigfun.cn/api/feweb?target=kan-gong-guild-report%2Fa&date={:s}"
import json
import logging
import webbrowser
from os import path
from http.cookiejar import CookieJar
from time import sleep
from typing import Dict, Sequence, Set, Union
import browser_cookie3
import requests
import constants
from utils import Info, Combat
def user_login():
"""
ask user to login for fetch cookies(strictly use Windows Edge)
:return:
"""
ctrl = webbrowser.get(using='edge')
ctrl.open(constants.LoginURL)
class Ctrl:
def __init__(self):
self.cookiesJar: Union[CookieJar, None] = None
self.dates: Sequence[str] = list()
self.uid_name: Dict[int, str] = dict()
self.person_info: Dict[int, Info] = dict()
self.combat: Union[Combat, None] = None
self.all_uid: Set = set()
def exec(self):
r"""
execute the whole generating stream
"""
user_login()
try:
guild_status = self.get_guild()
self.extract_guild_info(guild_status)
for i, date in enumerate(self.dates):
attack_status = self.get_date(date)
self.extract_date_info(i, attack_status)
self.combat.marshal(self.person_info.values(), "./report.csv")
logging.info("会战数据已写入报表 -> {:s}".format(path.abspath("./report.csv")))
logging.info("可以使用Excel或WPS等软件打开csv文件")
except requests.exceptions.ConnectionError:
raise RuntimeError("网络故障")
except ValueError:
raise RuntimeError("请关闭代理")
def get_guild(self) -> Dict:
r"""
send network requests and get guild status
"""
while True:
cj = browser_cookie3.edge(domain_name=".bigfun.cn")
r = requests.get(constants.GuildStatusURL, cookies=cj, headers=constants.Headers)
guild_status = json.loads(r.text)
if guild_status['code'] == 0:
logging.info("成功读取公会数据")
self.cookiesJar = cj
return guild_status
elif guild_status['code'] == 401:
logging.warning("cookies 未正确获取, 请完成登录,等待15秒重试...")
sleep(15)
continue
else:
raise RuntimeError("未知的错误码 {:d}".format(guild_status['code']))
def extract_guild_info(self, guild_status: Dict):
r"""
extract info from guild json data according colleagues and dates
"""
colleagues = guild_status['data']['member']
self.dates = guild_status['data']['date']
self.uid_name = {person['id']: person['name'] for person in colleagues}
self.person_info = {uid: Info(uid, name, len(self.dates)) for uid, name in self.uid_name.items()}
self.combat = Combat(self.dates)
self.all_uid = set(self.uid_name.keys())
def get_date(self, date: str) -> Dict:
r"""
send network requests and get combat status of the day
"""
url = constants.DateStatusURL.format(date)
r = requests.get(url, cookies=self.cookiesJar, headers=constants.Headers)
attack_status = json.loads(r.text)
if attack_status['code'] != 0:
raise RuntimeError("未知的错误码 {:d}".format(attack_status['code']))
else:
logging.info("成功读取{:s}会战数据".format(date))
return attack_status
def extract_date_info(self, index: int, attack_status: Dict):
r"""
extract info from guild json data according combat of a day
"""
uid_today = set()
# People join combat today
for person_attack in attack_status['data']:
uid = person_attack['user_id']
if uid not in uid_today:
uid_today.add(uid)
else:
logging.warning("重复玩家uid {:d}, 已去除".format(uid))
continue
hits = person_attack['damage_num']
damage = person_attack['damage_total']
self.person_info[uid].hits += hits
self.person_info[uid].damage += damage
self.person_info[uid].omission[index] = hits - 3
damage_list = person_attack['damage_list']
for damage_once in damage_list:
boss_name = damage_once['boss_name']
self.combat.add_boss(boss_name)
damage = damage_once['damage']
self.person_info[uid].add_hit(boss_name, damage)
# People don't join combat today
for uid in self.all_uid.difference(uid_today):
self.person_info[uid].omission[index] = -3
import logging
import sys
from ctrl import Ctrl
logging.basicConfig(level=logging.INFO,
filename='KnightReport.log',
filemode='a',
format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s')
fperr = open("KnightReport.err", "w")
sys.stderr = fperr
version = "0.1.0"
website = "https://github.com/cutecutecat/KnightReport"
if __name__ == "__main__":
logging.info("坎公骑冠剑————超级骑士报表————v{:s}".format(version))
logging.info("by [ NGA | bigfun | TapTap ]@星星星痕")
logging.info("如有疑问或建议可以到{:s}提交issue".format(website))
tool = Ctrl()
tool.exec()
fperr.close()
import csv
from typing import Sequence, Dict, List
class Info:
omit_mapping = {0: "", -1: "-1", -2: "-2", -3: "-3"}
def __init__(self, uid: int, name: str, days: int):
self.uid: int = uid
self.name: str = name
self.hits: int = 0
self.damage: int = 0
self.boss_hits: Dict[str, int] = {}
self.boss_damage: Dict[str, int] = {}
# omit can be [-3, 0], -x equals omit x hit one day
self.omission: List[int] = [0] * days
def add_hit(self, boss_target: str, damage: int):
if not self.boss_hits.__contains__(boss_target):
self.boss_hits[boss_target] = 1
self.boss_damage[boss_target] = damage
else:
self.boss_hits[boss_target] += 1
self.boss_damage[boss_target] += damage
def to_list(self, boss_names: Sequence[str]):
boss_status = []
for name in boss_names:
if self.boss_hits.__contains__(name):
boss_status.extend([self.boss_hits[name], self.boss_damage[name]])
else:
boss_status.extend([0, 0])
date_status = [self.omit_mapping[omit] for omit in self.omission]
status = [self.uid, self.name, self.hits, self.damage]
status.extend(boss_status)
status.extend(date_status)
return status
def __repr__(self):
return str(self.__dict__)
def __lt__(self, other):
if self.hits != other.hits:
return self.hits > other.hits
elif self.damage != other.damage:
return self.damage > other.damage
return 0
class Combat:
def __init__(self, dates: Sequence[str]):
self.dates = dates
self.boss_name = list()
self._boss_set = set()
def has_boss(self, name: str):
return self._boss_set.__contains__(name)
def add_boss(self, name: str):
if not self.has_boss(name):
self.boss_name.append(name)
self._boss_set.add(name)
def marshal(self, person_list: Sequence[Info], filename: str):
headers = ["uid", "玩家", "出刀", "伤害"]
for boss in self.boss_name:
headers.extend(["{:s}出刀".format(boss), "{:s}伤害".format(boss)])
headers.extend("{:s}漏刀".format(date) for date in self.dates)
person_list = sorted(person_list)
rows = [person.to_list(self.boss_name) for person in person_list]
with open(filename, 'w', encoding='utf_8_sig') as f:
f_csv = csv.writer(f)
f_csv.writerow(headers)
f_csv.writerows(rows)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment