♻️ 重写 wiki 模块和相关插件

1. 使用 `pydantic` 重写了 wiki 模块所使用的 model
2. 添加了 weapon_level.json 用于后续计算武器升级所需的经验
3. 修改了 wiki 插件,以适应新的 model
This commit is contained in:
Karako 2022-08-28 22:37:31 +08:00 committed by GitHub
parent 2b76ce9258
commit 91a133b694
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1521 additions and 643 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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'))
)

View File

@ -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

View File

@ -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
View 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'))

View 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
View 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
View 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')),
)

View File

@ -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, "")

View File

@ -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)

View File

@ -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>

View File

@ -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__":