回复
18
查看
1199
收藏
54

26

赠楼

89%

赠楼率

524

蒸汽

144

主题

1260

帖子

2584

积分
跳转到指定楼层
1
发表于 昨天 16:17 · 南非 | 只看该作者 |倒序浏览 |阅读模式

前情提要


《如何看到特定用户库里游戏的折扣情况?》
《游戏库“窃取”工具 + steam 愿望单导入工具》
《有什么方法能把我游戏库的分类共享给我家庭组成员?》
《想分享家庭组里的小号给朋友,有什么注意事项》

我在 Steam 游戏收集方面的哲学一贯是:
一、寻找有价值的统计指标;
二、围绕该指标建立数据库;
三、利用该数据库进行收集;
四、平衡性价比与收集速度;
五、在本地进行系统化管理;
六、实现成容易分享的形式。

原本这会是相当消耗精力的过程,但 ai 发展起来之后,几乎每个步骤都解放了我重复劳动的部分,让我能有余力把认知资源消耗在上述系统中更具创造力的部分上。出于这个背景,我制作了这个 Steam 库管理助手。吸取之前的教训,这次找朋友多次测试后才放出来。

声明


使用前请备份好 cloudstorage 文件夹,推荐从未做过 Steam 分类的朋友使用。尽管我现在能正常使用,但也请做好分类可能会完全丢失的心理准备!

使用界面与效果






主要功能



一、将 txt 格式的 appid 列表(每行一个)导入成 Steam 收藏夹,收藏夹名称为 txt 文件名称。
额外效果:就算你暂时没有这个 appid,等到你入库后同样能同步到这个 Steam 收藏夹内。从而实现某种伪动态效果。

一、在 Steam 库内建立收藏夹,动态显示对方的 Steam 游戏。
额外效果:对方不必是你的 Steam 好友!只要对方的 Steam 库是公开的就能实现动态同步。


使用方式






  1. import json
  2. import time
  3. import secrets
  4. import os
  5. import re
  6. import tkinter as tk
  7. from tkinter import filedialog, messagebox, ttk

  8. class SteamToolbox:
  9.     def __init__(self):
  10.         self.current_dir = os.path.dirname(os.path.abspath(__file__))
  11.         self.json_name = "cloud-storage-namespace-1.json"
  12.         self.json_path = os.path.join(self.current_dir, self.json_name)

  13.     def load_json(self):
  14.         if not os.path.exists(self.json_path):
  15.             messagebox.showerror("错误", f"找不到 {self.json_name}\n请确保脚本和它在同一文件夹。")
  16.             return None
  17.         try:
  18.             with open(self.json_path, 'r', encoding='utf-8') as f:
  19.                 return json.load(f)
  20.         except Exception as e:
  21.             messagebox.showerror("读取错误", f"解析失败: {e}")
  22.             return None

  23.     def save_json(self, data):
  24.         output_path = os.path.join(self.current_dir, "cloud-storage-namespace-1_NEW.json")
  25.         try:
  26.             with open(output_path, 'w', encoding='utf-8') as f:
  27.                 json.dump(data, f, ensure_ascii=False, separators=(',', ':'))
  28.             messagebox.showinfo("成功", f"文件已生成:\n{os.path.basename(output_path)}")
  29.         except Exception as e:
  30.             messagebox.showerror("保存失败", f"无法写入文件: {e}")

  31.     def import_from_txt(self):
  32.         data = self.load_json()
  33.         if data is None: return

  34.         txt_paths = filedialog.askopenfilenames(
  35.             initialdir=self.current_dir,
  36.             title="选择 AppID 列表 (TXT)",
  37.             filetypes=[("Text files", "*.txt")]
  38.         )
  39.         if not txt_paths: return

  40.         for path in txt_paths:
  41.             file_title = os.path.splitext(os.path.basename(path))[0]
  42.             with open(path, 'r', encoding='utf-8') as f:
  43.                 app_ids = [int(line.strip()) for line in f if line.strip().isdigit()]
  44.             
  45.             if not app_ids: continue
  46.             self._add_static_collection(data, file_title, app_ids)
  47.         
  48.         self.save_json(data)

  49.     def _add_static_collection(self, data, name, app_ids):
  50.         col_id = f"uc-{secrets.token_hex(6)}"
  51.         storage_key = f"user-collections.{col_id}"
  52.         val_obj = {"id": col_id, "name": name, "added": app_ids, "removed": []}
  53.         new_entry = [storage_key, {"key": storage_key, "timestamp": int(time.time()),
  54.                     "value": json.dumps(val_obj, ensure_ascii=False, separators=(',', ':')), "version": "1"}]
  55.         data.append(new_entry)

  56.     def open_friend_sync_ui(self):
  57.         data = self.load_json()
  58.         if data is None: return

  59.         sync_win = tk.Toplevel()
  60.         sync_win.title("批量同步 Steam 用户游戏库")
  61.         sync_win.geometry("550x620")
  62.         sync_win.attributes("-topmost", True)

  63.         tk.Label(sync_win, text="1. 请输入对方的 Steam 好友代码(每行一个)", font=("微软雅黑", 10, "bold")).pack(pady=(15,0))
  64.         codes_text = tk.Text(sync_win, height=8, width=60)
  65.         codes_text.pack(padx=20, pady=5)

  66.         tk.Label(sync_win, text="2. 生成的收藏夹名称 (每行一个)", font=("微软雅黑", 10, "bold")).pack(pady=(10,0))
  67.         names_text = tk.Text(sync_win, height=8, width=60)
  68.         names_text.pack(padx=20, pady=5)

  69.         def generate_default_names():
  70.             raw_content = codes_text.get("1.0", tk.END).strip()
  71.             raw_ids = re.findall(r'\d+', raw_content)
  72.             names_text.delete("1.0", tk.END)
  73.             for rid in raw_ids:
  74.                 names_text.insert(tk.END, f"好友代码 [{rid}]\n")

  75.         def commit_import():
  76.             codes = re.findall(r'\d+', codes_text.get("1.0", tk.END))
  77.             names = names_text.get("1.0", tk.END).strip().split('\n')
  78.             names = [n.strip() for n in names if n.strip()]
  79.             for i in range(len(codes)):
  80.                 cid = codes[i]
  81.                 cname = names[i] if i < len(names) else f"好友代码 [{cid}]"
  82.                 self._add_dynamic_collection(data, cname, cid)
  83.             if codes:
  84.                 self.save_json(data)
  85.                 sync_win.destroy()

  86.         btn_frame = tk.Frame(sync_win)
  87.         btn_frame.pack(pady=20)
  88.         tk.Button(btn_frame, text="✨ 生成默认名称", command=generate_default_names, width=18, height=2).pack(side=tk.LEFT, padx=10)
  89.         # 按钮改成黑字,去掉加粗绿色
  90.         tk.Button(btn_frame, text="开始导入", command=commit_import, width=18, height=2).pack(side=tk.LEFT, padx=10)

  91.     def _add_dynamic_collection(self, data, name, friend_code):
  92.         col_id = f"uc-{secrets.token_hex(4)}"
  93.         storage_key = f"user-collections.{col_id}"
  94.         filter_groups = [{"rgOptions": [], "bAcceptUnion": False} for _ in range(9)]
  95.         filter_groups[0]["bAcceptUnion"] = True
  96.         filter_groups[6]["rgOptions"] = [int(friend_code)]
  97.         val_obj = {"id": col_id, "name": name, "added": [], "removed": [],
  98.             "filterSpec": {"nFormatVersion": 2, "strSearchText": "", "filterGroups": filter_groups, "setSuggestions": {}}}
  99.         new_entry = [storage_key, {"key": storage_key, "timestamp": int(time.time()),
  100.                     "value": json.dumps(val_obj, ensure_ascii=False, separators=(',', ':')), "version": "1"}]
  101.         data.append(new_entry)

  102.     def main_ui(self):
  103.         root = tk.Tk()
  104.         root.title("Steam 库管理助手")
  105.         root.geometry("640x660")
  106.         sw, sh = root.winfo_screenwidth(), root.winfo_screenheight()
  107.         root.geometry(f'+{int((sw-640)/2)}+{int((sh-660)/2)}')

  108.         # --- 顶部文字说明区 ---
  109.         instruction_frame = tk.Frame(root, pady=15, padx=35)
  110.         instruction_frame.pack(fill=tk.X)
  111.         
  112.         t_top = tk.Text(instruction_frame, font=("微软雅黑", 10), height=8, bg=root.cget("bg"), relief=tk.FLAT, wrap=tk.WORD)
  113.         t_top.tag_config("red", foreground="red", font=("微软雅黑", 10, "bold"))
  114.         t_top.insert(tk.END, "一、导入前请")
  115.         t_top.insert(tk.END, "关闭", "red")
  116.         t_top.insert(tk.END, " Steam;\n\n")
  117.         t_top.insert(tk.END, "二、导入后,保险起见会创建一个新的文件cloud-storage-namespace-1_NEW.json。为了让修改生效,请您手动")
  118.         t_top.insert(tk.END, "备份", "red")
  119.         t_top.insert(tk.END, "原先的 cloud-storage-namespace-1.json,")
  120.         t_top.insert(tk.END, "替换", "red")
  121.         t_top.insert(tk.END, "成这个文件;\n\n")
  122.         t_top.insert(tk.END, "三、为了让收藏夹能上传到云,您必须")
  123.         t_top.insert(tk.END, "在 Steam 内手动修改", "red")
  124.         t_top.insert(tk.END, "新创建的收藏。例如更改标题,或是添加/删除收藏内的游戏等。")
  125.         t_top.config(state=tk.DISABLED)
  126.         t_top.pack(fill=tk.X)

  127.         # --- 按钮与对应说明 ---
  128.         style = ttk.Style()
  129.         style.configure("TButton", font=("微软雅黑", 11), padding=8)

  130.         # 按钮 1
  131.         ttk.Button(root, text="📁 批量导入 TXT 为收藏夹", width=45, command=self.import_from_txt).pack(pady=(10,0))
  132.         
  133.         desc1_frame = tk.Frame(root, padx=35)
  134.         desc1_frame.pack(fill=tk.X)
  135.         t1 = tk.Text(desc1_frame, font=("微软雅黑", 9), height=5, bg=root.cget("bg"), relief=tk.FLAT)
  136.         t1.tag_config("red", foreground="red")
  137.         t1.insert(tk.END, "一、导入文件必须是 ")
  138.         t1.insert(tk.END, "txt", "red")
  139.         t1.insert(tk.END, " 格式,文件名称会成为收藏夹名称;\n")
  140.         t1.insert(tk.END, "二、内容必须为 ")
  141.         t1.insert(tk.END, "每行一个 appid", "red")
  142.         t1.insert(tk.END, ";\n")
  143.         t1.insert(tk.END, "三、你不必拥有 txt 中的 appid,当你拥有后,它会自动同步进该收藏夹。")
  144.         t1.config(state=tk.DISABLED)
  145.         t1.pack(fill=tk.X, pady=5)

  146.         # 按钮 2
  147.         ttk.Button(root, text="👥 批量同步 Steam 用户游戏库", width=45, command=self.open_friend_sync_ui).pack(pady=(15,0))
  148.         
  149.         desc2_frame = tk.Frame(root, padx=35)
  150.         desc2_frame.pack(fill=tk.X)
  151.         t2 = tk.Text(desc2_frame, font=("微软雅黑", 9), height=3, bg=root.cget("bg"), relief=tk.FLAT)
  152.         t2.tag_config("red", foreground="red")
  153.         t2.insert(tk.END, "一、对方的 Steam 好友代码可在其 SteamDB 页面看到;\n")
  154.         t2.insert(tk.END, "二、对方必须 ")
  155.         t2.insert(tk.END, "公开", "red")
  156.         t2.insert(tk.END, " 了自己的 Steam 库。")
  157.         t2.config(state=tk.DISABLED)
  158.         t2.pack(fill=tk.X, pady=5)

  159.         root.mainloop()

  160. if __name__ == "__main__":
  161.     app = SteamToolbox()
  162.     app.main_ui()

复制代码


一、将上述 python 代码保存为 Steam_Library_Manager.py,连同要导入的 txt 文件放在 Steam 路径下运行
  1. ~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 列表。
  1. import re
  2. import os
  3. import tkinter as tk
  4. from tkinter import filedialog, messagebox, simpledialog

  5. def extract_and_merge_appids():
  6.     # 1. 初始化 tkinter
  7.     root = tk.Tk()
  8.     root.withdraw()
  9.     root.attributes("-topmost", True)

  10.     current_dir = os.path.dirname(os.path.abspath(__file__))

  11.     # 2. 弹出窗口选择多个 HTML 文件
  12.     messagebox.showinfo("第一步", "请选择所有需要合并的 SteamDB 源代码文件 (可多选)")
  13.     file_paths = filedialog.askopenfilenames(
  14.         initialdir=current_dir,
  15.         title="选择多个 HTML 源代码文件",
  16.         filetypes=[("HTML files", "*.html"), ("Text files", "*.txt"), ("All files", "*.*")]
  17.     )

  18.     if not file_paths:
  19.         print("操作取消:未选择任何文件。")
  20.         return

  21.     # 3. 询问输出文件名
  22.     default_name = "merged_steam_list"
  23.     output_title = simpledialog.askstring("第二步", "请输入合并后的分类标题:", initialvalue=default_name)
  24.    
  25.     if not output_title:
  26.         output_title = default_name

  27.     all_raw_ids = []
  28.     processed_files_count = 0

  29.     # 4. 循环处理选中的每一个文件
  30.     for path in file_paths:
  31.         try:
  32.             with open(path, 'r', encoding='utf-8') as f:
  33.                 content = f.read()

  34.             # 精准定位表格主体,排除页头热门游戏
  35.             tbody_match = re.search(r'<tbody.*?>(.*?)</tbody>', content, re.DOTALL)
  36.             
  37.             if not tbody_match:
  38.                 print(f"警告:文件 '{os.path.basename(path)}' 未找到列表主体,已跳过。")
  39.                 continue
  40.             
  41.             tbody_content = tbody_match.group(1)
  42.             # 提取该页面所有的 AppID (按页面顺序)
  43.             page_ids = re.findall(r'data-appid="(\d+)"', tbody_content)
  44.             
  45.             if page_ids:
  46.                 all_raw_ids.extend(page_ids)
  47.                 processed_files_count += 1
  48.                 print(f"已提取: {os.path.basename(path)} ({len(page_ids)} 个 ID)")

  49.         except Exception as e:
  50.             print(f"处理文件 {os.path.basename(path)} 时出错: {e}")

  51.     if not all_raw_ids:
  52.         messagebox.showwarning("处理失败", "所选文件中均未提取到有效的 AppID。")
  53.         return

  54.     # 5. 工具打开去重并保持顺序
  55.     # 逻辑:如果多个文件里有重复的游戏,只保留它第一次出现的位置
  56.     final_ordered_ids = list(dict.fromkeys(all_raw_ids))

  57.     # 6. 保存合并后的结果
  58.     output_filename = f"{output_title}.txt"
  59.     output_path = os.path.join(current_dir, output_filename)
  60.    
  61.     with open(output_path, 'w', encoding='utf-8') as f:
  62.         for app_id in final_ordered_ids:
  63.             f.write(f"{app_id}\n")

  64.     # 7. 统计并反馈
  65.     stats_msg = (
  66.         f"处理完成!\n\n"
  67.         f"● 成功处理页面数:{processed_files_count}\n"
  68.         f"● 原始 ID 总数:{len(all_raw_ids)}\n"
  69.         f"● 去重后唯一总数:{len(final_ordered_ids)}\n\n"
  70.         f"合并后的列表已保存至:\n{output_filename}"
  71.     )
  72.    
  73.     print("-" * 30)
  74.     print(stats_msg)
  75.     messagebox.showinfo("合并提取成功", stats_msg)

  76. if __name__ == "__main__":
  77.     extract_and_merge_appids()

复制代码

二、保存好 Steam 鉴赏家页面的完整 html 代码后,如
Thinky Awards (Unofficial)
Independent Game Festival Awarded Games
就能使用如下 python 代码输出列表中的 appid 列表。
  1. import re
  2. import os
  3. import tkinter as tk
  4. from tkinter import filedialog, messagebox, simpledialog

  5. def extract_steam_curator_list():
  6.     # 1. 界面初始化
  7.     root = tk.Tk()
  8.     root.withdraw()
  9.     root.attributes("-topmost", True)
  10.     current_dir = os.path.dirname(os.path.abspath(__file__))

  11.     # 2. 弹出窗口选择多个文件
  12.     messagebox.showinfo("Steam 鉴赏家提取器", "请选择已保存的鉴赏家 HTML 文件\n(支持多选,合并去重)")
  13.     file_paths = filedialog.askopenfilenames(
  14.         initialdir=current_dir,
  15.         title="选择 HTML 文件",
  16.         filetypes=[("HTML 文件", "*.html"), ("所有文件", "*.*")]
  17.     )
  18.     if not file_paths: return

  19.     # 3. 询问输出文件名
  20.     output_title = simpledialog.askstring("输出设置", "请输入生成的分类标题(TXT 文件名):", initialvalue="MyCuratorList")
  21.     if not output_title: output_title = "MyCuratorList"

  22.     all_ids = []
  23.    
  24.     # 4. 循环处理文件
  25.     for path in file_paths:
  26.         try:
  27.             with open(path, 'r', encoding='utf-8') as f:
  28.                 html = f.read()

  29.             # --- 智能区域识别 ---
  30.             # 逻辑:Steam 鉴赏家的纯列表部分通常在 RecommendationsRows 里
  31.             # 如果没找到(可能页面没滚完),则退而求其次找 creator_grid_ctn
  32.             # 这样可以最大程度避开页头和侧边栏的无关 ID
  33.             search_area = html
  34.             list_start = html.find('id="RecommendationsRows"')
  35.             if list_start == -1:
  36.                 list_start = html.find('class="creator_grid_ctn"')
  37.             
  38.             if list_start != -1:
  39.                 # 截取从列表开始到页脚结束的区域
  40.                 footer_start = html.find('id="footer"', list_start)
  41.                 search_area = html[list_start : (footer_start if footer_start != -1 else len(html))]

  42.             # --- 正则提取 ---
  43.             # 匹配 data-ds-appid,兼容单个数字和逗号分隔的情况
  44.             raw_matches = re.findall(r'data-ds-appid="([\d,]+)"', search_area)
  45.             
  46.             page_ids = []
  47.             for m in raw_matches:
  48.                 if ',' in m:
  49.                     page_ids.extend(m.split(','))
  50.                 else:
  51.                     page_ids.append(m)
  52.             
  53.             all_ids.extend(page_ids)
  54.             print(f"文件 '{os.path.basename(path)}': 提取到 {len(page_ids)} 个 ID")

  55.         except Exception as e:
  56.             print(f"处理文件 {os.path.basename(path)} 时发生错误: {e}")

  57.     if not all_ids:
  58.         messagebox.showwarning("提示", "未能从选定区域提取到任何 AppID。")
  59.         return

  60.     # 5. 工具打开去重并保持顺序
  61.     # 这样可以确保输出顺序与网页浏览顺序一致,且不包含重复项
  62.     final_ordered_ids = list(dict.fromkeys(all_ids))

  63.     # 6. 保存文件
  64.     output_file = f"{output_title}.txt"
  65.     output_path = os.path.join(current_dir, output_file)
  66.     with open(output_path, 'w', encoding='utf-8') as f:
  67.         for app_id in final_ordered_ids:
  68.             f.write(f"{app_id}\n")

  69.     # 7. 最终反馈
  70.     summary = (
  71.         f"✅ 提取成功!\n\n"
  72.         f"● 文件总数:{len(file_paths)}\n"
  73.         f"● 去重后唯一游戏总数:{len(final_ordered_ids)}\n\n"
  74.         f"结果已保存至同目录下的:\n{output_file}"
  75.     )
  76.     messagebox.showinfo("任务完成", summary)

  77. if __name__ == "__main__":
  78.     extract_steam_curator_list()

复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

×

本帖被以下淘专辑推荐:

收藏收藏54 分享淘帖2 支持支持1
回复

使用道具 举报

浏览本版块需要:
1. 初阶会员或更高等级;
2. (点击此处)绑定Steam账号
您需要登录后才可以回帖 登录 | 注册

本版积分规则

欢迎发帖参与讨论 o(*≧▽≦)ツ,请注意
1. 寻求帮助或答案的帖子请发到问题互助版块,悬赏有助于问题解决的速度。发错可能失去在该板块发布主题的权限(了解更多
2. 表达观点可以,也请务必注意语气和用词,以免影响他人浏览,特别是针对其他会员的内容。如觉得违规可使用举报功能 交由管理人员处理,请勿引用对方的内容。
3. 开箱晒物交易中心游戏互鉴福利放送版块请注意额外的置顶版规。
4. 除了提问帖和交易帖以外,不确认发在哪个版块的帖子可以先发在谈天说地

  作为民间站点,自 2004 年起为广大中文 Steam 用户提供技术支持与讨论空间。历经二十余载风雨,如今已发展为国内最大的正版玩家据点。

列表模式 · · 微博 · Bilibili频道 · Steam 群组 · 贴吧 · QQ群 
Keylol 其乐 ©2004-2026 Chinese Steam User Fan Site.
Designed by Lee in Balestier, Powered by Discuz!
推荐使用 ChromeMicrosoft Edge 来浏览本站
广告投放|手机版|广州数趣信息科技有限公司 版权所有|其乐 Keylol ( 粤ICP备17068105号 )
GMT+8, 2026-2-3 00:23
快速回复 返回顶部 返回列表