├── .gitignore
├── README.md
├── images
└── howto.jpg
└── main.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 |
163 | downloads/
164 | chromedriver.exe
165 | chromedriver/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QQ 表情包下载
2 |
3 | ~~自用,写得依托答辩,期待有大佬能改成不依托答辩的样子。~~
4 |
5 | 写出来放在隔壁给某人用后,看上去还比较能用,从 gist 里迁出来好了,也方便接受反馈。
6 |
7 | ## 依赖安装
8 |
9 | ```bash
10 | pip3 install selenium requests tqdm
11 | ```
12 |
13 | 使用 selenium 的自动获取 ID 时,记得去 下**符合自己当前 Chrome 版本的** chromedriver 文件,并放在 py 脚本可以找到的地方。
14 |
15 | ## 不一定能看懂的使用指南
16 |
17 | ~~建议新建个纯英文文件夹把 py 脚本放进去。~~
18 | 请准备一个**现代化**的 Python 环境(3.6+),并安装依赖。
19 | 请使用 `python3 main.py` 运行目录中的 `main.py` ,并参照交互操作。
20 | 或者,如果你拿到了表情包 ID 想直接下载,你也可以参照下方的命令行使用指南:
21 |
22 | ```bash
23 | python3 main.py 231867 # 直接调用下方的选项 2,仍包含表情包信息确认流程。
24 |
25 | # 如果效率至上,你也可以下载表情包不走确认流程,此时默认你已经知道按动态下载表情包还是静态下载(因为脚本不方便确定)
26 | python3 main.py 231867 --dynamic # 按动态方式下载表情包
27 | python3 main.py 231867 --static # 按静态方式下载表情包
28 | ```
29 |
30 | ### 获取表情包 ID
31 |
32 | **upd:更新最简单粗暴的 ID 获取指南:**
33 |
34 | 1. 在「表情详情」页面,右上角**点击复制链接**,粘贴到一个地方。
35 | 2. 正常情况下得到的链接会形如 `https://zb.vip.qq.com/hybrid/emoticonmall/detail?id=235237&traceDetail=xxxx&plg_auth=1&plg_auth=1` ,**查看这里链接中的 ID 值即可**。如示例链接中的 ID 值即为 235237 .
36 |
37 | 
38 |
39 | ---
40 |
41 | - 旧表情包可先尝试在 搜索一下,有现成 ID 可以直接填进去下。
42 | - 新表情包则需要用 selenium 模拟登录后的表情商店搜索流程。
43 | - 如果 chromedriver 版本选择无误,在新弹出的窗口中完成 QQ 登录。
44 | - 那个「登录」按钮一般鼠标点不动,记得在密码框**按下回车**。
45 | - 提示登录环境风险说明你号被风控了,换一个 QQ 号可解。
46 | - 登录成功后跳转到搜索框。在上面搜出自己想下载的表情,**点击右侧的「免费」按钮**。
47 | - 如果成功捕获到了 ID ,浏览器将关闭,请回到脚本终端进行后续流程。
48 |
49 | ### 下载
50 |
51 | - 下载到脚本所在位置的 `downloads` 文件夹下,所以记得别把脚本四处乱放。
52 | - 由于腾讯后端返回数据中 `type` 区分了个寂寞,所以需要在下载时自行确定是否为动态表情。**如在回复中选择 Y ,将下载`gif`格式的动态表情。**
53 |
54 | ## 免责声明
55 |
56 | 本脚本仅供学习交流使用,严禁用于商业用途。使用此脚本即**默认您已取得版权方(包括但不限于设计师、深圳市腾讯计算机系统有限公司等)的相关许可**,最终所有权仍然归属于版权方。如有侵权,请联系我删除。
57 |
--------------------------------------------------------------------------------
/images/howto.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abc1763613206/QQFaceHelper/222ba7bb5b9cf2b7be8f7eaf624a26229f45f742/images/howto.jpg
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import time
2 | from selenium import webdriver
3 | import re
4 | from urllib.parse import unquote
5 | import os
6 | import sys
7 | import requests
8 | from tqdm import tqdm
9 |
10 | DynamicFlag = False
11 | NoPromptFlag = False
12 |
13 |
14 | def fetchID():
15 | print("进入模拟浏览器流程,请确保同目录下有与 Chrome 版本相同的 chromedriver 文件")
16 | mobile_emulation = {
17 | "deviceMetrics": {
18 | "width": 720,
19 | "height": 1080,
20 | "pixelRatio": 1.75,
21 | "touch": True,
22 | "mobile": True,
23 | },
24 | "clientHints": {
25 | "platform": "Android",
26 | "mobile": True,
27 | },
28 | "userAgent": "Mozilla/5.0 (Linux; Android 13; 2206122SC Build/TKQ1.220829.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/109.0.5414.86 MQQBrowser/6.2 TBS/046613 Mobile Safari/537.36 V1_AND_SQ_8.9.80_4614_YYB_D QQ/8.9.80.12440 NetType/WIFI WebP/0.3.0 AppId/537176868 Pixel/1080 StatusBarHeight/82 SimpleUISwitch/0 QQTheme/1000 StudyMode/0 CurrentMode/0 CurrentFontScale/1.0 GlobalDensityScale/1.0 AllowLandscape/false InMagicWin/0",
29 | } # 8.9.80 QQ 自带 UA
30 | options = webdriver.ChromeOptions()
31 | caps = webdriver.DesiredCapabilities.CHROME
32 | caps["goog:loggingPrefs"] = {"performance": "ALL"}
33 | options.add_experimental_option("mobileEmulation", mobile_emulation)
34 | # 正常 UA 情况下会跳转登录页,而不是二维码扫描页
35 | driver = webdriver.Chrome(options=options, desired_capabilities=caps)
36 | driver.get(
37 | "https://zb.vip.qq.com/hybrid/emoticonmall/search?_wv=1027&appid=1"
38 | ) # QQ 表情商城搜索页
39 | print("QQ 登录页可能无法响应「登录」按钮操作,此时您可以尝试在输入好账号密码后,在密码框手动 Enter 回车...")
40 | while True:
41 | dat = driver.get_log("browser")
42 | for i in dat:
43 | if "jsbridge://ui/openUrl?" in i["message"]:
44 | txt = unquote(i["message"], "utf-8")
45 | match = re.match('.*id=(\d+)"', txt)
46 | if match:
47 | print("成功发现 ID " + match.group(1))
48 | driver.quit()
49 | return match.group(1)
50 | time.sleep(0.5)
51 |
52 |
53 | def download(id):
54 | widthURI = "https://gxh.vip.qq.com/club/item/parcel/{}/{}_android.json".format(
55 | str(id)[-1:], id
56 | )
57 | data = requests.get(widthURI).json()
58 | height = data["supportSize"][0]["Height"]
59 | width = data["supportSize"][0]["Width"]
60 | print("表情尺寸:{}x{}".format(width, height))
61 | type = data["type"]
62 | cnt = data["imgs"].__len__()
63 | print("表情数量:{}".format(cnt))
64 | if NoPromptFlag == False:
65 | choice = input("脚本无法判断表情类型,请自行判断原表情是否为动态表情?(Y/N)")
66 | else:
67 | if DynamicFlag == True:
68 | choice = "Y"
69 | else:
70 | choice = "N"
71 | if not os.path.exists(os.getcwd() + "/downloads"):
72 | os.mkdir(os.getcwd() + "/downloads")
73 | os.chdir(os.getcwd() + "/downloads")
74 | if choice == "Y" or choice == "y":
75 | print("已选择下载动态表情")
76 | if not os.path.exists("[{}] {} (动态)".format(id, data["name"])):
77 | os.mkdir("[{}] {} (动态)".format(id, data["name"]))
78 | os.chdir("[{}] {} (动态)".format(id, data["name"]))
79 | else:
80 | print("已选择下载静态表情")
81 | if not os.path.exists("[{}] {}".format(id, data["name"])):
82 | os.mkdir("[{}] {}".format(id, data["name"]))
83 | os.chdir("[{}] {}".format(id, data["name"]))
84 | plist = tqdm(data["imgs"])
85 | for i in plist:
86 | if choice == "Y":
87 | imgURI = (
88 | "https://gxh.vip.qq.com/club/item/parcel/item/{}/{}/raw{}.gif".format(
89 | i["id"][0:2], i["id"], height
90 | )
91 | )
92 | img = requests.get(imgURI)
93 | # print("{} - {} {}".format(i["name"], i["id"], img.status_code))
94 | with open("{}.gif".format(i["name"]), "wb") as f:
95 | f.write(img.content)
96 | else:
97 | imgURI = (
98 | "https://gxh.vip.qq.com/club/item/parcel/item/{}/{}/{}x{}.png".format(
99 | i["id"][0:2], i["id"], height, width
100 | )
101 | )
102 | img = requests.get(imgURI)
103 | # print("{} - {} {}".format(i["name"], i["id"], img.status_code))
104 | with open("{}.png".format(i["name"]), "wb") as f:
105 | f.write(img.content)
106 | print("下载完成!保存路径:{}".format(os.getcwd()))
107 |
108 |
109 | def check(id):
110 | infodataURI = "https://gxh.vip.qq.com/qqshow/admindata/comdata/vipEmoji_item_{}/xydata.json".format(
111 | id
112 | )
113 | try:
114 | infodataRAW = requests.get(infodataURI)
115 | infodata = infodataRAW.json()["data"]
116 | except Exception as e:
117 | print("请求错误!" + str(e))
118 | print(infodataRAW.status_code)
119 | sys.exit()
120 | print(
121 | "[{}] {} - {}".format(
122 | id, infodata["baseInfo"][0]["name"], infodata["baseInfo"][0]["desc"]
123 | )
124 | )
125 | print("{}".format(infodata["baseInfo"][0]["tag"][0]))
126 | if NoPromptFlag == False:
127 | choiceFlag = False
128 | choice = input("是否继续下载?(Y/N)")
129 | while not choiceFlag:
130 | if choice == "N" or choice == "n":
131 | choiceFlag = True
132 | return
133 | elif choice == "Y" or choice == "y":
134 | choiceFlag = True
135 | download(id)
136 | else:
137 | choice = input("输入错误,请重新输入:(Y/N)")
138 | else:
139 | download(id)
140 |
141 |
142 | if __name__ == "__main__":
143 | id = -1
144 | if len(sys.argv) == 2:
145 | id = sys.argv[1]
146 | try:
147 | assert id.isdigit()
148 | except AssertionError:
149 | print("参数错误,退出...")
150 | print("命令行使用方法:python main.py [表情 ID] [参数(--dynamic/--static)(可选)])")
151 | sys.exit()
152 | id = int(id)
153 | print("已检测到参数,直接下载 ID {}".format(id))
154 | elif len(sys.argv) == 3:
155 | id = sys.argv[1]
156 | try:
157 | assert id.isdigit()
158 | except AssertionError:
159 | print("参数错误,退出...")
160 | print("命令行使用方法:python main.py [表情 ID] [参数(--dynamic/--static)(可选)])")
161 | sys.exit()
162 | id = int(id)
163 | if sys.argv[2] == "--dynamic":
164 | print("已检测到参数,下载动态表情 ID {}".format(id))
165 | DynamicFlag = True
166 | NoPromptFlag = True
167 | elif sys.argv[2] == "--static":
168 | print("已检测到参数,下载静态表情 ID {}".format(id))
169 | DynamicFlag = False
170 | NoPromptFlag = True
171 | else:
172 | print("参数错误,退出...")
173 | print("命令行使用方法:python main.py [表情 ID] [参数(--dynamic/--static)(可选)])")
174 | sys.exit()
175 | else:
176 | print(
177 | """
178 | QQ 表情商城下载工具 V1.2 By @wowjerry
179 | 请选择操作:
180 | 1. 调用 selenium 获取表情 ID(请保证同目录下有与 Chrome 版本相同的 chromedriver.exe 文件)
181 | 2. 已知表情 ID,直接下载
182 |
183 | 开源地址:https://github.com/abc1763613206/QQFaceHelper
184 | 命令行使用方法: python main.py [表情 ID] [参数(--dynamic/--static)(可选)])
185 | """
186 | )
187 | try:
188 | choice = input("请输入操作序号:")
189 | except Exception as e:
190 | print(e)
191 | sys.exit()
192 | if choice == "1":
193 | id = fetchID()
194 | elif choice == "2":
195 | id = input("请输入表情 ID:")
196 | else:
197 | print("输入错误,退出...")
198 | sys.exit()
199 | check(id)
200 |
--------------------------------------------------------------------------------