mirror of
https://github.com/PaiGramTeam/PaiGram.git
synced 2024-11-25 09:37:30 +00:00
♻️ 重写 wiki 模块和相关插件
1. 使用 `pydantic` 重写了 wiki 模块所使用的 model 2. 添加了 weapon_level.json 用于后续计算武器升级所需的经验 3. 修改了 wiki 插件,以适应新的 model
This commit is contained in:
parent
2b76ce9258
commit
91a133b694
@ -1,5 +1,6 @@
|
|||||||
import ujson
|
import ujson as json
|
||||||
|
|
||||||
|
from models.wiki.base import Model
|
||||||
from utils.redisdb import RedisDB
|
from utils.redisdb import RedisDB
|
||||||
|
|
||||||
|
|
||||||
@ -8,14 +9,22 @@ class WikiCache:
|
|||||||
self.client = redis.client
|
self.client = redis.client
|
||||||
self.qname = "wiki"
|
self.qname = "wiki"
|
||||||
|
|
||||||
async def refresh_info_cache(self, key_name: str, info):
|
async def set(self, key: str, value):
|
||||||
qname = f"{self.qname}:{key_name}"
|
qname = f"{self.qname}:{key}"
|
||||||
await self.client.set(qname, ujson.dumps(info))
|
if isinstance(value, Model):
|
||||||
|
value = value.json()
|
||||||
|
elif isinstance(value, (dict, list)):
|
||||||
|
value = json.dumps(value)
|
||||||
|
await self.client.set(qname, value)
|
||||||
|
|
||||||
async def del_one(self, key_name: str):
|
async def delete(self, key: str):
|
||||||
qname = f"{self.qname}:{key_name}"
|
qname = f"{self.qname}:{key}"
|
||||||
await self.client.delete(qname)
|
await self.client.delete(qname)
|
||||||
|
|
||||||
async def get_one(self, key_name: str) -> str:
|
async def get(self, key: str) -> dict:
|
||||||
qname = f"{self.qname}:{key_name}"
|
qname = f"{self.qname}:{key}"
|
||||||
return await self.client.get(qname)
|
result = json.loads(await self.client.get(qname))
|
||||||
|
if isinstance(result, list) and len(result) > 0:
|
||||||
|
for num, item in enumerate(result):
|
||||||
|
result[num] = json.loads(item)
|
||||||
|
return result
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import asyncio
|
from typing import List, NoReturn, Optional
|
||||||
|
|
||||||
import ujson
|
|
||||||
|
|
||||||
|
from core.wiki.cache import WikiCache
|
||||||
from logger import Log
|
from logger import Log
|
||||||
from models.wiki.characters import Characters
|
from models.wiki.character import Character
|
||||||
from models.wiki.weapons import Weapons
|
from models.wiki.weapon import Weapon
|
||||||
from .cache import WikiCache
|
|
||||||
|
|
||||||
|
|
||||||
class WikiService:
|
class WikiService:
|
||||||
@ -13,58 +11,47 @@ class WikiService:
|
|||||||
def __init__(self, cache: WikiCache):
|
def __init__(self, cache: WikiCache):
|
||||||
self._cache = cache
|
self._cache = cache
|
||||||
"""Redis 在这里的作用是作为持久化"""
|
"""Redis 在这里的作用是作为持久化"""
|
||||||
self.weapons = Weapons()
|
self._character_list = []
|
||||||
self.characters = Characters()
|
self._character_name_list = []
|
||||||
self._characters_list = []
|
self._weapon_name_list = []
|
||||||
self._characters_name_list = []
|
self._weapon_list = []
|
||||||
self._weapons_name_list = []
|
|
||||||
self._weapons_list = []
|
|
||||||
self.first_run = True
|
self.first_run = True
|
||||||
|
|
||||||
async def refresh_weapon(self):
|
async def refresh_weapon(self) -> NoReturn:
|
||||||
weapon_url_list = await self.weapons.get_all_weapon_url()
|
weapon_name_list = await Weapon.get_name_list()
|
||||||
Log.info(f"一共找到 {len(weapon_url_list)} 把武器信息")
|
Log.info(f"一共找到 {len(weapon_name_list)} 把武器信息")
|
||||||
weapons_list = []
|
|
||||||
task_list = []
|
weapon_list = []
|
||||||
for index, weapon_url in enumerate(weapon_url_list):
|
num = 0
|
||||||
task_list.append(self.weapons.get_weapon_info(weapon_url))
|
async for weapon in Weapon.full_data_generator():
|
||||||
# weapon_info = await self.weapons.get_weapon_info(weapon_url)
|
weapon_list.append(weapon)
|
||||||
if index % 5 == 0:
|
num += 1
|
||||||
result_list = await asyncio.gather(*task_list)
|
if num % 10 == 0:
|
||||||
weapons_list.extend(result for result in result_list if isinstance(result, dict))
|
Log.info(f"现在已经获取到 {num} 把武器信息")
|
||||||
task_list.clear()
|
|
||||||
if index % 10 == 0 and index != 0:
|
|
||||||
Log.info(f"现在已经获取到 {index} 把武器信息")
|
|
||||||
result_list = await asyncio.gather(*task_list)
|
|
||||||
weapons_list.extend(result for result in result_list if isinstance(result, dict))
|
|
||||||
|
|
||||||
Log.info("写入武器信息到Redis")
|
Log.info("写入武器信息到Redis")
|
||||||
self._weapons_list = weapons_list
|
self._weapon_list = weapon_list
|
||||||
await self._cache.del_one("weapon")
|
await self._cache.delete("weapon")
|
||||||
await self._cache.refresh_info_cache("weapon", weapons_list)
|
await self._cache.set("weapon", [i.json() for i in weapon_list])
|
||||||
|
|
||||||
async def refresh_characters(self):
|
async def refresh_characters(self) -> NoReturn:
|
||||||
characters_url_list = await self.characters.get_all_characters_url()
|
character_name_list = await Character.get_name_list()
|
||||||
Log.info(f"一共找到 {len(characters_url_list)} 个角色信息")
|
Log.info(f"一共找到 {len(character_name_list)} 个角色信息")
|
||||||
characters_list = []
|
|
||||||
task_list = []
|
character_list = []
|
||||||
for index, characters_url in enumerate(characters_url_list):
|
num = 0
|
||||||
task_list.append(self.characters.get_characters(characters_url))
|
async for character in Character.full_data_generator():
|
||||||
if index % 5 == 0:
|
character_list.append(character)
|
||||||
result_list = await asyncio.gather(*task_list)
|
num += 1
|
||||||
characters_list.extend(result for result in result_list if isinstance(result, dict))
|
if num % 10 == 0:
|
||||||
task_list.clear()
|
Log.info(f"现在已经获取到 {num} 个角色信息")
|
||||||
if index % 10 == 0 and index != 0:
|
|
||||||
Log.info(f"现在已经获取到 {index} 个角色信息")
|
|
||||||
result_list = await asyncio.gather(*task_list)
|
|
||||||
characters_list.extend(result for result in result_list if isinstance(result, dict))
|
|
||||||
|
|
||||||
Log.info("写入角色信息到Redis")
|
Log.info("写入角色信息到Redis")
|
||||||
self._characters_list = characters_list
|
self._character_list = character_list
|
||||||
await self._cache.del_one("characters")
|
await self._cache.delete("characters")
|
||||||
await self._cache.refresh_info_cache("characters", characters_list)
|
await self._cache.set("characters", [i.json() for i in character_list])
|
||||||
|
|
||||||
async def refresh_wiki(self):
|
async def refresh_wiki(self) -> NoReturn:
|
||||||
"""
|
"""
|
||||||
用于把Redis的缓存全部加载进Python
|
用于把Redis的缓存全部加载进Python
|
||||||
:return:
|
:return:
|
||||||
@ -76,40 +63,39 @@ class WikiService:
|
|||||||
await self.refresh_characters()
|
await self.refresh_characters()
|
||||||
Log.info("刷新成功")
|
Log.info("刷新成功")
|
||||||
|
|
||||||
async def init(self):
|
async def init(self) -> NoReturn:
|
||||||
"""
|
"""
|
||||||
用于把Redis的缓存全部加载进Python
|
用于把Redis的缓存全部加载进Python
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if self.first_run:
|
if self.first_run:
|
||||||
weapon_dict = await self._cache.get_one("weapon")
|
weapon_dict = await self._cache.get("weapon")
|
||||||
self._weapons_list = ujson.loads(weapon_dict)
|
self._weapon_list = [Weapon.parse_obj(obj) for obj in weapon_dict]
|
||||||
for weapon in self._weapons_list:
|
self._weapon_name_list = await Weapon.get_name_list()
|
||||||
self._weapons_name_list.append(weapon["name"])
|
characters_dict = await self._cache.get("characters")
|
||||||
characters_dict = await self._cache.get_one("characters")
|
self._character_list = [Character.parse_obj(obj) for obj in characters_dict]
|
||||||
self._characters_list = ujson.loads(characters_dict)
|
self._character_name_list = await Character.get_name_list()
|
||||||
for characters in self._characters_list:
|
|
||||||
self._characters_name_list.append(characters["name"])
|
|
||||||
self.first_run = False
|
self.first_run = False
|
||||||
|
|
||||||
async def get_weapons(self, name: str):
|
async def get_weapons(self, name: str) -> Optional[Weapon]:
|
||||||
await self.init()
|
await self.init()
|
||||||
if len(self._weapons_list) == 0:
|
if len(self._weapon_list) == 0:
|
||||||
return {}
|
return None
|
||||||
return next((weapon for weapon in self._weapons_list if weapon["name"] == name), {})
|
return next((weapon for weapon in self._weapon_list if weapon.name == name), None)
|
||||||
|
|
||||||
async def get_weapons_name_list(self) -> list:
|
async def get_weapons_name_list(self) -> List[str]:
|
||||||
await self.init()
|
await self.init()
|
||||||
return self._weapons_name_list
|
return self._weapon_name_list
|
||||||
|
|
||||||
async def get_weapons_list(self) -> list:
|
async def get_weapons_list(self) -> List[Weapon]:
|
||||||
await self.init()
|
await self.init()
|
||||||
return self._weapons_list
|
return self._weapon_list
|
||||||
|
|
||||||
async def get_characters_list(self) -> list:
|
async def get_characters_list(self) -> List[Character]:
|
||||||
await self.init()
|
await self.init()
|
||||||
return self._characters_list
|
return self._character_list
|
||||||
|
|
||||||
async def get_characters_name_list(self) -> list:
|
async def get_characters_name_list(self) -> List[str]:
|
||||||
await self.init()
|
await self.init()
|
||||||
return self._characters_name_list
|
return self._character_name_list
|
||||||
|
@ -1,67 +1,195 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# noinspection SpellCheckingInspection
|
||||||
roles = {
|
roles = {
|
||||||
10000003: ["琴", "Jean", "jean", "团长", "代理团长", "琴团长", "蒲公英骑士"],
|
10000002: [
|
||||||
10000006: ["丽莎", "Lisa", "lisa", "图书管理员", "图书馆管理员", "蔷薇魔女"],
|
"神里绫华", "Kamisato Ayaka", "Ayaka", "ayaka", "神里", "绫华", "神里凌华", "凌华", "白鹭公主", "神里大小姐"
|
||||||
10000005: ["空", "男主", "男主角", "龙哥", "空哥", "旅行者", "卑鄙的外乡人", "荣誉骑士", "爷", "风主", "岩主", "雷主", "履刑者", "抽卡不歪真君"],
|
],
|
||||||
10000007: ["荧", "女主", "女主角", "莹", "萤", "黄毛阿姨", "荧妹"],
|
10000003: [
|
||||||
10000014: ["芭芭拉", "Barbara", "barbara", "巴巴拉", "拉粑粑", "拉巴巴", "内鬼", "加湿器", "闪耀偶像", "偶像"],
|
"琴", "Jean", "jean", "团长", "代理团长", "琴团长", "蒲公英骑士"
|
||||||
10000015: ["凯亚", "Kaeya", "kaeya", "盖亚", "凯子哥", "凯鸭", "矿工", "矿工头子", "骑兵队长", "凯子", "凝冰渡海真君"],
|
],
|
||||||
10000016: ["迪卢克", "diluc", "Diluc", "卢姥爷", "姥爷", "卢老爷", "卢锅巴", "正义人", "正e人", "正E人", "卢本伟", "暗夜英雄", "卢卢伯爵",
|
10000005: [
|
||||||
"落魄了", "落魄了家人们"],
|
"空", "男主", "男主角", "龙哥", "空哥"
|
||||||
10000020: ["雷泽", "razor", "Razor", "狼少年", "狼崽子", "狼崽", "卢皮卡", "小狼", "小狼狗"],
|
],
|
||||||
10000021: ["安柏", "Amber", "amber", "安伯", "兔兔伯爵", "飞行冠军", "侦查骑士", "点火姬", "点火机", "打火机", "打火姬", ],
|
10000006: [
|
||||||
10000022: ["温迪", "Venti", "venti", "温蒂", "风神", "卖唱的", "巴巴托斯", "巴巴脱丝", "芭芭托斯", "芭芭脱丝", "干点正事", "不干正事", "吟游诗人",
|
"丽莎", "Lisa", "lisa", "图书管理员", "图书馆管理员", "蔷薇魔女"
|
||||||
"诶嘿", "唉嘿", "摸鱼", ],
|
],
|
||||||
10000023: ["香菱", "Xiangling", "xiangling", "香玲", "锅巴", "厨师", "万民堂厨师", "香师傅"],
|
10000007: [
|
||||||
10000024: ["北斗", "Beidou", "beidou", "大姐头", "大姐", "无冕的龙王", "龙王"],
|
"荧", "女主", "女主角", "莹", "萤", "黄毛阿姨", "荧妹"
|
||||||
10000025: ["行秋", "Xingqiu", "xingqiu", "秋秋人", "秋妹妹", "书呆子", "水神", "飞云商会二少爷"],
|
],
|
||||||
10000026: ["魈", "Xiao", "xiao", "杏仁豆腐", "打桩机", "插秧", "三眼五显仙人", "三眼五显真人", "降魔大圣", "护法夜叉", "快乐风男", "无聊", "靖妖傩舞",
|
10000014: [
|
||||||
"矮子仙人", "三点五尺仙人", "跳跳虎"],
|
"芭芭拉", "Barbara", "barbara", "巴巴拉", "拉粑粑", "拉巴巴", "内鬼", "加湿器", "闪耀偶像", "偶像"
|
||||||
10000027: ["凝光", "Ningguang", "ningguang", "富婆", "天权星"],
|
],
|
||||||
10000029: ["可莉", "Klee", "klee", "嘟嘟可", "火花骑士", "蹦蹦炸弹", "炸鱼", "放火烧山", "放火烧山真君", "蒙德最强战力", "逃跑的太阳", "啦啦啦", "哒哒哒",
|
10000015: [
|
||||||
"炸弹人", "禁闭室", ],
|
"凯亚", "Kaeya", "kaeya", "盖亚", "凯子哥", "凯鸭", "矿工", "矿工头子", "骑兵队长", "凯子", "凝冰渡海真君"
|
||||||
10000030: ["钟离", "Zhongli", "zhongli", "摩拉克斯", "岩王爷", "岩神", "钟师傅", "天动万象", "岩王帝君", "未来可期", "帝君", "拒收病婿"],
|
],
|
||||||
10000031: ["菲谢尔", "Fischl", "fischl", "皇女", "小艾米", "小艾咪", "奥兹", "断罪皇女", "中二病", "中二少女", "中二皇女", "奥兹发射器"],
|
10000016: [
|
||||||
10000032: ["班尼特", "Bennett", "bennett", "点赞哥", "点赞", "倒霉少年", "倒霉蛋", "霹雳闪雷真君", "班神", "班爷", "倒霉", "火神", "六星真神"],
|
"迪卢克", "diluc", "Diluc", "卢姥爷", "姥爷", "卢老爷", "卢锅巴", "正义人", "正e人", "正E人", "卢本伟",
|
||||||
10000033: ["达达利亚", "Tartaglia", "tartaglia", "Childe", "childe", "Ajax", "ajax", "达达鸭", "达达利鸭", "公子", "玩具销售员",
|
"暗夜英雄", "卢卢伯爵", "落魄了", "落魄了家人们"
|
||||||
"玩具推销员", "钱包", "鸭鸭", "愚人众末席"],
|
],
|
||||||
10000034: ["诺艾尔", "Noelle", "noelle", "女仆", "高达", "岩王帝姬"],
|
10000020: [
|
||||||
10000035: ["七七", "Qiqi", "qiqi", "僵尸", "肚饿真君", "度厄真君"],
|
"雷泽", "razor", "Razor", "狼少年", "狼崽子", "狼崽", "卢皮卡", "小狼", "小狼狗"
|
||||||
10000036: ["重云", "Chongyun", "chongyun", "纯阳之体", "冰棍"],
|
],
|
||||||
10000037: ["甘雨", "Ganyu", "ganyu", "椰羊", "椰奶", "王小美"],
|
10000021: [
|
||||||
10000038: ["阿贝多", "Albedo", "albedo", "可莉哥哥", "升降机", "升降台", "电梯", "白垩之子", "贝爷", "白垩", "阿贝少", "花呗多", "阿贝夕",
|
"安柏", "Amber", "amber", "安伯", "兔兔伯爵", "飞行冠军", "侦查骑士", "点火姬", "点火机", "打火机", "打火姬"
|
||||||
"abd", "阿师傅"],
|
],
|
||||||
10000039: ["迪奥娜", "Diona", "diona", "迪欧娜", "dio", "dio娜", "冰猫", "猫猫", "猫娘", "喵喵", "调酒师"],
|
10000022: [
|
||||||
10000041: ["莫娜", "Mona", "mona", "穷鬼", "穷光蛋", "穷", "莫纳", "占星术士", "占星师", "讨龙真君", "半部讨龙真君", "阿斯托洛吉斯·莫娜·梅姬斯图斯"],
|
"温迪", "Venti", "venti", "温蒂", "风神", "卖唱的", "巴巴托斯", "巴巴脱丝", "芭芭托斯", "芭芭脱丝", "干点正事",
|
||||||
10000042: ["刻晴", "Keqing", "keqing", "刻情", "氪晴", "刻师傅", "刻师父", "牛杂", "牛杂师傅", "斩尽牛杂", "免疫", "免疫免疫", "屁斜剑法",
|
"不干正事", "吟游诗人", "诶嘿", "唉嘿", "摸鱼"
|
||||||
"玉衡星", "阿晴", "啊晴"],
|
],
|
||||||
10000043: ["砂糖", "Sucrose", "sucrose", "雷莹术士", "雷萤术士", "雷荧术士"],
|
10000023: [
|
||||||
10000044: ["辛焱", "Xinyan", "xinyan", "辛炎", "黑妹", "摇滚"],
|
"香菱", "Xiangling", "xiangling", "香玲", "锅巴", "厨师", "万民堂厨师", "香师傅"
|
||||||
10000045: ["罗莎莉亚", "Rosaria", "rosaria", "罗莎莉娅", "白色史莱姆", "白史莱姆", "修女", "罗莎利亚", "罗莎利娅", "罗沙莉亚", "罗沙莉娅", "罗沙利亚",
|
],
|
||||||
"罗沙利娅", "萝莎莉亚", "萝莎莉娅", "萝莎利亚", "萝莎利娅", "萝沙莉亚", "萝沙莉娅", "萝沙利亚", "萝沙利娅"],
|
10000024: [
|
||||||
10000046: ["胡桃", "Hu Tao", "hu tao", "HuTao", "hutao", "Hutao", "胡淘", "往生堂堂主", "火化", "抬棺的", "蝴蝶", "核桃", "堂主",
|
"北斗", "Beidou", "beidou", "大姐头", "大姐", "无冕的龙王", "龙王"
|
||||||
"胡堂主", "雪霁梅香"],
|
],
|
||||||
10000047: ["枫原万叶", "Kaedehara Kazuha", "Kazuha", "kazuha", "万叶", "叶天帝", "天帝", "叶师傅"],
|
10000025: [
|
||||||
10000048: ["烟绯", "Yanfei", "yanfei", "烟老师", "律师", "罗翔"],
|
"行秋", "Xingqiu", "xingqiu", "秋秋人", "秋妹妹", "书呆子", "水神", "飞云商会二少爷"
|
||||||
10000051: ["优菈", "Eula", "eula", "优拉", "尤拉", "尤菈", "浪花骑士", "记仇", "劳伦斯"],
|
],
|
||||||
10000002: ["神里绫华", "Kamisato Ayaka", "Ayaka", "ayaka", "神里", "绫华", "神里凌华", "凌华", "白鹭公主", "神里大小姐"],
|
10000026: [
|
||||||
10000049: ["宵宫", "Yoimiya", "yoimiya", "霄宫", "烟花", "肖宫", "肖工", "绷带女孩"],
|
"魈", "Xiao", "xiao", "杏仁豆腐", "打桩机", "插秧", "三眼五显仙人", "三眼五显真人", "降魔大圣", "护法夜叉",
|
||||||
10000052: ["雷电将军", "Raiden Shogun", "Raiden", "raiden", "雷神", "将军", "雷军", "巴尔", "阿影", "影", "巴尔泽布", "煮饭婆", "奶香一刀",
|
"快乐风男", "无聊", "靖妖傩舞", "矮子仙人", "三点五尺仙人", "跳跳虎"
|
||||||
"无想一刀", "宅女"],
|
],
|
||||||
10000053: ["早柚", "Sayu", "sayu", "小狸猫", "狸猫", "忍者"],
|
10000027: [
|
||||||
10000054: ["珊瑚宫心海", "Sangonomiya Kokomi", "Kokomi", "kokomi", "心海", "军师", "珊瑚宫", "书记", "观赏鱼", "水母", "鱼", "美人鱼"],
|
"凝光", "Ningguang", "ningguang", "富婆", "天权星"
|
||||||
10000056: ["九条裟罗", "Kujou Sara", "Sara", "sara", "九条", "九条沙罗", "裟罗", "沙罗", "天狗"],
|
],
|
||||||
10000062: ["埃洛伊", "Aloy", "aloy"],
|
10000029: [
|
||||||
10000050: ["托马", "Thoma", "thoma", "家政官", "太郎丸", "地头蛇", "男仆", "拖马"],
|
"可莉", "Klee", "klee", "嘟嘟可", "火花骑士", "蹦蹦炸弹", "炸鱼", "放火烧山", "放火烧山真君", "蒙德最强战力",
|
||||||
10000055: ["五郎", "Gorou", "gorou", "柴犬", "土狗", "希娜", "希娜小姐"],
|
"逃跑的太阳", "啦啦啦", "哒哒哒", "炸弹人", "禁闭室"
|
||||||
10000057: ["荒泷一斗", "Arataki Itto", "Itto", "itto", "荒龙一斗", "荒泷天下第一斗", "一斗", "一抖", "荒泷", "1斗", "牛牛", "斗子哥", "牛子哥",
|
],
|
||||||
"牛子", "孩子王", "斗虫", "巧乐兹", "放牛的"],
|
10000030: [
|
||||||
10000058: ["八重神子", "Yae Miko", "Miko", "miko", "八重", "神子", "狐狸", "想得美哦", "巫女", "屑狐狸", "骚狐狸", "八重宫司", "婶子", "小八"],
|
"钟离", "Zhongli", "zhongli", "摩拉克斯", "岩王爷", "岩神", "钟师傅", "天动万象", "岩王帝君", "未来可期",
|
||||||
10000059: ["鹿野院平藏", "shikanoin heizou", "Heizou", "heizou", "heizo", "鹿野苑", "鹿野院", "平藏", "鹿野苑平藏"],
|
"帝君", "拒收病婿"
|
||||||
10000060: ["夜兰", "Yelan", "yelan", "夜阑", "叶澜", "腋兰", "夜天后"],
|
],
|
||||||
10000063: ["申鹤", "Shenhe", "shenhe", "神鹤", "小姨", "小姨子", "审鹤"],
|
10000031: [
|
||||||
10000064: ["云堇", "Yun Jin", "yunjin", "yun jin", "云瑾", "云先生", "云锦", "神女劈观"],
|
"菲谢尔", "Fischl", "fischl", "皇女", "小艾米", "小艾咪", "奥兹", "断罪皇女", "中二病", "中二少女", "中二皇女",
|
||||||
10000065: ["久岐忍", "Kuki Shinobu", "Kuki", "kuki", "Shinobu", "shinobu", "97忍", "小忍", "久歧忍", "97", "茄忍", "阿忍", "忍姐"],
|
"奥兹发射器"
|
||||||
10000066: ["神里绫人", "Kamisato Ayato", "Ayato", "ayato", "绫人", "神里凌人", "凌人", "0人", "神人", "零人", "大舅哥"],
|
],
|
||||||
|
10000032: [
|
||||||
|
"班尼特", "Bennett", "bennett", "点赞哥", "点赞", "倒霉少年", "倒霉蛋", "霹雳闪雷真君", "班神", "班爷", "倒霉",
|
||||||
|
"火神", "六星真神"
|
||||||
|
],
|
||||||
|
10000033: [
|
||||||
|
"达达利亚", "Tartaglia", "tartaglia", "Childe", "childe", "Ajax", "ajax", "达达鸭", "达达利鸭", "公子",
|
||||||
|
"玩具销售员", "玩具推销员", "钱包", "鸭鸭", "愚人众末席"
|
||||||
|
],
|
||||||
|
10000034: [
|
||||||
|
"诺艾尔", "Noelle", "noelle", "女仆", "高达", "岩王帝姬"
|
||||||
|
],
|
||||||
|
10000035: [
|
||||||
|
"七七", "Qiqi", "qiqi", "僵尸", "肚饿真君", "度厄真君", "77"
|
||||||
|
],
|
||||||
|
10000036: [
|
||||||
|
"重云", "Chongyun", "chongyun", "纯阳之体", "冰棍"
|
||||||
|
],
|
||||||
|
10000037: [
|
||||||
|
"甘雨", "Ganyu", "ganyu", "椰羊", "椰奶", "王小美"
|
||||||
|
],
|
||||||
|
10000038: [
|
||||||
|
"阿贝多", "Albedo", "albedo", "可莉哥哥", "升降机", "升降台", "电梯", "白垩之子", "贝爷", "白垩", "阿贝少",
|
||||||
|
"花呗多", "阿贝夕", "abd", "阿师傅"
|
||||||
|
],
|
||||||
|
10000039: [
|
||||||
|
"迪奥娜", "Diona", "diona", "迪欧娜", "dio", "dio娜", "冰猫", "猫猫", "猫娘", "喵喵", "调酒师"
|
||||||
|
],
|
||||||
|
10000041: [
|
||||||
|
"莫娜", "Mona", "mona", "穷鬼", "穷光蛋", "穷", "莫纳", "占星术士", "占星师", "讨龙真君", "半部讨龙真君",
|
||||||
|
"阿斯托洛吉斯·莫娜·梅姬斯图斯", "梅姬斯图斯", "梅姬斯图斯卿"
|
||||||
|
],
|
||||||
|
10000042: [
|
||||||
|
"刻晴", "Keqing", "keqing", "刻情", "氪晴", "刻师傅", "刻师父", "牛杂", "牛杂师傅", "斩尽牛杂", "免疫",
|
||||||
|
"免疫免疫", "屁斜剑法", "玉衡星", "阿晴", "啊晴"
|
||||||
|
],
|
||||||
|
10000043: [
|
||||||
|
"砂糖", "Sucrose", "sucrose", "雷莹术士", "雷萤术士", "雷荧术士"
|
||||||
|
],
|
||||||
|
10000044: [
|
||||||
|
"辛焱", "Xinyan", "xinyan", "辛炎", "黑妹", "摇滚"
|
||||||
|
],
|
||||||
|
10000045: [
|
||||||
|
"罗莎莉亚", "Rosaria", "rosaria", "罗莎莉娅", "白色史莱姆", "白史莱姆", "修女", "罗莎利亚", "罗莎利娅",
|
||||||
|
"罗沙莉亚", "罗沙莉娅", "罗沙利亚", "罗沙利娅", "萝莎莉亚", "萝莎莉娅", "萝莎利亚", "萝莎利娅", "萝沙莉亚",
|
||||||
|
"萝沙莉娅", "萝沙利亚", "萝沙利娅"
|
||||||
|
],
|
||||||
|
10000046: [
|
||||||
|
"胡桃", "Hu Tao", "hu tao", "HuTao", "hutao", "Hutao", "胡淘", "往生堂堂主", "火化", "抬棺的", "蝴蝶", "核桃",
|
||||||
|
"堂主", "胡堂主", "雪霁梅香"
|
||||||
|
],
|
||||||
|
10000047: [
|
||||||
|
"枫原万叶", "Kaedehara Kazuha", "Kazuha", "kazuha", "万叶", "叶天帝", "天帝", "叶师傅"
|
||||||
|
],
|
||||||
|
10000048: [
|
||||||
|
"烟绯", "Yanfei", "yanfei", "烟老师", "律师", "罗翔"
|
||||||
|
],
|
||||||
|
10000049: [
|
||||||
|
"宵宫", "Yoimiya", "yoimiya", "霄宫", "烟花", "肖宫", "肖工", "绷带女孩"
|
||||||
|
],
|
||||||
|
10000050: [
|
||||||
|
"托马", "Thoma", "thoma", "家政官", "太郎丸", "地头蛇", "男仆", "拖马"
|
||||||
|
],
|
||||||
|
10000051: [
|
||||||
|
"优菈", "Eula", "eula", "优拉", "尤拉", "尤菈", "浪花骑士", "记仇", "劳伦斯"
|
||||||
|
],
|
||||||
|
10000052: [
|
||||||
|
"雷电将军", "Raiden Shogun", "Raiden", "raiden", "雷神", "将军", "雷军", "巴尔", "阿影", "影", "巴尔泽布",
|
||||||
|
"煮饭婆", "奶香一刀", "无想一刀", "宅女"
|
||||||
|
],
|
||||||
|
10000053: [
|
||||||
|
"早柚", "Sayu", "sayu", "小狸猫", "狸猫", "忍者"
|
||||||
|
],
|
||||||
|
10000054: [
|
||||||
|
"珊瑚宫心海", "Sangonomiya Kokomi", "Kokomi", "kokomi", "心海", "军师", "珊瑚宫", "书记", "观赏鱼", "水母",
|
||||||
|
"鱼", "美人鱼"
|
||||||
|
],
|
||||||
|
10000055: [
|
||||||
|
"五郎", "Gorou", "gorou", "柴犬", "土狗", "希娜", "希娜小姐"
|
||||||
|
],
|
||||||
|
10000056: [
|
||||||
|
"九条裟罗", "Kujou Sara", "Sara", "sara", "九条", "九条沙罗", "裟罗", "沙罗", "天狗"
|
||||||
|
],
|
||||||
|
10000057: [
|
||||||
|
"荒泷一斗", "Arataki Itto", "Itto", "itto", "荒龙一斗", "荒泷天下第一斗", "一斗", "一抖", "荒泷", "1斗", "牛牛",
|
||||||
|
"斗子哥", "牛子哥", "牛子", "孩子王", "斗虫", "巧乐兹", "放牛的"
|
||||||
|
],
|
||||||
|
10000058: [
|
||||||
|
"八重神子", "Yae Miko", "Miko", "miko", "八重", "神子", "狐狸", "想得美哦", "巫女", "屑狐狸", "骚狐狸",
|
||||||
|
"八重宫司", "婶子", "小八"
|
||||||
|
],
|
||||||
|
10000059: [
|
||||||
|
"鹿野院平藏", "shikanoin heizou", "Heizou", "heizou", "heizo", "鹿野苑", "鹿野院", "平藏", "鹿野苑平藏", "鹿野",
|
||||||
|
"小鹿"
|
||||||
|
],
|
||||||
|
10000060: [
|
||||||
|
"夜兰", "Yelan", "yelan", "夜阑", "叶澜", "腋兰", "夜天后"
|
||||||
|
],
|
||||||
|
10000062: [
|
||||||
|
"埃洛伊", "Aloy", "aloy"
|
||||||
|
],
|
||||||
|
10000063: [
|
||||||
|
"申鹤", "Shenhe", "shenhe", "神鹤", "小姨", "小姨子", "审鹤"
|
||||||
|
],
|
||||||
|
10000064: [
|
||||||
|
"云堇", "Yun Jin", "yunjin", "yun jin", "云瑾", "云先生", "云锦", "神女劈观"
|
||||||
|
],
|
||||||
|
10000065: [
|
||||||
|
"久岐忍", "Kuki Shinobu", "Kuki", "kuki", "Shinobu", "shinobu", "97忍", "小忍", "久歧忍", "97", "茄忍", "阿忍",
|
||||||
|
"忍姐"
|
||||||
|
],
|
||||||
|
10000066: [
|
||||||
|
"神里绫人", "Kamisato Ayato", "Ayato", "ayato", "绫人", "神里凌人", "凌人", "0人", "神人", "零人", "大舅哥"
|
||||||
|
],
|
||||||
|
10000067: [
|
||||||
|
"柯莱", "Collei", "collei", "克莱", "科莱", "须弥飞行冠军"
|
||||||
|
],
|
||||||
|
10000068: [
|
||||||
|
"多莉", "Dori", "dori", "多利", "多丽"
|
||||||
|
],
|
||||||
|
10000069: [
|
||||||
|
"提纳里", "Tighnari", "tighnari", "巡林官", "小提", "缇娜里", "提哪里", "提那里"
|
||||||
|
],
|
||||||
|
20000000: [
|
||||||
|
"主角", "旅行者", "卑鄙的外乡人", "荣誉骑士", "爷", "风主", "岩主", "雷主", "草主", "履刑者", "抽卡不歪真君"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
weapons = {
|
weapons = {
|
||||||
"磐岩结绿": ["绿箭", "绿剑"],
|
"磐岩结绿": ["绿箭", "绿剑"],
|
||||||
@ -145,14 +273,15 @@ weapons = {
|
|||||||
|
|
||||||
|
|
||||||
def roleToName(shortname: str) -> str:
|
def roleToName(shortname: str) -> str:
|
||||||
if not shortname:
|
"""讲角色昵称转为正式名"""
|
||||||
return shortname
|
return next((value[0] for value in roles.values() for name in value if name == shortname), shortname)
|
||||||
for value in roles.values():
|
|
||||||
for i in value:
|
|
||||||
if i == shortname:
|
def roleToId(name: str) -> Optional[int]:
|
||||||
return value[0]
|
"""获取角色ID"""
|
||||||
return shortname
|
return next((key for key, value in roles.items() for n in value if n == name), None)
|
||||||
|
|
||||||
|
|
||||||
def weaponToName(shortname: str) -> str:
|
def weaponToName(shortname: str) -> str:
|
||||||
|
"""讲武器昵称转为正式名"""
|
||||||
return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname)
|
return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname)
|
||||||
|
247
models/wiki/base.py
Normal file
247
models/wiki/base.py
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from abc import abstractmethod
|
||||||
|
from asyncio import Queue
|
||||||
|
from multiprocessing import Value
|
||||||
|
from typing import AsyncIterator, ClassVar, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
import ujson as json
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from httpx import AsyncClient, HTTPError, Response, URL
|
||||||
|
from pydantic import (
|
||||||
|
BaseConfig as PydanticBaseConfig,
|
||||||
|
BaseModel as PydanticBaseModel,
|
||||||
|
)
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
__all__ = ['Model', 'WikiModel', 'SCRAPE_HOST']
|
||||||
|
|
||||||
|
SCRAPE_HOST = URL("https://genshin.honeyhunterworld.com/")
|
||||||
|
|
||||||
|
|
||||||
|
class Model(PydanticBaseModel):
|
||||||
|
"""基类"""
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
# 让每次new的时候都解析
|
||||||
|
cls.update_forward_refs()
|
||||||
|
return super(Model, cls).__new__(cls)
|
||||||
|
|
||||||
|
class Config(PydanticBaseConfig):
|
||||||
|
# 使用 ujson 作为解析库
|
||||||
|
json_dumps = json.dumps
|
||||||
|
json_loads = json.loads
|
||||||
|
|
||||||
|
|
||||||
|
class WikiModel(Model):
|
||||||
|
"""wiki所用到的基类
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id (:obj:`int`): ID
|
||||||
|
name (:obj:`str`): 名称
|
||||||
|
rarity (:obj:`int`): 星级
|
||||||
|
|
||||||
|
_client (:class:`httpx.AsyncClient`): 发起 http 请求的 client
|
||||||
|
"""
|
||||||
|
_client: ClassVar[AsyncClient] = AsyncClient()
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
rarity: int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def scrape_urls() -> List[URL]:
|
||||||
|
"""爬取的目标网页集合
|
||||||
|
|
||||||
|
例如有关武器的页面有:
|
||||||
|
[单手剑](https://genshin.honeyhunterworld.com/fam_sword/?lang=CHS)
|
||||||
|
[双手剑](https://genshin.honeyhunterworld.com/fam_claymore/?lang=CHS)
|
||||||
|
[长柄武器](https://genshin.honeyhunterworld.com/fam_polearm/?lang=CHS)
|
||||||
|
。。。
|
||||||
|
这个函数就是返回这些页面的网址所组成的 List
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _client_get(cls, url: Union[URL, str], retry_times: int = 5, sleep: float = 1) -> Response:
|
||||||
|
"""用自己的 client 发起 get 请求的快捷函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 发起请求的 url
|
||||||
|
retry_times: 发生错误时的重复次数。不能小于 0 .
|
||||||
|
sleep: 发生错误后等待重试的时间,单位为秒。
|
||||||
|
Returns:
|
||||||
|
返回对应的请求
|
||||||
|
Raises:
|
||||||
|
请求所需要的异常
|
||||||
|
"""
|
||||||
|
for _ in range(retry_times):
|
||||||
|
try:
|
||||||
|
return await cls._client.get(url, follow_redirects=True)
|
||||||
|
except HTTPError:
|
||||||
|
await anyio.sleep(sleep)
|
||||||
|
return await cls._client.get(url, follow_redirects=True) # 防止 retry_times 等于 0 的时候无法发生请求
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
async def _parse_soup(cls, soup: BeautifulSoup) -> Self:
|
||||||
|
"""解析 soup 生成对应 WikiModel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: 需要解析的 soup
|
||||||
|
Returns:
|
||||||
|
返回对应的 WikiModel
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _scrape(cls, url: Union[URL, str]) -> Self:
|
||||||
|
"""从 url 中爬取数据,并返回对应的 Model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 目标 url. 可以为字符串 str , 也可以为 httpx.URL
|
||||||
|
Returns:
|
||||||
|
返回对应的 WikiModel
|
||||||
|
"""
|
||||||
|
response = await cls._client_get(url)
|
||||||
|
return await cls._parse_soup(BeautifulSoup(response.text, 'lxml'))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_by_id(cls, id_: str) -> Self:
|
||||||
|
"""通过ID获取Model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id_: 目标 ID
|
||||||
|
Returns:
|
||||||
|
返回对应的 WikiModel
|
||||||
|
"""
|
||||||
|
return await cls._scrape(await cls.get_url_by_id(id_))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_by_name(cls, name: str) -> Optional[Self]:
|
||||||
|
"""通过名称获取Model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 目标名
|
||||||
|
Returns:
|
||||||
|
返回对应的 WikiModel
|
||||||
|
"""
|
||||||
|
url = await cls.get_url_by_name(name)
|
||||||
|
if url is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return await cls._scrape(url)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_full_data(cls) -> List[Self]:
|
||||||
|
"""获取全部数据的 Model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
返回能爬到的所有的 Model 所组成的 List
|
||||||
|
"""
|
||||||
|
return [i async for i in cls.full_data_generator()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def full_data_generator(cls) -> AsyncIterator[Self]:
|
||||||
|
"""Model 生成器
|
||||||
|
|
||||||
|
这是一个异步生成器,该函数在使用时会爬取所有数据,并将其转为对应的 Model,然后存至一个队列中
|
||||||
|
当有需要时,再一个一个地迭代取出
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
返回能爬到的所有的 WikiModel 所组成的 List
|
||||||
|
"""
|
||||||
|
queue: Queue[Self] = Queue() # 存放 Model 的队列
|
||||||
|
signal = Value('i', 0) # 一个用于异步任务同步的信号
|
||||||
|
|
||||||
|
async def task(u):
|
||||||
|
# 包装的爬虫任务
|
||||||
|
await queue.put(await cls._scrape(u)) # 爬取一条数据,并将其放入队列中
|
||||||
|
signal.value -= 1 # 信号量减少 1 ,说明该爬虫任务已经完成
|
||||||
|
|
||||||
|
for _, url in await cls.get_name_list(with_url=True): # 遍历爬取所有需要爬取的页面
|
||||||
|
signal.value += 1 # 信号量增加 1 ,说明有一个爬虫任务被添加
|
||||||
|
asyncio.create_task(task(url)) # 创建一个爬虫任务
|
||||||
|
|
||||||
|
while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时
|
||||||
|
yield await queue.get() # 取出并返回一个存放的 Model
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} {super(WikiModel, self).__str__()}>"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_url_by_id(id_: str) -> URL:
|
||||||
|
"""根据 id 获取对应的 url
|
||||||
|
|
||||||
|
例如神里绫华的ID为 ayaka_002,对应的数据页url为 https://genshin.honeyhunterworld.com/ayaka_002/?lang=CHS
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id_ : 实列ID
|
||||||
|
Returns:
|
||||||
|
返回对应的 url
|
||||||
|
"""
|
||||||
|
return SCRAPE_HOST.join(f"{id_}/?lang=CHS")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _name_list_generator(cls, *, with_url: bool = False) -> AsyncIterator[Union[str, Tuple[str, URL]]]:
|
||||||
|
"""一个 Model 的名称 和 其对应 url 的异步生成器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
with_url: 是否返回相应的 url
|
||||||
|
Returns:
|
||||||
|
返回对应的名称列表 或者 名称与url 的列表
|
||||||
|
"""
|
||||||
|
urls = cls.scrape_urls()
|
||||||
|
queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列
|
||||||
|
signal = Value('i', len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数
|
||||||
|
|
||||||
|
async def task(page: URL, s: Value):
|
||||||
|
"""包装的爬虫任务"""
|
||||||
|
response = await cls._client_get(page)
|
||||||
|
# 从页面中获取对应的 chaos data (未处理的json格式字符串)
|
||||||
|
chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', response.text)[0]
|
||||||
|
json_data = json.loads(chaos_data) # 转为 json
|
||||||
|
for data in json_data: # 遍历 json
|
||||||
|
data_name = re.findall(r'>(.*)<', data[1])[0] # 获取 Model 的名称
|
||||||
|
if with_url: # 如果需要返回对应的 url
|
||||||
|
data_url = SCRAPE_HOST.join(re.findall(r'\"(.*?)\"', data[0])[0])
|
||||||
|
await queue.put((data_name, data_url))
|
||||||
|
else:
|
||||||
|
await queue.put(data_name)
|
||||||
|
signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成
|
||||||
|
|
||||||
|
for url in urls: # 遍历需要爬出的页面
|
||||||
|
asyncio.create_task(task(url, signal)) # 添加爬虫任务
|
||||||
|
while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时
|
||||||
|
yield await queue.get() # 取出并返回一个存放的 Model
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]:
|
||||||
|
"""获取全部 Model 的 名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
返回能爬到的所有的 Model 的名称所组成的 List
|
||||||
|
"""
|
||||||
|
return [i async for i in cls._name_list_generator(with_url=with_url)]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_url_by_name(cls, name: str) -> Optional[URL]:
|
||||||
|
"""通过 Model 的名称获取对应的 url
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 实列名
|
||||||
|
Returns:
|
||||||
|
若有对应的实列,则返回对应的 url; 若没有, 则返回 None
|
||||||
|
"""
|
||||||
|
async for n, url in cls._name_list_generator(with_url=True):
|
||||||
|
if name == n:
|
||||||
|
return url
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def icon(self):
|
||||||
|
"""返回此 Model 的图标链接"""
|
177
models/wiki/character.py
Normal file
177
models/wiki/character.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import re
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from httpx import URL
|
||||||
|
|
||||||
|
from models.wiki.base import Model, SCRAPE_HOST
|
||||||
|
from models.wiki.base import WikiModel
|
||||||
|
from models.wiki.other import Association, Element, WeaponType
|
||||||
|
|
||||||
|
|
||||||
|
class Birth(Model):
|
||||||
|
"""生日
|
||||||
|
Attributes:
|
||||||
|
day: 天
|
||||||
|
month: 月
|
||||||
|
"""
|
||||||
|
day: int
|
||||||
|
month: int
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterAscension(Model):
|
||||||
|
"""角色的突破材料
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
level: 等级突破材料
|
||||||
|
skill: 技能/天赋培养材料
|
||||||
|
"""
|
||||||
|
level: List[str] = []
|
||||||
|
skill: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterState(Model):
|
||||||
|
"""角色属性值
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
level: 等级
|
||||||
|
HP: 生命
|
||||||
|
ATK: 攻击力
|
||||||
|
DEF: 防御力
|
||||||
|
CR: 暴击率
|
||||||
|
CD: 暴击伤害
|
||||||
|
bonus: 突破属性
|
||||||
|
"""
|
||||||
|
level: str
|
||||||
|
HP: int
|
||||||
|
ATK: float
|
||||||
|
DEF: float
|
||||||
|
CR: str
|
||||||
|
CD: str
|
||||||
|
bonus: str
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterIcon(Model):
|
||||||
|
icon: str
|
||||||
|
side: str
|
||||||
|
gacha: str
|
||||||
|
splash: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Character(WikiModel):
|
||||||
|
"""角色
|
||||||
|
Attributes:
|
||||||
|
title: 称号
|
||||||
|
occupation: 所属
|
||||||
|
association: 地区
|
||||||
|
weapon_type: 武器类型
|
||||||
|
element: 元素
|
||||||
|
birth: 生日
|
||||||
|
constellation: 命之座
|
||||||
|
cn_cv: 中配
|
||||||
|
jp_cv: 日配
|
||||||
|
en_cv: 英配
|
||||||
|
kr_cv: 韩配
|
||||||
|
description: 描述
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
occupation: str
|
||||||
|
association: Association
|
||||||
|
weapon_type: WeaponType
|
||||||
|
element: Element
|
||||||
|
birth: Optional[Birth]
|
||||||
|
constellation: str
|
||||||
|
cn_cv: str
|
||||||
|
jp_cv: str
|
||||||
|
en_cv: str
|
||||||
|
kr_cv: str
|
||||||
|
description: str
|
||||||
|
ascension: CharacterAscension
|
||||||
|
|
||||||
|
stats: List[CharacterState]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scrape_urls(cls) -> List[URL]:
|
||||||
|
return [SCRAPE_HOST.join("fam_chars/?lang=CHS")]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _parse_soup(cls, soup: BeautifulSoup) -> 'Character':
|
||||||
|
"""解析角色页"""
|
||||||
|
soup = soup.select('.wp-block-post-content')[0]
|
||||||
|
tables = soup.find_all('table')
|
||||||
|
table_rows = tables[0].find_all('tr')
|
||||||
|
|
||||||
|
def get_table_text(row_num: int) -> str:
|
||||||
|
"""一个快捷函数,用于返回表格对应行的最后一个单元格中的文本"""
|
||||||
|
return table_rows[row_num].find_all('td')[-1].text.replace('\xa0', '')
|
||||||
|
|
||||||
|
id_ = re.findall(r'img/(.*?_\d+)_.*', table_rows[0].find('img').attrs['src'])[0]
|
||||||
|
name = get_table_text(0)
|
||||||
|
if name != '旅行者': # 如果角色名不是 旅行者
|
||||||
|
title = get_table_text(1)
|
||||||
|
occupation = get_table_text(2)
|
||||||
|
association = Association.convert(get_table_text(3).lower().title())
|
||||||
|
rarity = len(table_rows[4].find_all('img'))
|
||||||
|
weapon_type = WeaponType[get_table_text(5)]
|
||||||
|
element = Element[get_table_text(6)]
|
||||||
|
birth = Birth(day=int(get_table_text(7)), month=int(get_table_text(8)))
|
||||||
|
constellation = get_table_text(10)
|
||||||
|
cn_cv = get_table_text(11)
|
||||||
|
jp_cv = get_table_text(12)
|
||||||
|
en_cv = get_table_text(13)
|
||||||
|
kr_cv = get_table_text(14)
|
||||||
|
else:
|
||||||
|
name = '空' if id_.endswith('5') else '荧'
|
||||||
|
title = get_table_text(0)
|
||||||
|
occupation = get_table_text(1)
|
||||||
|
association = Association.convert(get_table_text(2).lower().title())
|
||||||
|
rarity = len(table_rows[3].find_all('img'))
|
||||||
|
weapon_type = WeaponType[get_table_text(4)]
|
||||||
|
element = Element[get_table_text(5)]
|
||||||
|
birth = None
|
||||||
|
constellation = get_table_text(7)
|
||||||
|
cn_cv = get_table_text(8)
|
||||||
|
jp_cv = get_table_text(9)
|
||||||
|
en_cv = get_table_text(10)
|
||||||
|
kr_cv = get_table_text(11)
|
||||||
|
description = get_table_text(-3)
|
||||||
|
ascension = CharacterAscension(
|
||||||
|
level=[
|
||||||
|
target[0] for i in table_rows[-2].find_all('a')
|
||||||
|
if (target := re.findall(r'/(.*)/', i.attrs['href'])) # 过滤掉错误的材料(honey网页的bug)
|
||||||
|
],
|
||||||
|
skill=[re.findall(r'/(.*)/', i.attrs['href'])[0] for i in table_rows[-1].find_all('a')]
|
||||||
|
)
|
||||||
|
stats = []
|
||||||
|
for row in tables[2].find_all('tr')[1:]:
|
||||||
|
cells = row.find_all('td')
|
||||||
|
stats.append(
|
||||||
|
CharacterState(
|
||||||
|
level=cells[0].text, HP=cells[1].text, ATK=cells[2].text, DEF=cells[3].text,
|
||||||
|
CR=cells[4].text, CD=cells[5].text, bonus=cells[6].text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Character(
|
||||||
|
id=id_, name=name, title=title, occupation=occupation, association=association, weapon_type=weapon_type,
|
||||||
|
element=element, birth=birth, constellation=constellation, cn_cv=cn_cv, jp_cv=jp_cv, rarity=rarity,
|
||||||
|
en_cv=en_cv, kr_cv=kr_cv, description=description, ascension=ascension, stats=stats
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_url_by_name(cls, name: str) -> Optional[URL]:
|
||||||
|
# 重写此函数的目的是处理主角名字的 ID
|
||||||
|
_map = {'荧': "playergirl_007", '空': "playerboy_005"}
|
||||||
|
if (id_ := _map.get(name, None)) is not None:
|
||||||
|
return await cls.get_url_by_id(id_)
|
||||||
|
return await super(Character, cls).get_url_by_name(name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> CharacterIcon:
|
||||||
|
return CharacterIcon(
|
||||||
|
icon=str(SCRAPE_HOST.join(f'/img/{self.id}_icon.png')),
|
||||||
|
side=str(SCRAPE_HOST.join(f'/img/{self.id}_side_icon.png')),
|
||||||
|
gacha=str(SCRAPE_HOST.join(f'/img/{self.id}_gacha_card.png')),
|
||||||
|
splash=str(SCRAPE_HOST.join(f'/img/{self.id}_gacha_splash.png'))
|
||||||
|
)
|
@ -1,189 +0,0 @@
|
|||||||
import re
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from .helpers import get_headers
|
|
||||||
|
|
||||||
|
|
||||||
class Characters:
|
|
||||||
CHARACTERS_LIST_URL = "https://genshin.honeyhunterworld.com/db/char/characters/?lang=CHS"
|
|
||||||
ROOT_URL = "https://genshin.honeyhunterworld.com"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.client = httpx.AsyncClient(headers=get_headers())
|
|
||||||
|
|
||||||
async def _get_soup(self, url: str) -> Optional[BeautifulSoup]:
|
|
||||||
request = await self.client.get(url)
|
|
||||||
return BeautifulSoup(request.text, "lxml")
|
|
||||||
|
|
||||||
async def get_all_characters_url(self):
|
|
||||||
url_list = []
|
|
||||||
soup = await self._get_soup(self.CHARACTERS_LIST_URL)
|
|
||||||
character_list = soup.find_all('div', {'class': 'char_sea_cont'})
|
|
||||||
for character in character_list:
|
|
||||||
name = character.find("span", {"class": "sea_charname"}).text
|
|
||||||
if "旅行者" in name:
|
|
||||||
continue
|
|
||||||
character_link = self.ROOT_URL + character.a['href']
|
|
||||||
url_list.append(character_link)
|
|
||||||
return url_list
|
|
||||||
|
|
||||||
def get_characters_info_template(self):
|
|
||||||
characters_info_dict = {
|
|
||||||
"name": "",
|
|
||||||
"title": "",
|
|
||||||
"rarity": 0,
|
|
||||||
"element": {"name": "", "icon": ""},
|
|
||||||
"description": "",
|
|
||||||
"constellations": {},
|
|
||||||
"skills": {
|
|
||||||
"normal_attack": self.get_skills_info_template(),
|
|
||||||
"skill_e": self.get_skills_info_template(),
|
|
||||||
"skill_q": self.get_skills_info_template(),
|
|
||||||
"skill_replace": self.get_skills_info_template(),
|
|
||||||
},
|
|
||||||
"gacha_splash": ""
|
|
||||||
}
|
|
||||||
return characters_info_dict
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_skills_info_template():
|
|
||||||
skills_info_dict = {
|
|
||||||
"icon": "",
|
|
||||||
"name": "",
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
return skills_info_dict
|
|
||||||
|
|
||||||
async def get_characters(self, url: str):
|
|
||||||
characters_info_dict = self.get_characters_info_template()
|
|
||||||
soup = await self._get_soup(url)
|
|
||||||
main_content = soup.find("div", {'class': 'wrappercont'})
|
|
||||||
char_name = main_content.find('div', {'class': 'custom_title'}).text
|
|
||||||
characters_info_dict["name"] = char_name
|
|
||||||
# 基础信息
|
|
||||||
char_info_table = main_content.find('table', {'class': 'item_main_table'}).find_all('tr')
|
|
||||||
for char_info_item in char_info_table:
|
|
||||||
content = char_info_item.find_all('td')
|
|
||||||
if content[0].text == "Title":
|
|
||||||
char_title = content[1].text
|
|
||||||
characters_info_dict["title"] = char_title
|
|
||||||
if content[0].text == "Allegiance":
|
|
||||||
char_allegiance = content[1].text
|
|
||||||
characters_info_dict["allegiance"] = char_allegiance
|
|
||||||
if content[0].text == "Rarity":
|
|
||||||
char_rarity = len(content[1].find_all('div', {'class': 'sea_char_stars_wrap'}))
|
|
||||||
characters_info_dict["rarity"] = char_rarity
|
|
||||||
if content[0].text == "Element":
|
|
||||||
char_element_icon_url = self.ROOT_URL + content[1].find('img')['data-src'].replace("_35", "")
|
|
||||||
characters_info_dict["element"]["icon"] = char_element_icon_url
|
|
||||||
if content[0].text == "Astrolabe Name":
|
|
||||||
char_astrolabe_name = content[1].text
|
|
||||||
if content[0].text == "In-game Description":
|
|
||||||
char_description = content[1].text
|
|
||||||
characters_info_dict["description"] = char_description
|
|
||||||
|
|
||||||
# 角色属性表格 咕咕咕
|
|
||||||
skill_dmg_wrapper = main_content.find('div', {'class': 'skilldmgwrapper'}).find_all('tr')
|
|
||||||
|
|
||||||
# 命之座
|
|
||||||
constellations_title = main_content.find('span', {'class': 'item_secondary_title'}, string="Constellations")
|
|
||||||
constellations_table = constellations_title.findNext('table', {'class': 'item_main_table'}).find_all('tr')
|
|
||||||
constellations_list = []
|
|
||||||
constellations_list_index = 0
|
|
||||||
for index, value in enumerate(constellations_table):
|
|
||||||
# 判断第一行
|
|
||||||
if index % 2 == 0:
|
|
||||||
constellations_dict = {
|
|
||||||
"icon": "",
|
|
||||||
"name": "",
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
constellations_list.append(constellations_dict)
|
|
||||||
icon_url = self.ROOT_URL + value.find_all('img', {'class': 'itempic'})[-1]['data-src']
|
|
||||||
constellations_name = value.find_all('a', href=re.compile("/db/skill"))[-1].text
|
|
||||||
constellations_list[constellations_list_index]["icon"] = icon_url
|
|
||||||
constellations_list[constellations_list_index]["name"] = constellations_name
|
|
||||||
if index % 2 == 1:
|
|
||||||
constellations_description = value.find('div', {'class': 'skill_desc_layout'}).text
|
|
||||||
constellations_list[constellations_list_index]["description"] = constellations_description
|
|
||||||
constellations_list_index += 1
|
|
||||||
|
|
||||||
characters_info_dict["constellations"] = constellations_list
|
|
||||||
|
|
||||||
# 技能
|
|
||||||
skills_title = main_content.find('span', string='Attack Talents')
|
|
||||||
|
|
||||||
# 普攻
|
|
||||||
normal_attack_area = skills_title.find_next_sibling()
|
|
||||||
normal_attack_info = normal_attack_area.find_all('tr')
|
|
||||||
normal_attack_icon = self.ROOT_URL + normal_attack_info[0].find('img', {'class': 'itempic'})['data-src']
|
|
||||||
normal_attack_name = normal_attack_info[0].find('a', href=re.compile('/db/skill/')).text
|
|
||||||
normal_attack_desc = normal_attack_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
|
||||||
normal_attack = characters_info_dict["skills"]["normal_attack"]
|
|
||||||
normal_attack["icon"] = normal_attack_icon
|
|
||||||
normal_attack["name"] = normal_attack_name
|
|
||||||
normal_attack["description"] = normal_attack_desc
|
|
||||||
|
|
||||||
normal_attack_table_area = normal_attack_area.find_next_sibling()
|
|
||||||
# normal_attack_table = normal_attack_table_area.find_all('tr')
|
|
||||||
|
|
||||||
skill_e_area = normal_attack_table_area.find_next_sibling()
|
|
||||||
skill_e_info = skill_e_area.find_all('tr')
|
|
||||||
skill_e_icon = self.ROOT_URL + skill_e_info[0].find('img', {'class': 'itempic'})['data-src']
|
|
||||||
skill_e_name = skill_e_info[0].find('a', href=re.compile('/db/skill/')).text
|
|
||||||
skill_e_desc = skill_e_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
|
||||||
skill_e = characters_info_dict["skills"]["skill_e"]
|
|
||||||
skill_e["icon"] = skill_e_icon
|
|
||||||
skill_e["name"] = skill_e_name
|
|
||||||
skill_e["description"] = skill_e_desc
|
|
||||||
|
|
||||||
skill_e_table_area = skill_e_area.find_next_sibling()
|
|
||||||
# skillE_table = skillE_table_area.find_all('tr')
|
|
||||||
|
|
||||||
load_another_talent_q: bool = False
|
|
||||||
if char_name in ("神里绫华", "莫娜"):
|
|
||||||
load_another_talent_q = True
|
|
||||||
|
|
||||||
skill_q_area = skill_e_table_area.find_next_sibling()
|
|
||||||
skill_q_info = skill_q_area.find_all('tr')
|
|
||||||
skill_q_icon = self.ROOT_URL + skill_q_info[0].find('img', {'class': 'itempic'})['data-src']
|
|
||||||
skill_q_name = skill_q_info[0].find('a', href=re.compile('/db/skill/')).text
|
|
||||||
skill_q_desc = skill_q_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
|
||||||
skill_q_table_area = skill_q_area.find_next_sibling()
|
|
||||||
# skill_q_table = skill_q_table_area.find_all('tr')
|
|
||||||
|
|
||||||
if load_another_talent_q:
|
|
||||||
skill_replace = characters_info_dict["skills"]["skill_replace"]
|
|
||||||
skill_replace["icon"] = skill_q_icon
|
|
||||||
skill_replace["name"] = skill_q_name
|
|
||||||
skill_replace["description"] = skill_q_desc
|
|
||||||
else:
|
|
||||||
skill_q = characters_info_dict["skills"]["skill_q"]
|
|
||||||
skill_q["icon"] = skill_q_icon
|
|
||||||
skill_q["name"] = skill_q_name
|
|
||||||
skill_q["description"] = skill_q_desc
|
|
||||||
|
|
||||||
if load_another_talent_q:
|
|
||||||
skill_q2_area = skill_q_table_area.find_next_sibling()
|
|
||||||
skill_q2_info = skill_q2_area.find_all('tr')
|
|
||||||
skill_q2_icon = self.ROOT_URL + skill_q2_info[0].find('img', {'class': 'itempic'})['data-src']
|
|
||||||
skill_q2_name = skill_q2_info[0].find('a', href=re.compile('/db/skill/')).text
|
|
||||||
skill_q2_desc = skill_q2_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
|
||||||
skill_q2 = characters_info_dict["skills"]["skill_q"]
|
|
||||||
skill_q2["icon"] = skill_q2_icon
|
|
||||||
skill_q2["name"] = skill_q2_name
|
|
||||||
skill_q2["description"] = skill_q2_desc
|
|
||||||
|
|
||||||
# 角色图片
|
|
||||||
char_pic_area = main_content.find('span', string='Character Gallery').find_next_sibling()
|
|
||||||
all_char_pic = char_pic_area.find("div", {"class": "gallery_cont"})
|
|
||||||
|
|
||||||
gacha_splash_text = all_char_pic.find("span", {"class": "gallery_cont_span"}, string="Gacha Splash")
|
|
||||||
gacha_splash_pic_url = self.ROOT_URL + gacha_splash_text.previous_element.previous_element["data-src"].replace(
|
|
||||||
"_70", "")
|
|
||||||
characters_info_dict["gacha_splash"] = gacha_splash_pic_url
|
|
||||||
|
|
||||||
return characters_info_dict
|
|
@ -1,27 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
ID_RGX = re.compile(r"/db/[^.]+_(?P<id>\d+)")
|
|
||||||
|
|
||||||
|
|
||||||
def get_headers():
|
|
||||||
headers = {
|
|
||||||
"accept": "text/html,application/xhtml+xml,application/xml;"
|
|
||||||
"q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36",
|
|
||||||
"referer": "https://genshin.honeyhunterworld.com/db/char/hutao/?lang=CHS",
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
|
|
||||||
|
|
||||||
def get_id_form_url(url: str):
|
|
||||||
matches = ID_RGX.search(url)
|
|
||||||
if matches is None:
|
|
||||||
return -1
|
|
||||||
entries = matches.groupdict()
|
|
||||||
if entries is None:
|
|
||||||
return -1
|
|
||||||
try:
|
|
||||||
return int(entries.get('id'))
|
|
||||||
except (IndexError, ValueError, TypeError):
|
|
||||||
return -1
|
|
57
models/wiki/material.py
Normal file
57
models/wiki/material.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import re
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from httpx import URL
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from models.wiki.base import SCRAPE_HOST, WikiModel
|
||||||
|
|
||||||
|
__all__ = ['Material']
|
||||||
|
|
||||||
|
|
||||||
|
class Material(WikiModel):
|
||||||
|
"""材料
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
type: 类型
|
||||||
|
source: 获取方式
|
||||||
|
description: 描述
|
||||||
|
serise: 材料系列
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: str
|
||||||
|
source: List[str]
|
||||||
|
description: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scrape_urls() -> List[URL]:
|
||||||
|
return [SCRAPE_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _parse_soup(cls, soup: BeautifulSoup) -> Self:
|
||||||
|
"""解析材料页"""
|
||||||
|
soup = soup.select('.wp-block-post-content')[0]
|
||||||
|
tables = soup.find_all('table')
|
||||||
|
table_rows = tables[0].find_all('tr')
|
||||||
|
|
||||||
|
def get_table_text(row_num: int) -> str:
|
||||||
|
"""一个快捷函数,用于返回表格对应行的最后一个单元格中的文本"""
|
||||||
|
return table_rows[row_num].find_all('td')[-1].text.replace('\xa0', '')
|
||||||
|
|
||||||
|
id_ = re.findall(r'/img/(.*?)\.webp', str(table_rows[0]))[0]
|
||||||
|
name = get_table_text(0)
|
||||||
|
rarity = len(table_rows[3].find_all('img'))
|
||||||
|
type_ = get_table_text(1)
|
||||||
|
source = list(
|
||||||
|
filter(
|
||||||
|
lambda x: x, # filter 在这里的作用是过滤掉为空的数据
|
||||||
|
table_rows[-2].find_all('td')[-1].encode_contents().decode().split('<br/>')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
description = get_table_text(-1)
|
||||||
|
return Material(id=id_, name=name, rarity=rarity, type=type_, source=source, description=description)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
return str(SCRAPE_HOST.join(f'/img/{self.id}.png'))
|
362
models/wiki/metadata/weapon_level.json
Normal file
362
models/wiki/metadata/weapon_level.json
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"requiredExps": [125, 175, 275, 400, 600]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"requiredExps": [200, 275, 425, 625, 950]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"requiredExps": [275, 400, 600, 900, 1350]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"requiredExps": [350, 550, 800, 1200, 1800]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"requiredExps": [475, 700, 1025, 1550, 2325]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"requiredExps": [575, 875, 1275, 1950, 2925]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"requiredExps": [700, 1050, 1550, 2350, 3525]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"requiredExps": [850, 1250, 1850, 2800, 4200]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"requiredExps": [1000, 1475, 2175, 3300, 4950]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"requiredExps": [1150, 1700, 2500, 3800, 5700]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"requiredExps": [1300, 1950, 2875, 4350, 6525]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"requiredExps": [1475, 2225, 3250, 4925, 7400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"requiredExps": [1650, 2475, 3650, 5525, 8300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"requiredExps": [1850, 2775, 4050, 6150, 9225]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"requiredExps": [2050, 3050, 4500, 6800, 10200]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"requiredExps": [2250, 3375, 4950, 7500, 11250]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"requiredExps": [2450, 3700, 5400, 8200, 12300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"requiredExps": [2675, 4025, 5900, 8950, 13425]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"requiredExps": [2925, 4375, 6425, 9725, 14600]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"requiredExps": [3150, 4725, 6925, 10500, 15750]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 21,
|
||||||
|
"requiredExps": [3575, 5350, 7850, 11900, 17850]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 22,
|
||||||
|
"requiredExps": [3825, 5750, 8425, 12775, 19175]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 23,
|
||||||
|
"requiredExps": [4100, 6175, 9050, 13700, 20550]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 24,
|
||||||
|
"requiredExps": [4400, 6600, 9675, 14650, 21975]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 25,
|
||||||
|
"requiredExps": [4700, 7025, 10325, 15625, 23450]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 26,
|
||||||
|
"requiredExps": [5000, 7475, 10975, 16625, 24950]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 27,
|
||||||
|
"requiredExps": [5300, 7950, 11650, 17650, 26475]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 28,
|
||||||
|
"requiredExps": [5600, 8425, 12350, 18700, 28050]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 29,
|
||||||
|
"requiredExps": [5925, 8900, 13050, 19775, 29675]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 30,
|
||||||
|
"requiredExps": [6275, 9400, 13800, 20900, 31350]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 31,
|
||||||
|
"requiredExps": [6600, 9900, 14525, 22025, 33050]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 32,
|
||||||
|
"requiredExps": [6950, 10450, 15300, 23200, 34800]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 33,
|
||||||
|
"requiredExps": [7325, 10975, 16100, 24375, 36575]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 34,
|
||||||
|
"requiredExps": [7675, 11525, 16900, 25600, 38400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 35,
|
||||||
|
"requiredExps": [8050, 12075, 17700, 26825, 40250]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 36,
|
||||||
|
"requiredExps": [8425, 12650, 18550, 28100, 42150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 37,
|
||||||
|
"requiredExps": [8825, 13225, 19400, 29400, 44100]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 38,
|
||||||
|
"requiredExps": [9225, 13825, 20275, 30725, 46100]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 39,
|
||||||
|
"requiredExps": [9625, 14425, 21175, 32075, 48125]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 40,
|
||||||
|
"requiredExps": [10025, 15050, 22050, 33425, 50150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 41,
|
||||||
|
"requiredExps": [10975, 16450, 24150, 36575, 54875]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 42,
|
||||||
|
"requiredExps": [11425, 17125, 25125, 38075, 57125]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 43,
|
||||||
|
"requiredExps": [11875, 17825, 26125, 39600, 59400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 44,
|
||||||
|
"requiredExps": [12350, 18525, 27150, 41150, 61725]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 45,
|
||||||
|
"requiredExps": [12825, 19225, 28200, 42725, 64100]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 46,
|
||||||
|
"requiredExps": [13300, 19950, 29250, 44325, 66500]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 47,
|
||||||
|
"requiredExps": [13775, 20675, 30325, 45950, 68925]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 48,
|
||||||
|
"requiredExps": [14275, 21425, 31425, 47600, 71400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 49,
|
||||||
|
"requiredExps": [14800, 22175, 32550, 49300, 73950]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 50,
|
||||||
|
"requiredExps": [15300, 22950, 33650, 51000, 76500]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 51,
|
||||||
|
"requiredExps": [16625, 24925, 36550, 55375, 83075]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 52,
|
||||||
|
"requiredExps": [17175, 25750, 37775, 57225, 85850]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 53,
|
||||||
|
"requiredExps": [17725, 26600, 39000, 59100, 88650]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 54,
|
||||||
|
"requiredExps": [18300, 27450, 40275, 61025, 91550]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 55,
|
||||||
|
"requiredExps": [18875, 28325, 41550, 62950, 94425]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 56,
|
||||||
|
"requiredExps": [19475, 29225, 42850, 64925, 97400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 57,
|
||||||
|
"requiredExps": [20075, 30100, 44150, 66900, 100350]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 58,
|
||||||
|
"requiredExps": [20675, 31025, 45500, 68925, 103400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 59,
|
||||||
|
"requiredExps": [21300, 31950, 46850, 70975, 106475]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 60,
|
||||||
|
"requiredExps": [21925, 32875, 48225, 73050, 109575]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 61,
|
||||||
|
"requiredExps": [23675, 35500, 52075, 78900, 118350]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 62,
|
||||||
|
"requiredExps": [24350, 36500, 53550, 81125, 121700]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 63,
|
||||||
|
"requiredExps": [25025, 37525, 55050, 83400, 125100]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 64,
|
||||||
|
"requiredExps": [25700, 38575, 56550, 85700, 128550]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 65,
|
||||||
|
"requiredExps": [26400, 39600, 58100, 88025, 132050]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 66,
|
||||||
|
"requiredExps": [27125, 40675, 59650, 90375, 135575]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 67,
|
||||||
|
"requiredExps": [27825, 41750, 61225, 92750, 139125]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 68,
|
||||||
|
"requiredExps": [28550, 42825, 62800, 95150, 142725]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 69,
|
||||||
|
"requiredExps": [29275, 43900, 64400, 97575, 146375]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 70,
|
||||||
|
"requiredExps": [30025, 45025, 66025, 100050, 150075]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 71,
|
||||||
|
"requiredExps": [32300, 48450, 71075, 107675, 161525]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 72,
|
||||||
|
"requiredExps": [33100, 49650, 72825, 110325, 165500]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 73,
|
||||||
|
"requiredExps": [33900, 50850, 74575, 113000, 169500]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 74,
|
||||||
|
"requiredExps": [34700, 52075, 76350, 115700, 173550]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 75,
|
||||||
|
"requiredExps": [35525, 53300, 78150, 118425, 177650]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 76,
|
||||||
|
"requiredExps": [36350, 54550, 80000, 121200, 181800]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 77,
|
||||||
|
"requiredExps": [37200, 55800, 81850, 124000, 186000]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 78,
|
||||||
|
"requiredExps": [38050, 57075, 83700, 126825, 190250]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 79,
|
||||||
|
"requiredExps": [38900, 58350, 85575, 129675, 194525]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 80,
|
||||||
|
"requiredExps": [39775, 59650, 87500, 132575, 198875]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 81,
|
||||||
|
"requiredExps": [46950, 70425, 103275, 156475, 234725]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 82,
|
||||||
|
"requiredExps": [52775, 79150, 116075, 175875, 263825]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 83,
|
||||||
|
"requiredExps": [59275, 88925, 130425, 197600, 296400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 84,
|
||||||
|
"requiredExps": [66600, 99900, 146500, 221975, 332975]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 85,
|
||||||
|
"requiredExps": [74800, 112175, 164550, 249300, 373950]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 86,
|
||||||
|
"requiredExps": [83975, 125975, 184775, 279950, 419925]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 87,
|
||||||
|
"requiredExps": [94275, 141425, 207400, 314250, 471375]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 88,
|
||||||
|
"requiredExps": [105800, 158725, 232775, 352700, 529050]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 89,
|
||||||
|
"requiredExps": [118725, 178100, 261200, 395775, 593675]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 90,
|
||||||
|
"requiredExps": [133200, 199800, 293050, 444025, 666050]
|
||||||
|
}
|
||||||
|
]
|
132
models/wiki/other.py
Normal file
132
models/wiki/other.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from models.wiki.base import SCRAPE_HOST
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Element',
|
||||||
|
'WeaponType',
|
||||||
|
'AttributeType',
|
||||||
|
'Association',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Element(Enum):
|
||||||
|
"""元素"""
|
||||||
|
Pyro = '火'
|
||||||
|
Hydro = '水'
|
||||||
|
Electro = '雷'
|
||||||
|
Cryo = '冰'
|
||||||
|
Dendro = '草'
|
||||||
|
Anemo = '风'
|
||||||
|
Geo = '岩'
|
||||||
|
Multi = '无' # 主角
|
||||||
|
|
||||||
|
|
||||||
|
_WEAPON_ICON_MAP = {
|
||||||
|
'Sword': SCRAPE_HOST.join('img/s_23101.png'),
|
||||||
|
'Claymore': SCRAPE_HOST.join('img/s_163101.png'),
|
||||||
|
'Polearm': SCRAPE_HOST.join('img/s_233101.png'),
|
||||||
|
'Catalyst': SCRAPE_HOST.join('img/s_43101.png'),
|
||||||
|
'Bow': SCRAPE_HOST.join('img/s_213101.png'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponType(Enum):
|
||||||
|
"""武器类型"""
|
||||||
|
Sword = '单手剑'
|
||||||
|
Claymore = '双手剑'
|
||||||
|
Polearm = '长柄武器'
|
||||||
|
Catalyst = '法器'
|
||||||
|
Bow = '弓'
|
||||||
|
|
||||||
|
def icon_url(self) -> str:
|
||||||
|
return str(_WEAPON_ICON_MAP.get(self.name))
|
||||||
|
|
||||||
|
|
||||||
|
_ATTR_TYPE_MAP = {
|
||||||
|
# 这个字典用于将 Honey 页面中遇到的 属性的缩写的字符 转为 AttributeType 的字符
|
||||||
|
# 例如 Honey 页面上写的 HP% 则对应 HP_p
|
||||||
|
"HP": ['Health'],
|
||||||
|
"HP_p": ['HP%', 'Health %'],
|
||||||
|
"ATK": ['Attack'],
|
||||||
|
"ATK_p": ['Atk%', 'Attack %'],
|
||||||
|
"DEF": ['Defense'],
|
||||||
|
"DEF_p": ['Def%', 'Defense %'],
|
||||||
|
"EM": ['Elemental Mastery'],
|
||||||
|
"ER": ['ER%', 'Energy Recharge %'],
|
||||||
|
"CR": ['CrR%', 'Critical Rate %', 'CritRate%'],
|
||||||
|
"CD": ['Crd%', 'Critical Damage %', 'CritDMG%'],
|
||||||
|
"PD": ['Phys%', 'Physical Damage %'],
|
||||||
|
"HB": [],
|
||||||
|
"Pyro": [],
|
||||||
|
"Hydro": [],
|
||||||
|
"Electro": [],
|
||||||
|
"Cryo": [],
|
||||||
|
"Dendro": [],
|
||||||
|
"Anemo": [],
|
||||||
|
"Geo": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeType(Enum):
|
||||||
|
"""属性枚举类。包含了武器和圣遗物的属性。"""
|
||||||
|
HP = "生命"
|
||||||
|
HP_p = "生命%"
|
||||||
|
ATK = "攻击力"
|
||||||
|
ATK_p = "攻击力%"
|
||||||
|
DEF = "防御力"
|
||||||
|
DEF_p = "防御力%"
|
||||||
|
EM = "元素精通"
|
||||||
|
ER = "元素充能效率"
|
||||||
|
CR = "暴击率"
|
||||||
|
CD = "暴击伤害"
|
||||||
|
PD = "物理伤害加成"
|
||||||
|
HB = "治疗加成"
|
||||||
|
Pyro = '火元素伤害加成'
|
||||||
|
Hydro = '水元素伤害加成'
|
||||||
|
Electro = '雷元素伤害加成'
|
||||||
|
Cryo = '冰元素伤害加成'
|
||||||
|
Dendro = '草元素伤害加成'
|
||||||
|
Anemo = '风元素伤害加成'
|
||||||
|
Geo = '岩元素伤害加成'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert(cls, string: str) -> Optional[Self]:
|
||||||
|
string = string.strip()
|
||||||
|
for k, v in _ATTR_TYPE_MAP.items():
|
||||||
|
if string == k or string in v or string.upper() == k:
|
||||||
|
return cls[k]
|
||||||
|
|
||||||
|
|
||||||
|
_ASSOCIATION_MAP = {
|
||||||
|
'Other': ['Mainactor', 'Ranger', 'Fatui'],
|
||||||
|
'Snezhnaya': [],
|
||||||
|
'Sumeru': [],
|
||||||
|
'Inazuma': [],
|
||||||
|
'Liyue': [],
|
||||||
|
'Mondstadt': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Association(Enum):
|
||||||
|
"""角色所属地区"""
|
||||||
|
Other = '其它'
|
||||||
|
Snezhnaya = '至冬'
|
||||||
|
Sumeru = '须弥'
|
||||||
|
Inazuma = '稻妻'
|
||||||
|
Liyue = '璃月'
|
||||||
|
Mondstadt = '蒙德'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert(cls, string: str) -> Optional[Self]:
|
||||||
|
string = string.strip()
|
||||||
|
for k, v in _ASSOCIATION_MAP.items():
|
||||||
|
if string == k or string in v:
|
||||||
|
return cls[k]
|
||||||
|
string = string.lower().title()
|
||||||
|
if string == k or string in v:
|
||||||
|
return cls[k]
|
||||||
|
return cls[string]
|
143
models/wiki/weapon.py
Normal file
143
models/wiki/weapon.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from httpx import URL
|
||||||
|
|
||||||
|
from models.wiki.base import Model, SCRAPE_HOST, WikiModel
|
||||||
|
from models.wiki.other import AttributeType, WeaponType
|
||||||
|
|
||||||
|
__all__ = ['Weapon', 'WeaponAffix', 'WeaponAttribute']
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponAttribute(Model):
|
||||||
|
"""武器词条"""
|
||||||
|
type: AttributeType
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponAffix(Model):
|
||||||
|
"""武器技能
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: 技能名
|
||||||
|
description: 技能描述
|
||||||
|
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
description: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponState(Model):
|
||||||
|
level: str
|
||||||
|
ATK: float
|
||||||
|
bonus: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponIcon(Model):
|
||||||
|
icon: str
|
||||||
|
awakened: str
|
||||||
|
gacha: str
|
||||||
|
|
||||||
|
|
||||||
|
class Weapon(WikiModel):
|
||||||
|
"""武器
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
type: 武器类型
|
||||||
|
attack: 基础攻击力
|
||||||
|
attribute:
|
||||||
|
affix: 武器技能
|
||||||
|
description: 描述
|
||||||
|
ascension: 突破材料
|
||||||
|
story: 武器故事
|
||||||
|
"""
|
||||||
|
|
||||||
|
weapon_type: WeaponType
|
||||||
|
attack: float
|
||||||
|
attribute: Optional[WeaponAttribute]
|
||||||
|
affix: Optional[WeaponAffix]
|
||||||
|
description: str
|
||||||
|
ascension: List[str]
|
||||||
|
story: Optional[str]
|
||||||
|
|
||||||
|
stats: List[WeaponState]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scrape_urls() -> List[URL]:
|
||||||
|
return [SCRAPE_HOST.join(f"fam_{i.lower()}/?lang=CHS") for i in WeaponType.__members__]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _parse_soup(cls, soup: BeautifulSoup) -> 'Weapon':
|
||||||
|
"""解析武器页"""
|
||||||
|
soup = soup.select('.wp-block-post-content')[0]
|
||||||
|
tables = soup.find_all('table')
|
||||||
|
table_rows = tables[0].find_all('tr')
|
||||||
|
|
||||||
|
def get_table_text(row_num: int) -> str:
|
||||||
|
"""一个快捷函数,用于返回表格对应行的最后一个单元格中的文本"""
|
||||||
|
return table_rows[row_num].find_all('td')[-1].text.replace('\xa0', '')
|
||||||
|
|
||||||
|
def find_table(select: str):
|
||||||
|
"""一个快捷函数,用于寻找对应表格头的表格"""
|
||||||
|
return list(filter(lambda x: select in ' '.join(x.attrs['class']), tables))
|
||||||
|
|
||||||
|
id_ = re.findall(r'/img/(.*?)_gacha', str(table_rows[0]))[0]
|
||||||
|
weapon_type = WeaponType[get_table_text(1).split(',')[-1].strip()]
|
||||||
|
name = get_table_text(0)
|
||||||
|
rarity = len(table_rows[2].find_all('img'))
|
||||||
|
attack = float(get_table_text(4))
|
||||||
|
ascension = [re.findall(r'/(.*)/', tag.attrs['href'])[0] for tag in table_rows[-1].find_all('a')]
|
||||||
|
if rarity > 2: # 如果是 3 星及其以上的武器
|
||||||
|
attribute = WeaponAttribute(
|
||||||
|
type=AttributeType.convert(
|
||||||
|
tables[2].find('thead').find('tr').find_all('td')[2].text.split(' ')[1]
|
||||||
|
),
|
||||||
|
value=get_table_text(6)
|
||||||
|
)
|
||||||
|
affix = WeaponAffix(name=get_table_text(7), description=[
|
||||||
|
i.find_all('td')[1].text for i in tables[3].find_all('tr')[1:]
|
||||||
|
])
|
||||||
|
if len(tables) < 11:
|
||||||
|
description = get_table_text(-1)
|
||||||
|
else:
|
||||||
|
description = get_table_text(9)
|
||||||
|
if story_table := find_table('quotes'):
|
||||||
|
story = story_table[0].text.strip()
|
||||||
|
else:
|
||||||
|
story = None
|
||||||
|
else: # 如果是 2 星及其以下的武器
|
||||||
|
attribute = affix = None
|
||||||
|
description = get_table_text(5)
|
||||||
|
story = tables[-1].text.strip()
|
||||||
|
stats = []
|
||||||
|
for row in tables[2].find_all('tr')[1:]:
|
||||||
|
cells = row.find_all('td')
|
||||||
|
if rarity > 2:
|
||||||
|
stats.append(WeaponState(level=cells[0].text, ATK=cells[1].text, bonus=cells[2].text))
|
||||||
|
else:
|
||||||
|
stats.append(WeaponState(level=cells[0].text, ATK=cells[1].text))
|
||||||
|
return Weapon(
|
||||||
|
id=id_, name=name, rarity=rarity, attack=attack, attribute=attribute, affix=affix, weapon_type=weapon_type,
|
||||||
|
story=story, stats=stats, description=description, ascension=ascension
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]:
|
||||||
|
# 重写此函数的目的是名字去重,例如单手剑页面中有三个 “「一心传」名刀”
|
||||||
|
name_list = [i async for i in cls._name_list_generator(with_url=with_url)]
|
||||||
|
if with_url:
|
||||||
|
return [
|
||||||
|
(i[0], list(i[1])[0][1]) for i in itertools.groupby(name_list, lambda x: x[0])
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return [i[0] for i in itertools.groupby(name_list, lambda x: x)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> WeaponIcon:
|
||||||
|
return WeaponIcon(
|
||||||
|
icon=str(SCRAPE_HOST.join(f'/img/{self.id}.png')),
|
||||||
|
awakened=str(SCRAPE_HOST.join(f'/img/{self.id}_awaken_icon.png')),
|
||||||
|
gacha=str(SCRAPE_HOST.join(f'/img/{self.id}_gacha_icon.png')),
|
||||||
|
)
|
@ -1,210 +0,0 @@
|
|||||||
import os
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import ujson
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from .helpers import get_headers, get_id_form_url
|
|
||||||
|
|
||||||
|
|
||||||
class WeaponType(Enum):
|
|
||||||
Sword = "sword" # 单手剑
|
|
||||||
Claymore = "claymore" # 双手剑
|
|
||||||
PoleArm = "polearm" # 长柄武器
|
|
||||||
Bow = "bow" # 弓
|
|
||||||
Catalyst = "catalyst" # 法器
|
|
||||||
|
|
||||||
|
|
||||||
class Weapons:
|
|
||||||
IGNORE_WEAPONS_ID = [
|
|
||||||
"1001", "1101", "1406",
|
|
||||||
"2001", "2101", "2204", "2406", "2407",
|
|
||||||
"3001", "3101", "3204", "3404",
|
|
||||||
"4001", "4101", "4201", "4403", "4405", "4406",
|
|
||||||
"5001", "5101", "5201", "5404", "5404", "5405",
|
|
||||||
] # 忽略的武器包括一星、二星武器,beta表格内无名武器,未上架到正服的武器
|
|
||||||
|
|
||||||
# 根地址
|
|
||||||
ROOT_URL = "https://genshin.honeyhunterworld.com"
|
|
||||||
|
|
||||||
TEXT_MAPPING = {
|
|
||||||
"Type": "类型",
|
|
||||||
"Rarity": "Rarity",
|
|
||||||
"Base Attack": "基础攻击力"
|
|
||||||
}
|
|
||||||
|
|
||||||
WEAPON_TYPE_MAPPING = {
|
|
||||||
"Sword": "https://genshin.honeyhunterworld.com/img/skills/s_33101.png", # 单手剑
|
|
||||||
"Claymore": "https://genshin.honeyhunterworld.com/img/skills/s_163101.png", # 双手剑
|
|
||||||
"Polearm": "https://genshin.honeyhunterworld.com/img/skills/s_233101.png", # 长枪
|
|
||||||
"Bow": "https://genshin.honeyhunterworld.com/img/skills/s_213101.png", # 弓箭
|
|
||||||
"Catalyst": "https://genshin.honeyhunterworld.com/img/skills/s_43101.png", # 法器
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.client = httpx.AsyncClient(headers=get_headers())
|
|
||||||
project_path = os.path.dirname(__file__)
|
|
||||||
characters_file = os.path.join(project_path, "metadata", "ascension.json")
|
|
||||||
monster_file = os.path.join(project_path, "metadata", "monster.json")
|
|
||||||
elite_file = os.path.join(project_path, "metadata", "elite.json")
|
|
||||||
with open(characters_file, "r", encoding="utf-8") as f:
|
|
||||||
self._ascension_json: dict = ujson.load(f)
|
|
||||||
with open(monster_file, "r", encoding="utf-8") as f:
|
|
||||||
self._monster_json: dict = ujson.load(f)
|
|
||||||
with open(elite_file, "r", encoding="utf-8") as f:
|
|
||||||
self._elite_json: dict = ujson.load(f)
|
|
||||||
|
|
||||||
async def _get_soup(self, url: str) -> Optional[BeautifulSoup]:
|
|
||||||
request = await self.client.get(url)
|
|
||||||
return BeautifulSoup(request.text, "lxml")
|
|
||||||
|
|
||||||
async def get_weapon_url_list(self, weapon_type: WeaponType):
|
|
||||||
weapon_url_list = []
|
|
||||||
url = self.ROOT_URL + f"/db/weapon/{weapon_type.value}/?lang=CHS"
|
|
||||||
soup = await self._get_soup(url)
|
|
||||||
weapon_table = soup.find("span", {"class": "item_secondary_title"},
|
|
||||||
string="Released (Codex) Weapons").find_next_sibling()
|
|
||||||
weapon_table_rows = weapon_table.find_all("tr")
|
|
||||||
for weapon_table_row in weapon_table_rows:
|
|
||||||
content = weapon_table_row.find_all("td")[2]
|
|
||||||
if content.find("a") is not None:
|
|
||||||
weapon_url = self.ROOT_URL + content.find("a")["href"]
|
|
||||||
weapon_id = str(get_id_form_url(weapon_url))
|
|
||||||
if weapon_id not in self.IGNORE_WEAPONS_ID:
|
|
||||||
weapon_url_list.append(weapon_url)
|
|
||||||
return weapon_url_list
|
|
||||||
|
|
||||||
async def get_all_weapon_url(self):
|
|
||||||
all_weapon_url = []
|
|
||||||
temp_data = await self.get_weapon_url_list(WeaponType.Bow)
|
|
||||||
all_weapon_url.extend(temp_data)
|
|
||||||
temp_data = await self.get_weapon_url_list(WeaponType.Sword)
|
|
||||||
all_weapon_url.extend(temp_data)
|
|
||||||
temp_data = await self.get_weapon_url_list(WeaponType.PoleArm)
|
|
||||||
all_weapon_url.extend(temp_data)
|
|
||||||
temp_data = await self.get_weapon_url_list(WeaponType.Catalyst)
|
|
||||||
all_weapon_url.extend(temp_data)
|
|
||||||
temp_data = await self.get_weapon_url_list(WeaponType.Claymore)
|
|
||||||
all_weapon_url.extend(temp_data)
|
|
||||||
return all_weapon_url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_weapon_info_template():
|
|
||||||
weapon_info_dict = {
|
|
||||||
"name": "",
|
|
||||||
"description": "",
|
|
||||||
"source_img": "",
|
|
||||||
"atk":
|
|
||||||
{
|
|
||||||
"min": 0,
|
|
||||||
"max": 999999,
|
|
||||||
"name": "基础攻击力"
|
|
||||||
},
|
|
||||||
"secondary":
|
|
||||||
{
|
|
||||||
"min": 0.1,
|
|
||||||
"max": 999999.9,
|
|
||||||
"name": ""
|
|
||||||
},
|
|
||||||
"star":
|
|
||||||
{
|
|
||||||
"value": -1,
|
|
||||||
"icon": ""
|
|
||||||
},
|
|
||||||
"type":
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"icon": ""
|
|
||||||
},
|
|
||||||
"passive_ability":
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
materials_dict = {
|
|
||||||
"name": "",
|
|
||||||
"star": {
|
|
||||||
"value": 0,
|
|
||||||
"icon": ""
|
|
||||||
},
|
|
||||||
"city": "",
|
|
||||||
"icon": ""
|
|
||||||
}
|
|
||||||
weapon_info_dict["materials"] = {
|
|
||||||
"ascension": materials_dict,
|
|
||||||
"elite": materials_dict,
|
|
||||||
"monster": materials_dict,
|
|
||||||
}
|
|
||||||
return weapon_info_dict
|
|
||||||
|
|
||||||
async def get_weapon_info(self, url: str):
|
|
||||||
weapon_info_dict = self.get_weapon_info_template()
|
|
||||||
soup = await self._get_soup(url)
|
|
||||||
weapon_content = soup.find("div", {"class": "wrappercont"})
|
|
||||||
data = weapon_content.find("div", {"class": "data_cont_wrapper", "style": "display: block"})
|
|
||||||
weapon_info = data.find("table", {"class": "item_main_table"})
|
|
||||||
weapon_name = weapon_content.find("div", {"class": "custom_title"}).text.replace("-", "").replace(" ", "")
|
|
||||||
weapon_info_dict["name"] = weapon_name
|
|
||||||
weapon_info_row = weapon_info.find_all("tr")
|
|
||||||
for weapon_info_ in weapon_info_row:
|
|
||||||
content = weapon_info_.find_all("td")
|
|
||||||
if len(content) == 3: # 第一行会有三个td,其中一个td是武器图片
|
|
||||||
weapon_info_dict["source_img"] = self.ROOT_URL + content[0].find("img",
|
|
||||||
{"class": "itempic lazy"})["data-src"]
|
|
||||||
weapon_info_dict["type"]["name"] = content[2].text
|
|
||||||
weapon_info_dict["type"]["icon"] = self.get_weapon_type(content[2].text)
|
|
||||||
elif len(content) == 2:
|
|
||||||
if content[0].text == "Rarity":
|
|
||||||
weapon_info_dict["star"]["value"] = len(
|
|
||||||
content[1].find_all("div", {"class": "sea_char_stars_wrap"}))
|
|
||||||
elif content[0].text == "Special (passive) Ability":
|
|
||||||
weapon_info_dict["passive_ability"]["name"] = content[1].text
|
|
||||||
elif content[0].text == "Special (passive) Ability Description":
|
|
||||||
weapon_info_dict["passive_ability"]["description"] = content[1].text
|
|
||||||
elif content[0].text == "In-game Description":
|
|
||||||
weapon_info_dict["description"] = content[1].text
|
|
||||||
elif content[0].text == "Secondary Stat":
|
|
||||||
weapon_info_dict["secondary"]["name"] = content[1].text
|
|
||||||
|
|
||||||
stat_table = data.find("span", {"class": "item_secondary_title"},
|
|
||||||
string=" Stat Progression ").find_next_sibling()
|
|
||||||
stat_table_row = stat_table.find_all("tr")
|
|
||||||
for stat_table_ in stat_table_row:
|
|
||||||
content = stat_table_.find_all("td")
|
|
||||||
# 通过等级判断
|
|
||||||
if content[0].text == "1":
|
|
||||||
weapon_info_dict["atk"]["min"] = int(content[1].text)
|
|
||||||
weapon_info_dict["secondary"]["min"] = float(content[2].text)
|
|
||||||
elif content[0].text == "80+":
|
|
||||||
item_hrefs = content[3].find_all("a")
|
|
||||||
for item_href in item_hrefs:
|
|
||||||
item_id = get_id_form_url(item_href["href"])
|
|
||||||
ascension = self.get_ascension(str(item_id))
|
|
||||||
if ascension.get("name") is not None:
|
|
||||||
weapon_info_dict["materials"]["ascension"] = ascension
|
|
||||||
monster = self.get_monster(str(item_id))
|
|
||||||
if monster.get("name") is not None:
|
|
||||||
weapon_info_dict["materials"]["monster"] = monster
|
|
||||||
elite = self.get_elite(str(item_id))
|
|
||||||
if elite.get("name") is not None:
|
|
||||||
weapon_info_dict["materials"]["elite"] = elite
|
|
||||||
elif content[0].text == "90":
|
|
||||||
weapon_info_dict["atk"]["max"] = int(content[1].text)
|
|
||||||
weapon_info_dict["secondary"]["max"] = float(content[2].text)
|
|
||||||
|
|
||||||
return weapon_info_dict
|
|
||||||
|
|
||||||
def get_ascension(self, item_id: str):
|
|
||||||
return self._ascension_json.get(item_id, {})
|
|
||||||
|
|
||||||
def get_monster(self, item_id: str):
|
|
||||||
return self._monster_json.get(item_id, {})
|
|
||||||
|
|
||||||
def get_elite(self, item_id: str):
|
|
||||||
return self._elite_json.get(item_id, {})
|
|
||||||
|
|
||||||
def get_weapon_type(self, weapon_type: str):
|
|
||||||
return self.WEAPON_TYPE_MAPPING.get(weapon_type, "")
|
|
@ -1,11 +1,13 @@
|
|||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||||
from telegram.constants import ChatAction
|
from telegram.constants import ChatAction
|
||||||
from telegram.ext import filters, CommandHandler, MessageHandler, CallbackContext
|
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters
|
||||||
|
|
||||||
from core.template.services import TemplateService
|
from core.template.services import TemplateService
|
||||||
from core.wiki.services import WikiService
|
from core.wiki.services import WikiService
|
||||||
from logger import Log
|
from logger import Log
|
||||||
from metadata.shortname import weaponToName
|
from metadata.shortname import weaponToName
|
||||||
|
from models.wiki.base import SCRAPE_HOST
|
||||||
|
from models.wiki.weapon import Weapon
|
||||||
from plugins.base import BasePlugins
|
from plugins.base import BasePlugins
|
||||||
from utils.bot import get_all_args
|
from utils.bot import get_all_args
|
||||||
from utils.decorators.error import error_callable
|
from utils.decorators.error import error_callable
|
||||||
@ -16,10 +18,12 @@ from utils.service.inject import inject
|
|||||||
|
|
||||||
|
|
||||||
@listener_plugins_class()
|
@listener_plugins_class()
|
||||||
class Weapon(BasePlugins):
|
class WeaponPlugin(BasePlugins):
|
||||||
"""武器查询"""
|
"""武器查询"""
|
||||||
|
|
||||||
KEYBOARD = [[InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询")]]
|
KEYBOARD = [[
|
||||||
|
InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询")
|
||||||
|
]]
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
def __init__(self, template_service: TemplateService, wiki_service: WikiService):
|
def __init__(self, template_service: TemplateService, wiki_service: WikiService):
|
||||||
@ -52,7 +56,7 @@ class Weapon(BasePlugins):
|
|||||||
weapon_name = weaponToName(weapon_name)
|
weapon_name = weaponToName(weapon_name)
|
||||||
weapons_list = await self.wiki_service.get_weapons_list()
|
weapons_list = await self.wiki_service.get_weapons_list()
|
||||||
for weapon in weapons_list:
|
for weapon in weapons_list:
|
||||||
if weapon["name"] == weapon_name:
|
if weapon.name == weapon_name:
|
||||||
weapon_data = weapon
|
weapon_data = weapon
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -65,27 +69,44 @@ class Weapon(BasePlugins):
|
|||||||
Log.info(f"用户 {user.full_name}[{user.id}] 查询武器命令请求 || 参数 {weapon_name}")
|
Log.info(f"用户 {user.full_name}[{user.id}] 查询武器命令请求 || 参数 {weapon_name}")
|
||||||
await message.reply_chat_action(ChatAction.TYPING)
|
await message.reply_chat_action(ChatAction.TYPING)
|
||||||
|
|
||||||
async def input_template_data(_weapon_data):
|
async def input_template_data(_weapon_data: Weapon):
|
||||||
|
if weapon.rarity > 2:
|
||||||
|
bonus = _weapon_data.stats[-1].bonus
|
||||||
|
if '%' in bonus:
|
||||||
|
bonus = str(round(float(bonus.rstrip('%')))) + '%'
|
||||||
|
else:
|
||||||
|
bonus = str(round(float(bonus)))
|
||||||
_template_data = {
|
_template_data = {
|
||||||
"weapon_name": _weapon_data["name"],
|
"weapon_name": _weapon_data.name,
|
||||||
"weapon_info_type_img": await url_to_file(_weapon_data["type"]["icon"]),
|
"weapon_info_type_img": await url_to_file(_weapon_data.weapon_type.icon_url()),
|
||||||
"progression_secondary_stat_value": _weapon_data["secondary"]["max"],
|
"progression_secondary_stat_value": bonus,
|
||||||
"progression_secondary_stat_name": _weapon_data["secondary"]["name"],
|
"progression_secondary_stat_name": _weapon_data.attribute.type.value,
|
||||||
"weapon_info_source_img": await url_to_file(_weapon_data["source_img"]),
|
"weapon_info_source_img": await url_to_file(_weapon_data.icon.icon),
|
||||||
"progression_base_atk": _weapon_data["atk"]["max"],
|
"weapon_info_max_level": _weapon_data.stats[-1].level,
|
||||||
"weapon_info_source_list": [],
|
"progression_base_atk": round(_weapon_data.stats[-1].ATK),
|
||||||
"special_ability_name": _weapon_data["passive_ability"]["name"],
|
"weapon_info_source_list": [
|
||||||
"special_ability_info": _weapon_data["passive_ability"]["description"],
|
await url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.png')))
|
||||||
|
for mid in _weapon_data.ascension[-3:]
|
||||||
|
],
|
||||||
|
"special_ability_name": _weapon_data.affix.name,
|
||||||
|
"special_ability_info": _weapon_data.affix.description[0],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
_template_data = {
|
||||||
|
"weapon_name": _weapon_data.name,
|
||||||
|
"weapon_info_type_img": await url_to_file(_weapon_data.weapon_type.icon_url()),
|
||||||
|
"progression_secondary_stat_value": ' ',
|
||||||
|
"progression_secondary_stat_name": '无其它属性加成',
|
||||||
|
"weapon_info_source_img": await url_to_file(_weapon_data.icon.icon),
|
||||||
|
"weapon_info_max_level": _weapon_data.stats[-1].level,
|
||||||
|
"progression_base_atk": round(_weapon_data.stats[-1].ATK),
|
||||||
|
"weapon_info_source_list": [
|
||||||
|
await url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.png')))
|
||||||
|
for mid in _weapon_data.ascension[-3:]
|
||||||
|
],
|
||||||
|
"special_ability_name": '',
|
||||||
|
"special_ability_info": _weapon_data.description,
|
||||||
}
|
}
|
||||||
_template_data["weapon_info_source_list"].append(
|
|
||||||
await url_to_file(_weapon_data["materials"]["ascension"]["icon"])
|
|
||||||
)
|
|
||||||
_template_data["weapon_info_source_list"].append(
|
|
||||||
await url_to_file(_weapon_data["materials"]["elite"]["icon"])
|
|
||||||
)
|
|
||||||
_template_data["weapon_info_source_list"].append(
|
|
||||||
await url_to_file(_weapon_data["materials"]["monster"]["icon"])
|
|
||||||
)
|
|
||||||
return _template_data
|
return _template_data
|
||||||
|
|
||||||
template_data = await input_template_data(weapon_data)
|
template_data = await input_template_data(weapon_data)
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<div class="stat-progression-2 flex py-2">
|
<div class="stat-progression-2 flex py-2">
|
||||||
<div class="flex-grow m-2">
|
<div class="flex-grow m-2">
|
||||||
<div class="stat-progression-level text-base font-semibold">
|
<div class="stat-progression-level text-base font-semibold">
|
||||||
<h1>Lv.90</h1>
|
<h1>Lv.{{ weapon_info_max_level }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-progression-base-atk text-base">
|
<div class="stat-progression-base-atk text-base">
|
||||||
<h2>攻击力 {{progression_base_atk}}</h2>
|
<h2>攻击力 {{progression_base_atk}}</h2>
|
||||||
|
@ -1,53 +1,94 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import IsolatedAsyncioTestCase
|
from unittest import IsolatedAsyncioTestCase
|
||||||
|
|
||||||
from models.wiki.characters import Characters
|
from models.wiki.character import Character
|
||||||
from models.wiki.weapons import Weapons
|
from models.wiki.material import Material
|
||||||
|
from models.wiki.weapon import Weapon
|
||||||
weapons = Weapons()
|
|
||||||
|
|
||||||
|
|
||||||
class TestWiki(IsolatedAsyncioTestCase):
|
class TestWeapon(IsolatedAsyncioTestCase):
|
||||||
TEST_WEAPONS_URL = "https://genshin.honeyhunterworld.com/db/weapon/w_3405/?lang=CHS"
|
async def test_get_by_id(self):
|
||||||
TEST_CHARACTERS_URL = "https://genshin.honeyhunterworld.com/db/char/hutao/?lang=CHS"
|
weapon = await Weapon.get_by_id('11417')
|
||||||
|
self.assertEqual(weapon.name, '原木刀')
|
||||||
|
self.assertEqual(weapon.rarity, 4)
|
||||||
|
self.assertEqual(weapon.attack, 43.73)
|
||||||
|
self.assertEqual(weapon.attribute.type.value, '元素充能效率')
|
||||||
|
self.assertEqual(weapon.affix.name, '森林的瑞佑')
|
||||||
|
|
||||||
def setUp(self):
|
async def test_get_by_name(self):
|
||||||
self.weapons = Weapons()
|
weapon = await Weapon.get_by_name('风鹰剑')
|
||||||
self.characters = Characters()
|
self.assertEqual(weapon.id, 11501)
|
||||||
|
self.assertEqual(weapon.rarity, 5)
|
||||||
|
self.assertEqual(weapon.attack, 47.54)
|
||||||
|
self.assertEqual(weapon.attribute.type.value, '物理伤害加成')
|
||||||
|
self.assertEqual(weapon.affix.name, '西风之鹰的抗争')
|
||||||
|
self.assertTrue('听凭风引,便是正义与自由之风' in weapon.story)
|
||||||
|
|
||||||
async def test_get_weapon(self):
|
async def test_get_full_gen(self):
|
||||||
weapon_info = await self.weapons.get_weapon_info(self.TEST_WEAPONS_URL)
|
async for weapon in Weapon.full_data_generator():
|
||||||
self.assertEqual(weapon_info["name"], "护摩之杖")
|
self.assertIsInstance(weapon, Weapon)
|
||||||
self.assertEqual(weapon_info["description"], "在早已失落的古老祭仪中,使用的朱赤「柴火杖」。")
|
|
||||||
self.assertEqual(weapon_info["atk"]["name"], "基础攻击力")
|
|
||||||
self.assertEqual(weapon_info["atk"]["min"], 46)
|
|
||||||
self.assertEqual(weapon_info["atk"]["max"], 608)
|
|
||||||
self.assertEqual(weapon_info["secondary"]["name"], "暴击伤害")
|
|
||||||
self.assertEqual(weapon_info["secondary"]["min"], 14.4)
|
|
||||||
self.assertEqual(weapon_info["secondary"]["max"], 66.2)
|
|
||||||
self.assertEqual(weapon_info["star"]["value"], 5)
|
|
||||||
self.assertEqual(weapon_info["type"]["name"], "Polearm")
|
|
||||||
self.assertEqual(weapon_info["passive_ability"]["name"], "无羁的朱赤之蝶")
|
|
||||||
self.assertEqual(weapon_info["passive_ability"]["description"], "生命值提升20%。"
|
|
||||||
"此外,基于装备该武器的角色生命值上限的0.8%,"
|
|
||||||
"获得攻击力加成。当装备该武器的角色生命值低于50%时,"
|
|
||||||
"进一步获得1%基于生命值上限的攻击力提升。")
|
|
||||||
|
|
||||||
async def test_get_all_weapon_url(self):
|
async def test_get_full(self):
|
||||||
url_list = await self.weapons.get_all_weapon_url()
|
full_data = await Weapon.get_full_data()
|
||||||
self.assertEqual(True, len(url_list) >= 123)
|
for weapon in full_data:
|
||||||
|
self.assertIsInstance(weapon, Weapon)
|
||||||
|
|
||||||
async def test_get_characters(self):
|
async def test_name_list(self):
|
||||||
characters_info = await self.characters.get_characters(self.TEST_CHARACTERS_URL)
|
from httpx import URL
|
||||||
self.assertEqual(characters_info["name"], "胡桃")
|
async for name in Weapon._name_list_generator(with_url=True):
|
||||||
self.assertEqual(characters_info["title"], "雪霁梅香")
|
self.assertIsInstance(name[0], str)
|
||||||
self.assertEqual(characters_info["rarity"], 5)
|
self.assertIsInstance(name[1], URL)
|
||||||
self.assertEqual(characters_info["description"], "「往生堂」七十七代堂主,年纪轻轻就已主掌璃月的葬仪事务。")
|
|
||||||
self.assertEqual(characters_info["allegiance"], "往生堂")
|
|
||||||
|
|
||||||
async def test_get_all_characters_url(self):
|
|
||||||
url_list = await self.characters.get_all_characters_url()
|
class TestCharacter(IsolatedAsyncioTestCase):
|
||||||
self.assertEqual(True, len(url_list) >= 49)
|
async def test_get_by_id(self):
|
||||||
|
character = await Character.get_by_id('ayaka_002')
|
||||||
|
self.assertEqual(character.name, '神里绫华')
|
||||||
|
self.assertEqual(character.title, '白鹭霜华')
|
||||||
|
self.assertEqual(character.occupation, '社奉行')
|
||||||
|
self.assertEqual(character.association.value, '稻妻')
|
||||||
|
self.assertEqual(character.cn_cv, '小N')
|
||||||
|
|
||||||
|
async def test_get_by_name(self):
|
||||||
|
character = await Character.get_by_name('神里绫华')
|
||||||
|
self.assertEqual(character.id, 'ayaka_002')
|
||||||
|
self.assertEqual(character.title, '白鹭霜华')
|
||||||
|
self.assertEqual(character.occupation, '社奉行')
|
||||||
|
self.assertEqual(character.association.value, '稻妻')
|
||||||
|
self.assertEqual(character.cn_cv, '小N')
|
||||||
|
|
||||||
|
main_character = await Character.get_by_name('荧')
|
||||||
|
self.assertEqual(main_character.constellation, '旅人座')
|
||||||
|
self.assertEqual(main_character.cn_cv, '宴宁&多多poi')
|
||||||
|
|
||||||
|
async def test_get_full(self):
|
||||||
|
async for character in Character.full_data_generator():
|
||||||
|
self.assertIsInstance(character, Character)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMaterial(IsolatedAsyncioTestCase):
|
||||||
|
async def test_get_full_gen(self):
|
||||||
|
async for material in Material.full_data_generator():
|
||||||
|
self.assertIsInstance(material, Material)
|
||||||
|
|
||||||
|
async def test_get_full(self):
|
||||||
|
material_list = await Material.get_full_data()
|
||||||
|
for material in material_list:
|
||||||
|
self.assertIsInstance(material, Material)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAll(IsolatedAsyncioTestCase):
|
||||||
|
async def test_all_get_full(self):
|
||||||
|
import asyncio
|
||||||
|
materials, weapons, characters = tuple(await asyncio.gather(
|
||||||
|
Material.get_full_data(),
|
||||||
|
Weapon.get_full_data(),
|
||||||
|
Character.get_full_data(),
|
||||||
|
return_exceptions=True
|
||||||
|
))
|
||||||
|
self.assertEqual(len(materials), 120)
|
||||||
|
self.assertEqual(len(weapons), 151)
|
||||||
|
self.assertEqual(len(characters), 58)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
Loading…
Reference in New Issue
Block a user