Files
Davy 656d06a527 feat(emoticon): add emoticon export CLI (#137, closes #118)
Closes #118 (bigmanBass666 提的表情包导出 feature).

作者 thedavidweng 实测验证 (983 NonStore + 204 Store emoticons / 1251 cache files):
- ❌ 解密本地 cache 文件不可行 — 加密方式未知, 不是 V1/V2/XOR/AES 任意标准方案
- ✅ CDN 下载 via cdn_url 可行 — 所有 20/20 测试条目返回 plain image (GIF/JPG/PNG), 无需登录态
- ❌ 构造 CDN URL 不可行 — filekey/storeid 是 per-emoticon

方案: 抽 emoji 公共逻辑到 emoticons.py + 新 CLI export_emoticons.py.

改动:
- emoticons.py 新增 (286 行) — build_emoji_lookup + download_emoji, 不依赖 monitor_web
- export_emoticons.py 新增 (125 行) — CLI 带 --dry-run / --filter / -o 参数
- monitor_web.py 净 -175 行 — emoji 逻辑迁到 emoticons.py, 用 thin wrapper 调用, 保留 emoji_ 文件名前缀兼容已有缓存
- main.py +5 行 — 加 emoticons subcommand
- Makefile + README 文档

注意: 现有 monitor_web 进程要重启才能用新代码 (refactor 内部接口, 行为对等).
2026-06-10 11:18:17 +08:00

126 lines
3.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
导出收藏的表情包
python3 export_emoticons.py # 导出全部到 ./exported_emoticons/
python3 export_emoticons.py -o ./my_emojis # 指定输出目录
python3 export_emoticons.py --dry-run # 仅列出,不下载
python3 export_emoticons.py --filter "猫" # 按关键词过滤(匹配 caption 或 product_id)
"""
import argparse
import json
import os
import sys
import functools
print = functools.partial(print, flush=True)
from config import load_config
from emoticons import build_emoji_lookup, download_emoji
from key_utils import strip_key_metadata
def main():
parser = argparse.ArgumentParser(
description="导出微信收藏的表情包图片",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python3 export_emoticons.py 全量导出
python3 export_emoticons.py -o ./my_emojis 指定输出目录
python3 export_emoticons.py --dry-run 预览模式
python3 export_emoticons.py --filter "猫" 按关键词过滤
""",
)
parser.add_argument(
"output_dir",
nargs="?",
default=None,
help="输出目录路径 (默认: ./exported_emoticons)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="预览模式:显示表情列表,不实际下载",
)
parser.add_argument(
"--filter",
default=None,
help="按关键词过滤(匹配 caption 或 product_id)",
)
args = parser.parse_args()
cfg = load_config()
keys_file = cfg["keys_file"]
db_dir = cfg["db_dir"]
if not os.path.exists(keys_file):
print("[!] 密钥文件不存在,请先运行 python main.py 提取密钥")
sys.exit(1)
with open(keys_file, encoding="utf-8") as f:
keys = json.load(f)
keys = strip_key_metadata(keys)
if not keys:
print("[!] 密钥为空,请先运行 python main.py 提取密钥")
sys.exit(1)
script_dir = os.path.dirname(os.path.abspath(__file__))
output_dir = args.output_dir or os.path.join(script_dir, "exported_emoticons")
print(f"[*] 构建表情映射...")
lookup = build_emoji_lookup(keys, db_dir)
if not lookup:
print("[!] 未找到任何表情")
sys.exit(1)
# 过滤
items = list(lookup.items())
if args.filter:
keyword = args.filter.lower()
items = [
(md5, info) for md5, info in items
if keyword in (info.get("caption", "") or "").lower()
or keyword in (info.get("product_id", "") or "").lower()
]
print(f"[*] 过滤后: {len(items)} 个表情")
if args.dry_run:
print(f"\n{'MD5':<34} {'格式':>6} {'来源':>8} {'描述'}")
print("-" * 80)
for md5, info in items:
src = "NonStore" if info.get("product_id", "") == "" else "Store"
caption = info.get("caption", "") or ""
has_cdn = "Y" if info.get("cdn_url") else "N"
print(f"{md5} cdn={has_cdn} {src:>8} {caption}")
print(f"\n共 {len(items)} 个表情")
return
os.makedirs(output_dir, exist_ok=True)
print(f"[*] 开始下载 {len(items)} 个表情到 {output_dir}")
print()
try:
from tqdm import tqdm
iterator = tqdm(items, desc="下载表情", unit="个")
except ImportError:
iterator = items
success = 0
failed = 0
for md5, info in iterator:
fname = download_emoji(md5, lookup, output_dir)
if fname:
success += 1
else:
failed += 1
print()
print(f"[+] 完成: {success} 成功, {failed} 失败")
print(f" 输出目录: {output_dir}")
if __name__ == "__main__":
main()