|
|
《如何看到特定用户库里游戏的折扣情况?》
《游戏库“窃取”工具 + steam 愿望单导入工具》
《有什么方法能把我游戏库的分类共享给我家庭组成员?》
《想分享家庭组里的小号给朋友,有什么注意事项》
我在 Steam 游戏收集方面的哲学一贯是:
一、寻找有价值的统计指标;
二、围绕该指标建立数据库;
三、利用该数据库进行收集;
四、平衡性价比与收集速度;
五、在本地进行系统化管理;
六、实现成容易分享的形式。
原本这会是相当消耗精力的过程,但 ai 发展起来之后,几乎每个步骤都解放了我重复劳动的部分,让我能有余力把认知资源消耗在上述系统中更具创造力的部分上。出于这个背景,我制作了这个 Steam 库管理助手。吸取之前的教训,这次找朋友多次测试后才放出来。
使用前请备份好 cloudstorage 文件夹,推荐从未做过 Steam 分类的朋友使用。尽管我现在能正常使用,但也请做好分类可能会完全丢失的心理准备!
一、将 txt 格式的 appid 列表(每行一个)导入成 Steam 收藏夹,收藏夹名称为 txt 文件名称。
额外效果:就算你暂时没有这个 appid,等到你入库后同样能同步到这个 Steam 收藏夹内。从而实现某种伪动态效果。
一、在 Steam 库内建立收藏夹,动态显示对方的 Steam 游戏。
额外效果:对方不必是你的 Steam 好友!只要对方的 Steam 库是公开的就能实现动态同步。
- import json
- import time
- import secrets
- import os
- import re
- import tkinter as tk
- from tkinter import filedialog, messagebox, ttk
- class SteamToolbox:
- def __init__(self):
- self.current_dir = os.path.dirname(os.path.abspath(__file__))
- self.json_name = "cloud-storage-namespace-1.json"
- self.json_path = os.path.join(self.current_dir, self.json_name)
- def load_json(self):
- if not os.path.exists(self.json_path):
- messagebox.showerror("错误", f"找不到 {self.json_name}\n请确保脚本和它在同一文件夹。")
- return None
- try:
- with open(self.json_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- except Exception as e:
- messagebox.showerror("读取错误", f"解析失败: {e}")
- return None
- def save_json(self, data):
- output_path = os.path.join(self.current_dir, "cloud-storage-namespace-1_NEW.json")
- try:
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(data, f, ensure_ascii=False, separators=(',', ':'))
- messagebox.showinfo("成功", f"文件已生成:\n{os.path.basename(output_path)}")
- except Exception as e:
- messagebox.showerror("保存失败", f"无法写入文件: {e}")
- def import_from_txt(self):
- data = self.load_json()
- if data is None: return
- txt_paths = filedialog.askopenfilenames(
- initialdir=self.current_dir,
- title="选择 AppID 列表 (TXT)",
- filetypes=[("Text files", "*.txt")]
- )
- if not txt_paths: return
- for path in txt_paths:
- file_title = os.path.splitext(os.path.basename(path))[0]
- with open(path, 'r', encoding='utf-8') as f:
- app_ids = [int(line.strip()) for line in f if line.strip().isdigit()]
-
- if not app_ids: continue
- self._add_static_collection(data, file_title, app_ids)
-
- self.save_json(data)
- def _add_static_collection(self, data, name, app_ids):
- col_id = f"uc-{secrets.token_hex(6)}"
- storage_key = f"user-collections.{col_id}"
- val_obj = {"id": col_id, "name": name, "added": app_ids, "removed": []}
- new_entry = [storage_key, {"key": storage_key, "timestamp": int(time.time()),
- "value": json.dumps(val_obj, ensure_ascii=False, separators=(',', ':')), "version": "1"}]
- data.append(new_entry)
- def open_friend_sync_ui(self):
- data = self.load_json()
- if data is None: return
- sync_win = tk.Toplevel()
- sync_win.title("批量同步 Steam 用户游戏库")
- sync_win.geometry("550x620")
- sync_win.attributes("-topmost", True)
- tk.Label(sync_win, text="1. 请输入对方的 Steam 好友代码(每行一个)", font=("微软雅黑", 10, "bold")).pack(pady=(15,0))
- codes_text = tk.Text(sync_win, height=8, width=60)
- codes_text.pack(padx=20, pady=5)
- tk.Label(sync_win, text="2. 生成的收藏夹名称 (每行一个)", font=("微软雅黑", 10, "bold")).pack(pady=(10,0))
- names_text = tk.Text(sync_win, height=8, width=60)
- names_text.pack(padx=20, pady=5)
- def generate_default_names():
- raw_content = codes_text.get("1.0", tk.END).strip()
- raw_ids = re.findall(r'\d+', raw_content)
- names_text.delete("1.0", tk.END)
- for rid in raw_ids:
- names_text.insert(tk.END, f"好友代码 [{rid}]\n")
- def commit_import():
- codes = re.findall(r'\d+', codes_text.get("1.0", tk.END))
- names = names_text.get("1.0", tk.END).strip().split('\n')
- names = [n.strip() for n in names if n.strip()]
- for i in range(len(codes)):
- cid = codes[i]
- cname = names[i] if i < len(names) else f"好友代码 [{cid}]"
- self._add_dynamic_collection(data, cname, cid)
- if codes:
- self.save_json(data)
- sync_win.destroy()
- btn_frame = tk.Frame(sync_win)
- btn_frame.pack(pady=20)
- tk.Button(btn_frame, text="✨ 生成默认名称", command=generate_default_names, width=18, height=2).pack(side=tk.LEFT, padx=10)
- # 按钮改成黑字,去掉加粗绿色
- tk.Button(btn_frame, text="开始导入", command=commit_import, width=18, height=2).pack(side=tk.LEFT, padx=10)
- def _add_dynamic_collection(self, data, name, friend_code):
- col_id = f"uc-{secrets.token_hex(4)}"
- storage_key = f"user-collections.{col_id}"
- filter_groups = [{"rgOptions": [], "bAcceptUnion": False} for _ in range(9)]
- filter_groups[0]["bAcceptUnion"] = True
- filter_groups[6]["rgOptions"] = [int(friend_code)]
- val_obj = {"id": col_id, "name": name, "added": [], "removed": [],
- "filterSpec": {"nFormatVersion": 2, "strSearchText": "", "filterGroups": filter_groups, "setSuggestions": {}}}
- new_entry = [storage_key, {"key": storage_key, "timestamp": int(time.time()),
- "value": json.dumps(val_obj, ensure_ascii=False, separators=(',', ':')), "version": "1"}]
- data.append(new_entry)
- def main_ui(self):
- root = tk.Tk()
- root.title("Steam 库管理助手")
- root.geometry("640x660")
- sw, sh = root.winfo_screenwidth(), root.winfo_screenheight()
- root.geometry(f'+{int((sw-640)/2)}+{int((sh-660)/2)}')
- # --- 顶部文字说明区 ---
- instruction_frame = tk.Frame(root, pady=15, padx=35)
- instruction_frame.pack(fill=tk.X)
-
- t_top = tk.Text(instruction_frame, font=("微软雅黑", 10), height=8, bg=root.cget("bg"), relief=tk.FLAT, wrap=tk.WORD)
- t_top.tag_config("red", foreground="red", font=("微软雅黑", 10, "bold"))
- t_top.insert(tk.END, "一、导入前请")
- t_top.insert(tk.END, "关闭", "red")
- t_top.insert(tk.END, " Steam;\n\n")
- t_top.insert(tk.END, "二、导入后,保险起见会创建一个新的文件cloud-storage-namespace-1_NEW.json。为了让修改生效,请您手动")
- t_top.insert(tk.END, "备份", "red")
- t_top.insert(tk.END, "原先的 cloud-storage-namespace-1.json,")
- t_top.insert(tk.END, "替换", "red")
- t_top.insert(tk.END, "成这个文件;\n\n")
- t_top.insert(tk.END, "三、为了让收藏夹能上传到云,您必须")
- t_top.insert(tk.END, "在 Steam 内手动修改", "red")
- t_top.insert(tk.END, "新创建的收藏。例如更改标题,或是添加/删除收藏内的游戏等。")
- t_top.config(state=tk.DISABLED)
- t_top.pack(fill=tk.X)
- # --- 按钮与对应说明 ---
- style = ttk.Style()
- style.configure("TButton", font=("微软雅黑", 11), padding=8)
- # 按钮 1
- ttk.Button(root, text="📁 批量导入 TXT 为收藏夹", width=45, command=self.import_from_txt).pack(pady=(10,0))
-
- desc1_frame = tk.Frame(root, padx=35)
- desc1_frame.pack(fill=tk.X)
- t1 = tk.Text(desc1_frame, font=("微软雅黑", 9), height=5, bg=root.cget("bg"), relief=tk.FLAT)
- t1.tag_config("red", foreground="red")
- t1.insert(tk.END, "一、导入文件必须是 ")
- t1.insert(tk.END, "txt", "red")
- t1.insert(tk.END, " 格式,文件名称会成为收藏夹名称;\n")
- t1.insert(tk.END, "二、内容必须为 ")
- t1.insert(tk.END, "每行一个 appid", "red")
- t1.insert(tk.END, ";\n")
- t1.insert(tk.END, "三、你不必拥有 txt 中的 appid,当你拥有后,它会自动同步进该收藏夹。")
- t1.config(state=tk.DISABLED)
- t1.pack(fill=tk.X, pady=5)
- # 按钮 2
- ttk.Button(root, text="👥 批量同步 Steam 用户游戏库", width=45, command=self.open_friend_sync_ui).pack(pady=(15,0))
-
- desc2_frame = tk.Frame(root, padx=35)
- desc2_frame.pack(fill=tk.X)
- t2 = tk.Text(desc2_frame, font=("微软雅黑", 9), height=3, bg=root.cget("bg"), relief=tk.FLAT)
- t2.tag_config("red", foreground="red")
- t2.insert(tk.END, "一、对方的 Steam 好友代码可在其 SteamDB 页面看到;\n")
- t2.insert(tk.END, "二、对方必须 ")
- t2.insert(tk.END, "公开", "red")
- t2.insert(tk.END, " 了自己的 Steam 库。")
- t2.config(state=tk.DISABLED)
- t2.pack(fill=tk.X, pady=5)
- root.mainloop()
- if __name__ == "__main__":
- app = SteamToolbox()
- app.main_ui()
复制代码
一、将上述 python 代码保存为 Steam_Library_Manager.py,连同要导入的 txt 文件放在 Steam 路径下运行
- ~Steam\userdata\[好友代码]\config\cloudstorage
复制代码
二、手动备份好原始配置文件cloud-storage-namespace-1.json,用新生成的配置文件cloud-storage-namespace-1_NEW.json替换。
一、导入前请退出 Steam;
二、用新文件替换后进入 Steam,手动修改所有新创建的收藏夹,这样这些收藏夹才会被传到云上。
只要和 ai 简单说几句话,就容易从任何页面爬取到 appid 列表。例如
一、保存好 SteamDB 列表页的 html 代码后,如
https://steamdb.info/publisher/Sekai+Project/
就能使用如下 python 代码输出列表中的 appid 列表。
- import re
- import os
- import tkinter as tk
- from tkinter import filedialog, messagebox, simpledialog
- def extract_and_merge_appids():
- # 1. 初始化 tkinter
- root = tk.Tk()
- root.withdraw()
- root.attributes("-topmost", True)
- current_dir = os.path.dirname(os.path.abspath(__file__))
- # 2. 弹出窗口选择多个 HTML 文件
- messagebox.showinfo("第一步", "请选择所有需要合并的 SteamDB 源代码文件 (可多选)")
- file_paths = filedialog.askopenfilenames(
- initialdir=current_dir,
- title="选择多个 HTML 源代码文件",
- filetypes=[("HTML files", "*.html"), ("Text files", "*.txt"), ("All files", "*.*")]
- )
- if not file_paths:
- print("操作取消:未选择任何文件。")
- return
- # 3. 询问输出文件名
- default_name = "merged_steam_list"
- output_title = simpledialog.askstring("第二步", "请输入合并后的分类标题:", initialvalue=default_name)
-
- if not output_title:
- output_title = default_name
- all_raw_ids = []
- processed_files_count = 0
- # 4. 循环处理选中的每一个文件
- for path in file_paths:
- try:
- with open(path, 'r', encoding='utf-8') as f:
- content = f.read()
- # 精准定位表格主体,排除页头热门游戏
- tbody_match = re.search(r'<tbody.*?>(.*?)</tbody>', content, re.DOTALL)
-
- if not tbody_match:
- print(f"警告:文件 '{os.path.basename(path)}' 未找到列表主体,已跳过。")
- continue
-
- tbody_content = tbody_match.group(1)
- # 提取该页面所有的 AppID (按页面顺序)
- page_ids = re.findall(r'data-appid="(\d+)"', tbody_content)
-
- if page_ids:
- all_raw_ids.extend(page_ids)
- processed_files_count += 1
- print(f"已提取: {os.path.basename(path)} ({len(page_ids)} 个 ID)")
- except Exception as e:
- print(f"处理文件 {os.path.basename(path)} 时出错: {e}")
- if not all_raw_ids:
- messagebox.showwarning("处理失败", "所选文件中均未提取到有效的 AppID。")
- return
- # 5. 工具打开去重并保持顺序
- # 逻辑:如果多个文件里有重复的游戏,只保留它第一次出现的位置
- final_ordered_ids = list(dict.fromkeys(all_raw_ids))
- # 6. 保存合并后的结果
- output_filename = f"{output_title}.txt"
- output_path = os.path.join(current_dir, output_filename)
-
- with open(output_path, 'w', encoding='utf-8') as f:
- for app_id in final_ordered_ids:
- f.write(f"{app_id}\n")
- # 7. 统计并反馈
- stats_msg = (
- f"处理完成!\n\n"
- f"● 成功处理页面数:{processed_files_count}\n"
- f"● 原始 ID 总数:{len(all_raw_ids)}\n"
- f"● 去重后唯一总数:{len(final_ordered_ids)}\n\n"
- f"合并后的列表已保存至:\n{output_filename}"
- )
-
- print("-" * 30)
- print(stats_msg)
- messagebox.showinfo("合并提取成功", stats_msg)
- if __name__ == "__main__":
- extract_and_merge_appids()
复制代码
二、保存好 Steam 鉴赏家页面的完整 html 代码后,如
Thinky Awards (Unofficial)
Independent Game Festival Awarded Games
就能使用如下 python 代码输出列表中的 appid 列表。
- import re
- import os
- import tkinter as tk
- from tkinter import filedialog, messagebox, simpledialog
- def extract_steam_curator_list():
- # 1. 界面初始化
- root = tk.Tk()
- root.withdraw()
- root.attributes("-topmost", True)
- current_dir = os.path.dirname(os.path.abspath(__file__))
- # 2. 弹出窗口选择多个文件
- messagebox.showinfo("Steam 鉴赏家提取器", "请选择已保存的鉴赏家 HTML 文件\n(支持多选,合并去重)")
- file_paths = filedialog.askopenfilenames(
- initialdir=current_dir,
- title="选择 HTML 文件",
- filetypes=[("HTML 文件", "*.html"), ("所有文件", "*.*")]
- )
- if not file_paths: return
- # 3. 询问输出文件名
- output_title = simpledialog.askstring("输出设置", "请输入生成的分类标题(TXT 文件名):", initialvalue="MyCuratorList")
- if not output_title: output_title = "MyCuratorList"
- all_ids = []
-
- # 4. 循环处理文件
- for path in file_paths:
- try:
- with open(path, 'r', encoding='utf-8') as f:
- html = f.read()
- # --- 智能区域识别 ---
- # 逻辑:Steam 鉴赏家的纯列表部分通常在 RecommendationsRows 里
- # 如果没找到(可能页面没滚完),则退而求其次找 creator_grid_ctn
- # 这样可以最大程度避开页头和侧边栏的无关 ID
- search_area = html
- list_start = html.find('id="RecommendationsRows"')
- if list_start == -1:
- list_start = html.find('class="creator_grid_ctn"')
-
- if list_start != -1:
- # 截取从列表开始到页脚结束的区域
- footer_start = html.find('id="footer"', list_start)
- search_area = html[list_start : (footer_start if footer_start != -1 else len(html))]
- # --- 正则提取 ---
- # 匹配 data-ds-appid,兼容单个数字和逗号分隔的情况
- raw_matches = re.findall(r'data-ds-appid="([\d,]+)"', search_area)
-
- page_ids = []
- for m in raw_matches:
- if ',' in m:
- page_ids.extend(m.split(','))
- else:
- page_ids.append(m)
-
- all_ids.extend(page_ids)
- print(f"文件 '{os.path.basename(path)}': 提取到 {len(page_ids)} 个 ID")
- except Exception as e:
- print(f"处理文件 {os.path.basename(path)} 时发生错误: {e}")
- if not all_ids:
- messagebox.showwarning("提示", "未能从选定区域提取到任何 AppID。")
- return
- # 5. 工具打开去重并保持顺序
- # 这样可以确保输出顺序与网页浏览顺序一致,且不包含重复项
- final_ordered_ids = list(dict.fromkeys(all_ids))
- # 6. 保存文件
- output_file = f"{output_title}.txt"
- output_path = os.path.join(current_dir, output_file)
- with open(output_path, 'w', encoding='utf-8') as f:
- for app_id in final_ordered_ids:
- f.write(f"{app_id}\n")
- # 7. 最终反馈
- summary = (
- f"✅ 提取成功!\n\n"
- f"● 文件总数:{len(file_paths)}\n"
- f"● 去重后唯一游戏总数:{len(final_ordered_ids)}\n\n"
- f"结果已保存至同目录下的:\n{output_file}"
- )
- messagebox.showinfo("任务完成", summary)
- if __name__ == "__main__":
- extract_steam_curator_list()
复制代码
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?注册
×
1、转载或引用本网站内容,必须注明本文网址:https://keylol.com/t1028220-1-1。如发文者注明禁止转载,则请勿转载
2、对于不当转载或引用本网站内容而引起的民事纷争、行政处理或其他损失,本网站不承担责任
3、对不遵守本声明或其他违法、恶意使用本网站内容者,本网站保留追究其法律责任的权利
4、所有帖子仅代表作者本人意见,不代表本社区立场
本帖被以下淘专辑推荐:
- · 淘宝|主题: 906, 订阅: 132
- · 收藏强迫症|主题: 294, 订阅: 61
|