├── .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 | ![获取链接](./images/howto.jpg) 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 | --------------------------------------------------------------------------------