├── data ├── help.png └── tag.json ├── README.md ├── config.py ├── steam_crawler_botV2.py ├── xjy.py ├── xiaoheihe.py ├── LICENSE └── take_my_money.py /data/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/half-ghost/steam_crawler_botV2/HEAD/data/help.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # steam_crawler_botV2 2 | 指令部分,与[第一版](https://github.com/half-ghost/steam_crawler_bot)基本无异,新增了一些功能,在群里发送“st帮助”以发送此图片 3 | 4 | ![help](https://user-images.githubusercontent.com/55418764/155833576-86e57da8-4814-457a-a71c-159c9ba0eb5b.png) 5 | 6 | 可在config.py中查看编辑配置项,配置项的作用已在config文件中说明,这里不在赘述 7 | 8 | 目前已知的问题:喜加一和steam爬虫数据处理部分还有点问题(2022.3.6 已修复,如发现其他问题请提pr) 9 | 10 | 以及还有一个新模块还在打磨中 11 | 12 | # 注意 13 | 1.目前steam在国内访问十分不稳定,导致插件与steam有关的部分经常报网络错误,如有条件可在config处填入代理地址(使用socks协议代理的话需要先pip安装pysocks)。如果没有代理的话,请尽量使用小黑盒作为数据源。 14 | 15 | 2.该插件中有一个模块(take_my_money.py)用到了字体,为微软雅黑(请重命名为msyh.ttc),请在windows自带的字体库中获取,并放至与该模块同目录下 16 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | 4 | import requests 5 | 6 | # 配置项 7 | # 代理ip设置 8 | proxies = {} 9 | 10 | header = { 11 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", 12 | "Accept-Encoding": "gzip, deflate, br", 13 | } 14 | # 图片形式的消息里最多展现的项目数量 15 | Limit_num = 20 16 | 17 | # 促销提醒的图片的数据来源,1为steam,0为小黑盒,当数据来源获取失败时会切换另一个来源 18 | sell_remind_data_from_steam = 1 19 | 20 | # 与steam和小黑盒有关的消息是否以图片形式发送,默认为否,注意,如果消息被风控发不出去,会自动转为图片发送 21 | send_pic_mes = False 22 | 23 | # 其他必需的配置项,不了解的话请勿乱改 24 | s = requests.session() 25 | FILE_PATH = os.path.dirname(__file__) 26 | url_new = "https://store.steampowered.com/search/results/?l=schinese&query&sort_by=Released_DESC&category1=998&os=win&start=0&count=50" 27 | url_specials = "https://store.steampowered.com/search/results/?l=schinese&query&sort_by=_ASC&category1=998&specials=1&os=win&filter=topsellers&start=0&count=50" 28 | 29 | 30 | def other_request(url, headers=None, cookie=None): 31 | try: 32 | content = s.get(url, headers=headers, cookies=cookie, timeout=4) 33 | except Exception: 34 | content = s.get(url, headers=headers, cookies=cookie, proxies=proxies, timeout=4) 35 | return content 36 | -------------------------------------------------------------------------------- /steam_crawler_botV2.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import json 4 | import os 5 | import re 6 | 7 | from bs4 import BeautifulSoup as bs 8 | from hoshino import Service 9 | from PIL import Image 10 | 11 | from .config import * 12 | from .take_my_money import pic_creater 13 | 14 | sv = Service("stbot-steam") 15 | 16 | with open(os.path.join(FILE_PATH, "data/tag.json"), "r", encoding="utf-8") as f: 17 | tagdata = json.loads(f.read()) 18 | # steam爬虫 19 | def steam_crawler(url_choose: str): 20 | result = [] 21 | get_url = other_request(url_choose).text 22 | soup = bs(get_url.replace(r"\n", "").replace(r"\t", "").replace(r"\r", "").replace("\\", ""), "lxml") 23 | row_list = soup.find_all(name="a", class_="search_result_row") 24 | for row in row_list: 25 | appid = row.get("data-ds-appid") 26 | gameinfo = { 27 | "标题": row.find(name="span", class_="title").text, 28 | "链接": row.get("href"), 29 | "appid": appid, 30 | "高分辨率图片": f"https://media.st.dl.pinyuncloud.com/steam/apps/{appid}/capsule_231x87.jpg", 31 | "低分辨率图片": row.find(name="img").get("src"), 32 | } 33 | if not row.find(name="div", class_="discount_pct"): 34 | try: 35 | price = ( 36 | row.find(name="div", class_="discount_final_price") 37 | .text.replace("\r", "") 38 | .replace("\n", "") 39 | .replace(" ", "") 40 | ) 41 | gameinfo["折扣价"] = " " 42 | if price != "" and "免费" not in price and "Free" not in price: 43 | gameinfo["原价"] = price 44 | elif "免费" in price or "Free" in price: 45 | gameinfo["原价"] = "免费开玩" 46 | else: 47 | gameinfo["原价"] = "无价格信息" 48 | except Exception: 49 | gameinfo["原价"] = "无价格信息" 50 | gameinfo["折扣价"] = " " 51 | else: 52 | discount_price = row.find(name="div", class_="discount_final_price").text.strip().replace(" ", "") 53 | discount_percent = row.find(name="div", class_="discount_pct").text.replace("\n", "").strip() 54 | gameinfo["原价"] = row.find(name="div", class_="discount_original_price").text.strip().replace(" ", "") 55 | gameinfo["折扣价"] = f'{str(discount_price).strip().replace(" ", "")}({discount_percent})' 56 | try: 57 | rate = row.find(name="span", class_="search_review_summary").get("data-tooltip-html") 58 | gameinfo["评测"] = rate.replace("
", ",").replace(" ", "") 59 | except Exception: 60 | gameinfo["评测"] = "暂无评测" 61 | try: 62 | tag = row.get("data-ds-tagids").strip("[]").split(",") 63 | tagk = "".join(tagdata["tag_dict"].get(i) + "," for i in tag) 64 | gameinfo["标签"] = tagk.strip(",") 65 | except Exception: 66 | gameinfo["标签"] = "无用户标签" 67 | result.append(gameinfo) 68 | 69 | return result 70 | 71 | 72 | # 根据传入的tag创建tag搜索链接,返回tag搜索链接以及传入的tag中有效的tag(有效tag具体参考data文件夹中的tag.json) 73 | def tagurl_creater(tag: list, page: int): 74 | tag_search_num = "&tags=" 75 | tag_name = "" 76 | tag_list = tag 77 | count = f"&start={(page-1)*50}&count=50" 78 | for i in tag_list: 79 | if tagdata["tag_dict"].get(i, "") != "": 80 | tag_search_num += tagdata["tag_dict"][i] + "," 81 | tag_name += f"{i}," 82 | tag_search_url = ( 83 | "https://store.steampowered.com/search/results/?l=schinese&query&force_infinite=1&filter=topsellers&category1=998&infinite=1" 84 | + tag_search_num.strip(",") 85 | + count 86 | ) 87 | return tag_search_url, tag_name.strip(",") 88 | 89 | 90 | def mes_creater(result: dict): 91 | mes_list = [] 92 | for i in range(len(result)): 93 | if result[i]["原价"] in ["免费开玩", "无价格信息"]: 94 | mes = f"[CQ:image,file={result[i]['低分辨率图片']}]\n{result[i]['标题']}\n原价:{result[i]['原价']}\ 95 | \n链接:{result[i]['链接']}\n{result[i]['评测']}\n用户标签:{result[i]['标签']}\nappid:{result[i]['appid']}" 96 | else: 97 | mes = f"[CQ:image,file={result[i]['低分辨率图片']}]\n{result[i]['标题']}\n原价:{result[i]['原价']} 折扣价:{result[i]['折扣价']}\ 98 | \n链接:{result[i]['链接']}\n{result[i]['评测']}\n用户标签:{result[i]['标签']}\nappid:{result[i]['appid']}" 99 | data = {"type": "node", "data": {"name": "sbeam机器人", "uin": "2854196310", "content": mes}} 100 | mes_list.append(data) 101 | return mes_list 102 | 103 | 104 | # 匹配关键词发送相关信息,例:今日特惠,发送今日特惠信息,今日新品则发送新品信息 105 | @sv.on_prefix("今日") 106 | async def Gameinfo(bot, ev): 107 | model = ev.message.extract_plain_text().strip() 108 | try: 109 | if model == "新品": 110 | data = steam_crawler(url_new) 111 | elif model == "特惠": 112 | data = steam_crawler(url_specials) 113 | else: 114 | return 115 | except Exception as e: 116 | sv.logger.error(f"Error:{traceback.format_exc()}") 117 | await bot.send(ev, f"哦吼,出错了,报错内容为:{e},请检查运行日志!") 118 | return 119 | await bot.send(ev, "正在生成消息,请稍等片刻!", at_sender=True) 120 | try: 121 | if send_pic_mes: 122 | await bot.send(ev, f"[CQ:image,file={pic_creater(data, is_steam=True)}]") 123 | return 124 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_creater(data)) 125 | except Exception as err: 126 | if "retcode=100" in str(err): 127 | await bot.send(ev, "消息可能被风控,正在转为其他形式发送!") 128 | try: 129 | if send_pic_mes: 130 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_creater(data)) 131 | return 132 | await bot.send(ev, f"[CQ:image,file={pic_creater(data, is_steam=True)}]") 133 | except Exception as err: 134 | if "retcode=100" in str(err): 135 | await bot.send(ev, "消息可能依旧被风控,无法完成发送!") 136 | else: 137 | sv.logger.error(f"Error:{traceback.format_exc()}") 138 | await bot.send(ev, f"发生了其他错误,报错内容为{err},请检查运行日志!") 139 | 140 | 141 | # 后接格式:页数(阿拉伯数字) 标签1 标签2,例:st搜标签 动作 射击 142 | @sv.on_prefix(("st搜", "St搜", "ST搜", "sT搜")) 143 | async def search_tag(bot, ev): 144 | mes = ev.message.extract_plain_text().strip() 145 | try: 146 | if "标签" in mes: 147 | tags = mes[2:].split(" ") 148 | tagurl = tagurl_creater(tags, 1) 149 | if tagurl[1] == "": 150 | await bot.send(ev, "没有匹配到有效标签") 151 | return 152 | data = steam_crawler(tagurl[0]) 153 | elif "游戏" in mes: 154 | gamename = mes[2:] 155 | search_url = f"https://store.steampowered.com/search/results/?l=schinese&query&start=0&count=50&dynamic_data=&sort_by=_ASC&snr=1_7_7_151_7&infinite=1&term={gamename}" 156 | data = steam_crawler(search_url) 157 | if len(data) == 0: 158 | await bot.send(ev, "无搜索结果") 159 | return 160 | except Exception as e: 161 | sv.logger.error(f"Error:{traceback.format_exc()}") 162 | await bot.send(ev, f"哦吼,出错了,报错内容为{e},请检查运行日志!") 163 | return 164 | try: 165 | await bot.send(ev, "正在搜索并生成合并消息中,请稍等片刻!", at_sender=True) 166 | if "标签" in mes: 167 | await bot.send(ev, f"标签{tagurl[1]}搜索结果如下:") 168 | elif "游戏" in mes: 169 | await bot.send(ev, f"游戏{gamename}搜索结果如下:") 170 | else: 171 | return 172 | if send_pic_mes: 173 | await bot.send(ev, f"[CQ:image,file={pic_creater(data, is_steam=True)}]") 174 | return 175 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_creater(data)) 176 | except Exception as err: 177 | if "retcode=100" in str(err): 178 | await bot.send(ev, "消息可能被风控,正在转为其他形式发送!") 179 | try: 180 | if send_pic_mes: 181 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_creater(data)) 182 | return 183 | await bot.send(ev, f"[CQ:image,file={pic_creater(data, is_steam=True)}]") 184 | except Exception as err: 185 | if "retcode=100" in str(err): 186 | await bot.send(ev, "消息可能依旧被风控,无法完成发送!") 187 | else: 188 | sv.logger.error(f"Error:{traceback.format_exc()}") 189 | await bot.send(ev, f"发生了其他错误,报错内容为{err},请检查运行日志!") 190 | 191 | 192 | @sv.on_fullmatch(("st帮助", "St帮助", "ST帮助", "sT帮助")) 193 | async def help(bot, ev): 194 | helpimg = Image.open(os.path.join(FILE_PATH, "data/help.png")) 195 | b_io = io.BytesIO() 196 | helpimg.save(b_io, format="png") 197 | base64_str = f"base64://{base64.b64encode(b_io.getvalue()).decode()}" 198 | await bot.send(ev, f"[CQ:image,file={base64_str}]") 199 | -------------------------------------------------------------------------------- /xjy.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import json 4 | import os 5 | import re 6 | 7 | from bs4 import BeautifulSoup as bs 8 | from hoshino import Service, get_bot, priv 9 | from PIL import Image, ImageDraw, ImageFont 10 | 11 | from .config import * 12 | 13 | sv = Service("stbot-喜加一") 14 | 15 | head = { 16 | "User-Agent": "Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; BLA-AL00 Build/HUAWEIBLA-AL00) AppleWebKit/537.36 \ 17 | (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/8.9 Mobile Safari/537.36" 18 | } 19 | 20 | 21 | def xjy_compare(): 22 | """ 23 | 爬取it之家喜加一页面数据,对比data文件夹中xjy_result.json已记录的数据及新数据 24 | 返回一个对比列表,列表里包含了新更新的文章链接 25 | """ 26 | data = {} 27 | xjy_url = "https://www.ithome.com/tag/xijiayi" 28 | try: 29 | xjy_page = other_request(url=xjy_url, headers=head).text 30 | soup = bs(xjy_page, "lxml") 31 | url_new = [] 32 | for xjy_info in soup.find_all(name="a", class_="title"): 33 | info_soup = bs(str(xjy_info), "lxml") 34 | url_new.append(info_soup.a["href"]) 35 | if url_new == []: 36 | return "Server Error" 37 | else: 38 | if not os.path.exists(os.path.join(FILE_PATH, "data/xjy_result.json")): 39 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "w+", encoding="utf-8") as f: 40 | data["url"] = url_new 41 | data["groupid"] = [] 42 | f.write(json.dumps(data, ensure_ascii=False)) 43 | url_old = [] 44 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "r+", encoding="utf-8") as f: 45 | content = json.loads(f.read()) 46 | url_old = content["url"] 47 | groupid = content["groupid"] 48 | seta = set(url_new) 49 | setb = set(url_old) 50 | compare_list = list(seta - setb) 51 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "w+", encoding="utf-8") as f: 52 | data["url"] = url_new 53 | data["groupid"] = groupid 54 | f.write(json.dumps(data, ensure_ascii=False)) 55 | except Exception as e: 56 | compare_list = f"xjy_compare_error:{e}" 57 | 58 | return compare_list 59 | 60 | 61 | def xjy_result(model, compare_list): 62 | """ 63 | model为Default时则compare_list为xjy_compare返回的对比列表 64 | model为Query时则compare_list为要查询的项目数量,从data文件夹中xjy_result.json取得对应数量的链接列表 65 | 按照一定格式处理从文章链接爬取的数据并返回 66 | """ 67 | result_text_list = [] 68 | xjy_list = [] 69 | if model == "Default": 70 | xjy_list = compare_list 71 | elif model == "Query": 72 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "r+", encoding="utf-8") as f: 73 | url = json.loads(f.read())["url"] 74 | for i in url: 75 | xjy_list.append(i.strip()) 76 | if url.index(i) == compare_list - 1: 77 | break 78 | try: 79 | for news_url in xjy_list: 80 | page = other_request(url=news_url, headers=head).text 81 | soup = bs(page, "lxml") 82 | info_soup = bs(str(soup.find(name="div", class_="post_content")), "lxml").find_all(name="p") 83 | second_text = "" 84 | for i in info_soup: 85 | if i.a != None: 86 | if i.a["href"] == "https://www.ithome.com/": 87 | text = i.text + "|" 88 | elif "ithome" in i.a["href"]: 89 | text = "" 90 | elif "ithome_super_player" in i.a.get("class", ""): 91 | text = i.text + "|" 92 | else: 93 | text = i.a["href"] + "|" 94 | first_text = text 95 | else: 96 | first_text = i.text + "|" 97 | second_text += first_text.replace("\xa0", " ") 98 | temp_text = second_text.split("|") 99 | third_text = list(set(temp_text)) 100 | third_text.sort(key=temp_text.index) 101 | xjy_url_text = "" 102 | for part in third_text: 103 | if "http" in part: 104 | xjy_url_text += "领取地址:" + part + "\n" 105 | full_text = "" 106 | for i in third_text: 107 | if "https://" in i or "http://" in i: 108 | continue 109 | full_text += i 110 | final_text = f"{third_text[0]}......(更多内容请阅读原文)\n{xjy_url_text}" 111 | result_text_list.append(final_text + f"原文地址:{news_url}") 112 | except Exception as e: 113 | full_text = "" 114 | result_text_list = f"xjy_result_error:{e}" 115 | 116 | return result_text_list, full_text 117 | 118 | 119 | def xjy_remind_group(groupid, add: bool): 120 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "r") as f: 121 | data = json.loads(f.read()) 122 | groupid_list = data["groupid"] 123 | if add: 124 | groupid_list.append(groupid) 125 | data["groupid"] = groupid_list 126 | if not add: 127 | data["groupid"].remove(groupid) 128 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "w") as f: 129 | f.write(json.dumps(data, ensure_ascii=False)) 130 | 131 | 132 | def text_to_img(text): 133 | font_path = os.path.join(FILE_PATH, "msyh.ttc") 134 | font = ImageFont.truetype(font_path, 16) 135 | a = re.findall(r".{1,30}", text.replace(" ", "")) 136 | text = "\n".join(a) 137 | width, height = font.getsize_multiline(text.strip()) 138 | img = Image.new("RGB", (width + 20, height + 20), (255, 255, 255)) 139 | draw = ImageDraw.Draw(img) 140 | draw.text((10, 10), text, font=font, fill=(0, 0, 0)) 141 | b_io = io.BytesIO() 142 | img.save(b_io, format="JPEG") 143 | base64_str = "base64://" + base64.b64encode(b_io.getvalue()).decode() 144 | return base64_str 145 | 146 | 147 | xjy_compare() 148 | 149 | # 后接想要的资讯条数(阿拉伯数字) 150 | @sv.on_prefix("喜加一资讯") 151 | async def xjy_info(bot, ev): 152 | if not os.path.exists(os.path.join(FILE_PATH, "data/xjy_result.json")): 153 | try: 154 | xjy_compare() 155 | except Exception as e: 156 | sv.logger.error(f"Error:{traceback.format_exc()}") 157 | await bot.send(ev, f"哦吼,出错了,报错内容为:{e},请检查运行日志!") 158 | num = ev.message.extract_plain_text().strip() 159 | result = xjy_result("Query", int(num))[0] 160 | mes_list = [] 161 | if "error" in result: 162 | sv.logger.error(result) 163 | await bot.send(ev, f"哦吼,出错了,报错内容为:{result},请检查运行日志!") 164 | return 165 | else: 166 | if len(result) <= 3: 167 | for i in result: 168 | await bot.send(ev, message=i) 169 | else: 170 | for i in result: 171 | data = {"type": "node", "data": {"name": "sbeam机器人", "uin": "2854196310", "content": i}} 172 | mes_list.append(data) 173 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_list) 174 | 175 | 176 | # 喜加一提醒开关 177 | @sv.on_suffix("喜加一提醒") 178 | async def xjy_remind_control(bot, ev): 179 | if not priv.check_priv(ev, priv.ADMIN): 180 | await bot.send(ev, "只有管理员才能控制开关哦") 181 | return 182 | mes = ev.message.extract_plain_text().strip() 183 | gid = str(ev.group_id) 184 | if mes == "开启": 185 | xjy_remind_group(gid, add=True) 186 | await bot.send(ev, "喜加一提醒已开启,如有新喜加一信息则会推送") 187 | elif mes == "关闭": 188 | try: 189 | xjy_remind_group(gid, add=False) 190 | await bot.send(ev, "喜加一提醒已关闭") 191 | except ValueError: 192 | await bot.send(ev, "本群并未开启喜加一提醒") 193 | 194 | 195 | # 定时检查是否有新的喜加一信息 196 | @sv.scheduled_job("cron", hour="*", minute="*") 197 | async def xjy_remind(): 198 | bot = get_bot() 199 | url_list = xjy_compare() 200 | with open(os.path.join(FILE_PATH, "data/xjy_result.json"), "r") as f: 201 | data = json.loads(f.read()) 202 | group_list = data["groupid"] 203 | if "Server Error" in url_list: 204 | sv.logger.info("访问it之家出错,非致命错误,可忽略") 205 | elif "error" in url_list: 206 | sv.logger.error(url_list) 207 | elif len(url_list) != 0: 208 | mes = xjy_result("Default", url_list) 209 | for gid in group_list: 210 | try: 211 | await bot.send_group_msg(group_id=int(gid), message="侦测到在途的喜加一信息,即将进行推送...") 212 | for i in mes[0]: 213 | await bot.send_group_msg(group_id=int(gid), message=i) 214 | except Exception as e: 215 | if "retcode=100" in str(e): 216 | mes_list = [] 217 | for i in mes[0]: 218 | data = {"type": "node", "data": {"name": "sbeam机器人", "uin": "2854196310", "content": i}} 219 | mes_list.append(data) 220 | try: 221 | await bot.send_group_forward_msg(group_id=int(gid), messages=mes_list) 222 | except Exception as e1: 223 | if "retcode=100" in str(e1): 224 | await bot.send_group_msg(group_id=int(gid), message=text_to_img(mes[1])) 225 | else: 226 | sv.logger.info("无新喜加一信息") 227 | -------------------------------------------------------------------------------- /xiaoheihe.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from hoshino import Service 4 | 5 | from .config import * 6 | from .take_my_money import pic_creater 7 | 8 | sv = Service("stbot-小黑盒") 9 | 10 | # 小黑盒爬虫 11 | def hey_box(page: int): 12 | url = f"https://api.xiaoheihe.cn/game/web/all_recommend/games/?os_type=web&version=999.0.0&show_type=discount&limit=30&offset={str((page - 1) * 30)}" 13 | json_page = json.loads(other_request(url, headers=header).text) 14 | result_list = json_page["result"]["list"] 15 | result = [] 16 | for i in result_list: 17 | lowest_stat = "无当前是否史低信息" 18 | is_lowest = i["price"].get("is_lowest", "") 19 | if not is_lowest: 20 | is_lowest = i["heybox_price"].get("is_lowest", "") 21 | if is_lowest == 0: 22 | lowest_stat = "不是史低哦" 23 | elif is_lowest == 1: 24 | lowest_stat = "是史低哦" 25 | discount = str(i["price"].get("discount", "")) 26 | if not discount: 27 | discount = str(i["heybox_price"].get("discount", "")) 28 | new_lowest = i["price"].get("new_lowest", " ") 29 | gameinfo = { 30 | "appid": str(i["appid"]), 31 | "链接": f"https://store.steampowered.com/app/{str(i['appid'])}", 32 | "图片": i["game_img"], 33 | "标题": i["game_name"], 34 | "原价": str(i["price"]["initial"]), 35 | "当前价": str(i["price"]["current"]), 36 | "平史低价": str(i["price"].get("lowest_price", "无平史低价格信息")), 37 | "折扣比": discount, 38 | "是否史低": lowest_stat, 39 | "是否新史低": "好耶!是新史低!" if new_lowest == 1 else " ", 40 | "截止日期": i["price"].get("deadline_date", "无截止日期信息"), 41 | } 42 | result.append(gameinfo) 43 | 44 | return result 45 | 46 | 47 | # 小黑盒搜索爬虫 48 | def hey_box_search(game_name: str): 49 | url = f"https://api.xiaoheihe.cn/game/search/?os_type=web&version=999.0.0&q={game_name}" 50 | json_page = json.loads(other_request(url, headers=header).text) 51 | game_result = json_page["result"]["games"] 52 | result = [] 53 | for i in game_result: 54 | gameinfo = {} 55 | platform = i.get("platforms", "") 56 | if "steam" in platform: 57 | if i.get("is_free"): 58 | gameinfo = { 59 | "appid": str(i["steam_appid"]), 60 | "链接": f"https://store.steampowered.com/app/{str(i['steam_appid'])}", 61 | "原价": "免费开玩", 62 | "标题": i["name"], 63 | "图片": i["image"], 64 | "其他平台图片": i["image"], 65 | "平台": platform, 66 | } 67 | result.append(gameinfo) 68 | continue 69 | if i.get("price", "") != "": 70 | original = i["price"]["initial"] 71 | current = i["price"]["current"] 72 | if original != current: 73 | discount = i["price"]["discount"] 74 | lowest_state = "是史低哦" if i["price"]["is_lowest"] == 1 else "不是史低哦" 75 | newlowest = "好耶!是新史低!" if i["price"].get("new_lowest", "") == 1 else " " 76 | deadline = i["price"].get("deadline_date", "无截止日期信息") 77 | else: 78 | discount = "当前无打折信息" 79 | lowest_state = newlowest = deadline = " " 80 | gameinfo = { 81 | "appid": str(i["steam_appid"]), 82 | "链接": f"https://store.steampowered.com/app/{str(i['steam_appid'])}", 83 | "原价": original, 84 | "当前价": current, 85 | "折扣比": discount, 86 | "是否史低": lowest_state, 87 | "是否新史低": newlowest, 88 | "截止日期": deadline, 89 | "平史低价": str(i["price"].get("lowest_price", "无平史低价格信息")), 90 | "标题": i["name"], 91 | "图片": i["image"], 92 | "其他平台图片": i["image"], 93 | "平台": platform, 94 | } 95 | else: 96 | gameinfo = { 97 | "appid": str(i["steam_appid"]), 98 | "链接": f"https://store.steampowered.com/app/{str(i['steam_appid'])}", 99 | "原价": "获取失败!可能为免费游戏", 100 | "标题": i["name"], 101 | "图片": i["image"], 102 | "其他平台图片": i["image"], 103 | "平台": platform, 104 | } 105 | else: 106 | gameinfo = { 107 | "appid": str(i["steam_appid"]), 108 | "链接": f"https://www.xiaoheihe.cn/games/detail/{str(i['steam_appid'])}", 109 | "标题": i["name"], 110 | "图片": i["image"], 111 | "其他平台图片": i["image"], 112 | "平台": "非steam平台,不进行解析,请自行查看链接", 113 | } 114 | result.append(gameinfo) 115 | 116 | return result 117 | 118 | 119 | def mes_creater(result, gamename): 120 | mes_list = [] 121 | if result[0].get("平台", "") == "": 122 | content = f" ***数据来源于小黑盒官网***\n***默认展示小黑盒steam促销页面***" 123 | for i in range(len(result)): 124 | mes = ( 125 | f"[CQ:image,file={result[i]['图片']}]\n{result[i]['标题']}\n原价:¥{result[i]['原价']} \ 126 | 当前价:¥{result[i]['当前价']}(-{result[i]['折扣比']}%)\n平史低价:¥{result[i]['平史低价']} {result[i]['是否史低']}\n链接:{result[i]['链接']}\ 127 | \n{result[i]['截止日期']}(不一定准确,请以steam为准)\n{result[i]['是否新史低']}\nappid:{result[i]['appid']}".strip() 128 | .replace("\n ", "") 129 | .replace(" ", "") 130 | ) 131 | data = {"type": "node", "data": {"name": "sbeam机器人", "uin": "2854196310", "content": mes}} 132 | mes_list.append(data) 133 | else: 134 | content = f"***数据来源于小黑盒官网***\n游戏{gamename}搜索结果如下" 135 | for i in range(len(result)): 136 | if "非steam平台" in result[i]["平台"]: 137 | mes = f"[CQ:image,file={result[i]['其他平台图片']}]\n{result[i]['标题']}\n{result[i]['平台']}\n{result[i]['链接']} (请在pc打开,在手机打开会下载小黑盒app)".strip().replace( 138 | "\n ", "" 139 | ) 140 | elif "免费" in result[i]["原价"]: 141 | mes = mes = ( 142 | f"[CQ:image,file={result[i]['图片']}]\n{result[i]['标题']}\n原价:{result[i]['原价']}\n链接:{result[i]['链接']}\nappid:{result[i]['appid']}".strip() 143 | .replace("\n ", "") 144 | .replace(" ", "") 145 | ) 146 | elif result[i]["折扣比"] == "当前无打折信息": 147 | mes = ( 148 | f"[CQ:image,file={result[i]['图片']}]\n{result[i]['标题']}\n{result[i]['折扣比']}\n当前价:¥{result[i]['当前价']} \ 149 | 平史低价:¥{result[i]['平史低价']}\n链接:{result[i]['链接']}\nappid:{result[i]['appid']}".strip() 150 | .replace("\n ", "") 151 | .replace(" ", "") 152 | ) 153 | else: 154 | mes = ( 155 | f"[CQ:image,file={result[i]['图片']}]\n{result[i]['标题']}\n原价:¥{result[i]['原价']} 当前价:¥{result[i]['当前价']}\ 156 | (-{result[i]['折扣比']}%)\n平史低价:¥{result[i]['平史低价']} {result[i]['是否史低']}\n链接:{result[i]['链接']}\n\ 157 | {result[i]['截止日期']}\n{result[i]['是否新史低']}\nappid:{result[i]['appid']}".strip() 158 | .replace("\n ", "") 159 | .replace(" ", "") 160 | ) 161 | data = {"type": "node", "data": {"name": "sbeam机器人", "uin": "2854196310", "content": mes}} 162 | mes_list.append(data) 163 | announce = {"type": "node", "data": {"name": "sbeam机器人", "uin": "2854196310", "content": content}} 164 | mes_list.insert(0, announce) 165 | return mes_list 166 | 167 | 168 | @sv.on_prefix("小黑盒") 169 | async def heybox(bot, ev): 170 | mes = ev.message.extract_plain_text().strip() 171 | gamename = "" 172 | try: 173 | if "特惠" in mes: 174 | data = hey_box(1) 175 | elif "搜" in mes: 176 | gamename = mes[1:] 177 | data = hey_box_search(gamename) 178 | if len(data) == 0: 179 | await bot.send(ev, "无搜索结果") 180 | return 181 | else: 182 | return 183 | await bot.send(ev, "正在搜索并生成消息中,请稍等片刻!", at_sender=True) 184 | except Exception as e: 185 | sv.logger.error(f"Error:{traceback.format_exc()}") 186 | await bot.send(ev, f"哦吼,获取信息出错了,报错内容为{e},请检查运行日志!") 187 | try: 188 | if send_pic_mes: 189 | await bot.send(ev, f"[CQ:image,file={pic_creater(data, is_steam=False)}]") 190 | return 191 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_creater(data, gamename)) 192 | except Exception as err: 193 | if "retcode=100" in str(err): 194 | await bot.send(ev, "消息可能被风控,正在转为其他形式发送!") 195 | try: 196 | if send_pic_mes: 197 | await bot.send_group_forward_msg(group_id=ev["group_id"], messages=mes_creater(data, gamename)) 198 | return 199 | await bot.send(ev, f"[CQ:image,file={pic_creater(data, is_steam=False)}]") 200 | except Exception as err: 201 | if "retcode=100" in str(err): 202 | await bot.send(ev, "消息可能依旧被风控,无法完成发送!") 203 | else: 204 | sv.logger.error(f"Error:{traceback.format_exc()}") 205 | await bot.send(ev, f"发生了其他错误,报错内容为{err},请检查运行日志!") 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License Version 2.0, 1月2004日 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 2 | -------------------------------------------------------------------------------- /take_my_money.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import json 4 | import math 5 | import os 6 | import re 7 | from ast import Return 8 | from datetime import datetime 9 | 10 | from bs4 import BeautifulSoup as bs 11 | from hoshino import Service, get_bot, priv 12 | from PIL import Image, ImageDraw, ImageFont 13 | 14 | from .config import * 15 | 16 | sv = Service("stbot-促销信息") 17 | 18 | font_path = os.path.join(FILE_PATH, "msyh.ttc") 19 | 20 | def get_size(font_num, text): 21 | font = ImageFont 22 | if font_num == 1: 23 | font = ImageFont.truetype(font_path, 18) 24 | elif font_num == 2: 25 | font = ImageFont.truetype(font_path, 12) 26 | elif font_num == 3: 27 | font = ImageFont.truetype(font_path, 13) 28 | bbox = font.getbbox(text) 29 | return bbox[2] - bbox[0], bbox[3] - bbox[1] 30 | 31 | 32 | def resize_font(font_size, text_str, limit_width): 33 | """ 34 | 在给定的长度内根据文字内容来改变文字的字体大小 35 | font_size为默认大小,即如果函数判断以此字体大小所绘制出来的文字内容不会超过给定的长度时,则保持这个大小 36 | 若绘制出来的文字内容长度大于给定长度,则会不断对减小字体大小直至刚好小于给定长度 37 | text_str为文字内容,limit_width为给定的长度 38 | 返回内容为PIL.ImageFont.FreeTypeFont对象,以及调整字体过后的文字长宽 39 | """ 40 | 41 | font = ImageFont.truetype(font_path, font_size) 42 | bbox = font.getbbox(str(text_str)) 43 | font_lenth = bbox[2] - bbox[0] 44 | while font_lenth > limit_width: 45 | font_size -= 1 46 | font = ImageFont.truetype(font_path, font_size) 47 | bbox = font.getbbox(str(text_str)) 48 | font_lenth = bbox[2] - bbox[0] 49 | font_width = bbox[3] - bbox[1] 50 | return font, font_lenth, font_width 51 | 52 | 53 | def steam_monitor(): 54 | url = "https://keylol.com" 55 | r = other_request(url).text 56 | soup = bs(r, "lxml") 57 | stat = soup.find(name="div", id="steam_monitor") 58 | a = stat.findAll(name="a") 59 | for i in a: 60 | if "状态" in str(i.text): 61 | continue 62 | sell_name = i.text.replace(" ", "").strip() 63 | script = stat.find_next_sibling(name="script").string 64 | date = re.findall(r'new Date\("(.*?)"', script)[0] 65 | if date == "": 66 | sell_date = "促销已经结束" 67 | return sell_name, sell_date 68 | a = datetime.strptime(date, "%Y-%m-%d %H:%M") 69 | b = datetime.now() 70 | xc = (a - b).total_seconds() 71 | m, s = divmod(int(xc), 60) 72 | h, m = divmod(m, 60) 73 | d, h = divmod(h, 24) 74 | sell_date = "%d日%d时%d分%d秒" % (d, h, m, s) 75 | return sell_name, sell_date 76 | 77 | 78 | def pic_creater(data: list, num=Limit_num, is_steam=True, monitor_on=False): 79 | """ 80 | 生成一个图片,data为小黑盒或steam爬取的数据 num为图片中游戏项目的数量,默认为config.py里设定的数量 81 | is_steam为判断传入的数据是否steam来源,monitor_on为是否加入促销活动信息 两者需手动指定 82 | """ 83 | if len(data) < num: 84 | num = len(data) 85 | 86 | if monitor_on: 87 | background = Image.new("RGB", (520, (60 + 10) * num + 10 + 110), (27, 40, 56)) 88 | start_pos = 110 89 | sell_info = steam_monitor() 90 | sell_bar = Image.new("RGB", (500, 100), (22, 32, 45)) 91 | draw_sell_bar = ImageDraw.Draw(sell_bar, "RGB") 92 | uppper_text = sell_info[0].split(":")[0] 93 | if "正在进行中" in sell_info[0].split(":")[1]: 94 | lower_text = f"正在进行中(预计{sell_info[1]}后结束)" 95 | cdtext_color = (255, 0, 0) 96 | elif "结束" in sell_info[1]: 97 | lower_text = f"{sell_info[1]}" 98 | cdtext_color = (109, 115, 126) 99 | else: 100 | lower_text = f"预计{sell_info[1]}后开始" 101 | cdtext_color = (0, 255, 0) 102 | uppper_text_font = resize_font(20, uppper_text, 490) 103 | draw_sell_bar.text( 104 | ((500 - uppper_text_font[1]) / 2, 20), uppper_text, font=uppper_text_font[0], fill=(199, 213, 224) 105 | ) 106 | draw_sell_bar.text(((500 - get_size(1, lower_text)[0]) / 2, 62), lower_text, font=ImageFont.truetype(font_path, 18), fill=cdtext_color) 107 | background.paste(sell_bar, (10, 10)) 108 | else: 109 | background = Image.new("RGB", (520, (60 + 10) * num + 10), (27, 40, 56)) 110 | start_pos = 0 111 | 112 | for i in range(num): 113 | game_bgbar = Image.new("RGB", (500, 60), (22, 32, 45)) 114 | draw_game_bgbar = ImageDraw.Draw(game_bgbar, "RGB") 115 | 116 | if not is_steam: 117 | if "非steam平台" in data[i].get("平台", ""): 118 | a = other_request(data[i].get("其他平台图片"), headers=header).content 119 | aimg_bytestream = io.BytesIO(a) 120 | a_imgb = Image.open(aimg_bytestream).resize((160, 60)) 121 | game_bgbar.paste(a_imgb, (0, 0)) 122 | draw_game_bgbar.text((165, 5), data[i].get("标题"), font=ImageFont.truetype(font_path, 18), fill=(199, 213, 224)) 123 | draw_game_bgbar.text((165, 35), data[i].get("平台"), font=ImageFont.truetype(font_path, 12), fill=(199, 213, 224)) 124 | background.paste(game_bgbar, (10, 60 * i + 10 * (i + 1))) 125 | continue 126 | 127 | try: 128 | if not is_steam: 129 | a = other_request(data[i].get("图片"), headers=header).content 130 | else: 131 | a = other_request(data[i].get("高分辨率图片")).content 132 | aimg_bytestream = io.BytesIO(a) 133 | a_imgb = Image.open(aimg_bytestream).resize((160, 60)) 134 | except: 135 | a = other_request(data[i].get("低分辨率图片")).content 136 | aimg_bytestream = io.BytesIO(a) 137 | a_imgb = Image.open(aimg_bytestream).resize((160, 60)) 138 | game_bgbar.paste(a_imgb, (0, 0)) 139 | 140 | if is_steam: 141 | rate_bg = Image.new("RGBA", (54, 18), (0, 0, 0, 200)) 142 | a = rate_bg.split()[3] 143 | game_bgbar.paste(rate_bg, (106, 0), a) 144 | draw_game_bgbar.text((107, 0), data[i].get("评测").split(",")[0], font=ImageFont.truetype(font_path, 13), fill=(255, 255, 225)) 145 | 146 | gameinfo_area = Image.new("RGB", (280, 60), (22, 32, 45)) 147 | draw_gameinfo_area = ImageDraw.Draw(gameinfo_area, "RGB") 148 | draw_gameinfo_area.text((0, 5), data[i].get("标题"), font=ImageFont.truetype(font_path, 18), fill=(199, 213, 224)) 149 | if is_steam: 150 | draw_gameinfo_area.text((0, 35), data[i].get("标签"), font=ImageFont.truetype(font_path, 12), fill=(199, 213, 224)) 151 | else: 152 | if data[i].get("原价") == "免费开玩": 153 | text = "免费开玩" 154 | elif "获取失败" in data[i].get("原价"): 155 | text = "获取失败!可能为免费游戏" 156 | elif data[i].get("平史低价") == "无平史低价格信息": 157 | text = "无平史低价格信息" 158 | elif data[i].get("折扣比") == "当前无打折信息": 159 | text = f"平史低价:¥{data[i].get('平史低价')} | 当前无打折信息" 160 | else: 161 | text = f"平史低价:¥{data[i].get('平史低价')} | {data[i].get('是否史低')} | {data[i].get('截止日期')} | {data[i].get('是否新史低') if data[i].get('是否新史低')!=' ' else '不是新史低'}" 162 | draw_gameinfo_area.text((0, 35), text, font=ImageFont.truetype(font_path, 12), fill=(199, 213, 224)) 163 | game_bgbar.paste(gameinfo_area, (165, 0)) 164 | 165 | if (is_steam and data[i].get("折扣价", " ") != " ") or ( 166 | not is_steam and "免费" not in data[i].get("原价") and data[i].get("折扣比") != "当前无打折信息" 167 | ): 168 | if is_steam: 169 | original_price = data[i].get("原价") 170 | discount_price, discount_percent = re.findall(r"^(.*?)\((.*?)\)", data[i].get("折扣价"))[0] 171 | else: 172 | original_price = f"¥{data[i].get('原价')}" 173 | discount_price = f"¥{data[i].get('当前价')}" 174 | discount_percent = f"-{data[i].get('折扣比')}%" 175 | green_bar = Image.new( 176 | "RGB", (get_size(2, discount_percent)[0], get_size(2, discount_percent)[1] + 4), (76, 107, 34) 177 | ) 178 | game_bgbar.paste(green_bar, (math.ceil(445 + (55 - get_size(2, discount_percent)[0]) / 2), 4)) 179 | draw_game_bgbar.text( 180 | (math.ceil(445 + (55 - get_size(2, discount_percent)[0]) / 2), 4), 181 | discount_percent, 182 | font=ImageFont.truetype(font_path, 12), 183 | fill=(199, 213, 224), 184 | ) 185 | draw_game_bgbar.text( 186 | (math.ceil(445 + (55 - get_size(2, original_price)[0]) / 2), 22), 187 | original_price, 188 | font=ImageFont.truetype(font_path, 12), 189 | fill=(136, 136, 136), 190 | ) 191 | del_line = Image.new("RGB", (get_size(2, original_price)[0], 1), (136, 136, 136)) 192 | game_bgbar.paste( 193 | del_line, 194 | ( 195 | 445 + math.ceil((55 - get_size(2, original_price)[0]) / 2), 196 | 22 + math.ceil(get_size(2, original_price)[1] / 2) + 2, 197 | ), 198 | ) 199 | draw_game_bgbar.text( 200 | (math.ceil(445 + (55 - get_size(2, discount_price)[0]) / 2), 40), 201 | discount_price, 202 | font=ImageFont.truetype(font_path, 12), 203 | fill=(199, 213, 224), 204 | ) 205 | else: 206 | if is_steam: 207 | original_price = data[i].get("原价") 208 | elif data[i].get("原价") == "免费开玩": 209 | original_price = "免费开玩" 210 | elif "获取失败" in data[i].get("原价"): 211 | original_price = "获取失败" 212 | else: 213 | original_price = "¥" + data[i].get("原价") 214 | temp_font = resize_font(12, original_price, 55) 215 | draw_game_bgbar.text( 216 | (math.ceil(445 + (55 - temp_font[1]) / 2), math.ceil(30 - temp_font[2] / 2)), 217 | original_price, 218 | font=temp_font[0], 219 | fill=(199, 213, 224), 220 | ) 221 | 222 | background.paste(game_bgbar, (10, start_pos + 60 * i + 10 * (i + 1))) 223 | 224 | b_io = io.BytesIO() 225 | background.save(b_io, format="JPEG") 226 | base64_str = "base64://" + base64.b64encode(b_io.getvalue()).decode() 227 | return base64_str 228 | 229 | 230 | def sell_remind_group(groupid, add: bool): 231 | data = {} 232 | if not os.path.exists(os.path.join(FILE_PATH, "data/sell_remind_group.txt")): 233 | with open(os.path.join(FILE_PATH, "data/sell_remind_group.txt"), "w", encoding="utf-8") as f: 234 | data["groupid"] = [] 235 | f.write(json.dumps(data, ensure_ascii=False)) 236 | with open(os.path.join(FILE_PATH, "data/sell_remind_group.txt"), "r", encoding="utf-8") as f: 237 | data = json.loads(f.read()) 238 | groupid_list = data["groupid"] 239 | if add: 240 | groupid_list.append(groupid) 241 | data["groupid"] = groupid_list 242 | if not add: 243 | data["groupid"].remove(groupid) 244 | with open(os.path.join(FILE_PATH, "data/sell_remind_group.txt"), "w", encoding="utf-8") as f: 245 | f.write(json.dumps(data, ensure_ascii=False)) 246 | 247 | 248 | # 每日促销提醒开关 249 | @sv.on_suffix("每日促销提醒") 250 | async def sell_remind_control(bot, ev): 251 | if not priv.check_priv(ev, priv.ADMIN): 252 | await bot.send(ev, "只有管理员才能控制开关哦") 253 | return 254 | mes = ev.message.extract_plain_text().strip() 255 | gid = str(ev.group_id) 256 | if mes == "开启": 257 | sell_remind_group(gid, add=True) 258 | await bot.send(ev, "每日促销提醒已开启") 259 | elif mes == "关闭": 260 | try: 261 | sell_remind_group(gid, add=False) 262 | await bot.send(ev, "每日促销提醒已关闭") 263 | except ValueError: 264 | await bot.send(ev, "本群并未开启每日促销提醒") 265 | 266 | 267 | # 每天9点发送每日促销提醒消息 268 | @sv.scheduled_job("cron", hour="9") 269 | async def sell_remind(): 270 | from .steam_crawler_botV2 import steam_crawler 271 | from .xiaoheihe import hey_box 272 | 273 | bot = get_bot() 274 | with open(os.path.join(FILE_PATH, "data/sell_remind_group.txt"), "r") as f: 275 | groupid_data = json.loads(f.read()) 276 | groupid = groupid_data["groupid"] 277 | try: 278 | if sell_remind_data_from_steam: 279 | try: 280 | data = steam_crawler(url_specials) 281 | steam = True 282 | except: 283 | data = hey_box(1) 284 | steam = False 285 | else: 286 | try: 287 | data = hey_box(1) 288 | steam = False 289 | except: 290 | data = steam_crawler(url_specials) 291 | steam = True 292 | for gid in groupid: 293 | try: 294 | await bot.send_group_msg( 295 | group_id=int(gid), message=f"[CQ:image,file={pic_creater(data, is_steam=steam, monitor_on=True)}]" 296 | ) 297 | except Exception as e: 298 | sv.logger.info(f"每日促销提醒出错,报错内容为:{traceback.format_exc()}") 299 | await bot.send_group_msg(group_id=int(gid), message=f"每日促销提醒出错,报错内容为:{e}") 300 | except Exception: 301 | sv.logger.info(f"每日促销提醒出错,报错内容为:{traceback.format_exc()}") 302 | 303 | 304 | @sv.on_fullmatch("查询促销") 305 | async def query_sell_info(bot, ev): 306 | try: 307 | sell_info = steam_monitor() 308 | except Exception as e: 309 | sv.logger.info(f"Error:{traceback.format_exc()}") 310 | await bot.send(ev, f"获取信息失败,报错内容为:{e}") 311 | return 312 | sell_name = sell_info[0].split(":")[0] 313 | if "正在进行中" in sell_info[0].split(":")[1]: 314 | sell_time = f"正在进行中(预计{sell_info[1]}后结束)" 315 | elif "结束" in sell_info[1]: 316 | sell_time = f"{sell_info[1]}" 317 | else: 318 | sell_time = f"预计{sell_info[1]}后开始" 319 | await bot.send(ev, f"{sell_name}:{sell_time}") 320 | -------------------------------------------------------------------------------- /data/tag.json: -------------------------------------------------------------------------------- 1 | {"tag_dict":{ 2 | "独立": "492", 3 | "动作": "19", 4 | "冒险": "21", 5 | "单人": "4182", 6 | "休闲": "597", 7 | "模拟": "599", 8 | "策略": "9", 9 | "角色扮演": "122", 10 | "氛围": "4166", 11 | "2D": "3871", 12 | "剧情丰富": "1742", 13 | "好评原声音轨": "1756", 14 | "多人": "3859", 15 | "解谜": "1664", 16 | "动漫": "4085", 17 | "欢乐": "4136", 18 | "女性主角": "7208", 19 | "可爱": "4726", 20 | "奇幻": "1684", 21 | "暴力": "4667", 22 | "困难": "4026", 23 | "第一人称": "3839", 24 | "开放世界": "1695", 25 | "血腥": "4345", 26 | "合作": "1685", 27 | "设计与插画": "84", 28 | "实用工具": "87", 29 | "免费": "113", 30 | "大型多人在线": "128", 31 | "抢先体验": "493", 32 | "竞速": "699", 33 | "体育": "701", 34 | "视频制作": "784", 35 | "照片编辑": "809", 36 | "动画制作和建模": "872", 37 | "音频制作": "1027", 38 | "教育": "1036", 39 | "网络出版": "1038", 40 | "软件培训": "1445", 41 | "火车": "1616", 42 | "音乐": "1621", 43 | "平台游戏": "1625", 44 | "类银河战士恶魔城": "1628", 45 | "狗": "1638", 46 | "建造": "1643", 47 | "驾驶": "1644", 48 | "塔防": "1645", 49 | "砍杀": "1646", 50 | "西部": "1647", 51 | "游戏制作": "1649", 52 | "讽刺": "1651", 53 | "放松": "1654", 54 | "僵尸": "1659", 55 | "生存": "1662", 56 | "第一人称射击": "1663", 57 | "三消": "1665", 58 | "卡牌游戏": "1666", 59 | "恐怖": "1667", 60 | "可模组化": "1669", 61 | "4X": "1670", 62 | "超级英雄": "1671", 63 | "外星人": "1673", 64 | "打字": "1674", 65 | "即时战略": "1676", 66 | "回合制": "1677", 67 | "战争": "1678", 68 | "足球": "1679", 69 | "劫掠": "1680", 70 | "海盗": "1681", 71 | "潜行": "1687", 72 | "忍者": "1688", 73 | "经典": "1693", 74 | "第三人称": "1697", 75 | "指向点击": "1698", 76 | "制作": "1702", 77 | "战术": "1708", 78 | "超现实": "1710", 79 | "迷幻": "1714", 80 | "类 Rogue": "1716", 81 | "六角格棋盘": "1717", 82 | "多人在线战术竞技": "1718", 83 | "喜剧": "1719", 84 | "迷宫探索": "1720", 85 | "心理恐怖": "1721", 86 | "动作即时战略": "1723", 87 | "推箱子": "1730", 88 | "体素": "1732", 89 | "低容错": "1733", 90 | "快节奏": "1734", 91 | "乐高": "1736", 92 | "隐藏物体": "1738", 93 | "回合战略": "1741", 94 | "格斗": "1743", 95 | "篮球": "1746", 96 | "漫画": "1751", 97 | "节奏": "1752", 98 | "滑板": "1753", 99 | "大型多人在线角色扮演": "1754", 100 | "太空": "1755", 101 | "永久死亡": "1759", 102 | "桌游": "1770", 103 | "街机": "1773", 104 | "射击": "1774", 105 | "玩家对战": "1775", 106 | "蒸汽朋克": "1777", 107 | "小说改编": "3796", 108 | "横向滚屏": "3798", 109 | "视觉小说": "3799", 110 | "沙盒": "3810", 111 | "即时战术": "3813", 112 | "第三人称射击": "3814", 113 | "探索": "3834", 114 | "后末日": "3835", 115 | "本地合作": "3841", 116 | "在线合作": "3843", 117 | "故事架构丰富 ": "3854", 118 | "精确平台": "3877", 119 | "竞技": "3878", 120 | "老式": "3916", 121 | "烹饪": "3920", 122 | "沉浸式": "3934", 123 | "科幻": "3942", 124 | "哥特": "3952", 125 | "角色动作": "3955", 126 | "轻度 Rogue": "3959", 127 | "像素图形": "3964", 128 | "史诗级": "3965", 129 | "物理": "3968", 130 | "生存恐怖": "3978", 131 | "历史": "3987", 132 | "战斗": "3993", 133 | "复古": "4004", 134 | "吸血鬼": "4018", 135 | "跑酷": "4036", 136 | "龙": "4046", 137 | "魔法": "4057", 138 | "惊悚": "4064", 139 | "极简主义": "4094", 140 | "战斗竞速": "4102", 141 | "动作冒险": "4106", 142 | "赛博朋克": "4115", 143 | "超人类主义": "4137", 144 | "电影式": "4145", 145 | "二战": "4150", 146 | "职业导向": "4155", 147 | "清版动作": "4158", 148 | "即时": "4161", 149 | "军事": "4168", 150 | "中世纪": "4172", 151 | "拟真": "4175", 152 | "棋类": "4184", 153 | "欲罢不能": "4190", 154 | "3D": "4191", 155 | "卡通风格": "4195", 156 | "贸易": "4202", 157 | "动作角色扮演": "4231", 158 | "短篇": "4234", 159 | "刷宝": "4236", 160 | "剧集": "4242", 161 | "风格化": "4252", 162 | "清版射击": "4255", 163 | "太空飞船": "4291", 164 | "未来": "4295", 165 | "彩色": "4305", 166 | "回合制战斗": "4325", 167 | "城市营造": "4328", 168 | "黑暗": "4342", 169 | "大战略": "4364", 170 | "暗杀": "4376", 171 | "抽象": "4400", 172 | "日系角色扮演": "4434", 173 | "电脑角色扮演": "4474", 174 | "自选历险体验": "4486", 175 | "合作战役": "4508", 176 | "农场管理": "4520", 177 | "快速反应事件": "4559", 178 | "卡通": "4562", 179 | "架空历史": "4598", 180 | "黑暗奇幻": "4604", 181 | "剑术": "4608", 182 | "俯视射击": "4637", 183 | "战争游戏": "4684", 184 | "经济": "4695", 185 | "电影": "4700", 186 | "重玩价值": "4711", 187 | "2D 格斗": "4736", 188 | "角色定制": "4747", 189 | "政治": "4754", 190 | "双摇杆射击": "4758", 191 | "华丽格斗": "4777", 192 | "俯视": "4791", 193 | "机甲": "4821", 194 | "六自由度": "4835", 195 | "4 人本地": "4840", 196 | "资本主义": "4845", 197 | "政治性": "4853", 198 | "谐仿": "4878", 199 | "弹幕射击": "4885", 200 | "爱情": "4947", 201 | "2.5D": "4975", 202 | "海战": "4994", 203 | "反乌托邦": "5030", 204 | "电竞": "5055", 205 | "记叙": "5094", 206 | "程序生成": "5125", 207 | "Kickstarter": "5153", 208 | "竞分": "5154", 209 | "恐龙": "5160", 210 | "冷战": "5179", 211 | "心理": "5186", 212 | "鲜血": "5228", 213 | "续作": "5230", 214 | "上帝模拟": "5300", 215 | "游戏工坊": "5310", 216 | "模组": "5348", 217 | "阖家": "5350", 218 | "破坏": "5363", 219 | "阴谋": "5372", 220 | "2D 平台": "5379", 221 | "一战": "5382", 222 | "时间竞速": "5390", 223 | "3D 平台": "5395", 224 | "标杆测试": "5407", 225 | "唯美": "5411", 226 | "编程": "5432", 227 | "黑客": "5502", 228 | "平台解谜": "5537", 229 | "竞技场射击": "5547", 230 | "RPG 制作大师": "5577", 231 | "情感": "5608", 232 | "成人": "5611", 233 | "推理": "5613", 234 | "收集马拉松": "5652", 235 | "现代": "5673", 236 | "重制": "5708", 237 | "团队导向": "5711", 238 | "悬疑": "5716", 239 | "棒球": "5727", 240 | "机器人": "5752", 241 | "枪械改装": "5765", 242 | "科学": "5794", 243 | "子弹时间": "5796", 244 | "等角视角": "5851", 245 | "步行模拟": "5900", 246 | "网球": "5914", 247 | "黑色幽默": "5923", 248 | "重启": "5941", 249 | "采矿": "5981", 250 | "剧情": "5984", 251 | "马匹": "6041", 252 | "黑色": "6052", 253 | "逻辑": "6129", 254 | "库存管理": "6276", 255 | "外交": "6310", 256 | "犯罪": "6378", 257 | "选择取向": "6426", 258 | "3D 格斗": "6506", 259 | "弹球": "6621", 260 | "时空操控": "6625", 261 | "裸露": "6650", 262 | "90 年代": "6691", 263 | "火星": "6702", 264 | "玩家对战环境": "6730", 265 | "手绘": "6815", 266 | "非线性": "6869", 267 | "海军": "6910", 268 | "武术": "6915", 269 | "罗马": "6948", 270 | "多结局": "6971", 271 | "高尔夫": "7038", 272 | "即时含暂停": "7107", 273 | "社交聚会": "7108", 274 | "众筹": "7113", 275 | "社交聚会游戏": "7178", 276 | "足球/美式足球": "7226", 277 | "单线剧情": "7250", 278 | "滑雪": "7309", 279 | "保龄球": "7328", 280 | "基地建设": "7332", 281 | "本地多人": "7368", 282 | "狙击手": "7423", 283 | "洛夫克拉夫特式": "7432", 284 | "光明会": "7478", 285 | "控制器": "7481", 286 | "网格导向动作": "7569", 287 | "越野": "7622", 288 | "叙事": "7702", 289 | "80 年代": "7743", 290 | "非主流经典": "7782", 291 | "人工智能": "7926", 292 | "原声音轨": "7948", 293 | "软件": "8013", 294 | "TrackIR": "8075", 295 | "小游戏": "8093", 296 | "关卡编辑": "8122", 297 | "基于音乐的程序生成": "8253", 298 | "调查": "8369", 299 | "精心编写": "8461", 300 | "奔跑": "8666", 301 | "资源管理": "8945", 302 | "动漫色情": "9130", 303 | "水底": "9157", 304 | "沉浸式模拟": "9204", 305 | "集换式卡牌游戏": "9271", 306 | "恶魔": "9541", 307 | "恋爱模拟": "9551", 308 | "狩猎": "9564", 309 | "动态旁白": "9592", 310 | "雪": "9803", 311 | "体验": "9994", 312 | "生活模拟": "10235", 313 | "交通运输": "10383", 314 | "网络梗": "10397", 315 | "益智问答": "10437", 316 | "时空旅行": "10679", 317 | "团队角色扮演": "10695", 318 | "灵异": "10808", 319 | "分屏": "10816", 320 | "互动小说": "11014", 321 | "车辆作战": "11104", 322 | "仅鼠标": "11123", 323 | "恶人主角": "11333", 324 | "教程": "12057", 325 | "色情内容": "12095", 326 | "拳击": "12190", 327 | "战锤 40K": "12286", 328 | "管理": "12472", 329 | "纸牌": "13070", 330 | "美国": "13190", 331 | "坦克": "13276", 332 | "射箭": "13382", 333 | "航海": "13577", 334 | "试验性": "13782", 335 | "游戏开发": "13906", 336 | "回合制战术": "14139", 337 | "龙与地下城": "14153", 338 | "怀旧": "14720", 339 | "蓄意操控困难": "14906", 340 | "飞行": "15045", 341 | "对话": "15172", 342 | "哲理": "15277", 343 | "纪录片": "15339", 344 | "钓鱼": "15564", 345 | "摩托车越野": "15868", 346 | "无声主角": "15954", 347 | "神话": "16094", 348 | "赌博": "16250", 349 | "太空模拟": "16598", 350 | "时间管理": "16689", 351 | "狼人": "17015", 352 | "策略角色扮演": "17305", 353 | "旅鼠": "17337", 354 | "桌上游戏": "17389", 355 | "异步多人": "17770", 356 | "猫": "17894", 357 | "台球": "17927", 358 | "全动态影像": "18594", 359 | "骑车": "19568", 360 | "潜水艇": "19780", 361 | "黑色喜剧": "19995", 362 | "地下": "21006", 363 | "战术角色扮演": "21725", 364 | "虚拟现实": "21978", 365 | "农业": "22602", 366 | "迷你高尔夫": "22955", 367 | "文字游戏": "24003", 368 | "工作场所不宜": "24904", 369 | "触控": "25085", 370 | "政治模拟": "26921", 371 | "声控": "27758", 372 | "单板滑雪": "28444", 373 | "3D 视觉": "29363", 374 | "类魂系列": "29482", 375 | "情境": "29855", 376 | "自然": "30358", 377 | "基于文字": "31275", 378 | "少女游戏": "31579", 379 | "牌组构建": "32322", 380 | "动作类 Rogue": "42804", 381 | "LGBTQ+": "44868", 382 | "摔角": "47827", 383 | " 外国": "51306", 384 | "轨道射击": "56690", 385 | "电子音乐": "61357", 386 | "拼字": "71389", 387 | "农场模拟": "87918", 388 | "喷气机": "92092", 389 | "滑行": "96359", 390 | "8-bit 音乐": "117648", 391 | "自行车": "123332", 392 | "ATV": "129761", 393 | "电子": "143739", 394 | "游戏相关": "150626", 395 | "大逃杀": "176981", 396 | "信仰": "180368", 397 | "器乐": "189941", 398 | "不可思议迷宫": "198631", 399 | "摩托车": "198913", 400 | "殖民模拟": "220585", 401 | "长篇电影": "233824", 402 | "自行车越野": "252854", 403 | "自动化": "255534", 404 | "冰球": "324176", 405 | "摇滚乐": "337964", 406 | "Steam 主机": "348922", 407 | "刷宝射击游戏": "353880", 408 | "点击游戏": "379975", 409 | "传统类 Rogue": "454187", 410 | "硬件": "603297", 411 | "懒人游戏": "615955", 412 | "英雄射击": "620519", 413 | "社交推理": "745697", 414 | "360 视频": "776177", 415 | "卡牌战斗": "791774", 416 | "非对称 VR": "856791", 417 | "生物收集": "916648", 418 | "Rogue 恶魔城": "922563", 419 | "自走棋": "1084988", 420 | "卡牌构建式类 Rogue": "1091588", 421 | "疫病爆发模拟": "1100686", 422 | "汽车模拟": "1100687", 423 | "医疗模拟": "1100688", 424 | "开放世界生存制作": "1100689", 425 | "492": "独立", 426 | "19": "动作", 427 | "21": "冒险", 428 | "4182": "单人", 429 | "597": "休闲", 430 | "599": "模拟", 431 | "9": "策略", 432 | "122": "角色扮演", 433 | "4166": "氛围", 434 | "3871": "2D", 435 | "1742": "剧情丰富", 436 | "1756": "好评原声音轨", 437 | "3859": "多人", 438 | "1664": "解谜", 439 | "4085": "动漫", 440 | "4136": "欢乐", 441 | "7208": "女性主角", 442 | "4726": "可爱", 443 | "1684": "奇幻", 444 | "4667": "暴力", 445 | "4026": "困难", 446 | "3839": "第一人称", 447 | "1695": "开放世界", 448 | "4345": "血腥", 449 | "1685": "合作", 450 | "84": "设计与插画", 451 | "87": "实用工具", 452 | "113": "免费", 453 | "128": "大型多人在线", 454 | "493": "抢先体验", 455 | "699": "竞速", 456 | "701": "体育", 457 | "784": "视频制作", 458 | "809": "照片编辑", 459 | "872": "动画制作和建模", 460 | "1027": "音频制作", 461 | "1036": "教育", 462 | "1038": "网络出版", 463 | "1445": "软件培训", 464 | "1616": "火车", 465 | "1621": "音乐", 466 | "1625": "平台游戏", 467 | "1628": "类银河战士恶魔城", 468 | "1638": "狗", 469 | "1643": "建造", 470 | "1644": "驾驶", 471 | "1645": "塔防", 472 | "1646": "砍杀", 473 | "1647": "西部", 474 | "1649": "游戏制作", 475 | "1651": "讽刺", 476 | "1654": "放松", 477 | "1659": "僵尸", 478 | "1662": "生存", 479 | "1663": "第一人称射击", 480 | "1665": "三消", 481 | "1666": "卡牌游戏", 482 | "1667": "恐怖", 483 | "1669": "可模组化", 484 | "1670": "4X", 485 | "1671": "超级英雄", 486 | "1673": "外星人", 487 | "1674": "打字", 488 | "1676": "即时战略", 489 | "1677": "回合制", 490 | "1678": "战争", 491 | "1679": "足球", 492 | "1680": "劫掠", 493 | "1681": "海盗", 494 | "1687": "潜行", 495 | "1688": "忍者", 496 | "1693": "经典", 497 | "1697": "第三人称", 498 | "1698": "指向点击", 499 | "1702": "制作", 500 | "1708": "战术", 501 | "1710": "超现实", 502 | "1714": "迷幻", 503 | "1716": "类 Rogue", 504 | "1717": "六角格棋盘", 505 | "1718": "多人在线战术竞技", 506 | "1719": "喜剧", 507 | "1720": "迷宫探索", 508 | "1721": "心理恐怖", 509 | "1723": "动作即时战略", 510 | "1730": "推箱子", 511 | "1732": "体素", 512 | "1733": "低容错", 513 | "1734": "快节奏", 514 | "1736": "乐高", 515 | "1738": "隐藏物体", 516 | "1741": "回合战略", 517 | "1743": "格斗", 518 | "1746": "篮球", 519 | "1751": "漫画", 520 | "1752": "节奏", 521 | "1753": "滑板", 522 | "1754": "大型多人在线角色扮演", 523 | "1755": "太空", 524 | "1759": "永久死亡", 525 | "1770": "桌游", 526 | "1773": "街机", 527 | "1774": "射击", 528 | "1775": "玩家对战", 529 | "1777": "蒸汽朋克", 530 | "3796": "小说改编", 531 | "3798": "横向滚屏", 532 | "3799": "视觉小说", 533 | "3810": "沙盒", 534 | "3813": "即时战术", 535 | "3814": "第三人称射击", 536 | "3834": "探索", 537 | "3835": "后末日", 538 | "3841": "本地合作", 539 | "3843": "在线合作", 540 | "3854": "故事架构丰富 ", 541 | "3877": "精确平台", 542 | "3878": "竞技", 543 | "3916": "老式", 544 | "3920": "烹饪", 545 | "3934": "沉浸式", 546 | "3942": "科幻", 547 | "3952": "哥特", 548 | "3955": "角色动作", 549 | "3959": "轻度 Rogue", 550 | "3964": "像素图形", 551 | "3965": "史诗级", 552 | "3968": "物理", 553 | "3978": "生存恐怖", 554 | "3987": "历史", 555 | "3993": "战斗", 556 | "4004": "复古", 557 | "4018": "吸血鬼", 558 | "4036": "跑酷", 559 | "4046": "龙", 560 | "4057": "魔法", 561 | "4064": "惊悚", 562 | "4094": "极简主义", 563 | "4102": "战斗竞速", 564 | "4106": "动作冒险", 565 | "4115": "赛博朋克", 566 | "4137": "超人类主义", 567 | "4145": "电影式", 568 | "4150": "二战", 569 | "4155": "职业导向", 570 | "4158": "清版动作", 571 | "4161": "即时", 572 | "4168": "军事", 573 | "4172": "中世纪", 574 | "4175": "拟真", 575 | "4184": "棋类", 576 | "4190": "欲罢不能", 577 | "4191": "3D", 578 | "4195": "卡通风格", 579 | "4202": "贸易", 580 | "4231": "动作角色扮演", 581 | "4234": "短篇", 582 | "4236": "刷宝", 583 | "4242": "剧集", 584 | "4252": "风格化", 585 | "4255": "清版射击", 586 | "4291": "太空飞船", 587 | "4295": "未来", 588 | "4305": "彩色", 589 | "4325": "回合制战斗", 590 | "4328": "城市营造", 591 | "4342": "黑暗", 592 | "4364": "大战略", 593 | "4376": "暗杀", 594 | "4400": "抽象", 595 | "4434": "日系角色扮演", 596 | "4474": "电脑角色扮演", 597 | "4486": "自选历险体验", 598 | "4508": "合作战役", 599 | "4520": "农场管理", 600 | "4559": "快速反应事件", 601 | "4562": "卡通", 602 | "4598": "架空历史", 603 | "4604": "黑暗奇幻", 604 | "4608": "剑术", 605 | "4637": "俯视射击", 606 | "4684": "战争游戏", 607 | "4695": "经济", 608 | "4700": "电影", 609 | "4711": "重玩价值", 610 | "4736": "2D 格斗", 611 | "4747": "角色定制", 612 | "4754": "政治", 613 | "4758": "双摇杆射击", 614 | "4777": "华丽格斗", 615 | "4791": "俯视", 616 | "4821": "机甲", 617 | "4835": "六自由度", 618 | "4840": "4 人本地", 619 | "4845": "资本主义", 620 | "4853": "政治性", 621 | "4878": "谐仿", 622 | "4885": "弹幕射击", 623 | "4947": "爱情", 624 | "4975": "2.5D", 625 | "4994": "海战", 626 | "5030": "反乌托邦", 627 | "5055": "电竞", 628 | "5094": "记叙", 629 | "5125": "程序生成", 630 | "5153": "Kickstarter", 631 | "5154": "竞分", 632 | "5160": "恐龙", 633 | "5179": "冷战", 634 | "5186": "心理", 635 | "5228": "鲜血", 636 | "5230": "续作", 637 | "5300": "上帝模拟", 638 | "5310": "游戏工坊", 639 | "5348": "模组", 640 | "5350": "阖家", 641 | "5363": "破坏", 642 | "5372": "阴谋", 643 | "5379": "2D 平台", 644 | "5382": "一战", 645 | "5390": "时间竞速", 646 | "5395": "3D 平台", 647 | "5407": "标杆测试", 648 | "5411": "唯美", 649 | "5432": "编程", 650 | "5502": "黑客", 651 | "5537": "平台解谜", 652 | "5547": "竞技场射击", 653 | "5577": "RPG 制作大师", 654 | "5608": "情感", 655 | "5611": "成人", 656 | "5613": "推理", 657 | "5652": "收集马拉松", 658 | "5673": "现代", 659 | "5708": "重制", 660 | "5711": "团队导向", 661 | "5716": "悬疑", 662 | "5727": "棒球", 663 | "5752": "机器人", 664 | "5765": "枪械改装", 665 | "5794": "科学", 666 | "5796": "子弹时间", 667 | "5851": "等角视角", 668 | "5900": "步行模拟", 669 | "5914": "网球", 670 | "5923": "黑色幽默", 671 | "5941": "重启", 672 | "5981": "采矿", 673 | "5984": "剧情", 674 | "6041": "马匹", 675 | "6052": "黑色", 676 | "6129": "逻辑", 677 | "6276": "库存管理", 678 | "6310": "外交", 679 | "6378": "犯罪", 680 | "6426": "选择取向", 681 | "6506": "3D 格斗", 682 | "6621": "弹球", 683 | "6625": "时空操控", 684 | "6650": "裸露", 685 | "6691": "90 年代", 686 | "6702": "火星", 687 | "6730": "玩家对战环境", 688 | "6815": "手绘", 689 | "6869": "非线性", 690 | "6910": "海军", 691 | "6915": "武术", 692 | "6948": "罗马", 693 | "6971": "多结局", 694 | "7038": "高尔夫", 695 | "7107": "即时含暂停", 696 | "7108": "社交聚会", 697 | "7113": "众筹", 698 | "7178": "社交聚会游戏", 699 | "7226": "足球/美式足球", 700 | "7250": "单线剧情", 701 | "7309": "滑雪", 702 | "7328": "保龄球", 703 | "7332": "基地建设", 704 | "7368": "本地多人", 705 | "7423": "狙击手", 706 | "7432": "洛夫克拉夫特式", 707 | "7478": "光明会", 708 | "7481": "控制器", 709 | "7569": "网格导向动作", 710 | "7622": "越野", 711 | "7702": "叙事", 712 | "7743": "80 年代", 713 | "7782": "非主流经典", 714 | "7926": "人工智能", 715 | "7948": "原声音轨", 716 | "8013": "软件", 717 | "8075": "TrackIR", 718 | "8093": "小游戏", 719 | "8122": "关卡编辑", 720 | "8253": "基于音乐的程序生成", 721 | "8369": "调查", 722 | "8461": "精心编写", 723 | "8666": "奔跑", 724 | "8945": "资源管理", 725 | "9130": "动漫色情", 726 | "9157": "水底", 727 | "9204": "沉浸式模拟", 728 | "9271": "集换式卡牌游戏", 729 | "9541": "恶魔", 730 | "9551": "恋爱模拟", 731 | "9564": "狩猎", 732 | "9592": "动态旁白", 733 | "9803": "雪", 734 | "9994": "体验", 735 | "10235": "生活模拟", 736 | "10383": "交通运输", 737 | "10397": "网络梗", 738 | "10437": "益智问答", 739 | "10679": "时空旅行", 740 | "10695": "团队角色扮演", 741 | "10808": "灵异", 742 | "10816": "分屏", 743 | "11014": "互动小说", 744 | "11104": "车辆作战", 745 | "11123": "仅鼠标", 746 | "11333": "恶人主角", 747 | "12057": "教程", 748 | "12095": "色情内容", 749 | "12190": "拳击", 750 | "12286": "战锤 40K", 751 | "12472": "管理", 752 | "13070": "纸牌", 753 | "13190": "美国", 754 | "13276": "坦克", 755 | "13382": "射箭", 756 | "13577": "航海", 757 | "13782": "试验性", 758 | "13906": "游戏开发", 759 | "14139": "回合制战术", 760 | "14153": "龙与地下城", 761 | "14720": "怀旧", 762 | "14906": "蓄意操控困难", 763 | "15045": "飞行", 764 | "15172": "对话", 765 | "15277": "哲理", 766 | "15339": "纪录片", 767 | "15564": "钓鱼", 768 | "15868": "摩托车越野", 769 | "15954": "无声主角", 770 | "16094": "神话", 771 | "16250": "赌博", 772 | "16598": "太空模拟", 773 | "16689": "时间管理", 774 | "17015": "狼人", 775 | "17305": "策略角色扮演", 776 | "17337": "旅鼠", 777 | "17389": "桌上游戏", 778 | "17770": "异步多人", 779 | "17894": "猫", 780 | "17927": "台球", 781 | "18594": "全动态影像", 782 | "19568": "骑车", 783 | "19780": "潜水艇", 784 | "19995": "黑色喜剧", 785 | "21006": "地下", 786 | "21725": "战术角色扮演", 787 | "21978": "虚拟现实", 788 | "22602": "农业", 789 | "22955": "迷你高尔夫", 790 | "24003": "文字游戏", 791 | "24904": "工作场所不宜", 792 | "25085": "触控", 793 | "26921": "政治模拟", 794 | "27758": "声控", 795 | "28444": "单板滑雪", 796 | "29363": "3D 视觉", 797 | "29482": "类魂系列", 798 | "29855": "情境", 799 | "30358": "自然", 800 | "31275": "基于文字", 801 | "31579": "少女游戏", 802 | "32322": "牌组构建", 803 | "42804": "动作类 Rogue", 804 | "44868": "LGBTQ+", 805 | "47827": "摔角", 806 | "51306": " 外国", 807 | "56690": "轨道射击", 808 | "61357": "电子音乐", 809 | "71389": "拼字", 810 | "87918": "农场模拟", 811 | "92092": "喷气机", 812 | "96359": "滑行", 813 | "117648": "8-bit 音乐", 814 | "123332": "自行车", 815 | "129761": "ATV", 816 | "143739": "电子", 817 | "150626": "游戏相关", 818 | "176981": "大逃杀", 819 | "180368": "信仰", 820 | "189941": "器乐", 821 | "198631": "不可思议迷宫", 822 | "198913": "摩托车", 823 | "220585": "殖民模拟", 824 | "233824": "长篇电影", 825 | "252854": "自行车越野", 826 | "255534": "自动化", 827 | "324176": "冰球", 828 | "337964": "摇滚乐", 829 | "348922": "Steam 主机", 830 | "353880": "刷宝射击游戏", 831 | "379975": "点击游戏", 832 | "454187": "传统类 Rogue", 833 | "603297": "硬件", 834 | "615955": "懒人游戏", 835 | "620519": "英雄射击", 836 | "745697": "社交推理", 837 | "776177": "360 视频", 838 | "791774": "卡牌战斗", 839 | "856791": "非对称 VR", 840 | "916648": "生物收集", 841 | "922563": "Rogue 恶魔城", 842 | "1084988": "自走棋", 843 | "1091588": "卡牌构建式类 Rogue", 844 | "1100686": "疫病爆发模拟", 845 | "1100687": "汽车模拟", 846 | "1100688": "医疗模拟", 847 | "1100689": "开放世界生存制作" 848 | }} --------------------------------------------------------------------------------