[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n**__pycache__/\n**.idea\n**dist/\n**build\n**__pycache__\n"
  },
  {
    "path": "001-Downloader/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "001-Downloader/README.md",
    "content": "# 资源下载器\n本项目主要通过网络上开源的项目聚合成了一个跨平台的下载工具，可批量下载抖音、快手视音频资源。下载地址：\n\nMacOS：[Downloader1.0.3.app](https://github.com/xhunmon/PythonIsTools/releases/download/v1.0.3/Downloader1.0.3.app.zip)  下载后解压后使用\n\nWindow：[Downloader1.0.3.exe](https://github.com/xhunmon/PythonIsTools/releases/download/v1.0.3/Downloader1.0.3.exe.zip) 下载后解压后使用\n\n效果如图：\n\n![下载器截图](./doc/example.jpg)\n\n#主要知识点\n\n## python GUI(界面)\n\n本文使用tkinter GUI(界面)框架进行界面显示：[./ui.py](ui.py) ，[学习参考](https://www.cnblogs.com/shwee/p/9427975.html) 。\n\n## [pyinstaller](https://pyinstaller.readthedocs.io/en/stable/) 打包\n\n使用pyinstaller把python程序打包成window和mac可执行文件，主要命令如下：\n```shell\n#① ：生成xxx.spec文件；（去掉命令窗口-w）\npyinstaller -F -i res/logo.ico main.py  -w\n#②：修改xxx.spec，参考main.spec\n#③：再次进行打包，参考installer-mac.sh\npyinstaller -F -i res/logo.ico main.spec  -w\n```\n打包脚本与配置已放在 `doc` 目录下，需要拷贝出根目录进行打包。\n\n注意：\npyinstaller打包工具的版本与python版本、python所需第三方库以及操作系统会存在各种问题，所以需要看日志查找问题。例如：打包后运用，发现导入pyppeteer报错，通过降低版本后能正常使用：pip install pyppeteer==0.2.2\n\n## 项目\n项目代码结构非常简单，看ui.py和downloader.py就能知道大概。支持多线程任务下载。如果自己添加其他网站的资源下载，通过增加实现downloader.py和并且在ui.py中start_download增加入口判读即可无缝接入。\n"
  },
  {
    "path": "001-Downloader/__init__.py",
    "content": ""
  },
  {
    "path": "001-Downloader/config.ini",
    "content": "# 常用配置模块\n[common]\n#软件使用截止日期\nexpired_time=2025/12/15 23:59:59\n\n#app的版本名称\nversion_name=1.0.4\n\n#app的版本号\nversion_code=1040"
  },
  {
    "path": "001-Downloader/doc/mac-sh/main.spec",
    "content": "# -*- mode: python ; coding: utf-8 -*-\n\n\nblock_cipher = None\n\n\na = Analysis(['main.py','type_enum.py','ui.py','utils.py','downloader.py','douyin/dy_download.py'],\n             pathex=['.'],\n             binaries=[],\n             datas=[('res/logo.ico', 'images'),('config.ini', '.')],\n             hiddenimports=[],\n             hookspath=[],\n             hooksconfig={},\n             runtime_hooks=[],\n             excludes=[],\n             win_no_prefer_redirects=False,\n             win_private_assemblies=False,\n             cipher=block_cipher,\n             noarchive=False)\npyz = PYZ(a.pure, a.zipped_data,\n             cipher=block_cipher)\n\nexe = EXE(pyz,\n          a.scripts,\n          a.binaries,\n          a.zipfiles,\n          a.datas,  \n          [],\n          name='main',\n          debug=False,\n          bootloader_ignore_signals=False,\n          strip=False,\n          upx=True,\n          upx_exclude=[],\n          runtime_tmpdir=None,\n          console=False,\n          disable_windowed_traceback=False,\n          target_arch=None,\n          codesign_identity=None,\n          entitlements_file=None , icon='res/logo.ico')\napp = BUNDLE(exe,\n             name='Downloader.app',\n             icon='res/logo.ico',\n             bundle_identifier=None)"
  },
  {
    "path": "001-Downloader/doc/mac-sh/pyinstaller.sh",
    "content": "#!/bin/bash\n\npyinstaller -F -i res/logo.ico main.spec main.py  -w \\\n-p type_enum.py \\\n-p ui.py \\\n-p utils.py \\\n-p downloader.py \\\n-p douyin/dy_download.py \\\n-p kuaishou/ks_download.py"
  },
  {
    "path": "001-Downloader/doc/win-sh/main.spec",
    "content": "# -*- mode: python ; coding: utf-8 -*-\n\n\nblock_cipher = None\n\n\na = Analysis(['main.py','type_enum.py','ui.py','utils.py','downloader.py','douyin\\\\dy_download.py'],\n             pathex=['.'],\n             binaries=[],\n             datas=[('res\\\\logo.ico', 'images'),('config.ini', '.')],\n             hiddenimports=[],\n             hookspath=[],\n             hooksconfig={},\n             runtime_hooks=[],\n             excludes=[],\n             win_no_prefer_redirects=False,\n             win_private_assemblies=False,\n             cipher=block_cipher,\n             noarchive=False)\npyz = PYZ(a.pure, a.zipped_data,\n             cipher=block_cipher)\n\nexe = EXE(pyz,\n          a.scripts,\n          a.binaries,\n          a.zipfiles,\n          a.datas,  \n          [],\n          name='main',\n          debug=False,\n          bootloader_ignore_signals=False,\n          strip=False,\n          upx=True,\n          upx_exclude=[],\n          runtime_tmpdir=None,\n          console=False,\n          disable_windowed_traceback=False,\n          target_arch=None,\n          codesign_identity=None,\n          entitlements_file=None , icon='res\\\\logo.ico')\napp = BUNDLE(exe,\n             name='Downloader.exe',\n             icon='res\\\\logo.ico',\n             bundle_identifier=None)\n"
  },
  {
    "path": "001-Downloader/doc/win-sh/pyinstaller.sh",
    "content": "#!/bin/bash\n\npyinstaller -F -i res\\\\logo.ico  -w main.spec  main.py\n-p type_enum.py\n-p ui.py\n-p utils.py\n-p downloader.py\n-p douyin\\\\dy_download.py\n-p kuaishou\\\\ks_download.py"
  },
  {
    "path": "001-Downloader/douyin/dy_download.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 抖音视频下载\n@Date       :2021/08/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport json\nimport os\nimport re\nimport time\n\nimport requests\n\nfrom downloader import Downloader\n\n\nclass DouYin(Downloader):\n    # 初始化\n    def __init__(self):\n        super().__init__()\n        self.headers = self._headers\n        # 抓获所有视频\n        self.end = False\n\n    def start(self, url, path):\n        Downloader.print_ui(\"开始解析下载链接\")\n        # 读取保存路径\n        self.save = path\n        # 读取下载视频个数\n        self.count = 10\n        # 读取下载是否下载音频\n        self.musicarg = True\n        # 读取用户主页地址\n        self.user = ''\n        # 读取单条\n        self.single = ''\n\n        # 读取下载模式 #下载模式选择 like为点赞 post为发布\n        self.mode = 'post'\n\n        # 保存用户名\n        self.nickname = ''\n\n        if '/user/' in url:\n            self.user = url\n        else:\n            self.single = url\n            # https://www.douyin.com/video/6979067378848042276?extra_params=%7B%22search_id%22%3A%22202109260757420101511740995D070AF5%22%2C%22search_result_id%22%3A%226979067378848042276%22%2C%22search_type%22%3A%22video%22%2C%22search_keyword%22%3A%22%E6%A8%A1%E7%89%B9%22%7D&previous_page=search_result\n            # try:\n            #     self.single = re.findall(r'(http.+?)\\?extra_params', url)[0]\n            # except:\n            #     self.single = url\n\n        if len(self.single) > 0:\n            self.count = 1\n            self.parse_single()\n        else:\n            self.judge_link()\n\n    # 单条数据页面\n    def parse_single(self):\n        url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', self.single)[\n            0]\n        r = requests.get(url=url)\n        key = re.findall('video/(\\d+)?', str(r.url))[0]\n        jx_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={key}'  # 官方接口\n        js = json.loads(requests.get(url=jx_url, headers=self.headers).text)\n        detail = js['item_list'][0]\n        # 作者信息\n        author_list = []\n        # 无水印视频链接\n        video_list = []\n        # 作品id\n        aweme_id = []\n        # 作者id\n        nickname = []\n        max_cursor = 0\n        author_list.append(str(detail['desc']))\n        video_list.append(str(detail['video']['play_addr']['url_list'][0]).replace('playwm', 'play'))\n        aweme_id.append(str(detail['aweme_id']))\n        nickname.append(str(detail['author']['nickname']))\n        Downloader.print_ui('开始下载单个视频' + video_list[0])\n        self.videos_download(author_list, video_list, aweme_id, nickname, max_cursor)\n\n    # 匹配粘贴的url地址\n    def Find(self, string):\n        # findall() 查找匹配正则表达式的字符串\n        url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string)\n        Downloader.print_ui('Find url: ' + url)\n        return url\n\n    # 判断个人主页api链接\n    def judge_link(self):\n        user_url: str = self.user\n\n        Downloader.print_ui('----为您下载多个视频----\\r')\n\n        key = re.findall('/user/(.*?)$', str(user_url))[0]\n        if not key:\n            key = user_url[28:83]\n        Downloader.print_ui('----' + '用户的sec_id=' + key + '----\\r')\n\n        # 第一次访问页码\n        max_cursor = 0\n\n        # 构造第一次访问链接\n        api_post_url = 'https://www.iesdouyin.com/web/api/v2/aweme/%s/?sec_uid=%s&count=%s&max_cursor=%s&aid=1128&_signature=PDHVOQAAXMfFyj02QEpGaDwx1S&dytk=' % (\n            self.mode, key, str(self.count), max_cursor)\n        self.get_data(api_post_url, max_cursor)\n        return api_post_url, max_cursor, key\n\n    # 获取第一次api数据\n    def get_data(self, api_post_url, max_cursor):\n        # 尝试次数\n        index = 0\n        # 存储api数据\n        result = []\n        while result == []:\n            index += 1\n            Downloader.print_ui('----正在进行第 %d 次尝试----\\r' % index)\n            time.sleep(0.3)\n            response = requests.get(url=api_post_url, headers=self.headers)\n            html = json.loads(response.content.decode())\n            if self.end == False:\n                # 下一页值\n                self.nickname = html['aweme_list'][0]['author']['nickname']\n                Downloader.print_ui('[  用户  ]:' + str(self.nickname) + '\\r')\n                max_cursor = html['max_cursor']\n                result = html['aweme_list']\n                Downloader.print_ui('----抓获数据成功----\\r')\n\n                # 处理第一页视频信息\n                self.video_info(result, max_cursor)\n            else:\n                max_cursor = html['max_cursor']\n                self.next_data(max_cursor)\n                # self.end = True\n                Downloader.print_ui('----此页无数据，为您跳过----\\r')\n\n        return result, max_cursor\n\n    # 下一页\n    def next_data(self, max_cursor):\n        if self.count == 1:\n            return\n        user_url = self.user\n        # 获取用户sec_uid\n        # key = re.findall('/user/(.*?)\\?', str(user_url))[0]\n        key = re.findall('/user/(.*?)$', str(user_url))[0]\n        if not key:\n            key = user_url[28:83]\n\n        # 构造下一次访问链接\n        api_naxt_post_url = 'https://www.iesdouyin.com/web/api/v2/aweme/%s/?sec_uid=%s&count=%s&max_cursor=%s&aid=1128&_signature=RuMN1wAAJu7w0.6HdIeO2EbjDc&dytk=' % (\n            self.mode, key, str(self.count), max_cursor)\n        index = 0\n        result = []\n        while self.end == False:\n            # 回到首页，则结束\n            if max_cursor == 0:\n                self.end = True\n                return\n            index += 1\n            # Downloader.print_ui('----正在对' + max_cursor + '页进行第 %d 次尝试----\\r' % index)\n            Downloader.print_ui('----正在对{}页进行第 {} 次尝试----\\r'.format(max_cursor, index))\n            time.sleep(3)\n            response = requests.get(url=api_naxt_post_url, headers=self.headers)\n            html = json.loads(response.content.decode())\n            if self.end == False:\n                # 下一页值\n                max_cursor = html['max_cursor']\n                result = html['aweme_list']\n                Downloader.print_ui('----{}页抓获数据成功----\\r'.format(max_cursor))\n                # 处理下一页视频信息\n                self.video_info(result, max_cursor)\n            else:\n                self.end = True\n                Downloader.print_ui('----{}页抓获数据失败----\\r'.format(max_cursor))\n                # sys.exit()\n\n    # 处理视频信息\n    def video_info(self, result, max_cursor):\n        # 作者信息\n        author_list = []\n\n        # 无水印视频链接\n        video_list = []\n\n        # 作品id\n        aweme_id = []\n\n        # 作者id\n        nickname = []\n\n        # 封面大图\n        # dynamic_cover = []\n\n        for i2 in range(len(result)):\n            try:\n                author_list.append(str(result[i2]['desc']))\n                video_list.append(str(result[i2]['video']['play_addr']['url_list'][0]))\n                aweme_id.append(str(result[i2]['aweme_id']))\n                nickname.append(str(result[i2]['author']['nickname']))\n                # dynamic_cover.append(str(result[i2]['video']['dynamic_cover']['url_list'][0]))\n            except Exception as error:\n                # Downloader.print_ui2(error)\n                pass\n        self.videos_download(author_list, video_list, aweme_id, nickname, max_cursor)\n        return self, author_list, video_list, aweme_id, nickname, max_cursor\n\n    def videos_download(self, author_list, video_list, aweme_id, nickname, max_cursor):\n        count = len(author_list)\n        Downloader.add_total_count(count)\n        for i in range(count):\n            if count == 1:\n                # 创建并检测下载目录是否存在\n                pre_save = os.path.join(self.save, \"单条\")\n            else:\n                pre_save = os.path.join(self.save, nickname[i])\n            try:\n                os.makedirs(pre_save)\n            except:\n                pass\n            Downloader.add_downloading_count()\n            # try:\n            #     jx_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={aweme_id[i]}'  # 官方接口\n            #     js = json.loads(requests.get(url=jx_url, headers=self.headers).text)\n            #     music_url = str(js['item_list'][0]['music']['play_url']['url_list'][0])\n            #     music_title = str(js['item_list'][0]['music']['author'])\n            #     if self.musicarg == \"yes\":  # 保留音频\n            #         music = requests.get(music_url)  # 保存音频\n            #         start = time.time()  # 下载开始时间\n            #         size = 0  # 初始化已下载大小\n            #         chunk_size = 1024  # 每次下载的数据大小\n            #         content_size = int(music.headers['content-length'])  # 下载文件总大小\n            #         if music.status_code == 200:  # 判断是否响应成功\n            #             Downloader.print_ui('[  音频  ]:' + author_list[i] + '[文件 大小]:{size:.2f} MB'.format(\n            #                 size=content_size / chunk_size / 1024))  # 开始下载，显示下载文件大小\n            #             # m_url = pre_save + music_title + '-[' + author_list[i] + '].mp3'\n            #             m_url = os.path.join(pre_save,\n            #                                  nickname[i] + \"-\" + music_title + '-[' + author_list[i] + '].mp3')\n            #             Downloader.print_ui(\"路径：\" + m_url)\n            #             with open(m_url, 'wb') as file:  # 显示进度条\n            #                 for data in music.iter_content(chunk_size=chunk_size):\n            #                     file.write(data)\n            #                     size += len(data)\n            #                     Downloader.print_ui('\\r' + music_title + '\\n[下载进度]:%s%.2f%%' % (\n            #                         '>' * int(size * 50 / content_size), float(size / content_size * 100)))\n            #                 end = time.time()  # 下载结束时间\n            #                 Downloader.print_ui('\\n' + music_title + '\\n[下载完成]:耗时: %.2f秒\\n' % (end - start))  # 输出下载用时时间\n            #                 Downloader.add_success_count()\n            # except Exception as error:\n            #     # Downloader.print_ui2(error)\n            #     Downloader.print_ui('该页音频没有' + str(self.count) + '个\\r')\n            #     # Downloader.add_failed_count()\n            #     # break\n\n            try:\n                v_url = os.path.join(pre_save, nickname[i] + \"-\" + '[' + author_list[i] + '].mp4')\n                # 如果本地已经有了就跳过\n                if os.path.exists(v_url):\n                    Downloader.print_ui('{}-已存在！'.format(v_url))\n                    Downloader.add_success_count()\n                    continue\n\n                video = requests.get(video_list[i], headers=self.headers)  # 保存视频\n                start = time.time()  # 下载开始时间\n                size = 0  # 初始化已下载大小\n                chunk_size = 100  # 每次下载的数据大小\n                content_size = int(video.headers['content-length'])  # 下载文件总大小\n                if video.status_code == 200:  # 判断是否响应成功\n                    Downloader.print_ui(\n                        '[  视频  ]:' + nickname[i] + '-' + author_list[i] + '[文件 大小]:{size:.2f} MB'.format(\n                            size=content_size / 1024 / 1024))  # 开始下载，显示下载文件大小\n                    # v_url = os.path.join(pre_save, nickname[i] + \"-\" + '[' + author_list[i] + '].mp4')\n                    # v_url = pre_save + '[' + author_list[i] + '].mp4'\n                    Downloader.print_ui(\"路径：\" + v_url)\n                    with open(v_url, 'wb') as file:  # 显示进度条\n                        for data in video.iter_content(chunk_size=chunk_size):\n                            file.write(data)\n                            size += len(data)\n                            Downloader.print_ui('\\r' + author_list[i] + '\\n[下载进度]:%s%.2f%%' % (\n                                '>' * int(size * 50 / content_size), float(size / content_size * 100)))\n                        end = time.time()  # 下载结束时间\n                        Downloader.print_ui('\\n' + author_list[i] + '\\n[下载完成]:耗时: %.2f秒\\n' % (end - start))  # 输出下载用时时间\n                        Downloader.add_success_count()\n            except Exception as error:\n                # Downloader.print_ui2(error)\n                Downloader.print_ui('该页视频没有' + str(count) + '个,已为您跳过\\r')\n                Downloader.add_failed_count()\n                break\n        self.next_data(max_cursor)\n"
  },
  {
    "path": "001-Downloader/downloader.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:downloader.py 所有下载类的基类，负责与UI界面的绑定\n@Date       :2021/08/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport time\nfrom threading import Lock\n\nimport requests\nfrom my_fake_useragent import UserAgent\n\nfrom type_enum import PrintType\nfrom utils import Config\n\nua = UserAgent(family='chrome')\n\n\nclass Downloader(object):\n    func_ui_print = None\n    __mutex_total = Lock()\n    __mutex_success = Lock()\n    __mutex_failed = Lock()\n    __mutex_downloading = Lock()\n    __count_total = 0\n    __count_success = 0\n    __count_failed = 0\n    __count_downloading = 0\n    __beijing_time = 0  # 在线北京时间\n\n    def __init__(self):\n        self._headers = {'user-agent': ua.random()}\n        self.get_beijing_time()\n\n    @staticmethod\n    def print_hint():\n        \"\"\"显示初始提示信息\"\"\"\n        Downloader.print_ui(\n            \"\"\"\n                使用说明：\n                    1、快手下载用户批量视频如：https://www.kuaishou.com/profile/xxx\n                    2、快手下载单条视频如：https://www.kuaishou.com/short-video/xxx\n                    3、抖音下载用户批量视频如：https://www.douyin.com/user/xxx\n                    4、抖音下载单条视频如：https://www.douyin.com/video/xxx\n            \"\"\"\n        )\n\n    def start(self, url, path):\n        \"\"\"业务逻辑由子类实现\"\"\"\n        pass\n\n    @staticmethod\n    def print_ui(txt):\n        \"\"\"在界面显示内容\"\"\"\n        Downloader.print_all_ui(txt=txt)  # 打印日志\n\n    @staticmethod\n    def print_all_ui(txt, print_type: PrintType = PrintType.log):\n        \"\"\"通知ui中func_ui_print更新内容\"\"\"\n        if Downloader.func_ui_print is not None:\n            Downloader.func_ui_print(txt=txt, print_type=print_type)\n\n    @staticmethod\n    def get_beijing_time():\n        \"\"\"静态方法：获取在线的北京时间\"\"\"\n        if Downloader.__beijing_time > 0:\n            return Downloader.__beijing_time\n        try:\n            response = requests.get(url='http://www.beijing-time.org/t/time.asp', headers={'user-agent': ua.random()})\n            result = response.text\n            data = result.split(\"\\r\\n\")\n            year = data[1][len(\"nyear\") + 1: len(data[1]) - 1]\n            month = data[2][len(\"nmonth\") + 1: len(data[2]) - 1]\n            day = data[3][len(\"nday\") + 1: len(data[3]) - 1]\n            # wday = data[4][len(\"nwday\")+1 : len(data[4])-1]\n            hrs = data[5][len(\"nhrs\") + 1: len(data[5]) - 1]\n            minute = data[6][len(\"nmin\") + 1: len(data[6]) - 1]\n            sec = data[7][len(\"nsec\") + 1: len(data[7]) - 1]\n\n            beijinTimeStr = \"%s/%s/%s %s:%s:%s\" % (year, month, day, hrs, minute, sec)\n            beijinTime = time.strptime(beijinTimeStr, \"%Y/%m/%d %X\")\n            Downloader.__beijing_time = int(time.mktime(beijinTime))\n        except:\n            pass\n        return Downloader.__beijing_time\n\n    @staticmethod\n    def is_expired():\n        \"\"\"静态方法：判断是否已过期\"\"\"\n        if Downloader.__beijing_time == 0:  # 还没获取到时间\n            return True\n        expired_time_str = time.strptime(Config.instance().get_expired_time(), \"%Y/%m/%d %X\")\n        expired_time_int = int(time.mktime(expired_time_str))\n        return Downloader.__beijing_time > expired_time_int\n\n    @staticmethod\n    def add_total_count(count=1):\n        \"\"\"静态方法：添加总下载任务数\"\"\"\n        Downloader.__mutex_total.acquire()\n        Downloader.__count_total += count\n        Downloader.__mutex_total.release()\n        Downloader.print_all_ui(txt=\"预计总数：%d\" % Downloader.__count_total, print_type=PrintType.total)\n\n    @staticmethod\n    def get_total_count():\n        \"\"\"静态方法：获取总下载任务数\"\"\"\n        return Downloader.__count_total\n\n    @staticmethod\n    def add_downloading_count():\n        \"\"\"静态方法：添加正在下载任务数\"\"\"\n        Downloader.__mutex_downloading.acquire()\n        Downloader.__count_downloading += 1\n        Downloader.__mutex_downloading.release()\n        Downloader.print_all_ui(txt=\"正在下载：%d\" % Downloader.__count_downloading, print_type=PrintType.downloading)\n\n    @staticmethod\n    def __sub_downloading_count():\n        \"\"\"静态方法：减去正在下载任务数\"\"\"\n        Downloader.__mutex_downloading.acquire()\n        Downloader.__count_downloading -= 1\n        Downloader.__mutex_downloading.release()\n        Downloader.print_all_ui(txt=\"正在下载：%d\" % Downloader.__count_downloading, print_type=PrintType.downloading)\n\n    @staticmethod\n    def get_downloading_count():\n        \"\"\"静态方法：获取正在下载任务数\"\"\"\n        return Downloader.__count_downloading\n\n    @staticmethod\n    def add_success_count():\n        \"\"\"静态方法：添加下载成功任务数\"\"\"\n        Downloader.__mutex_success.acquire()\n        Downloader.__count_success += 1\n        Downloader.__mutex_success.release()\n        # 成功一条，减正在下载的一条\n        Downloader.__sub_downloading_count()\n        Downloader.print_all_ui(txt=\"已完成：%d\" % Downloader.__count_success, print_type=PrintType.success)\n\n    @staticmethod\n    def get_success_count():\n        \"\"\"静态方法：获取下载成功任务数\"\"\"\n        return Downloader.__count_success\n\n    @staticmethod\n    def add_failed_count():\n        \"\"\"静态方法：添加下载失败任务数\"\"\"\n        Downloader.__mutex_failed.acquire()\n        Downloader.__count_failed += 1\n        Downloader.__mutex_failed.release()\n        # 失败一条，减正在下载的一条\n        Downloader.__sub_downloading_count()\n        Downloader.print_all_ui(txt=\"已失败：%d\" % Downloader.__count_failed, print_type=PrintType.failed)\n\n    @staticmethod\n    def get_failed_count():\n        \"\"\"静态方法：获取下载失败任务数\"\"\"\n        return Downloader.__count_failed\n"
  },
  {
    "path": "001-Downloader/kuaishou/ks_download.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 快手视频下载\n@Date       :2021/09/102\n@Author     :qincji\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport json\nimport os\nimport re\nimport time\nimport urllib\n\nimport requests\n\nfrom downloader import Downloader\n\nrequestUrl = 'https://video.kuaishou.com/graphql'\n\n\nclass KuaiShou(Downloader):\n    cookie = 'clientid=3; client_key=65890b29; kpf=PC_WEB; kpn=KUAISHOU_VISION; did=web_3e09c32da1db9d38c0122ffa25ad8b7d'\n\n    # 初始化\n    def __init__(self):\n        super().__init__()\n        self.headers = self._headers\n        # 抓获所有视频\n        self.end = False\n\n    def set_cookie(self, c):\n        \"\"\"当cookie过期时，需要从外界出入\"\"\"\n        KuaiShou.cookie = c\n\n    def start(self, url, path):\n        Downloader.print_ui(\"开始解析下载链接\")\n        # 读取保存路径\n        self.save = path\n        if '/profile/' in url:\n            self.parse_user(url)\n        elif '/short-video/' in url:\n            if 'trendingId' in url:\n                self.parse_single_trendingId(urllib.parse.unquote(url, encoding=\"utf-8\"))\n            elif 'streamSource' in url:\n                self.parse_single_streamSource(urllib.parse.unquote(url, encoding=\"utf-8\"))\n            else:\n                Downloader.print_ui('该链接不支持下载')\n        else:\n            Downloader.print_ui('该链接不支持下载')\n\n    # 单条数据页面\n    def parse_single_trendingId(self, url):\n        try:\n            Downloader.print_ui('----为您下载单个视频----\\r')\n            # userId = re.findall(r'/short-video/(.+?)\\?', url)[0].strip()\n            trendingId = re.findall(r'trendingId=(.+?)&', url)[0].strip()\n            area = re.findall(r'area=(.+?)$', url)[0].strip()\n        except:\n            Downloader.print_ui('地址%s输入错误' % url)\n            return\n        links = []\n        try:\n            result = self.post_single_trendingId(url, KuaiShou.cookie, trendingId, area)\n            data = json.loads(result)\n            feeds = data['data']['hotData']['feeds']\n            size = len(feeds)\n            links.append(feeds)\n        except Exception as e:\n            Downloader.print_ui(str(e))\n            return\n        if size < 1:\n            Downloader.print_ui('解析地址%s异常' % url)\n            return\n        Downloader.add_total_count(size)\n        for link in links:\n            self.download(link)\n\n    # 单条数据页面\n    def parse_single_streamSource(self, url):\n        try:\n            Downloader.print_ui('----为您下载单个视频----\\r')\n            userId = re.findall(r'/short-video/(.+?)\\?', url)[0].strip()\n            area = re.findall(r'area=(.+?)$', url)[0].strip()\n        except:\n            Downloader.print_ui('地址%s输入错误' % url)\n            return\n        links = []\n        try:\n            result = self.post_single_streamSource(url, KuaiShou.cookie, userId, area)\n            data = json.loads(result)\n            feeds = [data['data']['visionVideoDetail'], ]\n            size = len(feeds)\n            links.append(feeds)\n        except Exception as e:\n            Downloader.print_ui(str(e))\n            return\n        if size < 1:\n            Downloader.print_ui('解析地址%s异常' % url)\n            return\n        Downloader.add_total_count(size)\n        for link in links:\n            self.download(link)\n\n    # 判断个人主页api链接\n    def parse_user(self, url):\n        # https://www.kuaishou.com/profile/3xcx5qwycxzxdre\n        try:\n            Downloader.print_ui('----为您下载多个视频----\\r')\n            userId = re.findall(r'/profile/(.+?)$', url)[0].strip()\n        except:\n            Downloader.print_ui('地址%s输入错误' % url)\n            return\n        pcursor = ''\n        all_count = 0\n        links = []\n        while True:\n            try:\n                result = self.post_user(userId, KuaiShou.cookie, pcursor)\n                data = json.loads(result)\n                feeds = data['data']['visionProfilePhotoList']['feeds']\n                flen = len(feeds)\n                pcursor = data['data']['visionProfilePhotoList']['pcursor']\n                if flen == 0:\n                    break\n                all_count += flen\n                links.append(feeds)\n            except Exception as e:\n                Downloader.print_ui(str(e))\n                break\n        if len(links) < 1:\n            Downloader.print_ui('解析地址%s异常' % url)\n            return\n        Downloader.add_total_count(all_count)\n        for link in links:\n            self.download(link)\n\n    def post_single_trendingId(self, url, Cookie, trendingId, area):\n        data = {\n            \"operationName\": \"hotVideoQuery\",\n            \"variables\": {\n                \"trendingId\": trendingId,\n                \"page\": \"detail\",\n                \"webPageArea\": area\n            },\n            \"query\": \"query hotVideoQuery($trendingId: String, $page: String, $webPageArea: String) {\\n  hotData(trendingId: $trendingId, page: $page, webPageArea: $webPageArea) {\\n    result\\n    llsid\\n    expTag\\n    serverExpTag\\n    pcursor\\n    webPageArea\\n    feeds {\\n      type\\n      trendingId\\n      author {\\n        id\\n        name\\n        headerUrl\\n        following\\n        headerUrls {\\n          url\\n          __typename\\n        }\\n        __typename\\n      }\\n      photo {\\n        id\\n        duration\\n        caption\\n        likeCount\\n        realLikeCount\\n        coverUrl\\n        photoUrl\\n        coverUrls {\\n          url\\n          __typename\\n        }\\n        timestamp\\n        expTag\\n        animatedCoverUrl\\n        stereoType\\n        videoRatio\\n        __typename\\n      }\\n      canAddComment\\n      llsid\\n      status\\n      currentPcursor\\n      __typename\\n    }\\n    __typename\\n  }\\n}\\n\"\n        }\n        headers = {\n            'Host': 'www.kuaishou.com',\n            'Connection': 'keep-alive',\n            'Content-Length': '1261',\n            'accept': '*/*',\n            'User-Agent': 'Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/89.0.4389.114Safari/537.36Edg/89.0.774.68',\n            'content-type': 'application/json',\n            'Origin': 'https://www.kuaishou.com',\n            'Sec-Fetch-Site': 'same-origin',\n            'Sec-Fetch-Mode': 'cors',\n            'Sec-Fetch-Dest': 'empty',\n            'Referer': url.encode(encoding='utf-8'),\n            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6'\n            # 'Cookie': Cookie,\n        }\n        requests.packages.urllib3.disable_warnings()\n        r = requests.post('https://www.kuaishou.com/graphql', data=json.dumps(data), headers=headers)\n        r.encoding = r.apparent_encoding\n        html = r.text\n        return html\n\n    def post_single_streamSource(self, url, Cookie, photoId, area):\n        data = {\n            \"operationName\": \"visionVideoDetail\",\n            \"variables\": {\n                \"photoId\": photoId,\n                \"page\": \"detail\",\n                \"webPageArea\": area\n            },\n            \"query\": \"query visionVideoDetail($photoId: String, $type: String, $page: String, $webPageArea: String) {\\n  visionVideoDetail(photoId: $photoId, type: $type, page: $page, webPageArea: $webPageArea) {\\n    status\\n    type\\n    author {\\n      id\\n      name\\n      following\\n      headerUrl\\n      __typename\\n    }\\n    photo {\\n      id\\n      duration\\n      caption\\n      likeCount\\n      realLikeCount\\n      coverUrl\\n      photoUrl\\n      liked\\n      timestamp\\n      expTag\\n      llsid\\n      viewCount\\n      videoRatio\\n      stereoType\\n      croppedPhotoUrl\\n      manifest {\\n        mediaType\\n        businessType\\n        version\\n        adaptationSet {\\n          id\\n          duration\\n          representation {\\n            id\\n            defaultSelect\\n            backupUrl\\n            codecs\\n            url\\n            height\\n            width\\n            avgBitrate\\n            maxBitrate\\n            m3u8Slice\\n            qualityType\\n            qualityLabel\\n            frameRate\\n            featureP2sp\\n            hidden\\n            disableAdaptive\\n            __typename\\n          }\\n          __typename\\n        }\\n        __typename\\n      }\\n      __typename\\n    }\\n    tags {\\n      type\\n      name\\n      __typename\\n    }\\n    commentLimit {\\n      canAddComment\\n      __typename\\n    }\\n    llsid\\n    danmakuSwitch\\n    __typename\\n  }\\n}\\n\"\n        }\n        headers = {\n            'Host': 'www.kuaishou.com',\n            'Connection': 'keep-alive',\n            'Content-Length': '1261',\n            'accept': '*/*',\n            'User-Agent': 'Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/89.0.4389.114Safari/537.36Edg/89.0.774.68',\n            'content-type': 'application/json',\n            'Origin': 'https://www.kuaishou.com',\n            'Sec-Fetch-Site': 'same-origin',\n            'Sec-Fetch-Mode': 'cors',\n            'Sec-Fetch-Dest': 'empty',\n            'Referer': url.encode(encoding='utf-8'),\n            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',\n            'Cookie': Cookie,\n        }\n        requests.packages.urllib3.disable_warnings()\n        r = requests.post('https://www.kuaishou.com/graphql', data=json.dumps(data), headers=headers)\n        r.encoding = r.apparent_encoding\n        html = r.text\n        return html\n\n    def post_user(self, userId, Cookie, pcursor):\n        data = {\"operationName\": \"visionProfilePhotoList\",\n                \"variables\": {\"userId\": userId, \"pcursor\": pcursor, \"page\": \"profile\"},\n                \"query\": \"query visionProfilePhotoList($pcursor: String, $userId: String, $page: String, $webPageArea: String) {\\n  visionProfilePhotoList(pcursor: $pcursor, userId: $userId, page: $page, webPageArea: $webPageArea) {\\n    result\\n    llsid\\n    webPageArea\\n    feeds {\\n      type\\n      author {\\n        id\\n        name\\n        following\\n        headerUrl\\n        headerUrls {\\n          cdn\\n          url\\n          __typename\\n        }\\n        __typename\\n      }\\n      tags {\\n        type\\n        name\\n        __typename\\n      }\\n      photo {\\n        id\\n        duration\\n        caption\\n        likeCount\\n        realLikeCount\\n        coverUrl\\n        coverUrls {\\n          cdn\\n          url\\n          __typename\\n        }\\n        photoUrls {\\n          cdn\\n          url\\n          __typename\\n        }\\n        photoUrl\\n        liked\\n        timestamp\\n        expTag\\n        animatedCoverUrl\\n        stereoType\\n        videoRatio\\n        __typename\\n      }\\n      canAddComment\\n      currentPcursor\\n      llsid\\n      status\\n      __typename\\n    }\\n    hostName\\n    pcursor\\n    __typename\\n  }\\n}\\n\"}\n        failed = {'msg': 'failed...'}\n        headers = {\n            'Host': 'video.kuaishou.com',\n            'Connection': 'keep-alive',\n            'Content-Length': '1261',\n            'accept': '*/*',\n            'User-Agent': 'Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/89.0.4389.114Safari/537.36Edg/89.0.774.68',\n            'content-type': 'application/json',\n            'Origin': 'https://video.kuaishou.com',\n            'Sec-Fetch-Site': 'same-origin',\n            'Sec-Fetch-Mode': 'cors',\n            'Sec-Fetch-Dest': 'empty',\n            'Referer': 'https://video.kuaishou.com/profile/' + userId,\n            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',\n            'Cookie': Cookie,\n\n        }\n        requests.packages.urllib3.disable_warnings()\n        r = requests.post(requestUrl, data=json.dumps(data), headers=headers)\n        r.encoding = 'UTF-8'\n        html = r.text\n        return html\n\n    def progressbar(self, url, filepath, filename):\n        if not os.path.exists(filepath):\n            os.mkdir(filepath)\n        start = time.time()\n        response = requests.get(url, stream=True)\n        size = 0\n        chunk_size = 1024\n        content_size = int(response.headers['content-length'])\n        if response.status_code == 200:\n            # print('Start download,[File size]:{size:.2f} MB'.format(size=content_size / chunk_size / 1024))\n            Downloader.print_ui(('%s Start download,[File size]:{size:.2f} MB' % filename).format(\n                size=content_size / chunk_size / 1024))\n            filename = filename.replace(\"\\n\", \"\")\n            # filepath = filepath + filename\n            filepath = os.path.join(filepath, filename)\n            try:\n                with open(filepath, 'wb') as file:\n                    for data in response.iter_content(chunk_size=chunk_size):\n                        file.write(data)\n                        size += len(data)\n                        Downloader.print_ui('\\r' + '%s[下载进度]:%s%.2f%%' % (filename,\n                                                                          '>' * int(size * 50 / content_size),\n                                                                          float(size / content_size * 100)))\n                        # print('\\r' + '[下载进度]:%s%.2f%%' % (\n                        #     '>' * int(size * 50 / content_size), float(size / content_size * 100)), end=' ')\n                end = time.time()\n                # print('Download completed!,times: %.2f秒' % (end - start))\n                Downloader.print_ui('%s Download completed!,times: %.2f秒' % (filename, end - start))\n            except:\n                Downloader.add_failed_count()\n                Downloader.print_ui('%s [下载失败！！]' % filename)\n\n    def download(self, feeds):\n        author = ''\n        for feed in feeds:\n            try:\n                Downloader.add_downloading_count()\n                author = feed['author']['name']\n                filename = feed['photo']['caption'] + '.mp4'\n                # filepath = self.save + '/' + author + '/'\n                filepath = os.path.join(self.save, author)\n                filename_path = os.path.join(filepath, filename)\n                if not os.path.exists(filename_path):\n                    self.progressbar(feed['photo']['photoUrl'], filepath, filename)\n                    # print(filename + \",下载完成\")\n                    Downloader.print_ui('%s--下载完成' % filename)\n                    Downloader.add_success_count()\n                else:\n                    # print(filename + \",已存在，跳过\")\n                    Downloader.print_ui('%s--已存在，跳过' % filename)\n                    Downloader.add_success_count()\n            except:\n                Downloader.add_failed_count()\n                Downloader.print_ui('%s下载失败' % author)\n"
  },
  {
    "path": "001-Downloader/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 程序主入口\n@Date       :2021/08/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\nimport sys\n\nfrom ui import Ui\n\n# 主模块执行\nif __name__ == \"__main__\":\n    path = os.path.dirname(os.path.realpath(sys.argv[0]))\n    # path = os.path.dirname('/Users/Qincji/Documents/zmt/')\n    app = Ui()\n    app.set_dir(path)\n    # to do\n    app.mainloop()\n"
  },
  {
    "path": "001-Downloader/test/bilibili_video_download_v1.py",
    "content": "# !/usr/bin/python\n# -*- coding:utf-8 -*-\n# time: 2019/04/17--08:12\n__author__ = 'Henry'\n\n'''\n项目: B站视频下载\n\n版本1: 加密API版,不需要加入cookie,直接即可下载1080p视频\n\n20190422 - 增加多P视频单独下载其中一集的功能\n'''\n\nimport requests, time, hashlib, urllib.request, re, json\nfrom moviepy.editor import *\nimport os, sys\n\n\n# 访问API地址\ndef get_play_list(start_url, cid, quality):\n    entropy = 'rbMCKn@KuamXWlPMoJGsKcbiJKUfkPF_8dABscJntvqhRSETg'\n    appkey, sec = ''.join([chr(ord(i) + 2) for i in entropy[::-1]]).split(':')\n    params = 'appkey=%s&cid=%s&otype=json&qn=%s&quality=%s&type=' % (appkey, cid, quality, quality)\n    chksum = hashlib.md5(bytes(params + sec, 'utf8')).hexdigest()\n    url_api = 'https://interface.bilibili.com/v2/playurl?%s&sign=%s' % (params, chksum)\n    headers = {\n        'Referer': start_url,  # 注意加上referer\n        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'\n    }\n    # print(url_api)\n    html = requests.get(url_api, headers=headers).json()\n    # print(json.dumps(html))\n    video_list = []\n    for i in html['durl']:\n        video_list.append(i['url'])\n    # print(video_list)\n    return video_list\n\n\n# 下载视频\n'''\n urllib.urlretrieve 的回调函数：\ndef callbackfunc(blocknum, blocksize, totalsize):\n    @blocknum:  已经下载的数据块\n    @blocksize: 数据块的大小\n    @totalsize: 远程文件的大小\n'''\n\n\ndef Schedule_cmd(blocknum, blocksize, totalsize):\n    speed = (blocknum * blocksize) / (time.time() - start_time)\n    # speed_str = \" Speed: %.2f\" % speed\n    speed_str = \" Speed: %s\" % format_size(speed)\n    recv_size = blocknum * blocksize\n\n    # 设置下载进度条\n    f = sys.stdout\n    pervent = recv_size / totalsize\n    percent_str = \"%.2f%%\" % (pervent * 100)\n    n = round(pervent * 50)\n    s = ('#' * n).ljust(50, '-')\n    f.write(percent_str.ljust(8, ' ') + '[' + s + ']' + speed_str)\n    f.flush()\n    # time.sleep(0.1)\n    f.write('\\r')\n\n\ndef Schedule(blocknum, blocksize, totalsize):\n    speed = (blocknum * blocksize) / (time.time() - start_time)\n    # speed_str = \" Speed: %.2f\" % speed\n    speed_str = \" Speed: %s\" % format_size(speed)\n    recv_size = blocknum * blocksize\n\n    # 设置下载进度条\n    f = sys.stdout\n    pervent = recv_size / totalsize\n    percent_str = \"%.2f%%\" % (pervent * 100)\n    n = round(pervent * 50)\n    s = ('#' * n).ljust(50, '-')\n    print(percent_str.ljust(6, ' ') + '-' + speed_str)\n    f.flush()\n    time.sleep(2)\n    # print('\\r')\n\n\n# 字节bytes转化K\\M\\G\ndef format_size(bytes):\n    try:\n        bytes = float(bytes)\n        kb = bytes / 1024\n    except:\n        print(\"传入的字节格式不对\")\n        return \"Error\"\n    if kb >= 1024:\n        M = kb / 1024\n        if M >= 1024:\n            G = M / 1024\n            return \"%.3fG\" % (G)\n        else:\n            return \"%.3fM\" % (M)\n    else:\n        return \"%.3fK\" % (kb)\n\n\n#  下载视频\ndef down_video(video_list, title, start_url, page):\n    num = 1\n    print('[正在下载P{}段视频,请稍等...]:'.format(page) + title)\n    currentVideoPath = os.path.join(sys.path[0], 'bilibili_video', title)  # 当前目录作为下载目录\n    for i in video_list:\n        opener = urllib.request.build_opener()\n        # 请求头\n        opener.addheaders = [\n            # ('Host', 'upos-hz-mirrorks3.acgvideo.com'),  #注意修改host,不用也行\n            ('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:56.0) Gecko/20100101 Firefox/56.0'),\n            ('Accept', '*/*'),\n            ('Accept-Language', 'en-US,en;q=0.5'),\n            ('Accept-Encoding', 'gzip, deflate, br'),\n            ('Range', 'bytes=0-'),  # Range 的值要为 bytes=0- 才能下载完整视频\n            ('Referer', start_url),  # 注意修改referer,必须要加的!\n            ('Origin', 'https://www.bilibili.com'),\n            ('Connection', 'keep-alive'),\n        ]\n        urllib.request.install_opener(opener)\n        # 创建文件夹存放下载的视频\n        if not os.path.exists(currentVideoPath):\n            os.makedirs(currentVideoPath)\n        # 开始下载\n        if len(video_list) > 1:\n            urllib.request.urlretrieve(url=i, filename=os.path.join(currentVideoPath, r'{}-{}.mp4'.format(title, num)),\n                                       reporthook=Schedule_cmd)  # 写成mp4也行  title + '-' + num + '.flv'\n        else:\n            urllib.request.urlretrieve(url=i, filename=os.path.join(currentVideoPath, r'{}.mp4'.format(title)),\n                                       reporthook=Schedule_cmd)  # 写成mp4也行  title + '-' + num + '.flv'\n        num += 1\n\n\n# 合并视频\ndef combine_video(video_list, title):\n    currentVideoPath = os.path.join(sys.path[0], 'bilibili_video', title)  # 当前目录作为下载目录\n    if not os.path.exists(currentVideoPath):\n        os.makedirs(currentVideoPath)\n    if len(video_list) >= 2:\n        # 视频大于一段才要合并\n        print('[下载完成,正在合并视频...]:' + title)\n        # 定义一个数组\n        L = []\n        # 访问 video 文件夹 (假设视频都放在这里面)\n        root_dir = currentVideoPath\n        # 遍历所有文件\n        for file in sorted(os.listdir(root_dir), key=lambda x: int(x[x.rindex(\"-\") + 1:x.rindex(\".\")])):\n            # 如果后缀名为 .mp4/.flv\n            if os.path.splitext(file)[1] == '.flv':\n                # 拼接成完整路径\n                filePath = os.path.join(root_dir, file)\n                # 载入视频\n                video = VideoFileClip(filePath)\n                # 添加到数组\n                L.append(video)\n        # 拼接视频\n        final_clip = concatenate_videoclips(L)\n        # 生成目标视频文件\n        final_clip.to_videofile(os.path.join(root_dir, r'{}.mp4'.format(title)), fps=24, remove_temp=False)\n        print('[视频合并完成]' + title)\n\n    else:\n        # 视频只有一段则直接打印下载完成\n        print('[视频合并完成]:' + title)\n\n\ndef getAid(Bid):\n    headers = {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'\n    }\n    url = \"https://api.bilibili.com/x/web-interface/view?bvid=\" + Bid\n    print(url)\n    r = requests.get(url, headers=headers)\n    j = json.loads(r.text)\n    # print(j[\"data\"][\"aid\"])\n    print(j)\n    return j[\"data\"][\"aid\"]\n\n\nif __name__ == '__main__':\n    # 用户输入av号或者视频链接地址\n    print('*' * 30 + 'B站视频下载小助手' + '*' * 30)\n    start = input('请输入您要下载的B站av号、bv号或者视频链接地址:')\n    if 'http' in start:\n        if 'video/BV' in start:\n            bv = re.findall(r'video/(.*?)\\?', start)[0]\n            start = str(getAid(bv))\n    print(start)\n    if start.isdigit() == True:  # 如果输入的是av号\n        # 获取cid的api, 传入aid即可\n        start_url = 'https://api.bilibili.com/x/web-interface/view?aid=' + start\n    else:\n        # https://www.bilibili.com/video/av46958874/?spm_id_from=333.334.b_63686965665f7265636f6d6d656e64.16\n        start_url = 'https://api.bilibili.com/x/web-interface/view?aid=' + re.search(r'/av(\\d+)/*', start).group(1)\n        # https://www.bilibili.com/video/BV1jL4y1e7Uz?t=7.2\n        # start_url = 'https://api.bilibili.com/x/web-interface/view?aid=' + re.findall(r'video/(.*?)\\?', start)[0]\n    print(start_url)\n    # 视频质量\n    # <accept_format><![CDATA[flv,flv720,flv480,flv360]]></accept_format>\n    # <accept_description><![CDATA[高清 1080P,高清 720P,清晰 480P,流畅 360P]]></accept_description>\n    # <accept_quality><![CDATA[80,64,32,16]]></accept_quality>\n    quality = input('请输入您要下载视频的清晰度(1080p:80;720p:64;480p:32;360p:16)(填写80或64或32或16):')\n    # 获取视频的cid,title\n    headers = {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'\n    }\n    html = requests.get(start_url, headers=headers).json()\n    data = html['data']\n    video_title = data[\"title\"].replace(\" \", \"_\")\n    cid_list = []\n    if '?p=' in start:\n        # 单独下载分P视频中的一集\n        p = re.search(r'\\?p=(\\d+)', start).group(1)\n        cid_list.append(data['pages'][int(p) - 1])\n    else:\n        # 如果p不存在就是全集下载\n        cid_list = data['pages']\n    # print(cid_list)\n    for item in cid_list:\n        cid = str(item['cid'])\n        title = item['part']\n        if not title:\n            title = video_title\n        title = re.sub(r'[\\/\\\\:*?\"<>|]', '', title)  # 替换为空的\n        print('[下载视频的cid]:' + cid)\n        print('[下载视频的标题]:' + title)\n        page = str(item['page'])\n        start_url = start_url + \"/?p=\" + page\n        video_list = get_play_list(start_url, cid, quality)\n        start_time = time.time()\n        down_video(video_list, title, start_url, page)\n        combine_video(video_list, title)\n\n    # 如果是windows系统，下载完成后打开下载目录\n    currentVideoPath = os.path.join(sys.path[0], 'bilibili_video')  # 当前目录作为下载目录\n    if (sys.platform.startswith('win')):\n        os.startfile(currentVideoPath)\n\n# 分P视频下载测试: https://www.bilibili.com/video/av19516333/\n"
  },
  {
    "path": "001-Downloader/test/ff_video.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: ffmpeg去掉最后一帧，改变md5\n@Date       :2022/02/17\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\n\n\ndef cute_video(folder):\n    files = next(os.walk(folder))[2]  # 获取文件\n    for file in files:\n        file_path = os.path.join(folder, file)\n        shotname, extension = os.path.splitext(file)\n        if len(shotname) == 0 or len(extension) == 0:\n            continue\n        out_file = os.path.join(folder, 'out-{}{}'.format(shotname, extension))\n        # 获取时间。输入自己系统安装的ffmpeg，注意斜杠\n        time = os.popen(\n            r\"/usr/local/ffmpeg/bin/ffmpeg -i {} 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//\".format(\n                file_path)).read().replace('\\n', '').replace(' ', '')\n        if '.' in time:\n            match_time = time.split('.')[0]\n        else:\n            match_time = time\n        print(match_time)\n        ts = match_time.split(':')\n        sec = int(ts[0]) * 60 * 60 + int(ts[1]) * 60 + int(ts[2])\n        # 从0分0秒100毫秒开始截切（目的就是去头去尾）\n        os.popen(r\"/usr/local/ffmpeg/bin/ffmpeg -ss 0:00.100 -i {} -t {} -c:v copy -c:a copy {}\".format(file_path, sec,\n                                                                                                        out_file))\n\n\n# 主模块执行\nif __name__ == \"__main__\":\n    # path = os.path.dirname('/Users/Qincji/Downloads/ffmpeg/')\n    path = os.path.dirname('需要处理的目录')  # 目录下的所有视频\n    cute_video(path)\n"
  },
  {
    "path": "001-Downloader/test/test_pyinstaller.py",
    "content": "#!/usr/bin/env python\r\n# -*- coding:utf-8 -*-\r\nimport os\r\n# 测试打包\r\n# import pyppeteer\r\n# import sys\r\n# import asyncio\r\n# from urllib.parse import urlparse, urlunparse, urljoin\r\n# from concurrent.futures import ThreadPoolExecutor\r\n# from concurrent.futures._base import TimeoutError\r\n# from functools import partial\r\n# from typing import Set, Union, List, MutableMapping, Optional\r\n#\r\n# import requests\r\n# from pyquery import PyQuery\r\n#\r\n# from fake_useragent import UserAgent\r\n# from lxml.html.clean import Cleaner\r\n# import lxml\r\n# from lxml import etree\r\n# from lxml.html import HtmlElement\r\n# from lxml.html import tostring as lxml_html_tostring\r\n# from lxml.html.soupparser import fromstring as soup_parse\r\n# from parse import search as parse_search\r\n# from parse import findall, Result\r\n# from w3lib.encoding import html_to_unicode\r\n\r\n# from tkinter import *\r\nfrom tkinter.filedialog import (askdirectory)\r\nprint('输入和粗了了')"
  },
  {
    "path": "001-Downloader/test/urls.txt",
    "content": ""
  },
  {
    "path": "001-Downloader/test/xhs_download.py",
    "content": "import os\nimport random\nimport time\n\nimport requests\nfrom my_fake_useragent import UserAgent\n\nua = UserAgent(family='chrome')\npre_save = os.path.join(os.path.curdir, '0216')\n\n'''\n\n'''\n\n\ndef download_url(url, index):\n    try:\n        headers = {\n            'Accept': '*/*',\n            'Accept-Encoding': 'identity;q=1, *;q=0',\n            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',\n            'Cache-Control': 'no-cache',\n            'Connection': 'keep-alive',\n            'Cookie': 'xhsTrackerId=6970aca9-a496-4f50-cf98-118929f063bf; timestamp2=2022021544322a4e45f1e1dec93beb82; timestamp2.sig=jk1cFo-zHueSZUpZRvlqyJwTFoA1y8ch9t76Bfy28_Q; solar.beaker.session.id=1644906492328060192125; xhsTracker=url=index&searchengine=google',\n            'Host': 'v.xiaohongshu.com',\n            'Pragma': 'no-cache',\n            'Referer': url,\n            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36'\n        }\n        video = requests.get(url, headers=headers)  # 保存视频\n        start = time.time()  # 下载开始时间\n        size = 0  # 初始化已下载大小\n        chunk_size = 100  # 每次下载的数据大小\n        content_size = int(video.headers['content-length'])  # 下载文件总大小\n        print(video.status_code)\n        if video.status_code == 200:  # 判断是否响应成功\n            print(str(index) + '[文件 大小]:{size:.2f} MB'.format(size=content_size / 1024 / 1024))  # 开始下载，显示下载文件大小\n            v_url = os.path.join(pre_save, '{}.mp4'.format(index))\n            # v_url = pre_save + '[' + author_list[i] + '].mp4'\n            with open(v_url, 'wb') as file:  # 显示进度条\n                for data in video.iter_content(chunk_size=chunk_size):\n                    file.write(data)\n                    size += len(data)\n                    # print('\\r' + i + '\\n[下载进度]:%s%.2f%%' % (\n                    #     '>' * int(size * 50 / content_size), float(size / content_size * 100)))\n                end = time.time()  # 下载结束时间\n                print('\\n' + str(index) + '\\n[下载完成]:耗时: %.2f秒\\n' % (end - start))  # 输出下载用时时间\n    except Exception as error:\n        # Downloader.print_ui2(error)\n        print(error)\n        print('该页视频没有' + str(index) + ',已为您跳过\\r')\n\n\nif __name__ == '__main__':\n    ls = []\n    if not os.path.exists(pre_save):\n        os.makedirs(pre_save)\n    with open('../xhs/urls.txt', 'r') as f:\n        for line in f:\n            if 'http' in line:\n                ls.append(line.replace('\\n', '').replace(' ', ''))\n    size = len(ls)\n    for i in range(0, size):\n        url = ls[i]\n        print('{}-{}'.format(i, url))\n        download_url(url, i)\n        time.sleep(random.randint(5, 10))\n"
  },
  {
    "path": "001-Downloader/type_enum.py",
    "content": "#!/usr/bin/env python\n# -*- coding:utf-8 -*-\n\"\"\"\n@Description:dy_download.py\n@Date       :2021/08/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nfrom enum import Enum\n\n\nclass PrintType(Enum):\n    log = 1\n    total = 2\n    downloading = 3\n    success = 4\n    failed = 5\n"
  },
  {
    "path": "001-Downloader/ui.py",
    "content": "#!/usr/bin/env python\r\n# -*- coding:utf-8 -*-\r\n\"\"\"\r\n@Description: 用于GUI界面显示\r\n@Date       :2021/08/14\r\n@Author     :xhunmon\r\n@Mail       :xhunmon@gmail.com\r\n\"\"\"\r\nfrom tkinter import *\r\nfrom tkinter.filedialog import (askdirectory)\r\n\r\nfrom douyin.dy_download import DouYin\r\nfrom downloader import Downloader\r\nfrom kuaishou.ks_download import KuaiShou\r\nfrom type_enum import PrintType\r\nfrom utils import *\r\n\r\n\r\n# from PIL import Image, ImageTk\r\n\r\n\r\nclass Ui(Frame):\r\n    def __init__(self, master=None):\r\n        global bg_color\r\n        bg_color = '#373434'\r\n        Frame.__init__(self, master, bg=bg_color)\r\n        self.ui_width = 0\r\n        self.pack(expand=YES, fill=BOTH)\r\n        self.window_init()\r\n        self.createWidgets()\r\n\r\n    def window_init(self):\r\n        self.master.title(\r\n            '欢迎使用-自媒体资源下载器' + Config.instance().get_version_name() + '，本程序仅用于学习交流！如有疑问请联系：xhunmon@gmail.com')\r\n        self.master.bg = bg_color\r\n        width, height = self.master.maxsize()\r\n        # self.master.geometry(\"{}x{}\".format(width, height))\r\n        self.master.geometry(\"%dx%d+%d+%d\" % (width / 2, height / 2, width / 4, height / 4))\r\n        self.ui_width = width / 2\r\n\r\n    def createWidgets(self):\r\n        # fm1\r\n        self.fm1 = Frame(self, bg=bg_color)\r\n        self.fm1.pack(fill='y', pady=10)\r\n        # window没有原生PIL 64位支持\r\n        # load = Image.open('res/logo.png')\r\n        # load.thumbnail((38, 38), Image.ANTIALIAS)\r\n        # initIamge = ImageTk.PhotoImage(load)\r\n        # self.panel = Label(self.fm1, image=initIamge, bg=bg_color)\r\n        # self.panel.image = initIamge\r\n        # self.panel.pack(side=LEFT, fill='y', padx=5)\r\n        self.titleLabel = Label(self.fm1, text=\"资源下载器\", font=('微软雅黑', 32), fg=\"white\", bg=bg_color)\r\n        self.titleLabel.pack(side=LEFT, fill='y')\r\n\r\n        # fm2\r\n        self.fm2 = Frame(self, bg=bg_color)\r\n        self.fm2.pack(side=TOP, fill=\"y\")\r\n        self.fm2_right = Frame(self.fm2, bg=bg_color)\r\n        self.fm2_right.pack(side=RIGHT, padx=0, pady=10, expand=YES, fill='y')\r\n        self.fm2_left = Frame(self.fm2, bg=bg_color)\r\n        self.fm2_left.pack(side=LEFT, padx=15, pady=10, expand=YES, fill='x')\r\n        self.fm2_left_top = Frame(self.fm2_left, bg=bg_color)\r\n        self.fm2_left_bottom = Frame(self.fm2_left, bg=bg_color)\r\n\r\n        self.downloadBtn = Button(self.fm2_right, text='开始下载', fg=\"#ffffff\", bg=bg_color,\r\n                                  font=('微软雅黑', 18), command=self.start_download)\r\n        self.downloadBtn.pack(side=RIGHT)\r\n\r\n        self.dirEntry = Entry(self.fm2_left_top, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1)\r\n        self.dirEntry.config(insertbackground='#ffffff')\r\n        # self.set_dir(os.path.dirname(os.path.realpath(sys.argv[0])))\r\n        self.dirBtn = Button(self.fm2_left_top, text='选择保存目录：', bg=bg_color, fg='#aaaaaa',\r\n                             font=('微软雅黑', 12), width='10', command=self.save_dir)\r\n        self.dirBtn.pack(side=LEFT)\r\n        self.dirEntry.pack(side=LEFT, fill='y')\r\n        self.fm2_left_top.pack(side=TOP, fill='x')\r\n\r\n        self.urlEntry = Entry(self.fm2_left_bottom, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1)\r\n        self.urlEntry.config(insertbackground='#ffffff')\r\n        self.urlButton = Button(self.fm2_left_bottom, text='清空下载地址：', bg=bg_color, fg='#aaaaaa',\r\n                                font=('微软雅黑', 12), width='10', command=self.download_url)\r\n        self.urlButton.pack(side=LEFT)\r\n        self.urlEntry.pack(side=LEFT, fill='y')\r\n        self.fm2_left_bottom.pack(side=TOP, pady=10, fill='x')\r\n\r\n        # fm3 任务数状态\r\n        self.fm3 = Frame(self, bg=bg_color, height=6)\r\n        self.fm3.pack(side=TOP, fill=\"x\")\r\n        self.totalLabel = Label(self.fm3, width=10, text=\"预计总数：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.totalLabel.pack(side=LEFT, fill='y', padx=20)\r\n        self.downloadingLabel = Label(self.fm3, width=10, text=\"正在下载：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.downloadingLabel.pack(side=LEFT, fill='y', padx=20)\r\n        self.successLabel = Label(self.fm3, width=10, text=\"已完成：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.successLabel.pack(side=LEFT, fill='y', padx=20)\r\n        self.failLabel = Label(self.fm3, width=10, text=\"已失败：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.failLabel.pack(side=LEFT, fill='y', padx=20)\r\n\r\n        # fm4\r\n        self.fm4 = Frame(self, bg=bg_color)\r\n        self.fm4.pack(side=TOP, expand=YES, fill=\"both\")\r\n        self.logLabel = Label(self.fm4, anchor='w', wraplength=self.ui_width - 40, text=\"\", font=('微软雅黑', 12),\r\n                              fg=\"white\",\r\n                              bg=bg_color)\r\n        self.logLabel.pack(side=TOP, fill='both', padx=20)\r\n\r\n        # 注册回调\r\n        Downloader.func_ui_print = self.func_ui_print\r\n        # 判断是否有网络\r\n        if Downloader.get_beijing_time() == 0:\r\n            self.output(\"获取数据异常，请检查您的网络！\")\r\n        else:\r\n            Downloader.print_hint()\r\n\r\n    def save_dir(self):\r\n        path = askdirectory()\r\n        self.set_dir(path)\r\n\r\n    def set_dir(self, path):\r\n        self.dirEntry.delete(0, END)\r\n        self.dirEntry.insert(0, path)\r\n\r\n    def download_url(self):\r\n        ground_truth = ''\r\n        self.urlEntry.delete(0, END)\r\n        self.urlEntry.insert(0, ground_truth)\r\n\r\n    def output(self, txt):\r\n        self.logLabel.config(text=txt)\r\n\r\n    def func_ui_print(self, txt, print_type: PrintType = None):\r\n        if print_type == PrintType.log:\r\n            self.logLabel.config(text=txt)\r\n        elif print_type == PrintType.total:\r\n            self.totalLabel.config(text=txt)\r\n        elif print_type == PrintType.downloading:\r\n            self.downloadingLabel.config(text=txt)\r\n        elif print_type == PrintType.success:\r\n            self.successLabel.config(text=txt)\r\n        elif print_type == PrintType.failed:\r\n            self.failLabel.config(text=txt)\r\n\r\n    def start_download(self):\r\n        # 判断是否有网络\r\n        if Downloader.get_beijing_time() == 0:\r\n            self.output(\"获取数据异常，请检查您的网络！\")\r\n            return\r\n        if Downloader.is_expired():\r\n            self.output(\"授权证书已到期，请联系客服！\")\r\n            return\r\n        url = self.urlEntry.get()\r\n        path = self.dirEntry.get()\r\n        domain = get_domain(url)\r\n        if \"kwaicdn\" in domain or \"kuaishou\" in domain:\r\n            downloader: KuaiShou = KuaiShou()\r\n            # downloader.set_cookie()\r\n        else:\r\n            downloader: Downloader = DouYin()\r\n        downloader_t = threading.Thread(target=downloader.start, args=(url, path))\r\n        downloader_t.setDaemon(True)  # 设置守护进程，避免界面卡死\r\n        downloader_t.start()\r\n"
  },
  {
    "path": "001-Downloader/utils.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:工具类\n@Date       :2021/08/16\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport threading\nimport configparser\nimport os\nimport re\n\n\ndef get_domain(url: str = None):\n    \"\"\"\n    获取链接地址的域名\n    :param url:\n    :return:\n    \"\"\"\n    # http://youtube.com/watch\n    return re.match(r\"(http://|https://).*?\\/\", url, re.DOTALL).group(0)\n\n\nclass Config(object):\n    \"\"\"\n    配置文件的单例类\n    \"\"\"\n    _instance_lock = threading.Lock()\n\n    def __init__(self):\n        parent_dir = os.path.dirname(os.path.abspath(__file__))\n        conf_path = os.path.join(parent_dir, 'config.ini')\n        self.conf = configparser.ConfigParser()\n        self.conf.read(conf_path, encoding=\"utf-8\")\n\n    @classmethod\n    def instance(cls, *args, **kwargs):\n        with Config._instance_lock:\n            if not hasattr(Config, \"_instance\"):\n                Config._instance = Config(*args, **kwargs)\n        return Config._instance\n\n    def get_expired_time(self):\n        return self.conf.get(\"common\", \"expired_time\")\n\n    def get_version_name(self):\n        return self.conf.get(\"common\", \"version_name\")\n\n    def get_version_code(self):\n        return self.conf.get(\"common\", \"version_code\")\n"
  },
  {
    "path": "002-V2rayPool/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "002-V2rayPool/002-V2rayPool.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\" />\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "002-V2rayPool/README.md",
    "content": "# v2ray节点代理池\n学习python爬虫过程中，我们需要一些代理。而本项目则是通过收集网上公开的节点，来实现自己的代理请求的过程！\n\n## v2ray是什么？如何使用？\n- [v2ray官方指导](https://www.v2ray.com/index.html)\n  \n- [v2ray wiki](https://zh.wikipedia.org/wiki/V2Ray)\n\n## v2ray客户端推荐\n- [window：v2rayN](https://github.com/2dust/v2rayN/releases)\n- [macOS：V2rayU](https://github.com/yanue/V2rayU/releases)\n- [android：v2rayNG](https://github.com/2dust/v2rayNG/releases)\n- ios推荐Shadowrocket（俗称小火箭，注意不是shadowrocket VPN），但需要付费的，可以通过找\"美区apple id共享2021小火箭\"，找共享的账号下载软件，但是会有风险，请一定要注意。\n\n## v2ray服务端推荐\n有些应用场景会需要到用到专用的ip代理，如Tik Tok、亚马逊和Facebook等。这是，我们通过购买国外的服务器或者vps来搭建代理服务器，从而实现专有ip代理。\n\n推荐使用[x-ui](https://github.com/vaxilu/x-ui) 进行非常简单的\"一键式\"搭建开源框架。 \n\n#本项目主要知识点\n学习本项目需要先了解代理原理，以及v2ray实现的原理。\n\n## 实现思路\n![实现思路图](./doc/v2ray.jpg)\n\n## v2ray内核使用\n我们找的是v2ray节点，所以这些协议只能运行在v2ray特有程序中。因此，我们要找[v2ray内核](https://github.com/v2ray/v2ray-core/releases) 。 这里就以macOS系统举例说明：\n1. 下载[v2ray-core-v4.31.0](https://github.com/v2fly/v2ray-core/releases/download/v4.31.0/v2ray-macos-64.zip)\n2. 配置解压目录的路径：\n```python\nConfig.set_v2ray_core_path('xxx/v2ray-macos-64')\n```\n3. 查看是否能正常启动：\n```python\nclient.Creator().v2ray_start('xxx')\n#如果需要开启全局代理\nclient.Creator().v2ray_start('xxx',True)\n```\n\n## 2022-1-11检测可用测试节点(注意去掉后面\",\"开始的内容)：\n```shell\nss://YWVzLTI1Ni1nY206MWY2YWNhM2NlYmQyMWE0Y2Q1YTgwNzE4ZWQxNmI3NGNAMTIwLjIzMi4yMTQuMzY6NTAwMg#%F0%9F%87%B8%F0%9F%87%ACSingapore,8.25.96.100,美国 Level3\nss://YWVzLTI1Ni1nY206Y2RCSURWNDJEQ3duZklO@139.99.62.207:8119#github.com/freefq%20-%20%E6%96%B0%E5%8A%A0%E5%9D%A1OVH%201,139.99.62.207,新加坡 OVH\nss://YWVzLTI1Ni1nY206UmV4bkJnVTdFVjVBRHhH@167.88.61.60:7002#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%203,167.88.61.60,美国加利福尼亚圣克拉拉\nss://YWVzLTI1Ni1nY206WTZSOXBBdHZ4eHptR0M@38.143.66.71:3389#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%204,38.143.66.71,美国华盛顿西雅图 Cogent\ntrojan://e6c36d58-6070-4b55-a437-146e6b53ec57@t1.ssrsub.com:8443#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%20%2012,142.47.89.64,加拿大安大略\nss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@172.99.190.87:9101#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2013,172.99.190.87,美国乔治亚亚特兰大\nss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@46.29.218.6:8091#github.com/freefq%20-%20%E6%8C%AA%E5%A8%81%20%2019,46.29.218.6,挪威\n```\n\n注意：本程序虽可跨平台，但因博主能力有限，只在macos系统操作过，无法在更多系统上去尝试和改进，望谅解！\n\n-------\n\n如有可用节点增加，请推荐给博主吧：xhunmon@gmail.com\n"
  },
  {
    "path": "002-V2rayPool/base/net_proxy.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 网络代理请求的基类\n@Date       :2021/09/15\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport requests\nfrom my_fake_useragent import UserAgent\n\n\nclass Net(object):\n    \"\"\"\n    基类，封装常用接口\n    \"\"\"\n    TIMEOUT = 8\n\n    def __init__(self, timeout=8):\n        Net.TIMEOUT = timeout\n        self._ua = UserAgent()\n        self._agent = self._ua.random()  # 随机生成的agent\n        self.USER_AGENT = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0\"\n        self._headers = {\"user-agent\": self.USER_AGENT, 'Connection': 'close'}\n        # determine\n        self._proxy = '127.0.0.1:1080'\n        self._proxies_en = {\n            'http': 'socks5h://' + self._proxy,\n            'https': 'socks5h://' + self._proxy,\n        }\n        self._proxies_zh = {\n            'http': 'socks5://' + self._proxy,\n            'https': 'socks5://' + self._proxy,\n        }\n\n    def get_header(self, headers, key):\n        key_lower = key.lower()\n        headers_lower = {k.lower(): v for k, v in headers.items()}\n        if (key_lower in headers_lower):\n            return headers_lower[key_lower]\n        else:\n            return ''\n\n    def update_agent(self):\n        self.USER_AGENT = self._ua.random()  # 随机生成的agent\n        self._headers = {\"user-agent\": self.USER_AGENT, 'Connection': 'close'}\n\n    def get_urls(self) -> []:\n        \"\"\"需子类实现\"\"\"\n        pass\n\n    def request(self, url, allow_redirects=False, verify=False, timeout=TIMEOUT):\n        \"\"\"普通请求\"\"\"\n        return self.__request(url, allow_redirects=allow_redirects, verify=verify, timeout=timeout)\n\n    def request_en(self, url, allow_redirects=False, verify=False, timeout=TIMEOUT):\n        \"\"\"国外网站请求，需要开代理\"\"\"\n        return self.__request(url, allow_redirects=allow_redirects, verify=verify, proxies=self._proxies_en,\n                              timeout=timeout)\n\n    def request_zh(self, url, allow_redirects=False, verify=False, timeout=TIMEOUT):\n        \"\"\"国内网站请求，需要开代理\"\"\"\n        return self.__request(url, allow_redirects=allow_redirects, verify=verify, proxies=self._proxies_zh,\n                              timeout=timeout)\n\n    def __request(self, url, allow_redirects=False, verify=False, proxies=None, timeout=TIMEOUT):\n        \"\"\"最终的请求实现\"\"\"\n        requests.packages.urllib3.disable_warnings()\n        if proxies:\n            return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify,\n                                proxies=proxies, timeout=timeout)\n        else:\n            return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify,\n                                timeout=timeout)\n"
  },
  {
    "path": "002-V2rayPool/core/client.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport base64\nimport json\nimport os\nimport urllib\n\nfrom core.conf import Config\nfrom core.group import Vmess, Vless, Socks, SS, Mtproto, Trojan, Group, Dyport\nfrom core.utils import ProtocolType\nimport core.utils as util\n\n\nclass ClientWriter:\n    def __init__(self, group):\n        self.config_factory = Config()\n        # with open(self.config_factory.get_path('config_path'), 'r') as json_file:\n        #     self.config = json.load(json_file)\n\n        self.write_path = self.config_factory.get_path(\"config_path\")\n        self.template_path = self.config_factory.json_path\n        self.group = group\n        self.node = group.node\n\n    def load_template(self, template_name):\n        '''\n        load special template\n        '''\n        with open(self.template_path + \"/\" + template_name, 'r') as stream_file:\n            template = json.load(stream_file)\n        return template\n\n    def transform(self):\n        user_json = None\n        if type(self.node) == Vmess:\n            self.client_config = self.load_template('client.json')\n            user_json = self.client_config[\"outbounds\"][0][\"settings\"][\"vnext\"][0]\n            user_json[\"users\"][0][\"id\"] = self.node.password\n            user_json[\"users\"][0][\"alterId\"] = self.node.alter_id\n\n        elif type(self.node) == Vless:\n            self.client_config = self.load_template('client.json')\n            user_json = self.client_config[\"outbounds\"][0][\"settings\"][\"vnext\"][0]\n            user_json[\"users\"][0][\"id\"] = self.node.password\n            del user_json[\"users\"][0][\"alterId\"]\n            del user_json[\"users\"][0][\"security\"]\n            user_json[\"users\"][0][\"encryption\"] = self.node.encryption\n            if self.node.flow:\n                user_json[\"users\"][0][\"flow\"] = self.node.flow\n            self.client_config[\"outbounds\"][0][\"protocol\"] = \"vless\"\n\n        elif type(self.node) == Socks:\n            self.client_config = self.load_template('client_socks.json')\n            user_json = self.client_config[\"outbounds\"][0][\"settings\"][\"servers\"][0]\n            user_json[\"users\"][0][\"user\"] = self.node.user_info\n            user_json[\"users\"][0][\"pass\"] = self.node.password\n\n        elif type(self.node) == SS:\n            self.client_config = self.load_template('client_ss.json')\n            user_json = self.client_config[\"outbounds\"][0][\"settings\"][\"servers\"][0]\n            user_json[\"method\"] = self.node.method\n            user_json[\"password\"] = self.node.password\n\n        elif type(self.node) == Trojan:\n            self.client_config = self.load_template('client_trojan.json')\n            user_json = self.client_config[\"outbounds\"][0][\"settings\"][\"servers\"][0]\n            user_json[\"password\"] = self.node.password\n\n        elif type(self.node) == Mtproto:\n            print(\"\")\n            print(\"MTProto protocol only use Telegram, and can't generate client json!\")\n            print(\"\")\n            exit(-1)\n\n        try:\n            if isinstance(self.group.port, int):\n                user_json[\"port\"] = self.group.port\n            else:\n                user_json[\"port\"] = int(self.group.port)\n            user_json[\"address\"] = self.group.ip\n            self.client_config[\"inbounds\"][0][\"listen\"] = self.group.listen\n            self.client_config[\"inbounds\"][0][\"port\"] = self.group.i_port\n        except:\n            print('数据异常，启动失败')\n            return\n            # inbounds = self.client_config[\"inbounds\"]\n        # for inbound in inbounds:\n        #     inbound[\"listen\"] = self.group.listen\n\n        # if type(self.node) != SS:\n        #     self.client_config[\"outbounds\"][0][\"streamSettings\"] = self.config[\"inbounds\"][self.group.index][\n        #         \"streamSettings\"]\n\n        if self.group.tls == 'tls':\n            self.client_config[\"outbounds\"][0][\"streamSettings\"][\"tlsSettings\"] = {}\n        elif self.group.tls == 'xtls':\n            self.client_config[\"outbounds\"][0][\"streamSettings\"][\"xtlsSettings\"][\"serverName\"] = self.group.ip\n            del self.client_config[\"outbounds\"][0][\"streamSettings\"][\"xtlsSettings\"][\"certificates\"]\n            del self.client_config[\"outbounds\"][0][\"streamSettings\"][\"xtlsSettings\"][\"alpn\"]\n            del self.client_config[\"outbounds\"][0][\"mux\"]\n\n    def write(self):\n        '''\n        写客户端配置文件函数\n        '''\n        json_dump = json.dumps(self.client_config, indent=1)\n        with open(self.write_path, 'w') as write_json_file:\n            write_json_file.writelines(json_dump)\n        # print(\"{0}({1})\".format(\"save json success!\", self.write_path))\n\n\nclass Creator(object):\n    \"\"\"\n    生成代理json并启动\n    \"\"\"\n\n    def __init__(self):\n        self.__thread = None\n        self.__main_pid = os.getpid()\n\n    def parse_vmess(self, vmesslink):\n        \"\"\"返回：{'v': '2', 'ps': 'https://git.io/v9999 圣何塞sv2', 'add': 'sv2.free3333.xyz', 'port': '26707', 'id': '8f7a28a6-002a-11ec-b64a-00163cf00cd9', 'aid': '0', 'net': 'tcp', 'type': 'none', 'host': '', 'path': '', 'tls': '', 'sni': ''}\"\"\"\n        if vmesslink.startswith(ProtocolType.VMESS):\n            bs = vmesslink[len(ProtocolType.VMESS):]\n            # paddings\n            blen = len(bs)\n            if blen % 4 > 0:\n                bs += \"=\" * (4 - blen % 4)\n            vms = base64.b64decode(bs).decode()\n            return json.loads(vms)\n        else:\n            raise Exception(\"vmess link invalid\")\n\n    def parse_trojan(self, link):\n        link = urllib.parse.unquote(link)\n        trStr = link[link.find(\"//\") + 2:]\n        password = trStr[:trStr.find('@')]\n        trStr = trStr[trStr.find('@') + 1:]\n        sni = trStr[:trStr.find(':')]\n        trStr = trStr[trStr.find(':') + 1:]\n        port = trStr[:trStr.find('#')]\n        name = trStr[trStr.find('#') + 1:]\n        node = {\n            \"name\": name,\n            \"server\": sni,\n            \"port\": port,\n            \"type\": \"trojan\",\n            \"password\": password,\n            \"sni\": sni\n        }\n        return node\n\n    def parse_ss(self, sslink):\n        RETOBJ = {\n            \"v\": \"2\",\n            \"ps\": \"\",\n            \"add\": \"\",\n            \"port\": \"\",\n            \"id\": \"\",\n            \"aid\": \"\",\n            \"net\": \"shadowsocks\",\n            \"type\": \"\",\n            \"host\": \"\",\n            \"path\": \"\",\n            \"tls\": \"\"\n        }\n        if sslink.startswith(ProtocolType.SS):\n            info = sslink[len(ProtocolType.SS):]\n\n            if info.rfind(\"#\") > 0:\n                info, _ps = info.split(\"#\", 2)\n                RETOBJ[\"ps\"] = urllib.parse.unquote(_ps)\n\n            if info.find(\"@\") < 0:\n                # old style link\n                # paddings\n                blen = len(info)\n                if blen % 4 > 0:\n                    info += \"=\" * (4 - blen % 4)\n                info = base64.b64decode(info).decode()\n                atidx = info.rfind(\"@\")\n                method, password = info[:atidx].split(\":\", 2)\n                addr, port = info[atidx + 1:].split(\":\", 2)\n            else:\n                atidx = info.rfind(\"@\")\n                addr, port = info[atidx + 1:].split(\":\", 2)\n                info = info[:atidx]\n                blen = len(info)\n                if blen % 4 > 0:\n                    info += \"=\" * (4 - blen % 4)\n                info = base64.b64decode(info).decode()\n                method, password = info.split(\":\", 2)\n            RETOBJ[\"add\"] = addr\n            RETOBJ[\"port\"] = port\n            RETOBJ[\"aid\"] = method\n            RETOBJ[\"id\"] = password\n            return RETOBJ\n\n    def generateAndWrite(self, url: str):\n        # listen=\"127.0.0.1\",\n        group = Group(None, 1024, end_port=None, tls=\"none\", tfo=\"open\", dyp=Dyport(), index=0)\n        if url.startswith(ProtocolType.VMESS):\n            _json = self.parse_vmess(url.strip())\n            node = Vmess(uuid=_json['id'], alter_id=int(_json['aid']), network=_json['net'], user_number=1,\n                         path=_json['path'] if 'path' in _json else None,\n                         host=_json['host'], header=None, email=None,\n                         quic=None)\n            group.port = _json['port']\n            group.tls = _json['tls']\n            group.ip = _json['add']\n            group.protocol = node.__class__.__name__\n            group.node = node\n            print(_json)\n        elif url.startswith(ProtocolType.SS):\n            _json = self.parse_ss(url.strip())\n            # {'v': '2', 'ps': 'github.com/freefq - 罗马尼亚  10', 'add': '194.110.115.83', 'port': '43893', 'id': 'YyCBeDdYX4cadHpCkkmdJLq8', 'aid': 'aes-256-gcm', 'net': 'shadowsocks', 'type': '', 'host': '', 'path': '', 'tls': ''}\n            node = SS(0, _json['id'], _json['aid'], None)\n            group.port = _json['port']\n            group.tls = _json['tls']\n            group.ip = _json['add']\n            group.protocol = node.__class__.__name__\n            group.node = node\n            print(_json)\n        elif url.startswith(ProtocolType.TROJAN):\n            _json = self.parse_trojan(url.strip())\n            node = Trojan(0, _json['password'], None)\n            group.port = _json['port']\n            group.tls = ''\n            group.ip = _json['sni']\n            group.protocol = node.__class__.__name__\n            group.node = node\n            print(_json)\n        else:\n            print('无效地址：%s' % url)\n            return\n        cw = ClientWriter(group)\n        cw.transform()\n        cw.write()\n\n    def __kill_threading(self):\n        pids = os.popen(\"ps aux |grep v2ray |awk '{print $2}'\").read().split('\\n')\n        pid_all = []\n        for pid in pids:\n            temp = pid.strip()\n            if len(temp) > 1 and temp != 'PID' and temp != '0' and temp != str(self.__main_pid) and temp not in pid_all:\n                pid_all.append(temp)\n        for pid in pid_all:\n            try:\n                import subprocess\n                # subprocess.check_output(\"kill %d\" % int(pid))\n                a = os.popen(\"kill %d\" % int(pid)).read()\n            except Exception as e:\n                pass\n        util.sys_proxy_off()\n\n    def __child_thread(self, url: str, isSysOn=False):\n        self.generateAndWrite(url)\n        # 执行就可，不需要知道结果\n        if Config.get_v2ray_core_path() is None:\n            raise Exception('请先调用#Config.set_v2ray_core_path 设置路径')\n        v2ray_path = os.path.join(Config.get_v2ray_core_path(), 'v2ray')\n        config_path = os.path.join(Config.get_v2ray_core_path(), 'config.json')\n        os.popen(\"%s -config %s >/dev/null 2>&1\" % (v2ray_path, config_path))\n        print(\"%s -config %s >/dev/null 2>&1\" % (v2ray_path, config_path))\n        if isSysOn:\n            util.sys_v2ray_on()\n\n    def v2ray_start(self, url: str, isSysOn=False):\n        self.__kill_threading()\n        self.__child_thread(url, isSysOn)\n\n    def v2ray_start_with_log(self, url: str, isSysOn=False):\n        try:\n            self.v2ray_start(url, isSysOn)\n        except Exception as e:\n            print(e)\n            print(\"启动异常：%s\" % url)\n            return False\n        return True\n"
  },
  {
    "path": "002-V2rayPool/core/conf.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport configparser\n\nimport os\n\n\nclass Config:\n    __v2ray_core_path = None\n    __v2ray_node_path = None\n\n    def __init__(self):\n        self.config = configparser.ConfigParser()\n        parent_dir = os.path.dirname(os.path.abspath(__file__))\n        self.config_path = os.path.join(Config.__v2ray_core_path, 'config.json')\n        self.json_path = os.path.join(parent_dir, 'json_template')\n        # self.config.read(self.config_path)\n\n    def get_path(self, key):\n        # return self.config.get('path', key)\n        return self.config_path\n\n    def get_data(self, key):\n        return self.config.get('data', key)\n\n    def set_data(self, key, value):\n        self.config.set('data', key, value)\n        self.config.write(open(self.config_path, \"w\"))\n\n    @staticmethod\n    def set_v2ray_core_path(dir: str):\n        \"\"\"设置当前v2ray_core程序的目录\"\"\"\n        Config.__v2ray_core_path = dir\n\n    @staticmethod\n    def get_v2ray_core_path():\n        \"\"\"获取当前v2ray_core程序的目录\"\"\"\n        return Config.__v2ray_core_path\n\n    @staticmethod\n    def set_v2ray_node_path(dir: str):\n        \"\"\"设置当前v2ray保存节点的目录\"\"\"\n        Config.__v2ray_node_path = dir\n\n    @staticmethod\n    def get_v2ray_node_path():\n        \"\"\"获取当前v2ray保存节点的目录\"\"\"\n        return Config.__v2ray_node_path\n"
  },
  {
    "path": "002-V2rayPool/core/group.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport base64\nimport json\nfrom urllib.parse import quote\n\n__author__ = 'qincji'\n\n\nclass Dyport(object):\n    def __init__(self, status=False, aid=0):\n        self.status = status\n        self.aid = aid\n\n\nclass Quic(object):\n    def __init__(self, security=\"none\", key=\"\", header=\"none\"):\n        self.security = security\n        self.key = key\n        self.header = header\n\n\nclass User(object):\n    def __init__(self, user_number, password, user_info=None):\n        \"\"\"\n        user_info可能是email, 也可能user_name, 具体取决于group的protocol\n        password: id或者密码\n        \"\"\"\n        self.__password = password\n        self.user_info = user_info\n        self.user_number = user_number\n\n    @property\n    def password(self):\n        return self.__password\n\n\nclass SS(User):\n    def __init__(self, user_number, password, method, user_info):\n        super(SS, self).__init__(user_number, password, user_info)\n        self.method = method\n\n    def __str__(self):\n        if self.user_info:\n            return \"Email: {self.user_info}\\nMethod: {self.method}\\nPassword: {password}\\n\".format(self=self,\n                                                                                                   password=self.password)\n        else:\n            return \"Method: {self.method}\\nPassword: {password}\\n\".format(self=self, password=self.password)\n\n    def link(self, ip, port, tls):\n        ss_origin_url = \"{0}:{1}@{2}:{3}\".format(self.method, self.password, ip, port)\n        return \"ss://{}\".format(bytes.decode(base64.b64encode(bytes(ss_origin_url, 'utf-8'))))\n\n    def stream(self):\n        return \"shadowsocks\"\n\n\nclass Trojan(User):\n    def __init__(self, user_number, password, email):\n        super(Trojan, self).__init__(user_number, password, email)\n\n    def __str__(self):\n        if self.user_info:\n            return \"Email: {self.user_info}\\nPassword: {password}\\n\".format(self=self, password=self.password)\n        else:\n            return \"Password: {password}\\n\".format(password=self.password)\n\n    def link(self, ip, port, tls):\n        return \"trojan://{0}@{1}:{2}\".format(self.password, ip, port)\n\n    def stream(self):\n        return \"trojan\"\n\n\nclass Mtproto(User):\n    def __str__(self):\n        if self.user_info:\n            return \"Email: {}\\nSecret: {}\\n\".format(self.user_info, self.password)\n        else:\n            return \"Secret: {}\\n\".format(self.password)\n\n    def link(self, ip, port, tls):\n        return \"tg://proxy?server={0}&port={1}&secret={2}\".format(ip, port, self.password)\n\n    def stream(self):\n        return \"mtproto\"\n\n\nclass Socks(User):\n    def __str__(self):\n        return \"User: {0}\\nPass: {1}\\nUDP: true\\n\".format(self.user_info, self.password)\n\n    def link(self, ip, port, tls):\n        if tls == \"tls\":\n            return \"HTTPS Socks5 don't support telegram share link\"\n        else:\n            return \"tg://socks?server={0}&port={1}&user={2}&pass={3}\".format(ip, port, self.user_info, self.password)\n\n    def stream(self):\n        return \"socks\"\n\n\nclass Vless(User):\n    def __init__(self, uuid, user_number, encryption=None, email=None, network=None, path=None, host=None, header=None,\n                 flow=\"\", serviceName=\"\", mode=\"\"):\n        super(Vless, self).__init__(user_number, uuid, email)\n        self.encryption = encryption\n        self.path = path\n        self.host = host\n        self.header = header\n        self.network = network\n        self.flow = flow\n        self.serviceName = serviceName\n        self.mode = mode\n\n    def __str__(self):\n        email = \"\"\n        if self.user_info:\n            email = \"Email: {}\".format(self.user_info)\n        result = '''\n{email}\nID: {password}\nEncryption: {self.encryption}\nNetwork: {network}\n'''.format(self=self, password=self.password, email=email, network=self.stream()).strip() + \"\\n\"\n        return result\n\n    def stream(self):\n        if self.network == \"ws\":\n            return \"WebSocket host: {0}, path: {1}\".format(self.host, self.path)\n        elif self.network == \"tcp\":\n            return \"tcp\"\n        elif self.network == \"grpc\":\n            return \"grpc serviceName: {}, mode: {}\".format(self.serviceName, self.mode)\n        elif self.network == \"kcp\":\n            result = \"kcp\"\n            if self.header and self.header != 'none':\n                result = \"{} {}\".format(result, self.header)\n            if self.path != \"\":\n                result = \"{} seed: {}\".format(result, self.path)\n            return result\n\n    def link(self, ip, port, tls):\n        result_link = \"vless://{s.password}@{ip}:{port}?encryption={s.encryption}\".format(s=self, ip=ip, port=port)\n        if tls == \"tls\":\n            result_link += \"&security=tls\"\n        elif tls == \"xtls\":\n            result_link += \"&security=xtls&flow={}\".format(self.flow)\n        if self.network == \"ws\":\n            result_link += \"&type=ws&host={0}&path={1}\".format(self.host, quote(self.path))\n        elif self.network == \"tcp\":\n            result_link += \"&type=tcp\"\n        elif self.network == \"grpc\":\n            result_link += \"&type=grpc&serviceName={}&mode={}\".format(self.serviceName, self.mode)\n        elif self.network == \"kcp\":\n            result_link += \"&type=kcp&headerType={0}&seed={1}\".format(self.header, self.path)\n        return result_link\n\n\nclass Vmess(User):\n    def __init__(self, uuid, alter_id: int, network: str, user_number, *, path=None, host=None, header=None, email=None,\n                 quic=None):\n        super(Vmess, self).__init__(user_number, uuid, email)\n        self.alter_id = alter_id\n        self.network = network\n        self.path = path\n        self.host = host\n        self.header = header\n        self.quic = quic\n        if quic:\n            self.header = quic.header\n            self.host = quic.security\n            self.path = quic.key\n\n    def stream(self):\n        network = \"\"\n        if self.network == \"quic\":\n            network = \"Quic\\n{}\".format(self.quic)\n        elif self.network == \"h2\":\n            network = \"HTTP/2 path: {}\".format(self.path)\n        elif self.network == \"ws\":\n            network = \"WebSocket host: {0}, path: {1}\".format(self.host, self.path)\n        elif self.network == \"tcp\":\n            if self.host:\n                network = \"tcp host: {0}\".format(self.host)\n            else:\n                network = \"tcp\"\n        elif self.network == \"kcp\":\n            network = \"kcp\"\n            if self.header and self.header != 'none':\n                network = \"{} {}\".format(network, self.header)\n            if self.path != \"\":\n                network = \"{} seed: {}\".format(network, self.path)\n        return network\n\n    def __str__(self):\n        email = \"\"\n        if self.user_info:\n            email = \"Email: {}\".format(self.user_info)\n        result = '''\n{email}\nUUID: {uuid}\nAlter ID: {self.alter_id}\nNetwork: {network}\n'''.format(self=self, uuid=self.password, email=email, network=self.stream()).strip() + \"\\n\"\n        return result\n\n    def link(self, ip, port, tls):\n        json_dict = {\n            \"v\": \"2\",\n            \"ps\": \"\",\n            \"add\": ip,\n            \"port\": port,\n            \"aid\": self.alter_id,\n            \"type\": self.header,\n            \"net\": self.network,\n            \"path\": self.path,\n            \"host\": self.host,\n            \"id\": self.password,\n            \"tls\": tls\n        }\n        json_data = json.dumps(json_dict)\n        result_link = \"vmess://{}\".format(bytes.decode(base64.b64encode(bytes(json_data, 'utf-8'))))\n        return result_link\n\n\nclass Group(object):\n    def __init__(self, ip, port: str, *, end_port=None, tfo=None, tls=\"none\", i_port='1080', listen=\"0.0.0.0\",\n                 dyp=Dyport(),\n                 index=0,\n                 tag='A'):\n        self.ip = ip\n        self.port = port\n        self.end_port = end_port\n        self.tag = tag\n        self.node = None\n        self.tfo = tfo\n        self.tls = tls\n        self.dyp = dyp\n        self.protocol = None\n        self.index = index\n        self.listen = listen\n        self.i_port = i_port\n\n# port = 101\n# key = str(port) if isinstance(port, int) else port\n# print(key)\n# if isinstance(key, int):\n#     print('key is int')\n# if isinstance(key, str):\n#     print('key is str')\n"
  },
  {
    "path": "002-V2rayPool/core/json_template/client.json",
    "content": "{\n  \"log\": {\n    \"access\": \"\",\n    \"error\": \"\",\n    \"loglevel\": \"info\"\n  },\n  \"inbounds\": [\n    {\n      \"port\": 1080,\n      \"listen\": \"0.0.0.0\",\n      \"protocol\": \"socks\",\n      \"settings\": {\n        \"auth\": \"noauth\",\n        \"udp\": true,\n        \"ip\": \"127.0.0.1\",\n        \"clients\": null\n      },\n      \"streamSettings\": null\n    },\n    {\n      \"listen\": \"127.0.0.1\",\n      \"protocol\": \"http\",\n      \"settings\": {\n        \"timeout\": 360\n      },\n      \"port\": \"1087\"\n    }\n  ],\n  \"outbounds\": [\n    {\n      \"protocol\": \"vmess\",\n      \"settings\": {\n        \"vnext\": [\n          {\n            \"address\": \"123.ocm\",\n            \"port\": 1234,\n            \"users\": [\n              {\n                \"id\": \"cc4f8d5b-967b-4557-a4b6-bde92965bc27\",\n                \"alterId\": 0,\n                \"security\": \"aes-128-gcm\"\n              }\n            ]\n          }\n        ]\n      },\n      \"streamSettings\": {\n        \"security\": \"\",\n        \"tlsSettings\": {},\n        \"wsSettings\": {},\n        \"httpSettings\": {}, \n        \"network\": \"tcp\", \n        \"kcpSettings\": {}, \n        \"tcpSettings\": {},\n        \"quicSettings\": {}\n      },\n      \"mux\": {\n        \"enabled\": true\n      }\n    },\n    {\n      \"protocol\": \"freedom\",\n      \"settings\": {\n        \"response\": null\n      },\n      \"tag\": \"direct\"\n    }\n  ],\n  \"dns\": {\n    \"servers\": [\n      \"8.8.8.8\",\n      \"8.8.4.4\",\n      \"localhost\"\n    ]\n  },\n  \"routing\": {\n    \"domainStrategy\": \"IPIfNonMatch\",\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"ip\": [\"geoip:private\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geosite:cn\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geoip:cn\"],\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "002-V2rayPool/core/json_template/client_socks.json",
    "content": "{\n  \"log\": {\n    \"access\": \"\",\n    \"error\": \"\",\n    \"loglevel\": \"info\"\n  },\n  \"inbounds\": [\n    {\n      \"port\": 1080,\n      \"listen\": \"0.0.0.0\",\n      \"protocol\": \"socks\",\n      \"settings\": {\n        \"auth\": \"noauth\",\n        \"udp\": true,\n        \"ip\": \"127.0.0.1\",\n        \"clients\": null\n      },\n      \"streamSettings\": null\n    },\n    {\n      \"listen\": \"127.0.0.1\",\n      \"protocol\": \"http\",\n      \"settings\": {\n        \"timeout\": 360\n      },\n      \"port\": \"1087\"\n    }\n  ],\n    \"outbounds\": \n    [\n      {\n        \"protocol\": \"socks\",\n        \"settings\": {\n          \"servers\": [\n            {\n              \"address\": \"123.ocm\",\n              \"port\": 1234,\n              \"users\": [\n                {\n                    \"user\": \"hello\",\n                    \"pass\": \"3.1415\"\n                }\n              ]\n            }\n          ]\n        },\n        \"streamSettings\": {\n          \"security\": \"\",\n          \"tlsSettings\": {},\n          \"wsSettings\": {},\n          \"httpSettings\": {}, \n          \"network\": \"tcp\", \n          \"kcpSettings\": {}, \n          \"tcpSettings\": {},\n          \"quicSettings\": {}\n        },\n        \"mux\": {\n          \"enabled\": true\n        }\n      },\n      {\n        \"protocol\": \"freedom\",\n        \"settings\": {\n            \"response\": null\n        },\n        \"tag\": \"direct\"\n      }\n    ],\n  \"routing\": {\n    \"domainStrategy\": \"IPIfNonMatch\",\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"ip\": [\"geoip:private\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geosite:cn\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geoip:cn\"],\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "002-V2rayPool/core/json_template/client_ss.json",
    "content": "{\n  \"log\": {\n    \"access\": \"\",\n    \"error\": \"\",\n    \"loglevel\": \"info\"\n  },\n  \"inbounds\": [\n    {\n      \"port\": 1080,\n      \"listen\": \"0.0.0.0\",\n      \"protocol\": \"socks\",\n      \"settings\": {\n        \"auth\": \"noauth\",\n        \"udp\": true,\n        \"ip\": \"127.0.0.1\",\n        \"clients\": null\n      },\n      \"streamSettings\": null\n    },\n    {\n      \"listen\": \"127.0.0.1\",\n      \"protocol\": \"http\",\n      \"settings\": {\n        \"timeout\": 360\n      },\n      \"port\": \"1087\"\n    }\n  ],\n  \"outbounds\": [\n    {\n      \"protocol\": \"shadowsocks\",\n      \"settings\": {\n      \"servers\": [\n        {\n          \"address\": \"serveraddr.com\", \n          \"method\": \"aes-128-gcm\", \n          \"ota\": false, \n          \"password\": \"sspasswd\", \n          \"port\": 1024  \n        }\n      ]\n      }\n    },\n    {\n      \"protocol\": \"freedom\",\n      \"settings\": {\n        \"response\": null\n      },\n      \"tag\": \"direct\"\n    }\n  ],\n  \"routing\": {\n    \"domainStrategy\": \"IPIfNonMatch\",\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"ip\": [\"geoip:private\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geosite:cn\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geoip:cn\"],\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/client_trojan.json",
    "content": "{\n  \"log\": {\n    \"access\": \"\",\n    \"error\": \"\",\n    \"loglevel\": \"info\"\n  },\n  \"inbounds\": [\n    {\n      \"port\": 1080,\n      \"listen\": \"0.0.0.0\",\n      \"protocol\": \"socks\",\n      \"settings\": {\n        \"auth\": \"noauth\",\n        \"udp\": true,\n        \"ip\": \"127.0.0.1\",\n        \"clients\": null\n      },\n      \"streamSettings\": null\n    },\n    {\n      \"listen\": \"127.0.0.1\",\n      \"protocol\": \"http\",\n      \"settings\": {\n        \"timeout\": 360\n      },\n      \"port\": \"1087\"\n    }\n  ],\n  \"outbounds\": [\n    {\n      \"protocol\": \"trojan\",\n      \"settings\": {\n        \"servers\": [\n          {\n            \"address\": \"serveraddr.com\",\n            \"port\": 443,\n            \"password\": \"passwd\"\n          }\n        ]\n      },\n      \"streamSettings\": {\n        \"security\": \"tls\",\n        \"network\": \"tcp\"\n      }\n    },\n    {\n      \"protocol\": \"freedom\",\n      \"settings\": {\n        \"response\": null\n      },\n      \"tag\": \"direct\"\n    }\n  ],\n  \"routing\": {\n    \"domainStrategy\": \"IPIfNonMatch\",\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"ip\": [\"geoip:private\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geosite:cn\"],\n        \"outboundTag\": \"direct\"\n      },\n      {\n        \"type\": \"field\",\n        \"domain\": [\"geoip:cn\"],\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/dyn_port.json",
    "content": "{\n  \"protocol\": \"vmess\",\n  \"port\": \"10000-20000\",\n  \"tag\": \"dynamicPort\",       \n  \"settings\": {\n    \"default\": {\n      \"alterId\": 32\n    }\n  },\n  \"allocate\": {\n    \"strategy\": \"random\",\n    \"concurrency\": 3,\n    \"refresh\": 5\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/http.json",
    "content": "{\n  \"network\": \"tcp\",\n  \"security\": \"none\",\n  \"tlsSettings\": {},\n  \"httpSettings\": {},\n  \"tcpSettings\": {\n    \"header\": {\n      \"type\": \"http\",\n      \"request\": {\n        \"version\": \"1.1\",\n        \"method\": \"GET\",\n        \"path\": [\n          \"/\"\n        ],\n        \"headers\": {\n          \"Host\": [\n            \"\"\n          ],\n          \"User-Agent\": [\n            \"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36\",\n            \"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46\"\n          ],\n          \"Accept-Encoding\": [\n            \"gzip, deflate\"\n          ],\n          \"Connection\": [\n            \"keep-alive\"\n          ],\n          \"Pragma\": \"no-cache\"\n        }\n      },\n      \"response\": {\n        \"version\": \"1.1\",\n        \"status\": \"200\",\n        \"reason\": \"OK\",\n        \"headers\": {\n          \"Content-Type\": [\n            \"application/octet-stream\",\n            \"video/mpeg\"\n          ],\n          \"Transfer-Encoding\": [\n            \"chunked\"\n          ],\n          \"Connection\": [\n            \"keep-alive\"\n          ],\n          \"Pragma\": \"no-cache\"\n        }\n      }\n    }\n  },\n  \"kcpSettings\": {},\n  \"wsSettings\": {},\n  \"quicSettings\": {}\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/http2.json",
    "content": "{\n  \"network\": \"h2\",\n  \"security\": \"tls\",\n  \"tlsSettings\": {},\n  \"tcpSettings\": {},\n  \"httpSettings\": { \n    \"path\": \"/ray/\"\n  },\n  \"kcpSettings\": {},\n  \"wsSettings\": {},\n  \"quicSettings\": {}\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/kcp.json",
    "content": "{\n  \"network\": \"kcp\",\n  \"security\": \"none\",\n  \"tlsSettings\": {},\n  \"tcpSettings\": {},\n  \"httpSettings\": {},\n  \"kcpSettings\": {\n    \"mtu\": 1350,\n    \"tti\": 50,\n    \"uplinkCapacity\": 100,\n    \"downlinkCapacity\": 100,\n    \"congestion\": false,\n    \"readBufferSize\": 2,\n    \"writeBufferSize\": 2,\n    \"header\": {\n      \"type\": \"none\"\n    }\n  },\n  \"wsSettings\": {},\n  \"quicSettings\": {}\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/mtproto.json",
    "content": "{\n  \"mtproto-in\": {\n    \"tag\": \"tg-in\",\n    \"port\": 5829,\n    \"protocol\": \"mtproto\",\n    \"settings\": {\n      \"users\": [{\"secret\": \"b0cbcef5a486d9636472ac27f8e11a9d\"}]\n    }\n  },\n  \"mtproto-out\":  {\n    \"tag\": \"tg-out\",\n    \"protocol\": \"mtproto\",\n    \"settings\": {}\n  },\n  \"routing-bind\": {\n    \"type\": \"field\",\n    \"inboundTag\": [\"tg-in\"],\n    \"outboundTag\": \"tg-out\"\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/quic.json",
    "content": "{\n  \"network\": \"quic\",\n  \"security\": \"none\",\n  \"tlsSettings\": {},\n  \"tcpSettings\": {},\n  \"kcpSettings\": {},\n  \"httpSettings\": {},\n  \"wsSettings\": {},\n  \"quicSettings\": {\n    \"security\": \"none\",\n    \"key\": \"\",\n    \"header\": {\n      \"type\": \"none\"\n    }\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/server.json",
    "content": "{\n  \"log\": {\n    \"access\": \"/var/log/v2ray/access.log\",\n    \"error\": \"/var/log/v2ray/error.log\",\n    \"loglevel\": \"info\"\n  },\n  \"inbounds\": [\n  {\n    \"port\": 999999999,\n    \"protocol\": \"vmess\",\n    \"settings\": {\n      \"clients\": [\n        {\n          \"id\": \"cc4f8d5b-967b-4557-a4b6-bde92965bc27\",\n          \"alterId\": 0\n        }\n      ]\n    },\n    \"streamSettings\": {\n      \"security\": \"none\",\n      \"tlsSettings\": {},\n      \"wsSettings\": {},\n      \"httpSettings\": {},\n      \"network\": \"\",\n      \"kcpSettings\": {},\n      \"tcpSettings\": {},\n      \"quicSettings\": {}\n    }\n  }\n  ],\n  \"outbounds\": [\n  {\n    \"protocol\": \"freedom\",\n    \"settings\": {}\n  },\n  {\n    \"protocol\": \"blackhole\",\n    \"settings\": {},\n    \"tag\": \"blocked\"\n  }\n  ],\n  \"routing\": {\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"ip\": [\n          \"0.0.0.0/8\",\n          \"10.0.0.0/8\",\n          \"100.64.0.0/10\",\n          \"169.254.0.0/16\",\n          \"172.16.0.0/12\",\n          \"192.0.0.0/24\",\n          \"192.0.2.0/24\",\n          \"192.168.0.0/16\",\n          \"198.18.0.0/15\",\n          \"198.51.100.0/24\",\n          \"203.0.113.0/24\",\n          \"::1/128\",\n          \"fc00::/7\",\n          \"fe80::/10\"\n        ],\n        \"outboundTag\": \"blocked\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "002-V2rayPool/core/json_template/socks.json",
    "content": "{\n  \"auth\": \"password\",\n  \"accounts\": [\n    {\n      \"user\": \"hello\",\n      \"pass\": \"socks\"\n    }\n  ],\n  \"udp\": true\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/ss.json",
    "content": "{\n  \"port\": 1024,\n  \"protocol\": \"shadowsocks\",\n  \"settings\": {\n    \"method\": \"aes-128-gcm\",\n    \"password\": \"password\",\n    \"network\":\"tcp,udp\"\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/stats_settings.json",
    "content": "{\n  \"stats\": {},\n  \"api\": {\n    \"services\": [\n      \"StatsService\"\n    ],\n    \"tag\": \"api\"\n  },\n  \"policy\": {\n    \"levels\": {\n      \"0\": {\n        \"statsUserDownlink\": true,\n        \"statsUserUplink\": true\n      }\n    },\n    \"system\": {\n      \"statsInboundUplink\": true,\n      \"statsInboundDownlink\": true\n    }\n  },\n  \"routingRules\": {\n    \"inboundTag\": [\n      \"api\"\n    ],\n    \"outboundTag\": \"api\",\n    \"type\": \"field\"\n  },\n  \"dokodemoDoor\": {\n    \"listen\": \"127.0.0.1\",\n    \"port\": 10085,\n    \"protocol\": \"dokodemo-door\",\n    \"settings\": {\n      \"address\": \"127.0.0.1\"\n    },\n    \"tag\": \"api\"\n  }\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/tcp.json",
    "content": "{\n  \"network\": \"tcp\",\n  \"security\": \"none\",\n  \"tlsSettings\": {},\n  \"tcpSettings\": {},\n  \"kcpSettings\": {},\n  \"wsSettings\": {},\n  \"httpSettings\": {},\n  \"quicSettings\": {},\n  \"grpcSettings\": {}\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/vless.json",
    "content": "{\n  \"clients\": [\n    {\n      \"id\": \"d4e321ea-e118-11ea-a265-42010a8c0002\"\n    }\n  ],\n  \"decryption\": \"none\",\n  \"fallbacks\": [\n    {\n      \"dest\": 80\n    }\n  ]\n}"
  },
  {
    "path": "002-V2rayPool/core/json_template/ws.json",
    "content": "{\n  \"network\": \"ws\",\n  \"security\": \"none\",\n  \"tlsSettings\": {},\n  \"tcpSettings\": {},\n  \"kcpSettings\": {},\n  \"httpSettings\": {},\n  \"wsSettings\": {\n    \"path\": \"\",\n    \"headers\": {\n      \"Host\": \"\"\n    }\n  },\n  \"quicSettings\": {}\n}"
  },
  {
    "path": "002-V2rayPool/core/profile.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport json\nimport os\n\nfrom conf import Config\nfrom group import SS, Socks, Vmess, Vless, Mtproto, Quic, Group, Dyport, Trojan\n\n\nclass Stats:\n    def __init__(self, status=False, door_port=0):\n        self.status = status\n        self.door_port = door_port\n\n    def __str__(self):\n        return \"open\" if self.status else \"close\"\n\n\nclass Profile:\n    def __init__(self):\n        self.path = Config().get_path('config_path')\n        self.group_list = []\n        self.stats = None\n        self.ban_bt = False\n        self.user_number = 0\n        self.network = \"ipv4\"\n        self.modify_time = os.path.getmtime(self.path)\n        self.read_json()\n\n    def __str__(self):\n        result = \"\"\n        for group in self.group_list:\n            result = \"{}{}\".format(result, group)\n        result = result + \"Tip: The same group's node protocol, port, tls are the same.\"\n        return result\n\n    def read_json(self):\n\n        with open(self.path, 'r') as json_file:\n            self.config = json.load(json_file)\n\n        # 读取配置文件大框架\n        conf_inbounds = self.config[\"inbounds\"]\n        conf_rules = self.config[\"routing\"][\"rules\"]\n\n        stats = Stats()\n        if \"stats\" in self.config:\n            stats.status = True\n            for inbound in conf_inbounds:\n                if \"protocol\" in inbound and inbound[\"protocol\"] == \"dokodemo-door\":\n                    stats.door_port = inbound[\"port\"]\n                    break\n        self.stats = stats\n\n        for rule in conf_rules:\n            if \"protocol\" in rule and \"bittorrent\" in rule[\"protocol\"]:\n                self.ban_bt = True\n\n        # local_ip = get_ip()\n        local_ip = ''\n\n        if \":\" in local_ip:\n            self.network = \"ipv6\"\n\n        group_ascii = 64  # before 'A' ascii code\n        for index, json_part in enumerate(conf_inbounds):\n            group = self.parse_group(json_part, index, local_ip)\n            if group != None:\n                group_ascii = group_ascii + 1\n                if group_ascii > 90:\n                    group.tag = str(group_ascii)\n                else:\n                    group.tag = chr(group_ascii)\n                self.group_list.append(group)\n        del self.config\n\n    def parse_group(self, part_json, group_index, local_ip):\n        dyp, quic, end_port, tfo, header, tls, path, host, conf_ip, serviceName, mode = Dyport(), None, None, None, \"\", \"\", \"\", \"\", local_ip, \"\", \"gun\"\n\n        protocol = part_json[\"protocol\"]\n\n        if protocol == 'dokodemo-door' or (protocol == \"vmess\" and \"streamSettings\" not in part_json):\n            return\n\n        conf_settings = part_json[\"settings\"]\n\n        port_info = str(part_json[\"port\"]).split(\"-\", 2)\n\n        if \"domain\" in part_json and part_json[\"domain\"]:\n            conf_ip = part_json[\"domain\"]\n\n        if len(port_info) == 2:\n            port, end_port = port_info\n        else:\n            port = port_info[0]\n\n        if \"detour\" in conf_settings:\n            dynamic_port_tag = conf_settings[\"detour\"][\"to\"]\n            for inbound in self.config[\"inbounds\"]:\n                if \"tag\" in inbound and inbound[\"tag\"] == dynamic_port_tag:\n                    dyp.aid = inbound[\"settings\"][\"default\"][\"alterId\"]\n                    dyp.status = True\n                    break\n\n        if protocol in (\"vmess\", \"vless\", \"socks\", \"trojan\"):\n            conf_stream = part_json[\"streamSettings\"]\n            tls = conf_stream[\"security\"]\n\n            if \"sockopt\" in conf_stream and \"tcpFastOpen\" in conf_stream[\"sockopt\"]:\n                tfo = \"open\" if conf_stream[\"sockopt\"][\"tcpFastOpen\"] else \"close\"\n\n            if \"httpSettings\" in conf_stream and conf_stream[\"httpSettings\"]:\n                path = conf_stream[\"httpSettings\"][\"path\"]\n            elif \"wsSettings\" in conf_stream and conf_stream[\"wsSettings\"]:\n                host = conf_stream[\"wsSettings\"][\"headers\"][\"Host\"]\n                path = conf_stream[\"wsSettings\"][\"path\"]\n            elif \"tcpSettings\" in conf_stream and conf_stream[\"tcpSettings\"]:\n                host = conf_stream[\"tcpSettings\"][\"header\"][\"request\"][\"headers\"][\"Host\"]\n                header = \"http\"\n\n            if conf_stream[\"network\"] == \"kcp\" and \"header\" in conf_stream[\"kcpSettings\"]:\n                header = conf_stream[\"kcpSettings\"][\"header\"][\"type\"]\n                if \"seed\" in conf_stream[\"kcpSettings\"]:\n                    path = conf_stream[\"kcpSettings\"][\"seed\"]\n\n            if conf_stream[\"network\"] == \"quic\" and conf_stream[\"quicSettings\"]:\n                quic_settings = conf_stream[\"quicSettings\"]\n                quic = Quic(quic_settings[\"security\"], quic_settings[\"key\"], quic_settings[\"header\"][\"type\"])\n            if conf_stream[\"network\"] == \"grpc\" and conf_stream[\"grpcSettings\"]:\n                serviceName = conf_stream[\"grpcSettings\"][\"serviceName\"]\n                if \"multiMode\" in conf_stream[\"grpcSettings\"] and conf_stream[\"grpcSettings\"][\"multiMode\"]:\n                    mode = \"multi\"\n\n        group = Group(conf_ip, port, end_port=end_port, tls=tls, tfo=tfo, dyp=dyp, index=group_index)\n\n        if protocol == \"shadowsocks\":\n            self.user_number = self.user_number + 1\n            email = conf_settings[\"email\"] if 'email' in conf_settings else ''\n            ss = SS(self.user_number, conf_settings[\"password\"], conf_settings[\"method\"], email)\n            group.node = ss\n            group.protocol = ss.__class__.__name__\n            return group\n        elif protocol in (\"vmess\", \"vless\", \"trojan\"):\n            clients = conf_settings[\"clients\"]\n        elif protocol == \"socks\":\n            clients = conf_settings[\"accounts\"]\n        elif protocol == \"mtproto\":\n            clients = conf_settings[\"users\"]\n\n        for client in clients:\n            email, node, flow = \"\", None, \"\"\n            self.user_number = self.user_number + 1\n            if \"email\" in client and client[\"email\"]:\n                email = client[\"email\"]\n\n            if protocol == \"vmess\":\n                node = Vmess(client[\"id\"], client[\"alterId\"], conf_stream[\"network\"], self.user_number, path=path,\n                             host=host, header=header, email=email, quic=quic)\n\n            elif protocol == \"socks\":\n                node = Socks(self.user_number, client[\"pass\"], user_info=client[\"user\"])\n\n            elif protocol == \"mtproto\":\n                node = Mtproto(self.user_number, client[\"secret\"], user_info=email)\n\n            elif protocol == \"vless\":\n                if tls == \"xtls\":\n                    flow = client[\"flow\"]\n                node = Vless(client[\"id\"], self.user_number, conf_settings[\"decryption\"], email, conf_stream[\"network\"],\n                             path, host, header, flow, serviceName, mode)\n\n            elif protocol == \"trojan\":\n                node = Trojan(self.user_number, client[\"password\"], email)\n\n            if not group.protocol:\n                group.protocol = node.__class__.__name__\n\n            group.node = node\n        return group\n"
  },
  {
    "path": "002-V2rayPool/core/utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport random\nimport re\nimport os\nimport socket\nimport string\nimport sys\nimport termios\nimport tty\nimport urllib.request\nimport signal\nimport subprocess\nfrom enum import Enum, unique\n\n\nclass ProtocolType(object):\n    TROJAN = 'trojan://'\n    SS = 'ss://'\n    VMESS = 'vmess://'\n\n\n@unique\nclass StreamType(Enum):\n    TCP = 'tcp'\n    TCP_HOST = 'tcp_host'\n    SOCKS = 'socks'\n    SS = 'ss'\n    MTPROTO = 'mtproto'\n    H2 = 'h2'\n    WS = 'ws'\n    QUIC = 'quic'\n    KCP = 'kcp'\n    KCP_UTP = 'utp'\n    KCP_SRTP = 'srtp'\n    KCP_DTLS = 'dtls'\n    KCP_WECHAT = 'wechat'\n    KCP_WG = 'wireguard'\n    VLESS_KCP = 'vless_kcp'\n    VLESS_UTP = 'vless_utp'\n    VLESS_SRTP = 'vless_srtp'\n    VLESS_DTLS = 'vless_dtls'\n    VLESS_WECHAT = 'vless_wechat'\n    VLESS_WG = 'vless_wireguard'\n    VLESS_TCP = 'vless_tcp'\n    VLESS_TLS = 'vless_tls'\n    VLESS_WS = 'vless_ws'\n    VLESS_GRPC = 'vless_grpc'\n    VLESS_XTLS = 'vless_xtls'\n    TROJAN = 'trojan'\n\n\ndef header_type_list():\n    return (\"none\", \"srtp\", \"utp\", \"wechat-video\", \"dtls\", \"wireguard\")\n\n\ndef ss_method():\n    return (\"aes-256-gcm\", \"aes-128-gcm\", \"chacha20-poly1305\")\n\n\ndef xtls_flow():\n    return (\"\", \"xtls-rprx-origin\", \"xtls-rprx-direct\")\n\n\ndef get_ip():\n    \"\"\"\n    获取本地ip\n    \"\"\"\n    my_ip = \"\"\n    try:\n        my_ip = urllib.request.urlopen('http://api.ipify.org').read()\n    except Exception:\n        my_ip = urllib.request.urlopen('http://icanhazip.com').read()\n    return bytes.decode(my_ip).strip()\n\n\ndef port_is_use(port):\n    \"\"\"\n    判断端口是否占用\n    \"\"\"\n    tcp_use, udp_use = False, False\n    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.settimeout(3)\n    tcp_use = s.connect_ex(('127.0.0.1', int(port))) == 0\n    try:\n        u.bind(('127.0.0.1', int(port)))\n    except:\n        udp_use = True\n    finally:\n        u.close()\n    return tcp_use or udp_use\n\n\ndef random_port(start_port, end_port):\n    while True:\n        random_port = random.randint(start_port, end_port)\n        if not port_is_use(random_port):\n            return random_port\n\n\ndef is_email(email):\n    \"\"\"\n    判断是否是邮箱格式\n    \"\"\"\n    str = r'^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+){0,4}@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+){0,4}$'\n    return re.match(str, email)\n\n\ndef is_ipv4(ip):\n    try:\n        socket.inet_pton(socket.AF_INET, ip)\n    except AttributeError:  # no inet_pton here, sorry\n        try:\n            socket.inet_aton(ip)\n        except socket.error:\n            return False\n        return ip.count('.') == 3\n    except socket.error:  # not a valid ip\n        return False\n    return True\n\n\ndef is_ipv6(ip):\n    try:\n        socket.inet_pton(socket.AF_INET6, ip)\n    except socket.error:  # not a valid ip\n        return False\n    return True\n\n\ndef check_ip(ip):\n    return is_ipv4(ip) or is_ipv6(ip)\n\n\ndef bytes_2_human_readable(number_of_bytes, precision=1):\n    \"\"\"\n    流量bytes转换为kb, mb, gb等单位\n    \"\"\"\n    if number_of_bytes < 0:\n        raise ValueError(\"!!! number_of_bytes can't be smaller than 0 !!!\")\n\n    step_to_greater_unit = 1024.\n\n    number_of_bytes = float(number_of_bytes)\n    unit = 'bytes'\n\n    if (number_of_bytes / step_to_greater_unit) >= 1:\n        number_of_bytes /= step_to_greater_unit\n        unit = 'KB'\n\n    if (number_of_bytes / step_to_greater_unit) >= 1:\n        number_of_bytes /= step_to_greater_unit\n        unit = 'MB'\n\n    if (number_of_bytes / step_to_greater_unit) >= 1:\n        number_of_bytes /= step_to_greater_unit\n        unit = 'GB'\n\n    if (number_of_bytes / step_to_greater_unit) >= 1:\n        number_of_bytes /= step_to_greater_unit\n        unit = 'TB'\n\n    number_of_bytes = round(number_of_bytes, precision)\n\n    return str(number_of_bytes) + ' ' + unit\n\n\ndef random_email():\n    domain = ['163', 'qq', 'sina', '126', 'gmail', 'outlook', 'icloud']\n    core_email = \"@{}.com\".format(random.choice(domain))\n    return ''.join(random.sample(string.ascii_letters + string.digits, 8)) + core_email\n\n\ndef readchar(prompt=\"\"):\n    if prompt:\n        sys.stdout.write(prompt)\n        sys.stdout.flush()\n\n    fd = sys.stdin.fileno()\n    old_settings = termios.tcgetattr(fd)\n    try:\n        tty.setraw(sys.stdin.fileno())\n        ch = sys.stdin.read(1)\n    finally:\n        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)\n\n    print(ch)\n    return ch.strip()\n\n\ndef kill_all_v2ray():\n    pids = os.popen(\"ps aux |grep v2ray |awk '{print $2}'\").read().split('\\n')\n    for pid in pids:\n        try:\n            import subprocess\n            # subprocess.check_output(\"kill %d\" % int(pid))\n            a = os.popen(\"kill %d\" % int(pid)).read()\n        except Exception as e:\n            pass\n    sys_proxy_off()\n\n\n# netstat -nlp | grep :1080 | awk '{print $7}' | awk -F\\\" / \\\" '{ print $1 }'\ndef kill_process_by_port(port):\n    try:\n        pids = os.popen(\"pgrep -f v2ray|xargs kill -9\").read().split('\\n')\n        print(pids)\n    except:\n        pass\n\n\ndef sys_proxy_on(proxy, port):\n    ''''控制macOS系统代理'''\n    os.system('networksetup -setwebproxy wi-fi %s %d' % (proxy, port))  # http\n    os.system('networksetup -setsecurewebproxy wi-fi %s %d' % (proxy, port))  # https\n    os.system('networksetup -setsocksfirewallproxy wi-fi %s %d' % (proxy, port))  # socks\n\n\ndef sys_v2ray_on():\n    # proxy_on(\"127.0.0.1\", 1080)\n    '''端口要对应起v2ray开启的，具体要看写入config.json文件中inbounds节点部分'''\n    os.system('networksetup -setwebproxy wi-fi 127.0.0.1 1087')\n    os.system('networksetup -setsecurewebproxy wi-fi 127.0.0.1 1087')\n    os.system('networksetup -setsocksfirewallproxy wi-fi 127.0.0.1 1080')\n\n\ndef sys_proxy_off():\n    os.system('networksetup -setwebproxystate wi-fi off')\n    os.system('networksetup -setsecurewebproxystate wi-fi off')\n    os.system('networksetup -setsocksfirewallproxystate wi-fi off')\n"
  },
  {
    "path": "002-V2rayPool/db/db_main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 数据相关接口封装\n@Date       :2021/08/30\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport random\n\nfrom core import client\nfrom db.local import DbLocal, DbEnable\nfrom db.net import *\n\n\nclass DBManage(object):\n    def init(self):\n        self.dbLocal = DbLocal()\n        self.check = PYCheck()\n        self.dbEnable = DbEnable().get()\n\n    def __add_urls_de_dup(self, all_urls: [], new_urls: []) -> []:\n        \"\"\"合并数组，并且去重，去空\"\"\"\n        if not new_urls:  # 为 [] 或者 None\n            return all_urls\n        for url in new_urls:\n            temp = url.strip().replace('\\n', '')\n            if temp in all_urls or len(temp) < 20:\n                continue\n            all_urls.append(temp)\n\n    def start_random_v2ray_by_local(self, isSysOn=False):\n        \"\"\"从本地随机启动一个可用的proxy\"\"\"\n        urls = self.load_enable_urls_by_local()\n        for url in urls:\n            if client.Creator().v2ray_start_with_log(random.choice(urls), isSysOn) is False:\n                time.sleep(1)\n                continue\n            time.sleep(2)\n            ips = PYCheck().get_curren_ip()\n            if not ips:\n                print('无效地址：%s' % url)\n                continue\n            print('代理开启成功')\n            time.sleep(1)\n            return True\n        return False\n\n    def load_urls_and_save_auto(self):\n        \"\"\"首先通过不需要代理的网页获取节点，当代理有可用时，开启代理，获取需要代理获取的网页\"\"\"\n        self.dbLocal.clear_local()\n        all_urls = self.load_urls_by_not_proxy()\n        proxy_url = None\n        for url in all_urls:\n            if client.Creator().v2ray_start_with_log(url) is False:\n                time.sleep(1)\n                continue\n            time.sleep(2)\n            ips = PYCheck().get_curren_ip()\n            if not ips:\n                print('无效地址：%s' % url)\n                continue\n            proxy_url = url\n            break\n        if proxy_url is None:\n            raise Exception(\"无代理可用，退出！\")\n        print(\"获得可用代理地址：%s\" % proxy_url)\n        proxy_urls = self.load_urls_by_net_with_proxy(proxy_url=proxy_url)\n        all_urls = all_urls + proxy_urls\n        self.check_and_save(all_urls, append=False)\n\n    def load_urls_by_not_proxy(self, save_local=True):\n        all_urls = []\n        # 1. 先把不需要代理的先请求下来\n        self.__add_urls_de_dup(all_urls, PNTWGithubV2ray().get_urls())\n        print(\"获取https://hub.xn--gzu630h.xn--kpry57d/freefq/free后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNSsfree().get_urls())\n        print(\"获取https://view.ssfree.ru/后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNFreevpnX().get_urls())\n        print(\"获取https://freevpn-x.com/后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNGithubIwxf().get_urls())\n        print(\"获取https://github.com/iwxf/free-v2ray/blob/master/README.md 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNFreeV2ray().get_urls())\n        print(\"获取https://view.freev2ray.org/ 后数目：%d\" % len(all_urls))\n        if save_local:  # 保存到本地\n            self.dbLocal.save_urls(all_urls)\n        return all_urls\n\n    def load_urls_by_net_with_proxy(self, proxy_url=None, save_local=True):\n        all_urls = []\n        if save_local:  # 保存到本地\n            self.dbLocal.save_urls(all_urls)\n        if not proxy_url:\n            proxy_url = 'ss://YWVzLTI1Ni1nY206NGVqSjhuNWRkTHVZRFVIR1hKcmUydWZK@212.102.40.68:48938#github.com/freefq%20-%20%E6%84%8F%E5%A4%A7%E5%88%A9%20%201'\n        creator = client.Creator()\n        creator.v2ray_start(proxy_url)\n        time.sleep(2)\n        self.__add_urls_de_dup(all_urls, PYIvmess().get_urls())\n        print(\"获取https://t.me/s/ivmess 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PYFlyingboat().get_urls())\n        print(\"获取https://t.me/s/flyingboat 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PYFreevpnnet().get_urls())\n        print(\"获取https://www.freevpnnet.com/ 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PYMerlinblog().get_urls())\n        print(\"获取https://merlinblog.xyz/wiki/freess.html 后数目：%d\" % len(all_urls))\n        # __add_urls_de_dup(all_urls, PYFreeFq().get_urls())\n        self.__add_urls_de_dup(all_urls, PYFreeFq().download_urls(f_day=1))  # 前2天的地址\n        print(\"获取从https://freefq.com/ 后数目：%d\" % len(all_urls))\n        if save_local:  # 保存到本地\n            self.dbLocal.save_urls(all_urls)\n        return all_urls\n\n    def load_urls_by_net(self, proxy_url=None, save_local=True, need_proxy=True):\n        \"\"\"\n        通过网络获取最新的节点，但是需要代理\n        :param proxy_url: 代理url，默认的如果失效了则回去失败\n        :param save_local: 是否保存到本地\n        :param need_proxy: 如果程序本身就在外网跑，就不需要开启代理获取了\n        :return:\n        \"\"\"\n        all_urls = []\n        # 1. 先把不需要代理的先请求下来\n        self.__add_urls_de_dup(all_urls, PNTWGithubV2ray().get_urls())\n        print(\"获取https://hub.xn--gzu630h.xn--kpry57d/freefq/free后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNSsfree().get_urls())\n        print(\"获取https://view.ssfree.ru/后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNFreevpnX().get_urls())\n        print(\"获取https://freevpn-x.com/后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNGithubIwxf().get_urls())\n        print(\"获取https://github.com/iwxf/free-v2ray/blob/master/README.md 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PNFreeV2ray().get_urls())\n        print(\"获取https://view.freev2ray.org/ 后数目：%d\" % len(all_urls))\n        print(\"准备开启代理获取...\")\n        if need_proxy:\n            if not proxy_url:\n                proxy_url = 'ss://YWVzLTI1Ni1nY206NGVqSjhuNWRkTHVZRFVIR1hKcmUydWZK@212.102.40.68:48938#github.com/freefq%20-%20%E6%84%8F%E5%A4%A7%E5%88%A9%20%201'\n            # 需要代理\n            creator = client.Creator()\n            creator.v2ray_start(proxy_url)\n        time.sleep(2)\n        self.__add_urls_de_dup(all_urls, PYIvmess().get_urls())\n        print(\"获取https://t.me/s/ivmess 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PYFlyingboat().get_urls())\n        print(\"获取https://t.me/s/flyingboat 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PYFreevpnnet().get_urls())\n        print(\"获取https://www.freevpnnet.com/ 后数目：%d\" % len(all_urls))\n        self.__add_urls_de_dup(all_urls, PYMerlinblog().get_urls())\n        print(\"获取https://merlinblog.xyz/wiki/freess.html 后数目：%d\" % len(all_urls))\n        # __add_urls_de_dup(all_urls, PYFreeFq().get_urls())\n        self.__add_urls_de_dup(all_urls, PYFreeFq().download_urls(f_day=1))  # 前2天的地址\n        print(\"获取从https://freefq.com/ 后数目：%d\" % len(all_urls))\n        if save_local:  # 保存到本地\n            self.dbLocal.save_urls(all_urls, append=False)\n        return all_urls\n\n    def load_unchecked_urls_by_local(self):\n        \"\"\"获取本地未校验过的url\"\"\"\n        self.dbLocal.get_urls(False)\n        urls = self.dbLocal.get_checked_urls()\n        return urls\n\n    def load_enable_urls_by_local(self):\n        \"\"\"获取已检测过的url\"\"\"\n        return self.dbEnable.get_urls()\n\n    def check_url_single(self, url: str):\n        client.Creator().v2ray_start(url)\n        time.sleep(2)\n        ips = PYCheck().get_curren_ip()\n        if not ips:\n            print('地址无效！')\n            return False\n        print('检查地址结果：%s' % url)\n        print(ips)\n        return True\n\n    def check_and_save(self, urls: [], append=True):\n        \"\"\"检测url是否可用，并且保存到本地\"\"\"\n        if not append:\n            self.dbEnable.clear_local()\n        all_infos = self.dbEnable.get_infos()\n        new_infos = []\n        size = len(urls)\n        for i in range(size):\n            try:\n                if i % 30 == 0:  # 每三十个更新一次\n                    self.dbLocal.save_urls(urls=urls, append=False)  # 更新剩下的\n                url = urls.pop()\n                in_all = False\n                for item in all_infos:\n                    if url in item:\n                        in_all = True\n                        break\n                if in_all:\n                    print('取出地址已存在：%s' % url)\n                    continue\n                if client.Creator().v2ray_start_with_log(url) is False:\n                    time.sleep(1)\n                    continue\n                time.sleep(2)\n                ips = PYCheck().get_curren_ip()\n                if not ips:\n                    print('地址无效！')\n                    continue\n                ip, add = str(ips[0]), ips[1]\n                hase_item = False  # 已存在\n                for item in all_infos:\n                    if ip in item:\n                        hase_item = True\n                        break\n                if hase_item:\n                    print('ip=%s已存在！' % ip)\n                    continue\n                info = r'%s,%s,%s' % (url.strip(), ip.strip(), add.strip().replace('\\n', ''))\n                print('%s！总共：%d |待检测：%d |可用：%d' % ('地址有效', size, len(urls), len(all_infos)))\n                new_infos.append(info)\n                all_infos.append(info)\n                self.dbEnable.save_urls(new_infos)  # 写入已通过的\n                new_infos.clear()\n            except Exception as e:\n                print(e)\n        # 最后\n        print('%s！总共：%d |待检测：%d |可用：%d' % ('全部检测完毕！', size, len(urls), len(all_infos)))\n        self.dbEnable.save_urls(new_infos)\n        self.dbLocal.clear_local()\n"
  },
  {
    "path": "002-V2rayPool/db/local.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 数据库相关类\n@Date       :2021/08/25\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\nimport shutil\nfrom concurrent.futures import ThreadPoolExecutor\nfrom threading import Lock\n\nfrom core.conf import Config\n\n\nclass DbLocal(object):\n    \"\"\"\n    加载本地文件的url，并进行检测是否合法\n    \"\"\"\n\n    def __init__(self):\n        path = Config.get_v2ray_node_path()\n        parent_dir = os.path.dirname(os.path.abspath(__file__))\n        if path:\n            parent_dir = path\n        if not os.path.exists(parent_dir):\n            os.mkdir(parent_dir)\n\n        self.__save_path = os.path.join(parent_dir, '_db-uncheck.txt')\n        self.__get_path = os.path.join(parent_dir, '_db-uncheck.txt')\n        if not os.path.isfile(self.__save_path):\n            open(self.__save_path, mode='w', encoding=\"utf-8\").write('')\n        if not os.path.isfile(self.__get_path):\n            open(self.__get_path, mode='w', encoding=\"utf-8\").write('')\n        self.__reset()\n\n    def __reset(self):\n        \"\"\"恢复默认状态\"\"\"\n        self.__checked_urls = []\n        self.__load_urls = []\n        self.__checked_finish = False\n        self.__checked_count = 0\n\n    def __check_url(self, url: str):\n        \"\"\"在异步中检测url是否合法等\"\"\"\n        self.__checked_count += 1\n        # print('check_url: %s' % url)\n        # 如果检测通过\n        temp = url.strip()\n        if len(temp) > 10 and (temp.startswith('ss://') or temp.startswith('vmess://')):\n            self.__checked_urls.append(temp)\n        if len(self.__load_urls) == self.__checked_count:\n            self.__checked_finish = True\n            print('已经完成检测')\n\n    def is_checked_finished(self) -> bool:\n        \"\"\"是否已经完成检测\"\"\"\n        return self.__checked_finish\n\n    def get_checked_urls(self) -> []:\n        \"\"\"已通过检测的url\"\"\"\n        return self.__checked_urls\n\n    def get_urls(self, is_check=True) -> bool:\n        \"\"\"\n        开始加载本地的链接，异步处理结果。\n        :param get_path: 本地路径\n        :return: True：加载成功\n        \"\"\"\n        self.__reset()\n        get_path = self.__get_path\n        if not os.path.isfile(get_path):\n            return False\n        try:\n            with open(get_path, mode='r') as f:\n                for url in f:\n                    if url and url not in self.__load_urls:\n                        self.__load_urls.append(url)\n        except Exception as e:\n            print(e)\n        finally:\n            f.close()\n        size = len(self.__load_urls)\n        if size == 0:\n            print('本地无数据')\n            return False\n        if is_check:\n            # 创建线程池，传入max_workers参数来设置线程池中最多能同时运行的线程数目\n            executor = ThreadPoolExecutor(max_workers=3)\n            for url in self.__load_urls:\n                executor.submit(self.__check_url, url).done()\n        else:\n            self.__checked_urls = self.__load_urls\n            self.__checked_finish = True\n        print(\"已加载数目：%d\" % size)\n        return True\n\n    def clear_local(self):\n        \"\"\"通过写入空字符实现清除内容\"\"\"\n        try:\n            with open(self.__save_path, mode='w', encoding=\"utf-8\") as f:\n                f.write('')\n                f.close()\n        except Exception as e:\n            print(e)\n\n    def save_urls(self, urls, append=True):\n        \"\"\"\n        保存节点到本地\n        :param urls:\n        :param append: 添加到末尾\n        :return:\n        \"\"\"\n        if not urls:\n            return\n        all_url = []\n        if not append:  # 清空之后再继续\n            self.clear_local()\n            all_url = urls\n        else:  # 把本地的取出来，然后再进行去重\n            for url in urls:\n                if url not in all_url:\n                    all_url.append(url)\n            try:\n                with open(self.__save_path, mode='r') as f:\n                    for url in f:\n                        if url and url not in all_url:\n                            all_url.append(url)\n            except Exception as e:\n                print(e)\n            finally:\n                f.close()\n        size = len(all_url)\n        print('目前本地总共%d条' % size)\n        try:\n            self.clear_local()\n            with open(self.__save_path, mode='a', encoding=\"utf-8\") as f:\n                for url in all_url:\n                    url = url.strip().replace('\\n', '')\n                    if len(url) > 20:\n                        f.write(url + '\\n')\n                f.close()\n        except Exception as e:\n            print(e)\n\n\nclass DbEnable(object):\n    \"\"\"\n    单例模式，提供可用的v2ray对象\n    \"\"\"\n    _instance_lock = Lock()\n\n    def __init__(self):\n        self.__urls = []\n        self.__index = 0\n        self.__END = '.back'\n        self.__mutex = Lock()\n        self.__default_url = 'ss://YWVzLTI1Ni1nY206WWd1c0gyTVdBOFBXYzNwMlZEc1I3QVZ2@81.19.223.189:31764#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%208'\n        self.__config_path = ''\n        path = Config.get_v2ray_node_path()\n        # 获取本地文件实例？\n        parent_dir = os.path.dirname(os.path.abspath(__file__))\n        if path:\n            parent_dir = path\n        if not os.path.exists(parent_dir):\n            os.mkdir(parent_dir)\n        self.__path = os.path.join(parent_dir, '_db-checked.txt')\n        if not os.path.isfile(self.__path):\n            open(self.__path, mode='w', encoding=\"utf-8\").write('')\n\n    @classmethod\n    def get(cls, *args, **kwargs):\n        with DbEnable._instance_lock:\n            if not hasattr(DbEnable, \"_instance\"):\n                DbEnable._instance = DbEnable(*args, **kwargs)\n        return DbEnable._instance\n\n    def init(self, config_path, def_url=None):\n        \"\"\"\n        初始化所需要参数\n        :param config_path: 配置文件路径\n        :param def_url: 默认使用的 v2ray链接\n        :return:\n        \"\"\"\n        if def_url is not None:\n            self.__default_url = def_url\n        if len(config_path) < len('(参考用)config.json'):\n            return False\n        if not os.path.isfile(config_path):\n            return False\n        self.__config_path = config_path\n        return self.__back_config_json()\n\n    def __back_config_json(self):\n        \"\"\"备份配置文件\"\"\"\n        if not os.path.isfile(self.__config_path):\n            return False\n        back_path = self.__config_path + self.__END\n        try:\n            shutil.copy(self.__config_path, back_path)\n        except Exception as e:\n            print(e)\n            return False\n        return True\n\n    def __restore_config_json(self):\n        \"\"\"恢复配置文件\"\"\"\n        back_path = self.__config_path + self.__END\n        if not os.path.isfile(back_path):\n            return False\n        try:\n            if os.path.isfile(self.__config_path):\n                os.remove(self.__config_path)\n            shutil.copy(back_path, self.__config_path)\n        except Exception as e:\n            print(e)\n            return False\n        return True\n\n    def add_url(self, url: str):\n        self.__add(url)\n\n    def add_urls(self, url: []):\n        self.__adds(url)\n\n    def select_by_order(self):\n        \"\"\"选择下一个代理，并且更新配置文件，让他起作用\"\"\"\n        url = self.get_by_order()\n        return True\n\n    def get_by_order(self):\n        \"\"\"从队列中顺序获取一个v2ray协议实例\"\"\"\n        def_url = self.__default_url\n        if self.__check_and_enable():\n            def_url = self.__get(self.__index)\n        return def_url\n\n    def __check_and_enable(self):\n        \"\"\"检查是否已到结束，如果是，下一个从0开始\"\"\"\n        if len(self.__urls) == 0:\n            return False\n        if len(self.__urls) <= self.__index + 1:\n            self.__index = -1\n        self.__index += 1\n        return True\n\n    def __add(self, url):\n        self.__mutex.acquire()\n        self.__urls.append(url)\n        self.__mutex.release()\n\n    def __adds(self, urls: []):\n        self.__mutex.acquire()\n        self.__urls.extend(urls)\n        self.__mutex.release()\n\n    def __get(self, index):\n        self.__mutex.acquire()\n        url = self.__urls[index]\n        self.__mutex.release()\n        return url\n\n    def clear_local(self):\n        \"\"\"通过写入空字符实现清除内容\"\"\"\n        try:\n            with open(self.__path, mode='w', encoding=\"utf-8\") as f:\n                f.write('')\n                f.close()\n        except Exception as e:\n            print(e)\n\n    def get_urls(self) -> []:\n        \"\"\"获取截取后的url\"\"\"\n        urls = []\n        try:\n            with open(self.__path, mode='r') as f:\n                for url in f:\n                    url = url.strip().replace('\\n', '')\n                    if len(url) > 20:\n                        urls.append(url.split(',')[0])\n        except Exception as e:\n            print(e)\n        return urls\n\n    def get_infos(self) -> []:\n        \"\"\"获取所有信息，包括ip和地址\"\"\"\n        infos = []\n        try:\n            with open(self.__path, mode='r') as f:\n                for info in f:\n                    info = info.strip().replace('\\n', '')\n                    if len(info) > 20:\n                        infos.append(info)\n        except Exception as e:\n            print(e)\n        return infos\n\n    def de_duplication(self):\n        \"\"\"去重\"\"\"\n        infos = self.get_infos()\n        new_infos = []\n        for info in infos:\n            if len(info.strip()) <= 0:  # 去空行\n                continue\n            _in = False\n            for n in new_infos:\n                if info.split(',')[1] in n:\n                    _in = True\n                    break\n            if not _in:\n                new_infos.append(info)\n        print('去重前数目：%d，去重后的数目：%d' % (len(infos), len(new_infos)))\n        try:\n            self.clear_local()\n            with open(self.__path, mode='a', encoding=\"utf-8\") as f:\n                for url in new_infos:\n                    url = url.strip().replace('\\n', '')\n                    if len(url) > 20:\n                        f.write(url + '\\n')\n                f.close()\n        except Exception as e:\n            print(e)\n\n    def save_urls(self, urls, append=True):\n        \"\"\"\n        保存节点到本地\n        :param urls:\n        :param append: 添加到末尾\n        :return:\n        \"\"\"\n        if not urls:\n            return\n        all_url = []\n        if not append:  # 清空之后再继续\n            self.clear_local()\n            all_url = urls\n        else:  # 把本地的取出来，然后再进行去重\n            for url in urls:\n                if url not in all_url:\n                    all_url.append(url)\n            try:\n                with open(self.__path, mode='r') as f:\n                    for url in f:\n                        if url and url not in all_url:\n                            all_url.append(url)\n            except Exception as e:\n                print(e)\n            finally:\n                f.close()\n        size = len(all_url)\n        print('目前本地总共已检测可用%d条' % size)\n        try:\n            self.clear_local()\n            with open(self.__path, mode='a', encoding=\"utf-8\") as f:\n                for url in all_url:\n                    url = url.strip().replace('\\n', '')\n                    if len(url) > 20:\n                        f.write(url + '\\n')\n                f.close()\n        except Exception as e:\n            print(e)\n"
  },
  {
    "path": "002-V2rayPool/db/net.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 从网络获取\n@Date       :2021/08/30\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport json\n\nfrom base.net_proxy import Net\n\nimport re\nimport time\nimport chardet\n\n\ndef re_vmess_ss_trojan(pattern, html) -> []:\n    \"\"\"传入规则：r'xxx%sxxx', %s为固定的：(vmess://.+?|ss://.+?|trojan://.+?)\"\"\"\n    results = re.findall(pattern % '(vmess://.+?|ss://.+?|trojan://.+?)', html, re.DOTALL)\n    urls = []\n    for i in results:\n        temp: str = i.strip()\n        if len(temp) < 20 and temp in results:  # 凭感觉\n            continue\n        # 如果是换行的\n        if '\\n' in temp:\n            items = temp.split('\\n')\n            for j in items:\n                temp_j: str = j.strip()\n                if len(temp_j) < 20 and temp_j in results:  # 凭感觉\n                    continue\n                value_j = temp_j.replace('\\n', '')\n                if len(value_j) > 10:\n                    urls.append(value_j)\n            continue\n        value = temp.replace('\\n', '')\n        if len(value) > 10:\n            urls.append(value)\n    return urls\n\n\nclass PYCheck(Net):\n    def get_curren_ip(self, url='https://ip.cn/api/index?ip=&type=0'):\n        \"\"\"获取内容\"\"\"\n        try:\n            r = self.request_zh(url)\n            if r.status_code == 200:\n                charset = chardet.detect(r.content)\n                content = r.content.decode(charset['encoding'])\n                r.encoding = r.apparent_encoding\n                # {\"rs\":1,\"code\":0,\"address\":\"德国  Hessen  \",\"ip\":\"51.38.122.98\",\"isDomain\":0}\n                results = json.loads(content)\n                if not results:\n                    return None\n                return [results['ip'], results['address']]\n            elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n                location = r.headers['Location']\n                time.sleep(1)\n                return self.get_curren_ip(location)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PNFreeV2ray(Net):\n    \"\"\"\n    https://view.freev2ray.org/\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request(r'https://view.freev2ray.org/')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            return re_vmess_ss_trojan(r'\"%s\"', r.text)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PNTWGithubV2ray(Net):\n    \"\"\"\n    # https://hub.xn--gzu630h.xn--kpry57d/freefq/free\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request(r'https://hub.xn--gzu630h.xn--kpry57d/freefq/free')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            return re_vmess_ss_trojan(r'\"%s\"', r.text)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PNSsfree(Net):\n    \"\"\"\n    https://view.ssfree.ru/\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request(r'https://view.ssfree.ru/')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            return re_vmess_ss_trojan(r'\"%s\"', html)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PNFreevpnX(Net):\n    \"\"\"\n    https://freevpn-x.com/\n    \"\"\"\n\n    def get_urls(self) -> []:\n        \"\"\"\n        获取当前页面中文本的url\n        :param date: 如：2021/08/29\n        :return:\n        \"\"\"\n        url2 = r'https://url.cr/api/user.ashx?do=freevpn&ip=127.0.0.1&uuid=C5E0C9BA-FECB-44ED-9BD8-90C55365E11B&_=%d' % (\n                time.time() * 1000)\n        try:\n            r = self.request(url2)\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            results = html.split('\\n')\n            urls = []\n            for i in results:\n                temp = i.strip()\n                if len(temp) > 10:\n                    urls.append(temp)\n            return urls\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PNGithubIwxf(Net):\n    \"\"\"\n    https://github.com/iwxf/free-v2ray/blob/master/README.md\n    \"\"\"\n\n    def get_urls(self) -> []:\n        \"\"\"\n        获取当前页面中文本的url\n        :param date: 如：2021/08/29\n        :return:\n        \"\"\"\n        try:\n            r = self.request(r'https://github.com/iwxf/free-v2ray/blob/master/README.md')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            return re_vmess_ss_trojan(r'<pre><code>%s</code></pre>', html)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PYFreevpnnet(Net):\n    \"\"\"\n    https://www.freevpnnet.com/\n    需要代理\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request_en(r'https://www.freevpnnet.com/')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            return re_vmess_ss_trojan(r'>%s<', html)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PYMerlinblog(Net):\n    \"\"\"\n    https://merlinblog.xyz/wiki/freess.html\n    需要代理\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request_en(r'https://merlinblog.xyz/wiki/freess.html')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            return re_vmess_ss_trojan('%s<', html)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PYFlyingboat(Net):\n    \"\"\"\n    https://t.me/s/flyingboat\n    需要代理\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request_en(r'https://t.me/s/flyingboat')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            return re_vmess_ss_trojan('>%s<', html)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PYIvmess(Net):\n    \"\"\"\n    https://t.me/s/ivmess\n    需要代理\n    \"\"\"\n\n    def get_urls(self) -> []:\n        try:\n            r = self.request_en(r'https://t.me/s/ivmess')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            html = r.text\n            return re_vmess_ss_trojan('>%s<', html)\n        except Exception as e:\n            print(e)\n            return None\n\n\nclass PYFreeFq(Net):\n    \"\"\"\n    从https://freefq.com/获取免费节点，规则：https://freefq.com/v2ray/2021/08/30/v2ray.html\n    \"\"\"\n\n    def __get_content_url(self, date) -> str:\n        \"\"\"\n        获取当前页面中文本的url\n        :param date: 如：2021/08/29\n        :return:\n        \"\"\"\n        try:\n            r = self.request_en(r'https://freefq.com/v2ray/%s/v2ray.html' % date)\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            return self.__get_url_by_content_html(r.text)\n        except Exception as e:\n            print(e)\n            return None\n\n    def __get_url_by_content_html(self, html: str):\n        \"\"\"从文本中通过正则获取节点所在文本的url\"\"\"\n        results = re.findall(r'<td>.+?</td>', html)\n        tag = None\n        pre = 'https://www.freefq.com/d/file/v2ray'\n        for url in results:\n            if pre in url:\n                tag = url\n                break\n        if not tag:\n            print('无法获取节点url：%s' % results)\n            return None\n        try:\n            url = re.findall(r'%s.+?\\.htm' % pre, tag)[0]\n        except Exception as e:\n            print(e)\n            return None\n        return url\n\n    def __get_url_by_detail_html(self, html: str):\n        \"\"\"截取文本中的节点url\"\"\"\n        # results = re.findall(r'(trojan://.+?|vmess://.+?|ss://.+?)<br>', html)\n        results = re_vmess_ss_trojan(r'%s<br>', html)\n        urls = []\n        for url in results:\n            temp: str = url.strip()\n            if len(temp) < 20 and temp in results:  # 凭感觉\n                continue\n            urls.append(url)\n        return urls\n\n    def __get_detail_urls(self, url: str) -> []:\n        \"\"\"获取内容\"\"\"\n        try:\n            r = self.request_en(url)\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            # return self.__get_url_by_detail_html(r.text)\n            return re_vmess_ss_trojan(r'%s<br>', r.text)\n        except Exception as e:\n            print(e)\n            return None\n\n    def download_urls(self, f_day=1) -> []:\n        \"\"\"\n        开始获取\n        :param f_day: 需要获取往前的天数\n        :return:\n        \"\"\"\n        DAY = 24 * 60 * 60\n        l_time = time.time()\n        all_url = []\n        for day in range(1, f_day + 1):  # 需要从前一天开始\n            temp_day = time.strftime(\"%Y/%m/%d\", time.localtime(l_time - (day * DAY)))\n            url = self.__get_content_url(temp_day)\n            if not url:\n                continue\n            urls = self.__get_detail_urls(url)\n            if not urls:  # 如果是None或者[]\n                continue\n            for temp in urls:\n                if temp not in all_url:\n                    all_url.append(temp)\n            time.sleep(1)\n        return all_url\n\n    def get_urls(self) -> []:\n        return self.download_urls()\n"
  },
  {
    "path": "002-V2rayPool/doc/(参考用)config.json",
    "content": "{\n \"log\": {\n  \"access\": \"\",\n  \"error\": \"\",\n  \"loglevel\": \"info\"\n },\n \"inbounds\": [\n  {\n   \"port\": \"1080\",\n   \"listen\": \"0.0.0.0\",\n   \"protocol\": \"socks\",\n   \"settings\": {\n    \"auth\": \"noauth\",\n    \"udp\": true,\n    \"ip\": \"127.0.0.1\",\n    \"clients\": null\n   },\n   \"streamSettings\": null\n  },\n  {\n   \"listen\": \"127.0.0.1\",\n   \"protocol\": \"http\",\n   \"settings\": {\n    \"timeout\": 360\n   },\n   \"port\": \"1087\"\n  }\n ],\n \"outbounds\": [\n  {\n   \"protocol\": \"shadowsocks\",\n   \"settings\": {\n    \"servers\": [\n     {\n      \"address\": \"167.88.61.60\",\n      \"method\": \"aes-256-gcm\",\n      \"ota\": false,\n      \"password\": \"RexnBgU7EV5ADxG\",\n      \"port\": 7002\n     }\n    ]\n   }\n  },\n  {\n   \"protocol\": \"freedom\",\n   \"settings\": {\n    \"response\": null\n   },\n   \"tag\": \"direct\"\n  }\n ],\n \"routing\": {\n  \"domainStrategy\": \"IPIfNonMatch\",\n  \"rules\": [\n   {\n    \"type\": \"field\",\n    \"ip\": [\n     \"geoip:private\"\n    ],\n    \"outboundTag\": \"direct\"\n   },\n   {\n    \"type\": \"field\",\n    \"domain\": [\n     \"geosite:cn\"\n    ],\n    \"outboundTag\": \"direct\"\n   },\n   {\n    \"type\": \"field\",\n    \"domain\": [\n     \"geoip:cn\"\n    ],\n    \"outboundTag\": \"direct\"\n   }\n  ]\n }\n}"
  },
  {
    "path": "002-V2rayPool/test_main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 主要入口\n@Date       :2021/08/25\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nfrom core import utils\nfrom core.conf import Config\nfrom db.db_main import *\n\nEXIT_NUM = 100\n\nif __name__ == '__main__':\n    utils.kill_all_v2ray()\n    Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64')  # v2ray内核存放路径\n    Config.set_v2ray_node_path('/Users/Qincji/Desktop/develop/py/project/PythonIsTools/002-V2rayPool')  # 保存获取到节点的路径\n    proxy_url = 'vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIkBTU1JTVUItVjUyLeS7mOi0ueaOqOiNkDpzdW8ueXQvc3Nyc3ViIiwNCiAgImFkZCI6ICIxMTIuMzMuMzIuMTM2IiwNCiAgInBvcnQiOiAiMTAwMDMiLA0KICAiaWQiOiAiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwNCiAgImFpZCI6ICIxIiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiMTEyLjMzLjMyLjEzNiIsDQogICJwYXRoIjogIiIsDQogICJ0bHMiOiAiIiwNCiAgInNuaSI6ICIiDQp9'\n    dbm = DBManage()\n    dbm.init()  # 必须初始化\n    if dbm.check_url_single(proxy_url):\n        urls = dbm.load_urls_by_net(proxy_url=proxy_url)\n        dbm.check_and_save(urls, append=False)\n    # print(urls)\n    # urls = dbm.load_unchecked_urls_by_local()\n    # dbm.check_and_save(urls, append=False)\n    # urls = dbm.load_enable_urls_by_local()\n    # dbm.load_urls_and_save_auto()\n    utils.kill_all_v2ray()\n"
  },
  {
    "path": "003-Keywords/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "003-Keywords/README.md",
    "content": "# 通过google trends查找相关关键词，并且生成趋势\n\n## 效果\n\n例子：通过`women ring`关键词查找出有1千个相关关键词：[women ring.csv](women-ring/women ring.csv)\n\n![](women-ring/2.jpg)\n\n以及其生成关键词趋势，如：[swarovski rings.jpg](women-ring/swarovski rings.jpg)\n\n![](women-ring/1.jpg)\n\n![](women-ring/3.jpg)\n\n## 实现\n1. 使用[pytrends](https://github.com/GeneralMills/pytrends) 开源库。\n2. 使用[002-V2rayPool](../002-V2rayPool) 代理（可选择第三方代理）。\n3. 实现入口请参照：[main.py](main.py) \n\n> 注：项目架构是使用[Scrapy](https://www.osgeo.cn/scrapy/intro/overview.html) 实现的，可实现amazon关键词查询等。 "
  },
  {
    "path": "003-Keywords/__init__.py",
    "content": ""
  },
  {
    "path": "003-Keywords/amazon/items.py",
    "content": "# Define here the models for your scraped items\n#\n# See documentation in:\n# https://docs.scrapy.org/en/latest/topics/items.html\n\nimport scrapy\n\n\nclass AmazonItem(scrapy.Item):\n    # define the fields for your item here like:\n    # name = scrapy.Field()\n    pass\n"
  },
  {
    "path": "003-Keywords/amazon/middlewares.py",
    "content": "# Define here the models for your spider middleware\n#\n# See documentation in:\n# https://docs.scrapy.org/en/latest/topics/spider-middleware.html\nimport requests\nfrom scrapy import signals\n\n# useful for handling different item types with a single interface\nfrom scrapy.http import TextResponse\n\n\nclass AmazonSpiderMiddleware:\n    # Not all methods need to be defined. If a method is not defined,\n    # scrapy acts as if the spider middleware does not modify the\n    # passed objects.\n\n    @classmethod\n    def from_crawler(cls, crawler):\n        # This method is used by Scrapy to create your spiders.\n        s = cls()\n        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)\n        return s\n\n    def process_spider_input(self, response, spider):\n        # Called for each response that goes through the spider\n        # middleware and into the spider.\n\n        # Should return None or raise an exception.\n        return None\n\n    def process_spider_output(self, response, result, spider):\n        # Called with the results returned from the Spider, after\n        # it has processed the response.\n\n        # Must return an iterable of Request, or item objects.\n        for i in result:\n            yield i\n\n    def process_spider_exception(self, response, exception, spider):\n        # Called when a spider or process_spider_input() method\n        # (from other spider middleware) raises an exception.\n\n        # Should return either None or an iterable of Request or item objects.\n        pass\n\n    def process_start_requests(self, start_requests, spider):\n        # Called with the start requests of the spider, and works\n        # similarly to the process_spider_output() method, except\n        # that it doesn’t have a response associated.\n\n        # Must return only requests (not items).\n        for r in start_requests:\n            yield r\n\n    def spider_opened(self, spider):\n        spider.logger.info('Spider opened: %s' % spider.name)\n\n\nclass AmazonDownloaderMiddleware:\n    # Not all methods need to be defined. If a method is not defined,\n    # scrapy acts as if the downloader middleware does not modify the\n    # passed objects.\n\n    @classmethod\n    def from_crawler(cls, crawler):\n        # This method is used by Scrapy to create your spiders.\n        s = cls()\n        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)\n        return s\n\n    def process_request(self, request, spider):\n        # Called for each request that goes through the downloader\n        # middleware.\n\n        # Must either:\n        # - return None: continue processing this request\n        # - or return a Response object\n        # - or return a Request object\n        # - or raise IgnoreRequest: process_exception() methods of\n        #   installed downloader middleware will be called\n        return None\n\n    def process_response(self, request, response, spider):\n        # Called with the response returned from the downloader.\n\n        # Must either;\n        # - return a Response object\n        # - return a Request object\n        # - or raise IgnoreRequest\n        return response\n\n    def process_exception(self, request, exception, spider):\n        # Called when a download handler or a process_request()\n        # (from other downloader middleware) raises an exception.\n\n        # Must either:\n        # - return None: continue processing this exception\n        # - return a Response object: stops process_exception() chain\n        # - return a Request object: stops process_exception() chain\n        pass\n\n    def spider_opened(self, spider):\n        spider.logger.info('Spider opened: %s' % spider.name)\n\n\nclass AmazonProxyMiddleware(object):\n    def process_request(self, request, spider):\n        print('执行AmazonProxyMiddleware……')\n        # Set the location of the proxy\n        proxy_url = \"socks5://127.0.0.1:1080\"\n        request.meta['proxy'] = proxy_url\n        # 考虑socks代理，使用requests库进行请求\n        if proxy_url.startswith('socks'):\n            url = request.url\n            method = request.method\n            headers = {key: request.headers[key] for key in request.headers}\n            body = request.body\n            cookies = request.cookies\n            timeout = request.meta.get('download_timeout', 10)\n            proxies = {'http': proxy_url,\n                       'https': proxy_url}\n\n            resp = requests.request(method, url,\n                                    data=body,\n                                    headers=headers,\n                                    cookies=cookies,\n                                    verify=False, timeout=timeout, proxies=proxies)\n            resp.headers['content-encoding'] = None\n            response = TextResponse(url=url, headers=resp.headers, body=resp.content,\n                                    request=request, encoding=resp.encoding)\n            return response\n        return None"
  },
  {
    "path": "003-Keywords/amazon/pipelines.py",
    "content": "# Define your item pipelines here\n#\n# Don't forget to add your pipeline to the ITEM_PIPELINES setting\n# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html\n\n\n# useful for handling different item types with a single interface\nfrom itemadapter import ItemAdapter\n\n\nclass AmazonPipeline:\n    def process_item(self, item, spider):\n        return item\n"
  },
  {
    "path": "003-Keywords/amazon/settings.py",
    "content": "# Scrapy settings for amazon project\n#\n# For simplicity, this file contains only settings considered important or\n# commonly used. You can find more settings consulting the documentation:\n#\n#     https://docs.scrapy.org/en/latest/topics/settings.html\n#     https://docs.scrapy.org/en/latest/topics/downloader-middleware.html\n#     https://docs.scrapy.org/en/latest/topics/spider-middleware.html\n\nBOT_NAME = 'amazon'\n\nSPIDER_MODULES = ['amazon.spiders']\nNEWSPIDER_MODULE = 'amazon.spiders'\n\n\n# Crawl responsibly by identifying yourself (and your website) on the user-agent\n#USER_AGENT = 'amazon (+http://www.yourdomain.com)'\n\n# Obey robots.txt rules\nROBOTSTXT_OBEY = False\n\n# Configure maximum concurrent requests performed by Scrapy (default: 16)\n#CONCURRENT_REQUESTS = 32\n\n# Configure a delay for requests for the same website (default: 0)\n# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay\n# See also autothrottle settings and docs\nDOWNLOAD_DELAY = 2\n# The download delay setting will honor only one of:\n#CONCURRENT_REQUESTS_PER_DOMAIN = 16\n#CONCURRENT_REQUESTS_PER_IP = 16\n\n# Disable cookies (enabled by default)\n#COOKIES_ENABLED = False\n\n# Disable Telnet Console (enabled by default)\n#TELNETCONSOLE_ENABLED = False\n\n# Override the default request headers:\n#DEFAULT_REQUEST_HEADERS = {\n#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n#   'Accept-Language': 'en',\n#}\n\n# Enable or disable spider middlewares\n# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html\n#SPIDER_MIDDLEWARES = {\n#    'amazon.middlewares.AmazonSpiderMiddleware': 543,\n#}\n\n# Enable or disable downloader middlewares\n# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html\n# DOWNLOADER_MIDDLEWARES = {\n#    # 'amazon.middlewares.AmazonDownloaderMiddleware': 543,\n#    'amazon.middlewares.AmazonProxyMiddleware': 543\n# }\n\n# Enable or disable extensions\n# See https://docs.scrapy.org/en/latest/topics/extensions.html\n#EXTENSIONS = {\n#    'scrapy.extensions.telnet.TelnetConsole': None,\n#}\n\n# Configure item pipelines\n# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html\n#ITEM_PIPELINES = {\n#    'amazon.pipelines.AmazonPipeline': 300,\n#}\n\n# Enable and configure the AutoThrottle extension (disabled by default)\n# See https://docs.scrapy.org/en/latest/topics/autothrottle.html\n#AUTOTHROTTLE_ENABLED = True\n# The initial download delay\n#AUTOTHROTTLE_START_DELAY = 5\n# The maximum download delay to be set in case of high latencies\n#AUTOTHROTTLE_MAX_DELAY = 60\n# The average number of requests Scrapy should be sending in parallel to\n# each remote server\n#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0\n# Enable showing throttling stats for every response received:\n#AUTOTHROTTLE_DEBUG = False\n\n# Enable and configure HTTP caching (disabled by default)\n# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings\n#HTTPCACHE_ENABLED = True\n#HTTPCACHE_EXPIRATION_SECS = 0\n#HTTPCACHE_DIR = 'httpcache'\n#HTTPCACHE_IGNORE_HTTP_CODES = []\n#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'\n"
  },
  {
    "path": "003-Keywords/amazon/spiders/__init__.py",
    "content": "# This package will contain the spiders of your Scrapy project\n#\n# Please refer to the documentation for information on how to create and manage\n# your spiders.\n"
  },
  {
    "path": "003-Keywords/amazon/spiders/alibaba.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 亚马逊相关关键词获取\n@Date       :2021/09/24\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport re\nfrom urllib.parse import quote_plus\n\nimport scrapy\nfrom my_fake_useragent import UserAgent\n\n\nclass AlibabaSpider(scrapy.Spider):\n    name = 'alibaba'\n    allowed_domains = ['alibaba.com']\n    results = []\n    keywords = []\n    headers = {\n        'Host': 'www.alibaba.com',\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('alibaba start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n            \n        \"\"\"\n        start_urls = ['https://www.alibaba.com/trade/search?fsb=y&IndexArea=product_en&CatId=&SearchText={k}'.format(\n            k=quote_plus(k)) for k in self.keywords]\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('alibaba parse')\n        with open('alibaba.html', mode='w') as f:\n            f.write(response.text)"
  },
  {
    "path": "003-Keywords/amazon/spiders/amazon.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 亚马逊相关关键词获取\n@Date       :2021/09/24\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport re\nfrom urllib.parse import quote_plus\n\nimport scrapy\nfrom my_fake_useragent import UserAgent\n\n\nclass AmazonSpider(scrapy.Spider):\n    name = 'amazon'\n    allowed_domains = ['amazon.com']\n    results = []\n    keywords = []\n    headers = {\n        'Host': 'www.amazon.com',\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('amazon start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://www.amazon.com/s?k={k}'.format(k=quote_plus(k)) for k in self.keywords]\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('amazon parse')\n        temps = re.findall(r'<span class=\"a-size-base a-color-base s-line-clamp-2\">(.+?)</span>', response.text,\n                           re.DOTALL)\n        deal = [x.replace('\\n', '').strip() for x in temps]\n        print(deal)\n        self.results += deal\n"
  },
  {
    "path": "003-Keywords/amazon/spiders/checkip.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 检查当前代理是否起作用\n@Date       :2021/09/24\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport re\n\nimport scrapy\nfrom my_fake_useragent import UserAgent\n\n\nclass CheckIpSpider(scrapy.Spider):\n    name = 'ip138'\n    allowed_domains = ['ip138.com']\n    ips = None\n    headers = {\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('CheckIpSpider start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://2021.ip138.com']\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('CheckIpSpider parse')\n        results = re.findall(r'\\[<a.+?>(.+?)</a>.+?：(.+?)\\n</p>', response.text, re.DOTALL)\n        print(results)\n        self.ips = results\n"
  },
  {
    "path": "003-Keywords/amazon/spiders/spiders.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 爬虫页面集合\n@Date       :2021/09/26\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport re\nfrom urllib.parse import quote_plus\n\nimport scrapy\nfrom my_fake_useragent import UserAgent\n\n\nclass CheckIpSpider(scrapy.Spider):\n    \"\"\"\n    检查当前代理ip信息\n    \"\"\"\n    name = 'ip138'\n    allowed_domains = ['ip138.com']\n    ips = None\n    headers = {\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('CheckIpSpider start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://2021.ip138.com']\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('CheckIpSpider parse')\n        results = re.findall(r'\\[<a.+?>(.+?)</a>.+?：(.+?)\\n</p>', response.text, re.DOTALL)\n        print(results)\n        self.ips = results\n\n\nclass AmazonSpider(scrapy.Spider):\n    \"\"\"\n    https://www.amazon.com/s?k=？？\n    亚马逊页面获取搜索词相关：\n    \"\"\"\n    name = 'amazon'\n    allowed_domains = ['amazon.com']\n    results = []\n    keywords = []\n    headers = {\n        'Host': 'www.amazon.com',\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('amazon start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://www.amazon.com/s?k={k}'.format(k=quote_plus(k)) for k in self.keywords]\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('amazon parse')\n        temps = re.findall(r'<span class=\"a-size-base a-color-base s-line-clamp-2\">(.+?)</span>', response.text,\n                           re.DOTALL)\n        deal = [x.replace('\\n', '').strip() for x in temps]\n        print(deal)\n        self.results += deal\n\n\nclass EtsySpider(scrapy.Spider):\n    \"\"\"需要连接外网\n    https://www.etsy.com/market/\n    \"\"\"\n    name = 'etsy'\n    allowed_domains = ['etsy.com']\n    results = []\n    keywords = []\n    headers = {\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('etsy start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://www.etsy.com/market/{k}'.format(k=quote_plus(k)) for k in self.keywords]\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('etsy parse')\n        with open('etsy.html', mode='w') as f:\n            f.write(response.text)\n            f.close()\n        temps = re.findall(r'<span class=\"a-size-base a-color-base s-line-clamp-2\">(.+?)</span>', response.text,\n                           re.DOTALL)\n        deal = [x.replace('\\n', '').strip() for x in temps]\n        print(deal)\n        self.results += deal\n\n\nclass LakesideSpider(scrapy.Spider):\n    \"\"\"一个商城网站\n    https://www.lakeside.com/browse/Clothing-Accessories\n    \"\"\"\n    name = 'lakeside'\n    allowed_domains = ['lakeside.com']\n    results = []\n    keywords = []\n    headers = {\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('lakeside start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://www.lakeside.com/browse/{k}'.format(k=quote_plus(k)) for k in self.keywords]\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('lakeside parse')\n        with open('lakeside.html', mode='w') as f:\n            f.write(response.text)\n            f.close()\n        # temps = re.findall(r'<span class=\"a-size-base a-color-base s-line-clamp-2\">(.+?)</span>', response.text,\n        #                    re.DOTALL)\n        # deal = [x.replace('\\n', '').strip() for x in temps]\n        # print(deal)\n        # self.results += deal\n\n\nclass WordtrackerSpider(scrapy.Spider):\n    \"\"\"\n    https://www.wordtracker.com/search?query=food%20bags\n    \"\"\"\n    name = 'wordtracker'\n    allowed_domains = ['wordtracker.com']\n    results = []\n    keywords = []\n    headers = {\n        'User-Agent': UserAgent().random()\n    }\n\n    def start_requests(self):\n        print('wordtracker start_requests')\n        \"\"\"\n            start_requests做为程序的入口，可以重写，自定义第一批请求\n        \"\"\"\n        start_urls = ['https://www.wordtracker.com/search?query={k}'.format(k=quote_plus(k)) for k in self.keywords]\n        # , meta={'proxy': 'socks5h://127.0.0.1:1080/'}\n        for url in start_urls:\n            yield scrapy.Request(url, headers=self.headers,\n                                 callback=self.parse)\n\n    def parse(self, response):\n        print('wordtracker parse')\n        with open('wordtracker.html', mode='w') as f:\n            f.write(response.text)\n            f.close()\n        temps = re.findall(r'<span class=\"a-size-base a-color-base s-line-clamp-2\">(.+?)</span>', response.text,\n                           re.DOTALL)\n        deal = [x.replace('\\n', '').strip() for x in temps]\n        print(deal)\n        self.results += deal\n"
  },
  {
    "path": "003-Keywords/google.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: google相关获取\n@Date       :2021/10/08\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport xlsxwriter\nimport os.path\n\nimport matplotlib.pyplot as plt\n\nfrom mypytrends.request import TrendReq\n\n\nclass GoogleTrend(object):\n    def __init__(self):\n        self.data = {}\n        self.max_column = 0\n\n    def search(self, keyword, path, hl='en-US', proxies=False, retries=2, timeframe='2019-10-01 2022-01-01'):\n        if not os.path.exists(path):\n            os.makedirs(path)\n        csv_file = os.path.join(path, \"%s.xlsx\" % keyword)\n        workbook = xlsxwriter.Workbook(csv_file)\n        # 设置整个工作薄的格式\n        workbook.formats[0].set_align('vcenter')  # 单元格垂直居中\n        # workbook.formats[0].set_text_wrap()  # 自动换行\n\n        worksheet = workbook.add_worksheet('sheet1')\n        i_row, i_column = 0, 0\n        first_row = ['keyword', 'no', 'top keyword', 'top range', 'rising keyword', 'rising range']\n        self.max_column = len(first_row)\n        for i in range(self.max_column):\n            worksheet.write(i_row, i, first_row[i])\n        i_row += 1\n        tr = self.__get_req(hl=hl, proxies=proxies, retries=retries)\n        i_row = self.__search_trends(i_row, worksheet, path, keyword, timeframe, tr)\n        first_data = self.__search_related_queries(keyword, timeframe, tr)\n        tops = first_data[keyword]['top']\n        risings = first_data[keyword]['rising']\n        top_size = len(tops)\n        rising_size = len(risings)\n        max_len = top_size if top_size > rising_size else rising_size\n        for i in range(max_len):\n            top_key, top_range, rising_key, rising_range = None, None, None, None\n            if i < top_size:\n                top_key = tops[i]['keyword']\n                top_range = tops[i]['range']\n            if i < rising_size:\n                rising_key = risings[i]['keyword']\n                rising_range = risings[i]['range']\n            max_datas = [keyword, i, top_key, top_range, rising_key, rising_range]\n            for j in range(len(max_datas)):\n                worksheet.write(i_row, j, max_datas[j])\n            i_row += 1\n            # self.__save_line(csv_file, [keyword, i, top_key, top_range, rising_key, rising_range])\n        # self.__save_line(csv_file, ['', ''])  # 换行\n        i_row += 1\n        for top in tops:\n            top_key = top['keyword']\n            try:\n                i_row = self.__sub_search(top_key, i_row, worksheet, path, csv_file, timeframe, tr)\n                # self.__save_line(csv_file, ['', ''])  # 换行\n                i_row += 1\n            except:\n                pass\n        for rising in risings:\n            rising_key = rising['keyword']\n            try:\n                i_row = self.__sub_search(rising_key, i_row, worksheet, path, csv_file, timeframe, tr)\n                # self.__save_line(csv_file, ['', ''])  # 换行\n                i_row += 1\n            except:\n                pass\n        workbook.close()\n\n    def __sub_search(self, i_row, worksheet, keyword, path, csv_file, timeframe, tr):\n        i_row = self.__search_trends(i_row, worksheet, path, keyword, timeframe, tr)\n        first_data = self.__search_related_queries(keyword, timeframe, tr)\n        tops = first_data[keyword]['top']\n        risings = first_data[keyword]['rising']\n        top_size = len(tops)\n        rising_size = len(risings)\n        max_len = top_size if top_size > rising_size else rising_size\n        for i in range(max_len):\n            top_key, top_range, rising_key, rising_range = None, None, None, None\n            if i < top_size:\n                top_key = tops[i]['keyword']\n                top_range = tops[i]['range']\n                try:\n                    i_row = self.__search_trends(i_row, worksheet, path, top_key, timeframe, tr)\n                except:\n                    pass\n            if i < rising_size:\n                rising_key = risings[i]['keyword']\n                rising_range = risings[i]['range']\n                try:\n                    i_row = self.__search_trends(i_row, worksheet, path, rising_key, timeframe, tr)\n                except:\n                    pass\n            # self.__save_line(csv_file, [keyword, i, top_key, top_range, rising_key, rising_range])\n            max_datas = [keyword, i, top_key, top_range, rising_key, rising_range]\n            for j in range(len(max_datas)):\n                worksheet.write(i_row, j, max_datas[j])\n                i_row += 1\n\n        return i_row\n\n    def __search_related_queries(self, keyword, timeframe, tr: TrendReq) -> {}:\n        tr.build_payload([keyword, ], cat=0, timeframe=timeframe, geo='', gprop='')\n        related = tr.related_queries()\n        r_value = [related[key] for key in related][0]\n        r_top = r_value['top']\n        r_rising = r_value['rising']\n        tops = []\n        risings = []\n        print('---------top--------')\n        for index, row in r_top.iterrows():\n            print(index, row[\"query\"], row[\"value\"])\n            tops.append({\"keyword\": row[\"query\"], \"range\": row[\"value\"]})\n        print('---------rising--------')\n        for index, row in r_rising.iterrows():\n            print(index, row[\"query\"], row[\"value\"])\n            risings.append({\"keyword\": row[\"query\"], \"range\": row[\"value\"]})\n        return {keyword: {\"top\": tops, \"rising\": risings}}\n\n    def __search_trends(self, i_row, worksheet, path, keyword, timeframe, tr: TrendReq):\n        tr.build_payload([keyword, ], cat=0, timeframe=timeframe, geo='', gprop='')\n        trends = tr.interest_over_time()\n        x_data = []\n        y_data = []\n        year = ''\n        month = ''\n        temp_value = 0\n        count = 0\n        for time, row in trends.iterrows():\n            value = row[keyword]\n            date = str(time)\n            t = date.split(' ')[0] if ' ' in date else date\n            y = t.split('-')[0]\n            m = t.split('-')[1]\n            if month != m and month != '':\n                y_data.append(int(temp_value / count))\n                x_data.append(year[2:] + \"-\" + month)\n                year = ''\n                month = ''\n                count = 0\n                temp_value = 0\n                continue\n            year = y\n            month = m\n            count += 1\n            temp_value += value\n        y_data.append(int(temp_value / count))\n        x_data.append(year[2:] + \"-\" + month)\n        print(y_data)\n        print(x_data)\n        print('%s : %d - %d' % (keyword, len(y_data), len(x_data)))\n        # self.__draw_graph(x_data, y_data, 'pci.jpg', keyword)\n        img_path = os.path.join(path, \"%s.jpg\" % keyword)\n        self.__draw_histogram(x_data, y_data, img_path, keyword)\n        worksheet.insert_image(i_row - 1, self.max_column, img_path,\n                               {'x_scale': 0.2, 'y_scale': 0.2, 'object_position': 1})\n        i_row += 1\n        return i_row\n\n    def __draw_histogram(self, x: [], y: [], path, title, x_name='date', y_name='trends'):\n        plt.figure(dpi=60)\n        plt.ylim(0, 100)\n        plt.style.use('ggplot')\n        plt.bar(x, y, label=title)\n        # 显示图例（使绘制生效）\n        plt.legend()\n        # 横坐标名称\n        plt.xlabel(x_name)\n        # 纵坐标名称\n        plt.ylabel(y_name)\n        # 横坐标显示倒立\n        plt.xticks(rotation=90)\n        # 保存图片到本地\n        plt.savefig(path)\n        # 显示图片\n        # plt.show()\n\n    def __draw_graph(self, x: [], y: [], path, title, x_name='date', y_name='trends'):\n        plt.figure()\n        '''绘制第一条数据线\n        1、节点为圆圈\n        2、线颜色为红色\n        3、标签名字为y1-data\n        '''\n        plt.ylim(0, 100)\n        plt.plot(x, y, marker='o', color='r', label=title)\n        # 显示图例（使绘制生效）\n        plt.legend()\n        # 横坐标名称\n        plt.xlabel(x_name)\n        # 纵坐标名称\n        plt.ylabel(y_name)\n        # 横坐标显示倒立\n        plt.xticks(rotation=90)\n        # 保存图片到本地\n        plt.savefig(path)\n        # 显示图片\n        # plt.show()\n\n    def __save_line(self, path, line, mode='a'):\n        with open(path, mode=mode, encoding='utf-8', newline='') as csvfile:\n            writer = csv.writer(csvfile)\n            writer.writerow(line)\n            csvfile.close()\n\n    def __get_req(self, hl='en-US', proxies=False, retries=2) -> TrendReq:\n        if proxies:\n            return TrendReq(hl=hl, tz=360, timeout=(10, 35), proxies=['socks5h://127.0.0.1:1080', ], retries=retries,\n                            backoff_factor=0.1, requests_args={'verify': False})\n        else:\n            return TrendReq(hl=hl, tz=360, timeout=(10, 35), retries=retries, backoff_factor=0.1,\n                            requests_args={'verify': False})\n"
  },
  {
    "path": "003-Keywords/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 关键词获取\n@Date       :2021/09/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n# from amazon import run_api\nimport os\nimport time\n\nimport xlwt\n\nimport run_api\nimport v2ray_util as v2util\nfrom google import GoogleTrend\nfrom openpyxl import load_workbook\n\n\ndef Write_Img():\n    import xlsxwriter\n    book = xlsxwriter.Workbook('test_source.xlsx')\n    sheet = book.get_worksheet_by_name('Sheet1')\n    # sheet = book.add_worksheet('demo')\n    # sheet.insert_image(0, 5, 'Necklace/Necklace.jpg', {'x_scale': 0.2, 'y_scale': 0.2, 'object_position': 1})\n    print(sheet)\n    book.close()\n\n\ndef read_xlsl():\n    import pandas as pd\n    df = pd.read_excel('test_source.xlsx', sheet_name='Sheet1')\n    data = df.values\n    print(data)\n    print('\\n')\n    df = pd.read_excel('test_souce1.xlsx', sheet_name='2021xuqiu')\n    data = df.values\n    print(data)\n\n\n\nif __name__ == \"__main__\":\n    # v2util.restart_v2ray()\n    # 获取amazon中相关词，代理需要看：middlewares.py，\n    # results = run_api.crawl_amazon(['plastic packaging', ])\n    # 把通过google trends查出关键词相关词，以及其词的趋势图，如：\n    # GoogleTrend().search('Necklace', 'Necklace', proxies=True, timeframe='2021-01-01 2022-01-01')\n    # v2util.kill_all_v2ray()\n    # Write_Img()\n    read_xlsl()\n"
  },
  {
    "path": "003-Keywords/mypytrends/__init__.py",
    "content": ""
  },
  {
    "path": "003-Keywords/mypytrends/dailydata.py",
    "content": "from datetime import date, timedelta\nfrom functools import partial\nfrom time import sleep\nfrom calendar import monthrange\n\nimport pandas as pd\n\nfrom mypytrends.exceptions import ResponseError\nfrom mypytrends.request import TrendReq\n\n\ndef get_last_date_of_month(year: int, month: int) -> date:\n    \"\"\"Given a year and a month returns an instance of the date class\n    containing the last day of the corresponding month.\n\n    Source: https://stackoverflow.com/questions/42950/get-last-day-of-the-month-in-python\n    \"\"\"\n    return date(year, month, monthrange(year, month)[1])\n\n\ndef convert_dates_to_timeframe(start: date, stop: date) -> str:\n    \"\"\"Given two dates, returns a stringified version of the interval between\n    the two dates which is used to retrieve data for a specific time frame\n    from Google Trends.\n    \"\"\"\n    return f\"{start.strftime('%Y-%m-%d')} {stop.strftime('%Y-%m-%d')}\"\n\n\ndef _fetch_data(pytrends, build_payload, timeframe: str) -> pd.DataFrame:\n    \"\"\"Attempts to fecth data and retries in case of a ResponseError.\"\"\"\n    attempts, fetched = 0, False\n    while not fetched:\n        try:\n            build_payload(timeframe=timeframe)\n        except ResponseError as err:\n            print(err)\n            print(f'Trying again in {60 + 5 * attempts} seconds.')\n            sleep(60 + 5 * attempts)\n            attempts += 1\n            if attempts > 3:\n                print('Failed after 3 attemps, abort fetching.')\n                break\n        else:\n            fetched = True\n    return pytrends.interest_over_time()\n\n\ndef get_daily_data(word: str,\n                 start_year: int,\n                 start_mon: int,\n                 stop_year: int,\n                 stop_mon: int,\n                 geo: str = 'US',\n                 verbose: bool = True,\n                 wait_time: float = 5.0) -> pd.DataFrame:\n    \"\"\"Given a word, fetches daily search volume data from Google Trends and\n    returns results in a pandas DataFrame.\n\n    Details: Due to the way Google Trends scales and returns data, special\n    care needs to be taken to make the daily data comparable over different\n    months. To do that, we download daily data on a month by month basis,\n    and also monthly data. The monthly data is downloaded in one go, so that\n    the monthly values are comparable amongst themselves and can be used to\n    scale the daily data. The daily data is scaled by multiplying the daily\n    value by the monthly search volume divided by 100.\n    For a more detailed explanation see http://bit.ly/trendsscaling\n\n    Args:\n        word (str): Word to fetch daily data for.\n        start_year (int): the start year\n        start_mon (int): start 1st day of the month\n        stop_year (int): the end year\n        stop_mon (int): end at the last day of the month\n        geo (str): geolocation\n        verbose (bool): If True, then prints the word and current time frame\n            we are fecthing the data for.\n\n    Returns:\n        complete (pd.DataFrame): Contains 4 columns.\n            The column named after the word argument contains the daily search\n            volume already scaled and comparable through time.\n            The column f'{word}_unscaled' is the original daily data fetched\n            month by month, and it is not comparable across different months\n            (but is comparable within a month).\n            The column f'{word}_monthly' contains the original monthly data\n            fetched at once. The values in this column have been backfilled\n            so that there are no NaN present.\n            The column 'scale' contains the scale used to obtain the scaled\n            daily data.\n    \"\"\"\n\n    # Set up start and stop dates\n    start_date = date(start_year, start_mon, 1) \n    stop_date = get_last_date_of_month(stop_year, stop_mon)\n\n    # Start pytrends for US region\n    pytrends = TrendReq(hl='en-US', tz=360)\n    # Initialize build_payload with the word we need data for\n    build_payload = partial(pytrends.build_payload,\n                            kw_list=[word], cat=0, geo=geo, gprop='')\n\n    # Obtain monthly data for all months in years [start_year, stop_year]\n    monthly = _fetch_data(pytrends, build_payload,\n                         convert_dates_to_timeframe(start_date, stop_date))\n\n    # Get daily data, month by month\n    results = {}\n    # if a timeout or too many requests error occur we need to adjust wait time\n    current = start_date\n    while current < stop_date:\n        last_date_of_month = get_last_date_of_month(current.year, current.month)\n        timeframe = convert_dates_to_timeframe(current, last_date_of_month)\n        if verbose:\n            print(f'{word}:{timeframe}')\n        results[current] = _fetch_data(pytrends, build_payload, timeframe)\n        current = last_date_of_month + timedelta(days=1)\n        sleep(wait_time)  # don't go too fast or Google will send 429s\n\n    daily = pd.concat(results.values()).drop(columns=['isPartial'])\n    complete = daily.join(monthly, lsuffix='_unscaled', rsuffix='_monthly')\n\n    # Scale daily data by monthly weights so the data is comparable\n    complete[f'{word}_monthly'].ffill(inplace=True)  # fill NaN values\n    complete['scale'] = complete[f'{word}_monthly'] / 100\n    complete[word] = complete[f'{word}_unscaled'] * complete.scale\n\n    return complete\n"
  },
  {
    "path": "003-Keywords/mypytrends/exceptions.py",
    "content": "class ResponseError(Exception):\n    \"\"\"Something was wrong with the response from Google\"\"\"\n\n    def __init__(self, message, response):\n        super(Exception, self).__init__(message)\n\n        # pass response so it can be handled upstream\n        self.response = response\n"
  },
  {
    "path": "003-Keywords/mypytrends/request.py",
    "content": "import json\nimport sys\nimport time\nfrom datetime import datetime, timedelta\n\nimport pandas as pd\nimport requests\n\nfrom pandas.io.json._normalize import nested_to_record\nfrom requests.adapters import HTTPAdapter\nfrom requests.packages.urllib3.util.retry import Retry\n\nfrom mypytrends import exceptions\n\nfrom urllib.parse import quote\n\n\nclass TrendReq(object):\n    \"\"\"\n    Google Trends API\n    \"\"\"\n    GET_METHOD = 'get'\n    POST_METHOD = 'post'\n    GENERAL_URL = 'https://trends.google.com/trends/api/explore'\n    INTEREST_OVER_TIME_URL = 'https://trends.google.com/trends/api/widgetdata/multiline'\n    INTEREST_BY_REGION_URL = 'https://trends.google.com/trends/api/widgetdata/comparedgeo'\n    RELATED_QUERIES_URL = 'https://trends.google.com/trends/api/widgetdata/relatedsearches'\n    TRENDING_SEARCHES_URL = 'https://trends.google.com/trends/hottrends/visualize/internal/data'\n    TOP_CHARTS_URL = 'https://trends.google.com/trends/api/topcharts'\n    SUGGESTIONS_URL = 'https://trends.google.com/trends/api/autocomplete/'\n    CATEGORIES_URL = 'https://trends.google.com/trends/api/explore/pickers/category'\n    TODAY_SEARCHES_URL = 'https://trends.google.com/trends/api/dailytrends'\n    ERROR_CODES = (500, 502, 504, 429)\n\n    def __init__(self, hl='en-US', tz=360, geo='', timeout=(2, 5), proxies='',\n                 retries=0, backoff_factor=0, requests_args=None):\n        \"\"\"\n        Initialize default values for params\n        \"\"\"\n        # google rate limit\n        self.google_rl = 'You have reached your quota limit. Please try again later.'\n        self.results = None\n        # set user defined options used globally\n        self.tz = tz\n        self.hl = hl\n        self.geo = geo\n        self.kw_list = list()\n        self.timeout = timeout\n        self.proxies = proxies  # add a proxy option\n        self.retries = retries\n        self.backoff_factor = backoff_factor\n        self.proxy_index = 0\n        self.requests_args = requests_args or {}\n        self.cookies = self.GetGoogleCookie()\n        # intialize widget payloads\n        self.token_payload = dict()\n        self.interest_over_time_widget = dict()\n        self.interest_by_region_widget = dict()\n        self.related_topics_widget_list = list()\n        self.related_queries_widget_list = list()\n\n    def GetGoogleCookie(self):\n        \"\"\"\n        Gets google cookie (used for each and every proxy; once on init otherwise)\n        Removes proxy from the list on proxy error\n        \"\"\"\n        while True:\n            if \"proxies\" in self.requests_args:\n                try:\n                    return dict(filter(lambda i: i[0] == 'NID', requests.get(\n                        'https://trends.google.com/?geo={geo}'.format(\n                            geo=self.hl[-2:]),\n                        timeout=self.timeout,\n                        **self.requests_args\n                    ).cookies.items()))\n                except:\n                    continue\n            else:\n                if len(self.proxies) > 0:\n                    proxy = {'https': self.proxies[self.proxy_index]}\n                else:\n                    proxy = ''\n                try:\n                    return dict(filter(lambda i: i[0] == 'NID', requests.get(\n                        'https://trends.google.com/?geo={geo}'.format(\n                            geo=self.hl[-2:]),\n                        timeout=self.timeout,\n                        proxies=proxy,\n                        **self.requests_args\n                    ).cookies.items()))\n                except requests.exceptions.ProxyError:\n                    print('Proxy error. Changing IP')\n                    if len(self.proxies) > 1:\n                        self.proxies.remove(self.proxies[self.proxy_index])\n                    else:\n                        print('No more proxies available. Bye!')\n                        raise\n                    continue\n\n    def GetNewProxy(self):\n        \"\"\"\n        Increment proxy INDEX; zero on overflow\n        \"\"\"\n        if self.proxy_index < (len(self.proxies) - 1):\n            self.proxy_index += 1\n        else:\n            self.proxy_index = 0\n\n    def _get_data(self, url, method=GET_METHOD, trim_chars=0, **kwargs):\n        \"\"\"Send a request to Google and return the JSON response as a Python object\n        :param url: the url to which the request will be sent\n        :param method: the HTTP method ('get' or 'post')\n        :param trim_chars: how many characters should be trimmed off the beginning of the content of the response\n            before this is passed to the JSON parser\n        :param kwargs: any extra key arguments passed to the request builder (usually query parameters or data)\n        :return:\n        \"\"\"\n        s = requests.session()\n        # Retries mechanism. Activated when one of statements >0 (best used for proxy)\n        if self.retries > 0 or self.backoff_factor > 0:\n            retry = Retry(total=self.retries, read=self.retries,\n                          connect=self.retries,\n                          backoff_factor=self.backoff_factor,\n                          status_forcelist=TrendReq.ERROR_CODES,\n                          method_whitelist=frozenset(['GET', 'POST']))\n            s.mount('https://', HTTPAdapter(max_retries=retry))\n\n        s.headers.update({'accept-language': self.hl})\n        if len(self.proxies) > 0:\n            self.cookies = self.GetGoogleCookie()\n            s.proxies.update({'https': self.proxies[self.proxy_index]})\n        if method == TrendReq.POST_METHOD:\n            response = s.post(url, timeout=self.timeout,\n                              cookies=self.cookies, **kwargs,\n                              **self.requests_args)  # DO NOT USE retries or backoff_factor here\n        else:\n            response = s.get(url, timeout=self.timeout, cookies=self.cookies,\n                             **kwargs, **self.requests_args)  # DO NOT USE retries or backoff_factor here\n        # check if the response contains json and throw an exception otherwise\n        # Google mostly sends 'application/json' in the Content-Type header,\n        # but occasionally it sends 'application/javascript\n        # and sometimes even 'text/javascript\n        if response.status_code == 200 and 'application/json' in \\\n                response.headers['Content-Type'] or \\\n                'application/javascript' in response.headers['Content-Type'] or \\\n                'text/javascript' in response.headers['Content-Type']:\n            # trim initial characters\n            # some responses start with garbage characters, like \")]}',\"\n            # these have to be cleaned before being passed to the json parser\n            content = response.text[trim_chars:]\n            # parse json\n            self.GetNewProxy()\n            return json.loads(content)\n        else:\n            # error\n            raise exceptions.ResponseError(\n                'The request failed: Google returned a '\n                'response with code {0}.'.format(response.status_code),\n                response=response)\n\n    def build_payload(self, kw_list, cat=0, timeframe='today 5-y', geo='',\n                      gprop=''):\n        \"\"\"Create the payload for related queries, interest over time and interest by region\"\"\"\n        if gprop not in ['', 'images', 'news', 'youtube', 'froogle']:\n            raise ValueError('gprop must be empty (to indicate web), images, news, youtube, or froogle')\n        self.kw_list = kw_list\n        self.geo = geo or self.geo\n        self.token_payload = {\n            'hl': self.hl,\n            'tz': self.tz,\n            'req': {'comparisonItem': [], 'category': cat, 'property': gprop}\n        }\n\n        # build out json for each keyword\n        for kw in self.kw_list:\n            keyword_payload = {'keyword': kw, 'time': timeframe,\n                               'geo': self.geo}\n            self.token_payload['req']['comparisonItem'].append(keyword_payload)\n        # requests will mangle this if it is not a string\n        self.token_payload['req'] = json.dumps(self.token_payload['req'])\n        # get tokens\n        self._tokens()\n        return\n\n    def _tokens(self):\n        \"\"\"Makes request to Google to get API tokens for interest over time, interest by region and related queries\"\"\"\n        # make the request and parse the returned json\n        widget_dicts = self._get_data(\n            url=TrendReq.GENERAL_URL,\n            method=TrendReq.GET_METHOD,\n            params=self.token_payload,\n            trim_chars=4,\n        )['widgets']\n        # order of the json matters...\n        first_region_token = True\n        # clear self.related_queries_widget_list and self.related_topics_widget_list\n        # of old keywords'widgets\n        self.related_queries_widget_list[:] = []\n        self.related_topics_widget_list[:] = []\n        # assign requests\n        for widget in widget_dicts:\n            if widget['id'] == 'TIMESERIES':\n                self.interest_over_time_widget = widget\n            if widget['id'] == 'GEO_MAP' and first_region_token:\n                self.interest_by_region_widget = widget\n                first_region_token = False\n            # response for each term, put into a list\n            if 'RELATED_TOPICS' in widget['id']:\n                self.related_topics_widget_list.append(widget)\n            if 'RELATED_QUERIES' in widget['id']:\n                self.related_queries_widget_list.append(widget)\n        return\n\n    def interest_over_time(self):\n        \"\"\"Request data from Google's Interest Over Time section and return a dataframe\"\"\"\n\n        over_time_payload = {\n            # convert to string as requests will mangle\n            'req': json.dumps(self.interest_over_time_widget['request']),\n            'token': self.interest_over_time_widget['token'],\n            'tz': self.tz\n        }\n\n        # make the request and parse the returned json\n        req_json = self._get_data(\n            url=TrendReq.INTEREST_OVER_TIME_URL,\n            method=TrendReq.GET_METHOD,\n            trim_chars=5,\n            params=over_time_payload,\n        )\n\n        df = pd.DataFrame(req_json['default']['timelineData'])\n        if (df.empty):\n            return df\n\n        df['date'] = pd.to_datetime(df['time'].astype(dtype='float64'),\n                                    unit='s')\n        df = df.set_index(['date']).sort_index()\n        # split list columns into seperate ones, remove brackets and split on comma\n        result_df = df['value'].apply(lambda x: pd.Series(\n            str(x).replace('[', '').replace(']', '').split(',')))\n        # rename each column with its search term, relying on order that google provides...\n        for idx, kw in enumerate(self.kw_list):\n            # there is currently a bug with assigning columns that may be\n            # parsed as a date in pandas: use explicit insert column method\n            result_df.insert(len(result_df.columns), kw,\n                             result_df[idx].astype('int'))\n            del result_df[idx]\n\n        if 'isPartial' in df:\n            # make other dataframe from isPartial key data\n            # split list columns into seperate ones, remove brackets and split on comma\n            df = df.fillna(False)\n            result_df2 = df['isPartial'].apply(lambda x: pd.Series(\n                str(x).replace('[', '').replace(']', '').split(',')))\n            result_df2.columns = ['isPartial']\n            # Change to a bool type.\n            result_df2.isPartial = result_df2.isPartial == 'True'\n            # concatenate the two dataframes\n            final = pd.concat([result_df, result_df2], axis=1)\n        else:\n            final = result_df\n            final['isPartial'] = False\n\n        return final\n\n    def interest_by_region(self, resolution='COUNTRY', inc_low_vol=False,\n                           inc_geo_code=False):\n        \"\"\"Request data from Google's Interest by Region section and return a dataframe\"\"\"\n\n        # make the request\n        region_payload = dict()\n        if self.geo == '':\n            self.interest_by_region_widget['request'][\n                'resolution'] = resolution\n        elif self.geo == 'US' and resolution in ['DMA', 'CITY', 'REGION']:\n            self.interest_by_region_widget['request'][\n                'resolution'] = resolution\n\n        self.interest_by_region_widget['request'][\n            'includeLowSearchVolumeGeos'] = inc_low_vol\n\n        # convert to string as requests will mangle\n        region_payload['req'] = json.dumps(\n            self.interest_by_region_widget['request'])\n        region_payload['token'] = self.interest_by_region_widget['token']\n        region_payload['tz'] = self.tz\n\n        # parse returned json\n        req_json = self._get_data(\n            url=TrendReq.INTEREST_BY_REGION_URL,\n            method=TrendReq.GET_METHOD,\n            trim_chars=5,\n            params=region_payload,\n        )\n        df = pd.DataFrame(req_json['default']['geoMapData'])\n        if (df.empty):\n            return df\n\n        # rename the column with the search keyword\n        df = df[['geoName', 'geoCode', 'value']].set_index(\n            ['geoName']).sort_index()\n        # split list columns into separate ones, remove brackets and split on comma\n        result_df = df['value'].apply(lambda x: pd.Series(\n            str(x).replace('[', '').replace(']', '').split(',')))\n        if inc_geo_code:\n            result_df['geoCode'] = df['geoCode']\n\n        # rename each column with its search term\n        for idx, kw in enumerate(self.kw_list):\n            result_df[kw] = result_df[idx].astype('int')\n            del result_df[idx]\n\n        return result_df\n\n    def related_topics(self):\n        \"\"\"Request data from Google's Related Topics section and return a dictionary of dataframes\n\n        If no top and/or rising related topics are found, the value for the key \"top\" and/or \"rising\" will be None\n        \"\"\"\n\n        # make the request\n        related_payload = dict()\n        result_dict = dict()\n        for request_json in self.related_topics_widget_list:\n            # ensure we know which keyword we are looking at rather than relying on order\n            kw = request_json['request']['restriction'][\n                'complexKeywordsRestriction']['keyword'][0]['value']\n            # convert to string as requests will mangle\n            related_payload['req'] = json.dumps(request_json['request'])\n            related_payload['token'] = request_json['token']\n            related_payload['tz'] = self.tz\n\n            # parse the returned json\n            req_json = self._get_data(\n                url=TrendReq.RELATED_QUERIES_URL,\n                method=TrendReq.GET_METHOD,\n                trim_chars=5,\n                params=related_payload,\n            )\n\n            # top topics\n            try:\n                top_list = req_json['default']['rankedList'][0][\n                    'rankedKeyword']\n                df_top = pd.DataFrame(\n                    [nested_to_record(d, sep='_') for d in top_list])\n            except KeyError:\n                # in case no top topics are found, the lines above will throw a KeyError\n                df_top = None\n\n            # rising topics\n            try:\n                rising_list = req_json['default']['rankedList'][1][\n                    'rankedKeyword']\n                df_rising = pd.DataFrame(\n                    [nested_to_record(d, sep='_') for d in rising_list])\n            except KeyError:\n                # in case no rising topics are found, the lines above will throw a KeyError\n                df_rising = None\n\n            result_dict[kw] = {'rising': df_rising, 'top': df_top}\n        return result_dict\n\n    def related_queries(self):\n        \"\"\"Request data from Google's Related Queries section and return a dictionary of dataframes\n\n        If no top and/or rising related queries are found, the value for the key \"top\" and/or \"rising\" will be None\n        \"\"\"\n\n        # make the request\n        related_payload = dict()\n        result_dict = dict()\n        for request_json in self.related_queries_widget_list:\n            # ensure we know which keyword we are looking at rather than relying on order\n            kw = request_json['request']['restriction'][\n                'complexKeywordsRestriction']['keyword'][0]['value']\n            # convert to string as requests will mangle\n            related_payload['req'] = json.dumps(request_json['request'])\n            related_payload['token'] = request_json['token']\n            related_payload['tz'] = self.tz\n\n            # parse the returned json\n            req_json = self._get_data(\n                url=TrendReq.RELATED_QUERIES_URL,\n                method=TrendReq.GET_METHOD,\n                trim_chars=5,\n                params=related_payload,\n            )\n\n            # top queries\n            try:\n                top_df = pd.DataFrame(\n                    req_json['default']['rankedList'][0]['rankedKeyword'])\n                top_df = top_df[['query', 'value']]\n            except KeyError:\n                # in case no top queries are found, the lines above will throw a KeyError\n                top_df = None\n\n            # rising queries\n            try:\n                rising_df = pd.DataFrame(\n                    req_json['default']['rankedList'][1]['rankedKeyword'])\n                rising_df = rising_df[['query', 'value']]\n            except KeyError:\n                # in case no rising queries are found, the lines above will throw a KeyError\n                rising_df = None\n\n            result_dict[kw] = {'top': top_df, 'rising': rising_df}\n        return result_dict\n\n    def trending_searches(self, pn='united_states'):\n        \"\"\"Request data from Google's Hot Searches section and return a dataframe\"\"\"\n\n        # make the request\n        # forms become obsolete due to the new TRENDING_SEARCHES_URL\n        # forms = {'ajax': 1, 'pn': pn, 'htd': '', 'htv': 'l'}\n        req_json = self._get_data(\n            url=TrendReq.TRENDING_SEARCHES_URL,\n            method=TrendReq.GET_METHOD\n        )[pn]\n        result_df = pd.DataFrame(req_json)\n        return result_df\n\n    def today_searches(self, pn='US'):\n        \"\"\"Request data from Google Daily Trends section and returns a dataframe\"\"\"\n        forms = {'ns': 15, 'geo': pn, 'tz': '-180', 'hl': 'en-US'}\n        req_json = self._get_data(\n            url=TrendReq.TODAY_SEARCHES_URL,\n            method=TrendReq.GET_METHOD,\n            trim_chars=5,\n            params=forms,\n            **self.requests_args\n        )['default']['trendingSearchesDays'][0]['trendingSearches']\n        result_df = pd.DataFrame()\n        # parse the returned json\n        sub_df = pd.DataFrame()\n        for trend in req_json:\n            sub_df = sub_df.append(trend['title'], ignore_index=True)\n        result_df = pd.concat([result_df, sub_df])\n        return result_df.iloc[:, -1]\n\n    def top_charts(self, date, hl='en-US', tz=300, geo='GLOBAL'):\n        \"\"\"Request data from Google's Top Charts section and return a dataframe\"\"\"\n\n        try:\n            date = int(date)\n        except:\n            raise ValueError(\n                'The date must be a year with format YYYY. See https://github.com/GeneralMills/pytrends/issues/355')\n\n        # create the payload\n        chart_payload = {'hl': hl, 'tz': tz, 'date': date, 'geo': geo,\n                         'isMobile': False}\n\n        # make the request and parse the returned json\n        req_json = self._get_data(\n            url=TrendReq.TOP_CHARTS_URL,\n            method=TrendReq.GET_METHOD,\n            trim_chars=5,\n            params=chart_payload,\n            **self.requests_args\n        )\n        try:\n            df = pd.DataFrame(req_json['topCharts'][0]['listItems'])\n        except IndexError:\n            df = None\n        return df\n\n    def suggestions(self, keyword):\n        \"\"\"Request data from Google's Keyword Suggestion dropdown and return a dictionary\"\"\"\n\n        # make the request\n        kw_param = quote(keyword)\n        parameters = {'hl': self.hl}\n\n        req_json = self._get_data(\n            url=TrendReq.SUGGESTIONS_URL + kw_param,\n            params=parameters,\n            method=TrendReq.GET_METHOD,\n            trim_chars=5,\n            **self.requests_args\n        )['default']['topics']\n        return req_json\n\n    def categories(self):\n        \"\"\"Request available categories data from Google's API and return a dictionary\"\"\"\n\n        params = {'hl': self.hl}\n\n        req_json = self._get_data(\n            url=TrendReq.CATEGORIES_URL,\n            params=params,\n            method=TrendReq.GET_METHOD,\n            trim_chars=5,\n            **self.requests_args\n        )\n        return req_json\n\n    def get_historical_interest(self, keywords, year_start=2018, month_start=1,\n                                day_start=1, hour_start=0, year_end=2018,\n                                month_end=2, day_end=1, hour_end=0, cat=0,\n                                geo='', gprop='', sleep=0):\n        \"\"\"Gets historical hourly data for interest by chunking requests to 1 week at a time (which is what Google allows)\"\"\"\n\n        # construct datetime objects - raises ValueError if invalid parameters\n        initial_start_date = start_date = datetime(year_start, month_start,\n                                                   day_start, hour_start)\n        end_date = datetime(year_end, month_end, day_end, hour_end)\n\n        # the timeframe has to be in 1 week intervals or Google will reject it\n        delta = timedelta(days=7)\n\n        df = pd.DataFrame()\n\n        date_iterator = start_date\n        date_iterator += delta\n\n        while True:\n            # format date to comply with API call\n\n            start_date_str = start_date.strftime('%Y-%m-%dT%H')\n            date_iterator_str = date_iterator.strftime('%Y-%m-%dT%H')\n\n            tf = start_date_str + ' ' + date_iterator_str\n\n            try:\n                self.build_payload(keywords, cat, tf, geo, gprop)\n                week_df = self.interest_over_time()\n                df = df.append(week_df)\n            except Exception as e:\n                print(e)\n                pass\n\n            start_date += delta\n            date_iterator += delta\n\n            if (date_iterator > end_date):\n                # Run for 7 more days to get remaining data that would have been truncated if we stopped now\n                # This is needed because google requires 7 days yet we may end up with a week result less than a full week\n                start_date_str = start_date.strftime('%Y-%m-%dT%H')\n                date_iterator_str = date_iterator.strftime('%Y-%m-%dT%H')\n\n                tf = start_date_str + ' ' + date_iterator_str\n\n                try:\n                    self.build_payload(keywords, cat, tf, geo, gprop)\n                    week_df = self.interest_over_time()\n                    df = df.append(week_df)\n                except Exception as e:\n                    print(e)\n                    pass\n                break\n\n            # just in case you are rate-limited by Google. Recommended is 60 if you are.\n            if sleep > 0:\n                time.sleep(sleep)\n\n        # Return the dataframe with results from our timeframe\n        return df.loc[initial_start_date:end_date]\n"
  },
  {
    "path": "003-Keywords/mypytrends/test_trendReq.py",
    "content": "from unittest import TestCase\nimport pandas.api.types as ptypes\n\nfrom mypytrends.request import TrendReq\n\n\nclass TestTrendReq(TestCase):\n\n    def test__get_data(self):\n        \"\"\"Should use same values as in the documentation\"\"\"\n        pytrend = TrendReq()\n        self.assertEqual(pytrend.hl, 'en-US')\n        self.assertEqual(pytrend.tz, 360)\n        self.assertEqual(pytrend.geo, '')\n        self.assertTrue(pytrend.cookies['NID'])\n\n    def test_build_payload(self):\n        \"\"\"Should return the widgets to get data\"\"\"\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.token_payload)\n\n    def test__tokens(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.related_queries_widget_list)\n\n    def test_interest_over_time(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.interest_over_time())\n\n    def test_interest_over_time_images(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='images')\n        self.assertIsNotNone(pytrend.interest_over_time())\n\n    def test_interest_over_time_news(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='news')\n        self.assertIsNotNone(pytrend.interest_over_time())\n\n    def test_interest_over_time_youtube(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='youtube')\n        self.assertIsNotNone(pytrend.interest_over_time())\n\n    def test_interest_over_time_froogle(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='froogle')\n        self.assertIsNotNone(pytrend.interest_over_time())\n\n    def test_interest_over_time_bad_gprop(self):\n        pytrend = TrendReq()\n        with self.assertRaises(ValueError):\n            pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop=' ')\n\n    def test_interest_by_region(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.interest_by_region())\n\n    def test_related_topics(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.related_topics())\n\n    def test_related_queries(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.related_queries())\n\n    def test_trending_searches(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.trending_searches())\n\n    def test_top_charts(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.top_charts(date=2019))\n\n    def test_suggestions(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        self.assertIsNotNone(pytrend.suggestions(keyword='pizza'))\n\n    def test_ispartial_dtype(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'])\n        df = pytrend.interest_over_time()\n        assert ptypes.is_bool_dtype(df.isPartial)\n\n    def test_ispartial_dtype_timeframe_all(self):\n        pytrend = TrendReq()\n        pytrend.build_payload(kw_list=['pizza', 'bagel'],\n                              timeframe='all')\n        df = pytrend.interest_over_time()\n        assert ptypes.is_bool_dtype(df.isPartial)\n"
  },
  {
    "path": "003-Keywords/run_api.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 通过框架自带命令 启动命令脚本\n@Date       :2021/09/20\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nfrom scrapy.crawler import CrawlerProcess\nfrom scrapy.utils.log import configure_logging\nfrom scrapy.utils.project import get_project_settings\n\nfrom amazon.spiders.alibaba import AlibabaSpider\nfrom amazon.spiders.amazon import AmazonSpider\nfrom amazon.spiders.checkip import CheckIpSpider\n\nsettings = get_project_settings()\nconfigure_logging(settings)\ncrawler = CrawlerProcess(settings)\n\n\ndef check_ip():\n    spider = CheckIpSpider\n    crawler.crawl(spider)\n    crawler.start()\n    return spider.ips\n\n\ndef crawl_amazon(keywords: []):\n    spider = AmazonSpider\n    spider.keywords = keywords\n    crawler.crawl(spider)\n    crawler.start()\n    return spider.results\n\n\ndef crawl_alibaba(keywords: []):\n    spider = AlibabaSpider\n    spider.keywords = keywords\n    crawler.crawl(spider)\n    crawler.start()\n"
  },
  {
    "path": "003-Keywords/scrapy.cfg",
    "content": "# Automatically created by: scrapy startproject\n#\n# For more information about the [deploy] section see:\n# https://scrapyd.readthedocs.io/en/latest/deploy.html\n\n[settings]\ndefault = amazon.settings\n\n[deploy]\n#url = http://localhost:6800/\nproject = amazon\n"
  },
  {
    "path": "003-Keywords/v2ray_pool/__init__.py",
    "content": "# 运行时路径。并非__init__.py的路径\nimport os\nimport sys\n\nBASE_DIR = \"../002-V2rayPool\"\nif os.path.exists(BASE_DIR):\n    sys.path.append(BASE_DIR)\n\nfrom core import utils\nfrom core.conf import Config\nfrom core.client import Creator\nfrom db.db_main import DBManage\nfrom base.net_proxy import Net"
  },
  {
    "path": "003-Keywords/v2ray_pool/_db-checked.txt",
    "content": "ss://YWVzLTI1Ni1nY206ZzVNZUQ2RnQzQ1dsSklkQDE0Mi4yMDIuNDguMTA4OjUwMDM#%E7%BE%8E%E5%9B%BD-2.48MB/s,142.202.48.108,加拿大  魁北克 魁北克\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3QDM4LjY4LjEzNC4xOTE6MjM3NQ#%E7%BE%8E%E5%9B%BD-1.81MB/s(Youtube:%E4%B8%8D%E8%89%AF%E6%9E%97),38.68.134.191,美国  新泽西  科进\nss://YWVzLTI1Ni1nY206ZzVNZUQ2RnQzQ1dsSklkQDE2OS4xOTcuMTQzLjE1Nzo1MDA0#%E7%BE%8E%E5%9B%BD-988.6KB/s(Youtube:%E4%B8%8D%E8%89%AF%E6%9E%97),169.197.143.157,美国  佐治亚 亚特兰大\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3QDM4LjEyMS40My43MToyMzc2#%E7%BE%8E%E5%9B%BD-2.82MB/s,38.121.43.71,美国  加利福尼亚  科进\nss://YWVzLTI1Ni1nY206Rm9PaUdsa0FBOXlQRUdQ@167.88.61.204:7307#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%203,167.88.61.204,瑞典  斯德哥尔摩\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@38.68.134.202:2376#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%205,38.68.134.202,美国  新泽西  科进\nss://YWVzLTI1Ni1nY206ZzVNZUQ2RnQzQ1dsSklk@38.68.134.23:5003#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%206,38.68.134.23,美国  新泽西  科进\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@38.91.101.11:2375#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%207,38.91.101.11,美国  纽约 纽约 科进\nss://YWVzLTI1Ni1nY206Rm9PaUdsa0FBOXlQRUdQ@142.202.48.52:7306#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%20%208,142.202.48.52,加拿大  魁北克 魁北克\nss://YWVzLTI1Ni1nY206Rm9PaUdsa0FBOXlQRUdQ@142.202.48.34:7307#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%20%2010,142.202.48.34,加拿大  魁北克 魁北克\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@38.75.136.45:2376#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%2011,38.75.136.45,美国    科进\nss://YWVzLTI1Ni1nY206WTZSOXBBdHZ4eHptR0M@38.143.66.71:3389#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%2012,38.143.66.71,美国    科进\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t1.ssrsub.com:8443#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%20%2014,142.47.89.64,加拿大  安大略 多伦多\nss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@145.239.1.137:8091#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2015,51.38.122.98,德国  Hessen\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t2.ssrsub.com:8443#github.com/freefq%20-%20%E4%BF%84%E7%BD%97%E6%96%AF%20%2020,195.133.53.209,俄罗斯  莫斯科 莫斯科\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t7.ssrsub.com:8443#github.com/freefq%20-%20%E4%BF%84%E7%BD%97%E6%96%AF%E8%8E%AB%E6%96%AF%E7%A7%91Relcom%E7%BD%91%E7%BB%9C%2022,194.87.238.109,俄罗斯  莫斯科 莫斯科\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t6.ssrsub.com:8443#github.com/freefq%20-%20%E4%BF%84%E7%BD%97%E6%96%AF%20%2023,195.133.48.9,俄罗斯  莫斯科 莫斯科\nss://YWVzLTI1Ni1nY206Y2RCSURWNDJEQ3duZklO@167.88.61.60:8118#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%2025,167.88.61.60,瑞典  斯德哥尔摩\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@169.197.142.39:2375#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%2026,169.197.142.39,美国  佐治亚 亚特兰大\nss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@172.99.190.87:9101#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2030,172.99.190.87,美国  康涅狄格\nss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpzRjQzWHQyZ09OcWNnRlg1NjM@141.95.0.26:826#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2031,54.38.217.138,美国  新泽西\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t3.ssrsub.com:8443#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%E5%AE%89%E5%A4%A7%E7%95%A5%E7%9C%81%E5%9F%BA%E5%A5%87%E7%BA%B3DataCity%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83%2032,45.62.247.153,加拿大  安大略 基奇纳\nvmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIkBTU1JTVUItVjI4LeS7mOi0ueaOqOiNkDpzdW8ueXQvc3Nyc3ViIiwNCiAgImFkZCI6ICI0Mi4xNTcuOC4xNjIiLA0KICAicG9ydCI6ICI1MDAwMiIsDQogICJpZCI6ICI0MTgwNDhhZi1hMjkzLTRiOTktOWIwYy05OGNhMzU4MGRkMjQiLA0KICAiYWlkIjogIjY0IiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiNDIuMTU3LjguMTYyIiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiINCn0=,42.157.8.162,中国  安徽省 合肥市 联通\nvmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogImh0dHBzOi8vZ2l0aHViLmNvbS9BbHZpbjk5OTkvbmV3LXBhYy93aWtpIOS/hOe9l+aWr2cyIiwNCiAgImFkZCI6ICIxOTUuMTMzLjUzLjg4IiwNCiAgInBvcnQiOiAiMjU5NjQiLA0KICAiaWQiOiAiYmE5M2U1NmMtNzdmOS0xMWVjLWFmNDMtZDJlOGI0YzNhNzVhIiwNCiAgImFpZCI6ICIwIiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiIsDQogICJhbHBuIjogIiINCn0=,195.133.53.88,俄罗斯  莫斯科 莫斯科\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzE4IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwMyIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiJhaWNvbzZkdS5jb20iLCJwYXRoIjoiL3dzIiwidGxzIjoiIiwic25pIjoiIn0=,114.43.130.6,中国  台湾省  中华电信\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzI1IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii9zL2QwYTg2OTIuZm0uYXBwbGUuY29tOjMyNjY3IiwidGxzIjoiIiwic25pIjoiIn0=,172.70.214.120,美国  加利福尼亚 旧金山\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzI2IiwiYWRkIjoic2hjdTAxLmlwbGMxODguY29tIiwicG9ydCI6IjEwMDA0IiwiaWQiOiI2NWNhYzU2ZC00MTU1LTQzYzgtYmFlMC1mMzY4Y2IyMWY3NzEiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6InRjcCIsInR5cGUiOiJub25lIiwiaG9zdCI6InNoY3UwMS5pcGxjMTg4LmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiIiLCJzbmkiOiIifQ==,210.71.214.218,中国  台湾省  中华电信\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzI4IiwiYWRkIjoiNDIuMTU3LjguNTIiLCJwb3J0IjoiNDg3MjciLCJpZCI6IjU3YWE1YWMzLWQxZDAtNGUyZi1iMzJlLTY0ODhkNWE3Y2I0NSIsImFpZCI6IjY0Iiwic2N5IjoiYXV0byIsIm5ldCI6InRjcCIsInR5cGUiOiJub25lIiwiaG9zdCI6IiIsInBhdGgiOiIiLCJ0bHMiOiIiLCJzbmkiOiIifQ==,42.157.8.52,中国  安徽省 合肥市 联通\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzMzIiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxNDYuNTYuMTc3LjM0IiwicGF0aCI6Ii92MnJheSIsInRscyI6IiIsInNuaSI6IiJ9,172.70.211.7,美国  加利福尼亚 旧金山\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM0IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii93cyIsInRscyI6IiIsInNuaSI6IiJ9,172.70.210.228,美国  加利福尼亚 旧金山\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM1IiwiYWRkIjoiNDIuMTkzLjQ4LjY0IiwicG9ydCI6IjUwMDAyIiwiaWQiOiI0MTgwNDhhZi1hMjkzLTRiOTktOWIwYy05OGNhMzU4MGRkMjQiLCJhaWQiOiI2NCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiI0Mi4xOTMuNDguNjQiLCJwYXRoIjoiIiwidGxzIjoiIiwic25pIjoiIn0=,42.193.48.64,中国  上海 上海市 电信\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM3IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii8iLCJ0bHMiOiIiLCJzbmkiOiIifQ==,172.70.206.182,美国  加利福尼亚 旧金山\nvmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM5IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii9zLzUzMTg0NDIuZm0uYXBwbGUuY29tOjU0MDgwIiwidGxzIjoiIiwic25pIjoiIn0=,172.69.33.158,美国  加利福尼亚"
  },
  {
    "path": "003-Keywords/v2ray_pool/_db-uncheck.txt",
    "content": "vmess://eyJ2IjogIjIiLCAicHMiOiAiZ2l0aHViLmNvbS9mcmVlZnEgLSBcdTViODlcdTVmYmRcdTc3MDFcdTgwNTRcdTkwMWEgMSIsICJhZGQiOiAiNDIuMTU3LjguMTYyIiwgInBvcnQiOiAiNTAwMDIiLCAiaWQiOiAiNDE4MDQ4YWYtYTI5My00Yjk5LTliMGMtOThjYTM1ODBkZDI0IiwgImFpZCI6ICI2NCIsICJzY3kiOiAiYXV0byIsICJuZXQiOiAidGNwIiwgInR5cGUiOiAibm9uZSIsICJob3N0IjogIiIsICJwYXRoIjogIiIsICJ0bHMiOiAiIiwgInNuaSI6ICIifQ==,42.157.8.162,中国  安徽省 合肥市 联通\nss://YWVzLTI1Ni1nY206ZzVNZUQ2RnQzQ1dsSklk@38.86.135.27:5003#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%204,38.86.135.27,美国    科进\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@169.197.142.39:2375#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%205,169.197.142.39,美国  佐治亚 亚特兰大\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t4.ssrsub.com:8443#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%E5%AE%89%E5%A4%A7%E7%95%A5%E7%9C%81%E5%9F%BA%E5%A5%87%E7%BA%B3DataCity%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83%206,45.62.250.251,加拿大  安大略 基奇纳\nss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@46.29.218.6:2376#github.com/freefq%20-%20%E6%8C%AA%E5%A8%81%20%207,46.29.218.6,挪威\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t3.ssrsub.com:8443#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%E5%AE%89%E5%A4%A7%E7%95%A5%E7%9C%81%E5%9F%BA%E5%A5%87%E7%BA%B3DataCity%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83%2011,45.62.247.153,加拿大  安大略 基奇纳\nss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@172.99.190.87:9101#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2012,172.99.190.87,美国  康涅狄格\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t6.ssrsub.com:8443#github.com/freefq%20-%20%E4%BF%84%E7%BD%97%E6%96%AF%20%2013,195.133.48.9,俄罗斯  莫斯科 莫斯科\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t7.ssrsub.com:8443#github.com/freefq%20-%20%E4%BF%84%E7%BD%97%E6%96%AF%E8%8E%AB%E6%96%AF%E7%A7%91Relcom%E7%BD%91%E7%BB%9C%2022,194.87.238.109,俄罗斯  莫斯科 莫斯科\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t1.ssrsub.com:8443#github.com/freefq%20-%20%E5%8A%A0%E6%8B%BF%E5%A4%A7%20%2023,142.47.89.64,加拿大  安大略 多伦多\nss://YWVzLTI1Ni1nY206cEtFVzhKUEJ5VFZUTHRN@134.195.196.12:443#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%2024,134.195.196.12,美国\nss://YWVzLTI1Ni1nY206cEtFVzhKUEJ5VFZUTHRN@38.143.66.71:443#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%E5%8D%8E%E7%9B%9B%E9%A1%BFCogent%E9%80%9A%E4%BF%A1%E5%85%AC%E5%8F%B8%2026,38.143.66.71,美国    科进\nss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@145.239.1.137:8091#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2029,51.38.122.98,德国  Hessen\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t8.ssrsub.com:8443#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2030,152.69.200.26,美国  加利福尼亚\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t2.ssrsub.com:8443#github.com/freefq%20-%20%E4%BF%84%E7%BD%97%E6%96%AF%20%2032,195.133.53.209,俄罗斯  莫斯科 莫斯科\nss://YWVzLTI1Ni1nY206WTZSOXBBdHZ4eHptR0M@167.88.61.60:5601#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%2034,167.88.61.60,瑞典  斯德哥尔摩\nss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpzRjQzWHQyZ09OcWNnRlg1NjM@141.95.0.26:826#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2037,54.38.217.138,美国  新泽西\ntrojan://64c3ab43-dc1b-401c-9437-9adf7bcf4a28@t9.ssrsub.com:8443#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2038,150.230.215.123,美国  加利福尼亚\nvmess://eyJ2IjogIjIiLCAicHMiOiAiZ2l0aHViLmNvbS9mcmVlZnEgLSBcdTRlMGFcdTZkNzdcdTVlMDJcdTVmOTBcdTZjNDdcdTUzM2FcdTgwNTRcdTkwMWFcdTZmMTVcdTZjYjNcdTZjZmVcdTY1NzBcdTYzNmVcdTRlMmRcdTVmYzMgNDAiLCAiYWRkIjogInNoY3UwMS5pcGxjMTg4LmNvbSIsICJwb3J0IjogIjEwMDA0IiwgImlkIjogIjY1Y2FjNTZkLTQxNTUtNDNjOC1iYWUwLWYzNjhjYjIxZjc3MSIsICJhaWQiOiAiMSIsICJzY3kiOiAiYXV0byIsICJuZXQiOiAidGNwIiwgInR5cGUiOiAibm9uZSIsICJob3N0IjogInNoY3UwMS5pcGxjMTg4LmNvbSIsICJwYXRoIjogIiIsICJ0bHMiOiAiIiwgInNuaSI6ICIifQ==,61.216.19.199,中国  台湾省  中华电信\n"
  },
  {
    "path": "003-Keywords/v2ray_util.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 管理v2ray_pool的工具\n@Date       :2022/1/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport time\n\nfrom v2ray_pool import utils, Config, DBManage\n\n\ndef search_node():\n    # 如果有系统全局代理，可不需要开启v2ray_core代理，GoogleTrend(proxies=False)\n    utils.kill_all_v2ray()\n    Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64')  # v2ray内核存放路径\n    Config.set_v2ray_node_path(\n        '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool')  # 保存获取到节点的路径\n    proxy_url = 'ss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@145.239.1.137:8091#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%207'\n    dbm = DBManage()\n    dbm.init()  # 必须初始化\n    # if dbm.check_url_single(proxy_url):\n    #     urls = dbm.load_urls_by_net(proxy_url=proxy_url)\n    #     dbm.check_and_save(urls, append=False)\n    dbm.load_urls_and_save_auto()\n    # urls = dbm.load_unchecked_urls_by_local()\n    # dbm.check_and_save(urls, append=False)\n    utils.kill_all_v2ray()\n\n\ndef restart_v2ray(isSysOn=False):\n    utils.kill_all_v2ray()\n    Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64')  # v2ray内核存放路径\n    Config.set_v2ray_node_path(\n        '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool')  # 保存获取到节点的路径\n    dbm = DBManage()\n    dbm.init()  # 必须初始化\n    while 1:\n        if dbm.start_random_v2ray_by_local(isSysOn=isSysOn):\n            break\n        else:\n            print(\"启动失败，进行重试！\")\n            time.sleep(1)\n\n\ndef kill_all_v2ray():\n    utils.kill_all_v2ray()\n"
  },
  {
    "path": "003-Keywords/women-ring/women ring.csv",
    "content": "keyword,no,top keyword,top range,rising keyword,rising range\r\nwomen ring,0,ring for women,100,are men and women ring sizes the same,550\r\nwomen ring,1,gold women ring,40,emerald rings for women,200\r\nwomen ring,2,gold ring,40,ruby ring for women,200\r\nwomen ring,3,ring for women gold,31,gucci rings women,200\r\nwomen ring,4,women rings,31,cartier ring,190\r\nwomen ring,5,rings,30,emerald ring for women,170\r\nwomen ring,6,rings for women,24,thumb ring women,170\r\nwomen ring,7,wedding ring women,20,cartier,170\r\nwomen ring,8,wedding ring,20,macys,170\r\nwomen ring,9,diamond ring,17,pinky rings for women,160\r\nwomen ring,10,diamond,17,moonstone ring,160\r\nwomen ring,11,diamond ring women,16,gold price today,150\r\nwomen ring,12,women ring size,16,versace ring women,140\r\nwomen ring,13,ring size,16,gold rate today,130\r\nwomen ring,14,engagement,14,how to measure ring size,130\r\nwomen ring,15,engagement ring women,14,cartier love ring,120\r\nwomen ring,16,diamond ring for women,14,kay jewelers,110\r\nwomen ring,17,engagement ring,13,thumb ring for women,110\r\nwomen ring,18,wedding ring for women,13,opal ring for women,110\r\nwomen ring,19,engagement ring for women,12,swarovski,110\r\nwomen ring,20,silver ring women,11,diamond ring for women price,100\r\nwomen ring,21,silver ring,11,louis vuitton,100\r\nwomen ring,22,women ring design,11,the bling ring,100\r\nwomen ring,23,ring design,11,how to measure ring size women,100\r\nwomen ring,24,women ring finger,10,eternity ring for women,90\r\n,\r\nring for women,0,gold ring women,100,gold ring for women under 5000,3950\r\nring for women,1,ring for women gold,98,fendi ring,450\r\nring for women,2,gold ring,94,dior ring,350\r\nring for women,3,rings,70,silver rate today,300\r\nring for women,4,rings for women,69,opal rings for women,250\r\nring for women,5,diamond,49,ruby ring for women,250\r\nring for women,6,diamond ring for women,46,pinky rings for women,200\r\nring for women,7,diamond ring,44,emerald ring for women,200\r\nring for women,8,engagement ring,41,ruby rings for women,190\r\nring for women,9,wedding ring for women,41,eternity ring for women,150\r\nring for women,10,engagement,39,solitaire ring for women,150\r\nring for women,11,wedding ring,38,gold pendant,120\r\nring for women,12,engagement ring for women,36,diamond ring price for women,120\r\nring for women,13,ring size for women,28,pearl ring,120\r\nring for women,14,ring design,26,gold ring for women under 10000,110\r\nring for women,15,silver ring,26,thumb ring for women,110\r\nring for women,16,engagement rings,23,pearl ring for women,100\r\nring for women,17,silver ring for women,23,gucci ring,100\r\nring for women,18,ring design for women,23,custom rings for women,100\r\nring for women,19,engagement rings for women,23,kay jewelers,90\r\nring for women,20,ring finger for women,18,amethyst ring for women,90\r\nring for women,21,gold rings for women,18,emerald rings for women,80\r\nring for women,22,gold ring design,18,rose gold rings for women,80\r\nring for women,23,wedding rings,17,opal ring for women,70\r\nring for women,24,ring finger,17,types of rings for women,70\r\n,\r\ngold women ring,0,ring for women gold,100,gold ring for women under 10000,36200\r\ngold women ring,1,ring for women,98,gold price today,350\r\ngold women ring,2,gold rings,24,gold ring for women under 5000,250\r\ngold women ring,3,gold ring design,23,white gold engagement ring for women,130\r\ngold women ring,4,gold rings women,23,ring design for men,110\r\ngold women ring,5,women gold ring design,22,silver ring for women,80\r\ngold women ring,6,women ring design,22,ring design for women,80\r\ngold women ring,7,ring design,22,gold rate today,70\r\ngold women ring,8,gold ring design for women,21,gold ring design for women,60\r\ngold women ring,9,ring design for women,20,ring for women gold,50\r\ngold women ring,10,gold rings for women,20,ring for women,50\r\ngold women ring,11,rings for women,19,gold ring design,50\r\ngold women ring,12,gold ring women price,19,gold ring women price,50\r\ngold women ring,13,gold ring price,19,gold ring for girls,50\r\ngold women ring,14,gold price,18,women ring design,50\r\ngold women ring,15,gold ring for women price,15,white gold ring for women,50\r\ngold women ring,16,diamond ring,12,women gold ring design,40\r\ngold women ring,17,diamond ring for women,11,gold rate,40\r\ngold women ring,18,ring designs for women,9,gold chain for women,40\r\ngold women ring,19,gold ring designs for women,8,,\r\ngold women ring,20,gold engagement ring for women,7,,\r\ngold women ring,21,gold rate,7,,\r\ngold women ring,22,engagement ring for women,7,,\r\ngold women ring,23,gold ring for men,7,,\r\ngold women ring,24,silver ring for women,6,,\r\n,\r\ngold ring,0,diamond,100,nose ring designs in gold for female,750\r\ngold ring,1,gold diamond ring,99,simple gold ring design for female,200\r\ngold ring,2,diamond ring,97,how to measure ring size,170\r\ngold ring,3,white gold ring,93,new gold ring design for female,150\r\ngold ring,4,white gold,90,ear ring design in gold,150\r\ngold ring,5,rose gold ring,81,2 gram gold ring price,130\r\ngold ring,6,rose gold,79,boys gold ring design,130\r\ngold ring,7,gold rings,77,3 gram gold ring,120\r\ngold ring,8,gold ring price,77,today gold price,120\r\ngold ring,9,gold price,77,2 gram gold ring,110\r\ngold ring,10,ring design gold,77,gold ring price in bd,110\r\ngold ring,11,ring design,76,nose ring gold design,100\r\ngold ring,12,rings,74,gold engagement ring designs for female,90\r\ngold ring,13,engagement ring gold,57,today gold rate,80\r\ngold ring,14,engagement ring,56,bulgari ring gold,80\r\ngold ring,15,gold ring men,47,white gold ring for women,80\r\ngold ring,16,gold wedding ring,47,ear ring design gold,80\r\ngold ring,17,wedding ring,46,3 gram gold ring price,70\r\ngold ring,18,mens gold ring,40,ring designs for girls gold,70\r\ngold ring,19,gold band ring,34,white gold nose ring,70\r\ngold ring,20,ring for men gold,32,gold dome ring,70\r\ngold ring,21,women gold ring,31,ring design for women,60\r\ngold ring,22,14k gold ring,30,simple gold ring design,60\r\ngold ring,23,yellow gold ring,30,couple gold ring design,60\r\ngold ring,24,gold nose ring,27,ring light,60\r\n,\r\nring for women gold,0,rings for women,100,gold ring for women under 5000,400\r\nring for women gold,1,gold rings for women,96,gold rate today,200\r\nring for women gold,2,gold ring design for women,90,gold ring for women under 10000,200\r\nring for women gold,3,gold ring design,87,gold ring for girls,170\r\nring for women gold,4,ring design for women,82,white gold ring for women,60\r\nring for women gold,5,gold ring price for women,65,engagement ring for women,50\r\nring for women gold,6,gold price,65,gold ring design for women,50\r\nring for women gold,7,diamond ring,58,tanishq gold ring for women,50\r\nring for women gold,8,diamond ring for women,48,ring design for women,50\r\nring for women gold,9,engagement ring for women,41,gold ring design,50\r\nring for women gold,10,ring designs for women,38,,\r\nring for women gold,11,gold ring designs for women,37,,\r\nring for women gold,12,white gold ring for women,33,,\r\nring for women gold,13,wedding ring for women,30,,\r\nring for women gold,14,gold rate,29,,\r\nring for women gold,15,white gold ring,29,,\r\nring for women gold,16,gold ring for men,28,,\r\nring for women gold,17,gold engagement rings for women,26,,\r\nring for women gold,18,engagement rings for women,24,,\r\nring for women gold,19,tanishq gold ring for women,24,,\r\nring for women gold,20,tanishq,24,,\r\nring for women gold,21,gold rate today,23,,\r\nring for women gold,22,silver ring for women,22,,\r\nring for women gold,23,gold earrings for women,22,,\r\nring for women gold,24,gold chain,21,,\r\n,\r\nwomen rings,0,rings for women,100,diamond engagement rings for women with price,1750\r\nwomen rings,1,engagement,25,turquoise rings for women,1300\r\nwomen rings,2,engagement rings,25,adjustable rings for women,250\r\nwomen rings,3,women engagement rings,25,gucci ring,250\r\nwomen rings,4,wedding rings,24,personalized rings for women,200\r\nwomen rings,5,wedding rings women,24,fendi rings women,200\r\nwomen rings,6,engagement rings for women,24,eternity ring,200\r\nwomen rings,7,ring,20,unique engagement rings for women,180\r\nwomen rings,8,wedding rings for women,20,opal rings for women,170\r\nwomen rings,9,gold rings women,20,emerald rings for women,160\r\nwomen rings,10,gold rings,19,class rings for women,130\r\nwomen rings,11,rings for women gold,16,chocolate diamond rings for women,130\r\nwomen rings,12,ring for women,16,types of rings for women,130\r\nwomen rings,13,diamond,13,rubber rings for women,130\r\nwomen rings,14,women diamond rings,12,celtic rings for women,130\r\nwomen rings,15,diamond rings for women,12,gucci ring women,110\r\nwomen rings,16,diamond rings,12,silicone rings for women,110\r\nwomen rings,17,silver rings,7,solitaire rings for women,110\r\nwomen rings,18,silver rings women,7,silicone wedding rings for women,110\r\nwomen rings,19,silver rings for women,6,simple engagement rings for women,100\r\nwomen rings,20,engagement ring,5,gucci rings women,100\r\nwomen rings,21,gold ring,5,tiffany rings for women,90\r\nwomen rings,22,men rings,5,nose rings for women,90\r\nwomen rings,23,jewelry,5,tiffany and co,90\r\nwomen rings,24,pandora,5,eternity ring for women,90\r\n,\r\nrings,0,lord of the rings,100,rolex rings ipo,34750\r\nrings,1,the lord of the rings,98,rolex rings share price,11350\r\nrings,2,engagement rings,65,floating rings weeping woods,9650\r\nrings,3,ring,55,rolex rings ipo gmp,7050\r\nrings,4,wedding rings,39,rolex rings ipo allotment status,5950\r\nrings,5,gold rings,29,floating rings at weeping woods,5400\r\nrings,6,diamond rings,25,rolex rings ipo allotment date,5350\r\nrings,7,pandora rings,16,shang-chi and the legend of the ten rings,5000\r\nrings,8,pandora,16,shang chi and the legend of the ten rings,2450\r\nrings,9,silver rings,15,shang chi,2200\r\nrings,10,mens rings,14,ten rings,1250\r\nrings,11,onion rings,13,where to watch lord of the rings,550\r\nrings,12,men rings,12,lord of the rings 4k,350\r\nrings,13,promise rings,12,clay rings,350\r\nrings,14,rings for women,10,air fryer onion rings,300\r\nrings,15,diamond engagement rings,9,mejuri rings,250\r\nrings,16,rings movie,8,onion rings in air fryer,200\r\nrings,17,nose rings,8,akatsuki rings,200\r\nrings,18,amazon rings,8,chunky rings,180\r\nrings,19,rings for men,8,glamira rings,170\r\nrings,20,ten rings,6,harry styles rings,160\r\nrings,21,gold engagement rings,6,matching rings,120\r\nrings,22,the hobbit,5,engagement rings for women,110\r\nrings,23,cheap rings,5,pura vida rings,100\r\nrings,24,lord of the rings cast,5,peach rings,90\r\n,\r\nrings for women,0,engagement,100,amethyst rings for women,12700\r\nrings for women,1,engagement rings,100,gold thumb rings for women,4800\r\nrings for women,2,engagement rings for women,99,emerald rings for women,350\r\nrings for women,3,wedding rings for women,78,key rings for women,300\r\nrings for women,4,wedding rings,76,sapphire rings for women,300\r\nrings for women,5,rings for women gold,70,chocolate diamond rings for women,250\r\nrings for women,6,gold rings,67,turquoise rings for women,250\r\nrings for women,7,ring,62,unique engagement rings for women,250\r\nrings for women,8,ring for women,60,nose rings for women,200\r\nrings for women,9,diamond rings,45,cartier rings for women,200\r\nrings for women,10,diamond rings for women,43,louis vuitton,190\r\nrings for women,11,diamond,42,engagement rings for women near me,170\r\nrings for women,12,rings for women silver,27,real gold rings for women,160\r\nrings for women,13,silver rings,26,sapphire rings,160\r\nrings for women,14,engagement ring for women,19,opal rings for women,150\r\nrings for women,15,engagement ring,18,engagement rings for men and women,150\r\nrings for women,16,gold ring for women,17,eternity rings for women,150\r\nrings for women,17,gold ring,16,anklets for women,150\r\nrings for women,18,jewelry,16,diamond engagement rings for women with price,140\r\nrings for women,19,pandora,16,james avery,130\r\nrings for women,20,promise rings for women,15,ruby rings for women,120\r\nrings for women,21,pandora rings for women,15,ruby ring for women,90\r\nrings for women,22,pandora rings,14,adjustable rings for women,90\r\nrings for women,23,promise rings,14,gemstone rings for women,90\r\nrings for women,24,diamond ring,14,unique rings for women,90\r\n,\r\nwedding ring women,0,wedding ring for women,100,wedding sets for women,90\r\nwedding ring women,1,wedding rings,54,wedding ring sets,70\r\nwedding ring women,2,rings for women,44,wedding ring sets for women,60\r\nwedding ring women,3,wedding rings for women,43,engagement rings for women,50\r\nwedding ring women,4,wedding bands,25,wedding dresses,40\r\nwedding ring women,5,wedding ring sets,23,,\r\nwedding ring women,6,diamond ring for women,20,,\r\nwedding ring women,7,wedding sets for women,19,,\r\nwedding ring women,8,engagement rings,19,,\r\nwedding ring women,9,wedding bands for women,19,,\r\nwedding ring women,10,engagement rings for women,18,,\r\nwedding ring women,11,wedding ring sets for women,18,,\r\nwedding ring women,12,wedding band for women,17,,\r\nwedding ring women,13,women wedding ring set,12,,\r\nwedding ring women,14,wedding ring finger women,10,,\r\nwedding ring women,15,wedding ring finger,9,,\r\nwedding ring women,16,wedding ring set for women,8,,\r\nwedding ring women,17,wedding ring finger for women,5,,\r\nwedding ring women,18,wedding dresses,4,,\r\nwedding ring women,19,men and women wedding ring sets,2,,\r\nwedding ring women,20,kay jewelers,1,,\r\n,\r\nwedding ring,0,wedding rings,100,wedding ring sheathing,69850\r\nwedding ring,1,rings,96,vanessa bryant wedding ring,10700\r\nwedding ring,2,engagement ring,93,wedding ring sheating,2200\r\nwedding ring,3,ring for wedding,79,alex lovell no wedding ring,2150\r\nwedding ring,4,wedding band,76,andy murray wedding ring,300\r\nwedding ring,5,gold wedding ring,58,hailey bieber wedding ring,300\r\nwedding ring,6,the wedding ring,57,ariana grande wedding ring,250\r\nwedding ring,7,diamond ring,56,lotr wedding ring,250\r\nwedding ring,8,diamond wedding ring,55,kobe bryant wedding ring,190\r\nwedding ring,9,wedding ring hand,50,couples wedding ring sets,150\r\nwedding ring,10,ring finger,42,how many people should i invite to my wedding,130\r\nwedding ring,11,wedding ring finger,42,finance wedding ring,100\r\nwedding ring,12,mens wedding ring,36,which hand does wedding ring go on,100\r\nwedding ring,13,mens ring,36,wedding ring tattoo ideas,100\r\nwedding ring,14,wedding ring sets,35,his and her wedding ring sets,90\r\nwedding ring,15,engagement rings,32,what hand does a wedding ring go on for a man,90\r\nwedding ring,16,wedding ring bands,30,which hand does your wedding ring go on,90\r\nwedding ring,17,wedding bands,30,outlander wedding ring,80\r\nwedding ring,18,wedding ring set,27,do you wear your engagement ring on your wedding day,80\r\nwedding ring,19,engagement ring and wedding ring,25,which finger does a wedding ring go on,80\r\nwedding ring,20,men wedding ring,25,which hand does a wedding ring go on,80\r\nwedding ring,21,wedding dress,24,opal engagement ring,80\r\nwedding ring,22,wedding ring men,24,which hand do you wear a wedding ring on,80\r\nwedding ring,23,what hand wedding ring,21,wedding showers,70\r\nwedding ring,24,black wedding ring,17,what finger for wedding ring,70\r\n,\r\ndiamond ring,0,diamond engagement ring,100,lab grown diamonds,300\r\ndiamond ring,1,engagement ring,96,ban maxxis diamond ring 14,300\r\ndiamond ring,2,gold diamond ring,85,diamond hoop nose ring,130\r\ndiamond ring,3,rings,70,diamond ring price in bangladesh,120\r\ndiamond ring,4,diamond rings,67,salt and pepper diamond,110\r\ndiamond ring,5,diamond price,51,levian chocolate diamond ring,110\r\ndiamond ring,6,diamond ring price,49,diamond ring design for female,110\r\ndiamond ring,7,wedding ring,38,salt and pepper diamond ring,100\r\ndiamond ring,8,diamond wedding ring,37,diamond ring price in uae,100\r\ndiamond ring,9,diamond engagement rings,32,10 carat diamond ring price,90\r\ndiamond ring,10,engagement rings,31,diamond ring price in pakistan,70\r\ndiamond ring,11,diamond band ring,30,radiant cut diamond ring,70\r\ndiamond ring,12,black diamond,25,heart shape diamond ring,70\r\ndiamond ring,13,black diamond ring,25,radiant diamond ring,70\r\ndiamond ring,14,white gold diamond ring,22,3k diamond ring,70\r\ndiamond ring,15,emerald diamond ring,22,diamond ring for women,60\r\ndiamond ring,16,solitaire diamond ring,20,4 ct diamond ring,50\r\ndiamond ring,17,princess diamond ring,20,best way to clean diamond ring,40\r\ndiamond ring,18,solitaire ring,20,diamond ring for girls,40\r\ndiamond ring,19,2 carat diamond ring,19,2 carat emerald cut diamond ring,40\r\ndiamond ring,20,1 carat diamond,19,,\r\ndiamond ring,21,1 carat diamond ring,18,,\r\ndiamond ring,22,diamond sapphire ring,17,,\r\ndiamond ring,23,princess cut ring,17,,\r\ndiamond ring,24,oval diamond ring,17,,\r\n,\r\ndiamond,0,diamond ring,100,diamond casino heist,22950\r\ndiamond,1,black diamond,78,pokemon brilliant diamond,12700\r\ndiamond,2,free diamond,73,diamond league 2021,10600\r\ndiamond,3,diamond free fire,60,free fire hack diamond 2020,9400\r\ndiamond,4,free fire,58,diamond jeje,7850\r\ndiamond,5,free fire free diamond,57,double diamond top up,7150\r\ndiamond,6,diamond painting,50,lil uzi vert diamond,6750\r\ndiamond,7,blue diamond,42,lil uzi vert,6000\r\ndiamond,8,diamond rings,41,lil uzi diamond,5050\r\ndiamond,9,diamond price,38,blaq diamond love letter,4100\r\ndiamond,10,diamond earrings,32,free fire redeem code,3100\r\ndiamond,11,diamonds,29,diamond pang,2900\r\ndiamond,12,diamond cut,28,avenues of the diamond,2700\r\ndiamond,13,neil diamond,26,dunia games free fire 70 diamond,1950\r\ndiamond,14,diamond white,26,diamond princess cruise ship,1850\r\ndiamond,15,diamond necklace,26,diamond and pearl remake,900\r\ndiamond,16,white diamond,25,free fire diamond hack generator,900\r\ndiamond,17,dustin diamond,22,pokemon diamond and pearl remake,900\r\ndiamond,18,diamond princess,21,dustin diamond,900\r\ndiamond,19,diamond ff,20,diamond painting wereld,700\r\ndiamond,20,kinemaster,20,ff diamond hack,700\r\ndiamond,21,kinemaster diamond,20,kinemaster diamond mod apk,700\r\ndiamond,22,pokemon diamond,19,kinemaster diamond apk download,650\r\ndiamond,23,diamond platnumz,18,generator diamond online free fire,650\r\ndiamond,24,minecraft diamond,16,diamond princess cruise,600\r\n,\r\ndiamond ring women,0,diamond ring for women,100,eternity ring for women,46600\r\ndiamond ring women,1,diamond rings,41,platinum ring for women,70\r\ndiamond ring women,2,diamond rings for women,36,,\r\ndiamond ring women,3,rings for women,36,,\r\ndiamond ring women,4,gold ring for women,23,,\r\ndiamond ring women,5,gold diamond ring for women,21,,\r\ndiamond ring women,6,engagement rings,17,,\r\ndiamond ring women,7,diamond ring price,16,,\r\ndiamond ring women,8,engagement rings for women,16,,\r\ndiamond ring women,9,diamond ring for women price,13,,\r\ndiamond ring women,10,wedding rings,13,,\r\ndiamond ring women,11,wedding rings for women,9,,\r\ndiamond ring women,12,gold rings for women,8,,\r\ndiamond ring women,13,platinum ring for women,7,,\r\ndiamond ring women,14,diamond ring for men,6,,\r\ndiamond ring women,15,tanishq,6,,\r\ndiamond ring women,16,tanishq diamond ring for women,5,,\r\ndiamond ring women,17,black diamond ring,5,,\r\ndiamond ring women,18,wedding bands for women,4,,\r\ndiamond ring women,19,black diamond ring for women,4,,\r\ndiamond ring women,20,diamond bands for women,3,,\r\ndiamond ring women,21,white gold rings for women,2,,\r\ndiamond ring women,22,eternity ring for women,1,,\r\n,\r\nwomen ring size,0,ring size for women,100,etsy,41000\r\nwomen ring size,1,average women ring size,53,average women ring size,130\r\nwomen ring size,2,average ring size for women,43,how to measure ring size women,100\r\nwomen ring size,3,ring size chart,38,gold rings for women,100\r\nwomen ring size,4,women ring size chart,37,average ring size for women,90\r\nwomen ring size,5,rings for women,26,engagement rings for women,80\r\nwomen ring size,6,ring size chart for women,26,how to measure ring size,80\r\nwomen ring size,7,how to measure ring size,23,rings for women,60\r\nwomen ring size,8,how to measure ring size women,22,,\r\nwomen ring size,9,how to measure ring size for women,6,,\r\nwomen ring size,10,ring sizes for women,5,,\r\nwomen ring size,11,wedding rings for women,5,,\r\nwomen ring size,12,engagement rings for women,4,,\r\nwomen ring size,13,how to find ring size,3,,\r\nwomen ring size,14,gold rings for women,3,,\r\nwomen ring size,15,etsy,2,,\r\n,\r\nring size,0,ring size chart,100,what is the screen size of new fire-boltt ring smartwatch?,3700\r\nring size,1,measure ring size,60,how to figure out ring size at home,600\r\nring size,2,ring size mm,57,how to know the size of your ring finger,500\r\nring size,3,how to measure ring size,48,measuring ring size at home,500\r\nring size,4,uk ring size,45,how can you measure your ring size,400\r\nring size,5,rings,44,how to know the size of your ring,350\r\nring size,6,us ring size,39,fendi ring,350\r\nring size,7,size of ring,37,charmed aroma,250\r\nring size,8,ring size in mm,26,royal essence,250\r\nring size,9,pandora ring,25,ring size chart nz,250\r\nring size,10,pandora ring size,25,2.25 inches ring size,200\r\nring size,11,find ring size,24,how to.measure ring size,190\r\nring size,12,pandora,24,2.5 inches to mm,180\r\nring size,13,ring finger size,24,how do you measure your ring size,170\r\nring size,14,how to size a ring,23,how to find ring size at home,170\r\nring size,15,cm to ring size,22,size 8 ring in cm,140\r\nring size,16,ring size in cm,19,how to measure ring size women,140\r\nring size,17,mens ring size,18,how to know what size ring you are,140\r\nring size,18,average ring size,17,how do i measure my ring size,130\r\nring size,19,how to find ring size,16,how to know your ring size female,120\r\nring size,20,engagement ring,16,52 ring size in letters,110\r\nring size,21,men ring size,16,how to find your ring size at home,110\r\nring size,22,engagement ring size,16,how to work out ring size uk,110\r\nring size,23,ring sizes,15,us ring size to eu,100\r\nring size,24,cm to mm,14,how do i figure out my ring size,100\r\n,\r\nengagement,0,ring engagement,100,bakhtawar bhutto engagement,4800\r\nengagement,1,rings,99,himanshi khurana engagement,4600\r\nengagement,2,engagement rings,96,ankita lokhande engagement,4300\r\nengagement,3,diamond engagement ring,14,ankita lokhande,4150\r\nengagement,4,diamond rings,13,engagement awc.odisha.gov.in,2950\r\nengagement,5,diamond engagement rings,13,emma stone engagement ring,2700\r\nengagement,6,employee engagement,11,hardik pandya engagement,1850\r\nengagement,7,wedding ring,10,youth engagement for global action,1750\r\nengagement,8,gold engagement rings,8,gwen stefani engagement ring,1450\r\nengagement,9,engagement party,8,katrina kaif engagement,1250\r\nengagement,10,engagement meaning,7,lily collins engagement ring,1200\r\nengagement,11,engagement dress,7,aaron rodgers engagement,1000\r\nengagement,12,instagram engagement,7,demi lovato engagement,800\r\nengagement,13,wedding rings,6,instagram engagement rate calculator,300\r\nengagement,14,engagement photos,6,engagement makeup look,300\r\nengagement,15,rules of engagement,6,mia khalifa engagement,250\r\nengagement,16,engagement wishes,5,engagement artinya,250\r\nengagement,17,community engagement,5,happy engagement wishes,200\r\nengagement,18,engagement rate,5,engagement cake design,190\r\nengagement,19,engagement quotes,4,engagement rate calculator,180\r\nengagement,20,engagement gifts,4,instagram engagement calculator,160\r\nengagement,21,customer engagement,4,engagement anniversary wishes to husband,160\r\nengagement,22,forfait sans engagement,4,engagement rings for couples,140\r\nengagement,23,tiffany,4,engagement adalah,140\r\nengagement,24,engagement rings for women,4,engagement anniversary wishes,140\r\n,\r\nengagement ring women,0,engagement ring for women,100,white gold engagement ring for women,60\r\nengagement ring women,1,women engagement rings,71,,\r\nengagement ring women,2,engagement rings,65,,\r\nengagement ring women,3,rings for women,59,,\r\nengagement ring women,4,engagement rings for women,55,,\r\nengagement ring women,5,diamond ring for women,26,,\r\nengagement ring women,6,wedding rings,22,,\r\nengagement ring women,7,diamond rings for women,18,,\r\nengagement ring women,8,diamond engagement rings for women,17,,\r\nengagement ring women,9,wedding rings for women,16,,\r\nengagement ring women,10,engagement ring finger,16,,\r\nengagement ring women,11,gold engagement rings for women,13,,\r\nengagement ring women,12,engagement ring finger for women,12,,\r\nengagement ring women,13,wedding bands,8,,\r\nengagement ring women,14,wedding bands for women,8,,\r\nengagement ring women,15,engagement ring sets for women,5,,\r\nengagement ring women,16,white gold engagement ring for women,5,,\r\nengagement ring women,17,wedding ring sets for women,4,,\r\nengagement ring women,18,kay jewelers,2,,\r\n,\r\ndiamond ring for women,0,diamond rings,100,eternity ring for women,550\r\ndiamond ring for women,1,diamond rings for women,98,,\r\ndiamond ring for women,2,rings for women,98,,\r\ndiamond ring for women,3,gold diamond ring for women,74,,\r\ndiamond ring for women,4,diamond ring price for women,46,,\r\ndiamond ring for women,5,diamond ring price,46,,\r\ndiamond ring for women,6,diamond engagement rings for women,46,,\r\ndiamond ring for women,7,engagement rings for women,40,,\r\ndiamond ring for women,8,engagement rings,39,,\r\ndiamond ring for women,9,wedding rings for women,25,,\r\ndiamond ring for women,10,tanishq diamond ring for women,21,,\r\ndiamond ring for women,11,tanishq,19,,\r\ndiamond ring for women,12,platinum diamond ring for women,15,,\r\ndiamond ring for women,13,platinum ring for women,12,,\r\ndiamond ring for women,14,black diamond ring for women,12,,\r\ndiamond ring for women,15,diamond ring for men,12,,\r\ndiamond ring for women,16,diamond earrings for women,10,,\r\ndiamond ring for women,17,eternity ring for women,10,,\r\ndiamond ring for women,18,wedding bands for women,9,,\r\n,\r\nengagement ring,0,engagement rings,100,patrick mahomes engagement ring,11650\r\nengagement ring,1,rings,98,heather rae young engagement ring,11100\r\nengagement ring,2,diamond engagement ring,87,shailene woodley engagement ring,5700\r\nengagement ring,3,diamond ring,82,gwen stefani,2700\r\nengagement ring,4,wedding ring,59,katie thurston engagement ring,1850\r\nengagement ring,5,engagement ring gold,44,gwen stefani engagement ring,1550\r\nengagement ring,6,diamond engagement rings,29,emma stone engagement ring,1350\r\nengagement ring,7,halo engagement ring,19,demi lovato engagement ring,600\r\nengagement ring,8,wedding rings,18,hidden halo engagement ring,550\r\nengagement ring,9,wedding ring and engagement ring,18,engagement ring booking,450\r\nengagement ring,10,engagement ring finger,18,1000 dollar engagement ring,300\r\nengagement ring,11,engagement ring hand,17,gold engagement ring designs for female,110\r\nengagement ring,12,tiffany,16,engagement ring platter,110\r\nengagement ring,13,tiffany engagement ring,15,how much are you supposed to spend on an engagement ring,110\r\nengagement ring,14,oval engagement ring,14,engagement ring designs for female,100\r\nengagement ring,15,rose engagement ring,14,three stone oval engagement ring,100\r\nengagement ring,16,sapphire engagement ring,13,which finger is for engagement ring,90\r\nengagement ring,17,engagement ring set,13,engagement and wedding ring set,80\r\nengagement ring,18,solitaire engagement ring,13,princess diana engagement ring,70\r\nengagement ring,19,pear engagement ring,13,nikki bella engagement ring,70\r\nengagement ring,20,engagement ring price,12,oval solitaire engagement ring,70\r\nengagement ring,21,emerald engagement ring,12,engagement ring cost rule,70\r\nengagement ring,22,rose gold engagement ring,12,do you wear your engagement ring on your wedding day,60\r\nengagement ring,23,white gold engagement ring,11,how much is an engagement ring,60\r\nengagement ring,24,engagement ring size,11,what finger does the engagement ring go on,60\r\n,\r\nwedding ring for women,0,wedding rings for women,100,wedding ring set for women,110\r\nwedding ring for women,1,wedding rings,98,wedding ring sets for women,80\r\nwedding ring for women,2,rings for women,93,wedding ring sets,60\r\nwedding ring for women,3,wedding sets for women,48,wedding sets for women,60\r\nwedding ring for women,4,wedding ring sets,46,,\r\nwedding ring for women,5,wedding ring sets for women,45,,\r\nwedding ring for women,6,wedding bands for women,40,,\r\nwedding ring for women,7,wedding band for women,39,,\r\nwedding ring for women,8,engagement rings for women,39,,\r\nwedding ring for women,9,wedding ring set for women,20,,\r\nwedding ring for women,10,wedding ring for men,19,,\r\nwedding ring for women,11,wedding ring finger for women,15,,\r\nwedding ring for women,12,ring finger for women,14,,\r\nwedding ring for women,13,gold wedding bands for women,8,,\r\nwedding ring for women,14,best wedding rings for women,5,,\r\n,\r\nengagement ring for women,0,rings for women,100,unique engagement rings for women,300\r\nengagement ring for women,1,engagement rings,97,wedding ring sets for women,70\r\nengagement ring for women,2,engagement rings for women,92,diamond rings for women,60\r\nengagement ring for women,3,diamond ring for women,43,diamond engagement rings for women,50\r\nengagement ring for women,4,diamond rings for women,34,engagement rings for women,40\r\nengagement ring for women,5,diamond engagement rings for women,33,engagement rings,40\r\nengagement ring for women,6,wedding rings for women,31,,\r\nengagement ring for women,7,gold engagement rings for women,26,,\r\nengagement ring for women,8,engagement ring finger for women,18,,\r\nengagement ring for women,9,engagement ring finger,18,,\r\nengagement ring for women,10,wedding bands for women,10,,\r\nengagement ring for women,11,white gold engagement ring for women,6,,\r\nengagement ring for women,12,unique engagement rings for women,4,,\r\nengagement ring for women,13,wedding ring sets for women,4,,\r\n,\r\nsilver ring,0,sterling silver,100,re8 silver ring,9950\r\nsilver ring,1,sterling silver ring,95,dior ring silver,900\r\nsilver ring,2,silver rings,68,dior ring,900\r\nsilver ring,3,rings,68,silver ring design for girl,400\r\nsilver ring,4,gold ring,61,fendi ring,350\r\nsilver ring,5,men silver ring,35,covetous silver serpent ring ds3,300\r\nsilver ring,6,men ring,34,sterling silver ring blanks,250\r\nsilver ring,7,mens silver ring,32,gold price today,250\r\nsilver ring,8,silver ring price,30,boy ring design silver,250\r\nsilver ring,9,diamond ring,30,silver price today,200\r\nsilver ring,10,diamond silver ring,30,evil eye ring silver,180\r\nsilver ring,11,silver price,29,sterling silver thumb ring,180\r\nsilver ring,12,diamond,29,boys ring design,170\r\nsilver ring,13,ring for men,24,gucci silver heart ring,170\r\nsilver ring,14,silver 925 ring,23,silver wishbone ring,160\r\nsilver ring,15,silver ring for men,23,sterling silver turtle ring,140\r\nsilver ring,16,ring for men silver,23,snake ring,140\r\nsilver ring,17,wedding ring silver,22,silver ring designs for men,130\r\nsilver ring,18,925 silver,22,silver ring designs for girls,130\r\nsilver ring,19,black silver ring,21,silver serpent ring ds3,120\r\nsilver ring,20,ring design,21,today gold rate,120\r\nsilver ring,21,silver ring design,20,vivienne westwood,120\r\nsilver ring,22,silver band ring,18,leg ring silver,110\r\nsilver ring,23,sterling silver rings,18,etsy uk,110\r\nsilver ring,24,pandora ring,18,wrap around ring silver,110\r\n,\r\nwomen ring design,0,ring design for women,100,gold rate today,300\r\nwomen ring design,1,women gold ring design,71,,\r\nwomen ring design,2,gold ring,70,,\r\nwomen ring design,3,gold ring design,66,,\r\nwomen ring design,4,gold ring for women design,64,,\r\nwomen ring design,5,gold ring for women,62,,\r\nwomen ring design,6,silver ring for women,6,,\r\nwomen ring design,7,ring design for men,5,,\r\nwomen ring design,8,gold ring design for men,4,,\r\nwomen ring design,9,gold rate today,3,,\r\n,\r\nring design,0,gold,100,gold ring design 2020,70700\r\nring design,1,ring gold,98,latest ring design 2020,32500\r\nring design,2,gold ring design,97,\"gold ring design for male under 10,000\",9650\r\nring design,3,men ring design,17,blue stone ring design for female,5350\r\nring design,4,female ring design,15,silver ring design for girl,350\r\nring design,5,ring designs,15,umbrella ring design,350\r\nring design,6,ear ring design,15,girl finger ring design,300\r\nring design,7,ring design for female,14,ear ring design for girl,300\r\nring design,8,diamond ring,14,boy ring design silver,250\r\nring design,9,design diamond ring,14,gold ear ring design for girl,200\r\nring design,10,ear ring,13,today gold price,200\r\nring design,11,design engagement ring,13,ring ceremony,200\r\nring design,12,silver ring,13,simple gold ring design for female,200\r\nring design,13,engagement ring,13,big gold ring design for female,190\r\nring design,14,silver ring design,13,toe ring design,160\r\nring design,15,stone ring design,13,simple ring design for female,150\r\nring design,16,gold price,13,simple ring design for girl,140\r\nring design,17,new ring design,12,gold rate today,130\r\nring design,18,design of ring,12,small ear ring design,130\r\nring design,19,ring design for men,11,queen ring design,110\r\nring design,20,female gold ring design,11,latest ring design for girl,110\r\nring design,21,ring gold design female,11,boy ring design gold,100\r\nring design,22,gold ring design for female,11,chandi ring design for girl,100\r\nring design,23,women ring design,11,boys gold ring design,100\r\nring design,24,girl ring design,10,finger ring design for girl,100\r\n,\r\nwomen ring finger,0,ring finger for women,100,gold ring for women,110\r\nwomen ring finger,1,wedding ring finger women,33,gold finger ring for women,40\r\nwomen ring finger,2,wedding ring,30,,\r\nwomen ring finger,3,wedding ring finger,30,,\r\nwomen ring finger,4,engagement ring finger for women,22,,\r\nwomen ring finger,5,wedding ring for women,20,,\r\nwomen ring finger,6,wedding ring finger for women,18,,\r\nwomen ring finger,7,gold ring for women,18,,\r\nwomen ring finger,8,gold finger ring for women,16,,\r\nwomen ring finger,9,ring finger for men,10,,\r\n,\r\ncartier ring,0,love ring,100,cartier 750 ring 52833a real or fake,18050\r\ncartier ring,1,love ring cartier,97,gucci ghost ring,9100\r\ncartier ring,2,cartier love,95,cartier 750 ring 52833a leve,6900\r\ncartier ring,3,cartier gold ring,31,dhgate cartier ring,1450\r\ncartier ring,4,gold ring,30,chaumet,500\r\ncartier ring,5,cartier bracelet,28,cartier clash ring,400\r\ncartier ring,6,diamond ring,26,clash de cartier ring,400\r\ncartier ring,7,cartier diamond ring,24,cartier ring dupe amazon,350\r\ncartier ring,8,rings,21,cartier dupe ring,350\r\ncartier ring,9,cartier rings,21,dhgate,300\r\ncartier ring,10,tiffany,20,panthère de cartier ring,300\r\ncartier ring,11,tiffany ring,19,fendi ring,250\r\ncartier ring,12,wedding ring,18,dior ring,250\r\ncartier ring,13,engagement ring,18,dior,250\r\ncartier ring,14,cartier engagement ring,18,cartier love ring dupe,200\r\ncartier ring,15,cartier wedding ring,17,rings for women,200\r\ncartier ring,16,cartier ring price,14,chanel bracelet,190\r\ncartier ring,17,trinity cartier ring,14,cartier 750 ring 52833a,170\r\ncartier ring,18,trinity ring,14,carters,150\r\ncartier ring,19,cartier trinity,13,cartier friendship ring,150\r\ncartier ring,20,trinity,13,christ,140\r\ncartier ring,21,cartier band,12,cartier live ring,130\r\ncartier ring,22,cartier love bracelet,12,cartier 750 ring,120\r\ncartier ring,23,gucci,11,louis vuitton,110\r\ncartier ring,24,gucci ring,11,gucci rings,110\r\n,\r\ncartier,0,bracelet cartier,100,adam and cartier love island,5700\r\ncartier,1,love cartier,72,cartier surjan,5550\r\ncartier,2,cartier ring,71,adam and cartier,4850\r\ncartier,3,cartier watch,60,cartier love island,2700\r\ncartier,4,jacques cartier,42,gabbie cartier,1700\r\ncartier,5,bracelet love cartier,36,lilou cartier,1400\r\ncartier,6,santos cartier,27,pasha de cartier parfum,800\r\ncartier,7,cartier tank,25,cartier skeleton watch,300\r\ncartier,8,cartier glasses,23,กํา ไล cartier,200\r\ncartier,9,cartier watches,20,cartier santos skeleton,170\r\ncartier,10,cartier ring love,19,anel cartier,170\r\ncartier,11,tiffany,17,cartier frames men,160\r\ncartier,12,rolex,15,cartier bilezik,140\r\ncartier,13,cartier panthere,14,bratara cartier,130\r\ncartier,14,cartier bague,13,cartier crash,130\r\ncartier,15,cartier necklace,12,van cleef & arpels,130\r\ncartier,16,gucci,11,cartier bileklik altın,120\r\ncartier,17,cartier rings,11,cartier brille,120\r\ncartier,18,cartier pasha,11,pizzeria jacques cartier,110\r\ncartier,19,cartier perfume,11,bague clou cartier,110\r\ncartier,20,montre cartier,10,pulseira cartier,110\r\ncartier,21,parfum cartier,10,bague cartier femme,100\r\ncartier,22,louis vuitton,10,cartier trinity armband,100\r\ncartier,23,cartier sunglasses,9,cartier ohrringe,90\r\ncartier,24,chanel,9,cartier glasses men,90\r\n,\r\nmacys,0,macys near me,100,macys cyber monday 2019,15650\r\nmacys,1,macys sale,66,macys parade 2020,11400\r\nmacys,2,macys hours,59,macys closing stores 2020,4500\r\nmacys,3,macys furniture,59,macys fireworks 2020,2950\r\nmacys,4,nordstrom,53,macys parade 2019,1000\r\nmacys,5,kohls,46,is macys open,250\r\nmacys,6,macys shoes,46,is macys closing,250\r\nmacys,7,jcpenney,45,macys stock,150\r\nmacys,8,macys dresses,42,macys stock price,150\r\nmacys,9,macys coupon,42,coach outlet,130\r\nmacys,10,macys store,41,macys thanksgiving parade time,120\r\nmacys,11,insite macys,41,macys closing,110\r\nmacys,12,macys mens,40,macys outdoor furniture,110\r\nmacys,13,macys card,38,macys open,100\r\nmacys,14,target,37,bath and body works,100\r\nmacys,15,dillards,34,dillards near me,90\r\nmacys,16,macys credit,33,macys palm desert,90\r\nmacys,17,macys login,32,macys radley sectional,90\r\nmacys,18,macys credit card,27,macys pr,80\r\nmacys,19,macys home,26,macys el centro,80\r\nmacys,20,macys men,24,gap factory,80\r\nmacys,21,macys parade,24,macys guam,70\r\nmacys,22,macys boots,21,macys backstage,70\r\nmacys,23,macys online,20,macys bedding sale,70\r\nmacys,24,macys stock,19,macys returns,70\r\n,\r\nmoonstone ring,0,moonstone engagement ring,100,rainbow moonstone engagement ring,300\r\nmoonstone ring,1,moonstone rings,77,vintage moonstone engagement ring,300\r\nmoonstone ring,2,gold moonstone ring,49,moonstone promise ring,250\r\nmoonstone ring,3,moon ring,46,moon magic,140\r\nmoonstone ring,4,silver moonstone ring,42,moon ring,110\r\nmoonstone ring,5,moonstone diamond ring,35,moonstone wedding ring,100\r\nmoonstone ring,6,moonstone wedding ring,31,mens moonstone ring,80\r\nmoonstone ring,7,moonstone meaning,30,moon stone,80\r\nmoonstone ring,8,rainbow moonstone ring,27,raw moonstone ring,70\r\nmoonstone ring,9,etsy moonstone ring,25,moonstone ring uk,60\r\nmoonstone ring,10,etsy,24,rose gold moonstone ring,60\r\nmoonstone ring,11,moonstone ring meaning,24,promise rings,60\r\nmoonstone ring,12,moonstone ring uk,17,silver moonstone ring,40\r\nmoonstone ring,13,rose gold moonstone ring,17,moonstone rings,40\r\nmoonstone ring,14,moon stone,16,,\r\nmoonstone ring,15,raw moonstone ring,13,,\r\nmoonstone ring,16,vintage moonstone ring,13,,\r\nmoonstone ring,17,mens moonstone ring,12,,\r\nmoonstone ring,18,alexandrite,12,,\r\nmoonstone ring,19,moonstone and diamond ring,11,,\r\nmoonstone ring,20,june birthstone,11,,\r\nmoonstone ring,21,opal rings,10,,\r\nmoonstone ring,22,moonstone engagement ring meaning,7,,\r\nmoonstone ring,23,moon magic,6,,\r\nmoonstone ring,24,vintage moonstone engagement ring,5,,\r\n,\r\ngold price today,0,gold price in today,100,gold price in pakistan 2020 today,26000\r\ngold price today,1,gold rate,31,gold price today siliguri,200\r\ngold price today,2,gold rate today,30,gold price today in siliguri,200\r\ngold price today,3,today gold price india,20,gold price today kota,180\r\ngold price today,4,gold price in india today,17,gold price today jammu,120\r\ngold price today,5,gold price in india,16,yes bank share price,110\r\ngold price today,6,today price of gold,15,sbi share price,100\r\ngold price today,7,price of gold,15,gold price today varanasi,100\r\ngold price today,8,today silver price,14,24ct gold price today,90\r\ngold price today,9,silver price,14,yes bank share,90\r\ngold price today,10,gold price today delhi,12,gold price today in moradabad,90\r\ngold price today,11,today gold price kolkata,9,gold price today jaipur,90\r\ngold price today,12,today gold price in delhi,9,gold price today patna,90\r\ngold price today,13,gold price in delhi,9,gold price today in kota,90\r\ngold price today,14,today gold price hyderabad,8,gold price today amritsar,80\r\ngold price today,15,today gold price 22k,7,gold price today kanpur,70\r\ngold price today,16,today 22 carat gold price,7,gold price today in jammu,70\r\ngold price today,17,22k gold price today,7,ril share price,70\r\ngold price today,18,gold price today mumbai,6,gold price today 22k kolkata,70\r\ngold price today,19,gold price mumbai,6,gold price today in meerut,60\r\ngold price today,20,gold price today pakistan,6,gold price today bbsr,60\r\ngold price today,21,today gold price in kolkata,6,24 carat gold price in ahmedabad today,60\r\ngold price today,22,gold price today in hyderabad,6,gold price today jodhpur,60\r\ngold price today,23,today gold price in hyderabad,6,gold price today in up,60\r\ngold price today,24,gold price today ahmedabad,5,gold price today gwalior,50\r\n,\r\ngold rate today,0,today gold price,100,gold rate in pakistan today 2020,22350\r\ngold rate today,1,gold price,98,gold rate in kerala today 1gm,2900\r\ngold rate today,2,gold rate today chennai,77,today gold rate in ap,150\r\ngold rate today,3,today gold rate hyderabad,70,today gold rate mysore,100\r\ngold rate today,4,gold rate today india,60,today gold rate in karnataka,90\r\ngold rate today,5,gold rate today in chennai,57,today gold and silver rate in hyderabad,80\r\ngold rate today,6,gold rate in chennai,54,gold rate today in latur,80\r\ngold rate today,7,today bangalore gold rate,52,gold rate today bhopal,80\r\ngold rate today,8,gold silver rate today,48,gold rate today in karnataka,80\r\ngold rate today,9,silver rate today,48,svbc gold rate today,70\r\ngold rate today,10,gold rate in india today,48,today gold rate in kakinada,60\r\ngold rate today,11,gold rate in india,47,gold rate in mysore today,60\r\ngold rate today,12,gold rate today in india,46,gold rate today kakinada,60\r\ngold rate today,13,gold rate in hyderabad today,45,today gold rate hyderabad,60\r\ngold rate today,14,gold rate mumbai today,44,gold silver rate today,50\r\ngold rate today,15,gold rate in hyderabad,43,today gold rate in up,50\r\ngold rate today,16,today gold rate delhi,42,gold rate today vizag,50\r\ngold rate today,17,gold rate delhi today,42,today gold rate visakhapatnam,50\r\ngold rate today,18,22 carat gold rate,35,gold rate today rajahmundry,50\r\ngold rate today,19,today gold rate 22 carat,35,silver rate today,50\r\ngold rate today,20,today gold rate in delhi,33,today gold rate vijayawada,40\r\ngold rate today,21,gold rate today in bangalore,33,gold rate today nashik,40\r\ngold rate today,22,gold rate today kerala,32,gold rate today lucknow,40\r\ngold rate today,23,gold rate in bangalore,32,,\r\ngold rate today,24,gold rate in delhi,32,,\r\n,\r\nhow to measure ring size,0,how to measure your ring size,100,how to measure my ring size at home,69350\r\nhow to measure ring size,1,how to measure for ring size,88,how to figure out ring size,25850\r\nhow to measure ring size,2,how to measure ring size at home,75,how to measure a ring size at home,500\r\nhow to measure ring size,3,cm to mm,68,how to measure ring size with string,450\r\nhow to measure ring size,4,rings,63,ring size guide,250\r\nhow to measure ring size,5,how to measure ring size in cm,57,how to measure bra size,200\r\nhow to measure ring size,6,how to measure ring size in mm,56,etsy,200\r\nhow to measure ring size,7,how to measure ring size uk,52,how do you measure ring size,200\r\nhow to measure ring size,8,how to measure ring finger size,50,pandora,180\r\nhow to measure ring size,9,ring size chart,45,how to measure ring size women,170\r\nhow to measure ring size,10,inches to mm,44,how to measure your ring size at home,160\r\nhow to measure ring size,11,how to measure ring size in inches,39,how to measure ring size uk,150\r\nhow to measure ring size,12,ring size in inches,37,how to measure ring size at home,140\r\nhow to measure ring size,13,how to measure ring size men,36,how to measure my ring size,140\r\nhow to measure ring size,14,how to measure ring size women,34,how to measure ring size for men,120\r\nhow to measure ring size,15,how to measure my ring size,33,mejuri,120\r\nhow to measure ring size,16,how to measure finger size for ring,23,how to measure finger size for ring,110\r\nhow to measure ring size,17,how to measure mens ring size,23,cm to mm,110\r\nhow to measure ring size,18,how to measure ring size with tape measure,22,how to measure ring size in cm,100\r\nhow to measure ring size,19,ring sizes,21,how to measure ring size in mm,100\r\nhow to measure ring size,20,how to measure ring size us,19,how to measure ring size in inches,100\r\nhow to measure ring size,21,how to measure your ring size at home,15,how to measure ring size us,100\r\nhow to measure ring size,22,how to measure ring size for men,14,ring size in inches,90\r\nhow to measure ring size,23,how to measure a ring size at home,14,kay jewelers,90\r\nhow to measure ring size,24,pandora,14,how to measure your finger for a ring,80\r\n,\r\ncartier love ring,0,cartier love bracelet,100,dhgate,23150\r\ncartier love ring,1,cartier bracelet,91,cartier love ring dupe,350\r\ncartier love ring,2,love ring cartier gold,71,tiffany and co,160\r\ncartier love ring,3,cartier love ring diamond,43,cartier love ring silver,50\r\ncartier love ring,4,cartier rings,32,love ring cartier gold,40\r\ncartier love ring,5,tiffany ring,31,cartier love ring diamond,40\r\ncartier love ring,6,cartier love ring price,30,,\r\ncartier love ring,7,tiffany,28,,\r\ncartier love ring,8,fake cartier ring,23,,\r\ncartier love ring,9,cartier love ring dupe,23,,\r\ncartier love ring,10,the love ring cartier,20,,\r\ncartier love ring,11,fake cartier love ring,20,,\r\ncartier love ring,12,cartier white gold love ring,17,,\r\ncartier love ring,13,cartier love ring men,16,,\r\ncartier love ring,14,cartier love necklace,14,,\r\ncartier love ring,15,cartier love ring silver,14,,\r\ncartier love ring,16,louis vuitton,13,,\r\ncartier love ring,17,cartier love ring rose gold,13,,\r\ncartier love ring,18,mens cartier love ring,13,,\r\ncartier love ring,19,tiffany and co,11,,\r\ncartier love ring,20,hermes bracelet,6,,\r\ncartier love ring,21,cartier love bangle,6,,\r\ncartier love ring,22,cartier love ring with diamonds,6,,\r\ncartier love ring,23,cartier trinity ring,5,,\r\ncartier love ring,24,pandora,4,,\r\n,\r\nkay jewelers,0,kay jewelers rings,100,kay jewelers center of me,12900\r\nkay jewelers,1,rings,99,kays fine jewelry,3250\r\nkay jewelers,2,kay jewelry,97,kay jewelers hanover pa,2750\r\nkay jewelers,3,jewelry,96,kay jewelers black friday 2020,1650\r\nkay jewelers,4,jewelers near me,90,does kay jewelers sell fake diamonds,1600\r\nkay jewelers,5,kay jewelers near me,88,kayleigh mcenany,1550\r\nkay jewelers,6,zales jewelers,62,kay jewelers cyber monday 2019,1000\r\nkay jewelers,7,zales,59,kay jewelers sioux falls,750\r\nkay jewelers,8,kay jewelers necklace,58,kay jewelers black friday sale,450\r\nkay jewelers,9,necklace,57,kay jewelers grove city,400\r\nkay jewelers,10,kay jewelers card,56,kay jewelers virginia beach,350\r\nkay jewelers,11,kay jewelers credit,55,kay jewelers anklet,350\r\nkay jewelers,12,kay jewelers credit card,50,kay jewelers salem oregon,250\r\nkay jewelers,13,pandora,36,kay jewelers mother rings,200\r\nkay jewelers,14,engagement rings,35,kay jewelers tracking,190\r\nkay jewelers,15,kay jewelers engagement rings,34,kay pee jewelers,180\r\nkay jewelers,16,jared jewelers,34,engagement rings for women,170\r\nkay jewelers,17,jared,33,is kay jewelers good,150\r\nkay jewelers,18,kay jewelers earrings,31,kay jewelers promo code,140\r\nkay jewelers,19,kay jewelers outlet,30,kay jewelers memphis,140\r\nkay jewelers,20,kay outlet,30,zales outlet,130\r\nkay jewelers,21,kays jewelers,28,kay jewelers rochester mn,120\r\nkay jewelers,22,kays,27,kay jewelers discount code,120\r\nkay jewelers,23,kay jewelers sale,26,jewelry stores near me,120\r\nkay jewelers,24,jewelry stores,24,zales near me,120\r\n,\r\nswarovski,0,pandora,100,swarovski nl pure,8300\r\nswarovski,1,swarovski crystal,74,swarovski christmas ornament 2020,5150\r\nswarovski,2,crystal,74,swarovski 2019 ornament,1700\r\nswarovski,3,bracelet swarovski,61,swarovski optik dg,1400\r\nswarovski,4,swarovski necklace,60,swarovski schwanger,750\r\nswarovski,5,bracelet,60,harga kalung swarovski,650\r\nswarovski,6,earrings,53,anelli swarovski 2021,650\r\nswarovski,7,swarovski crystals,52,victoria swarovski schwanger,550\r\nswarovski,8,swarovski earrings,51,swarovski adalah,250\r\nswarovski,9,ring swarovski,50,bratara swarovski,250\r\nswarovski,10,swarovski sale,48,swarovski tennis bracelet,110\r\nswarovski,11,swarovski victoria,43,swarovski tennis necklace,100\r\nswarovski,12,outlet swarovski,42,swarovski ireland,90\r\nswarovski,13,swarovski uk,42,swarovski near me,90\r\nswarovski,14,jewelry,27,collana swarovski uomo,90\r\nswarovski,15,swarovski jewelry,27,swarovski store near me,90\r\nswarovski,16,swarovski canada,26,colar swarovski,90\r\nswarovski,17,swarovski rings,25,swarovski black friday,80\r\nswarovski,18,swarovski watch,24,swarovski discount code,80\r\nswarovski,19,swarovski online,24,idee cadeau femme,80\r\nswarovski,20,swarovski kette,23,swarovski voucher code,70\r\nswarovski,21,swarovski jewellery,20,swar,70\r\nswarovski,22,jewellery,20,swarovski crystals for nails,70\r\nswarovski,23,tiffany,19,สร้อย swarovski,70\r\nswarovski,24,swarovski armband,18,swarovski egypt,70\r\n,\r\nlouis vuitton,0,louis vuitton bag,100,louis vuitton california dream,3150\r\nlouis vuitton,1,gucci,89,louis vuitton iphone 11 case,2950\r\nlouis vuitton,2,louis vuitton bags,38,louis vuitton face mask,2900\r\nlouis vuitton,3,lv,36,masque louis vuitton,2250\r\nlouis vuitton,4,chanel,29,louis vuitton filter,1450\r\nlouis vuitton,5,louis vuitton purse,27,multi pochette louis vuitton,1300\r\nlouis vuitton,6,louis vuitton shoes,27,louis vuitton maske,1250\r\nlouis vuitton,7,louis vuitton wallet,27,louis vuitton mask,950\r\nlouis vuitton,8,dior,27,louis partridge,900\r\nlouis vuitton,9,louis vuitton pochette,26,louis vuitton bts,600\r\nlouis vuitton,10,louis vuitton sac,22,on the go louis vuitton,600\r\nlouis vuitton,11,louis vuitton belt,19,louis vuitton ombre nomade,500\r\nlouis vuitton,12,prada,18,louis vuitton league of legends,450\r\nlouis vuitton,13,louis vuitton sale,15,louis vuitton airpod case,400\r\nlouis vuitton,14,neverfull louis vuitton,15,christian dior,250\r\nlouis vuitton,15,louis vuitton backpack,14,bonnet louis vuitton,200\r\nlouis vuitton,16,supreme,13,louis vuitton bucket hat,200\r\nlouis vuitton,17,louis vuitton supreme,13,louis vuitton stencil,200\r\nlouis vuitton,18,louis vuitton tasche,13,louis vuitton pochette accessoires,200\r\nlouis vuitton,19,louis vuitton sneakers,13,louis vuitton felicie pochette,170\r\nlouis vuitton,20,hermes,13,louis vuitton beanie,170\r\nlouis vuitton,21,burberry,12,dior,160\r\nlouis vuitton,22,balenciaga,12,coach outlet,120\r\nlouis vuitton,23,fendi,12,louis vuitton lock necklace,120\r\nlouis vuitton,24,louis vuitton bracelet,11,louis tomlinson,110\r\n,\r\nthe bling ring,0,the real bling ring,100,best movies on netflix,36900\r\nthe bling ring,1,bling ring movie,48,what happened to the bling ring,29050\r\nthe bling ring,2,the bling ring movie,46,the bling ring real story,400\r\nthe bling ring,3,the bling ring netflix,43,the bling ring netflix,400\r\nthe bling ring,4,emma watson,42,the bling ring rotten tomatoes,300\r\nthe bling ring,5,the bling ring emma watson,35,the bling ring real people,250\r\nthe bling ring,6,the bling ring cast,31,the bling ring imdb,250\r\nthe bling ring,7,the bling ring story,29,the bling ring review,150\r\nthe bling ring,8,alexis neiers,24,the real bling ring,130\r\nthe bling ring,9,the bling ring real people,22,the bling ring real life,130\r\nthe bling ring,10,the bling ring film,19,the bling ring cast,70\r\nthe bling ring,11,the bling ring real story,13,,\r\nthe bling ring,12,the bling ring streaming,13,,\r\nthe bling ring,13,the bling ring real life,12,,\r\nthe bling ring,14,the bling ring trailer,11,,\r\nthe bling ring,15,the bling ring true story,11,,\r\nthe bling ring,16,the bling ring review,6,,\r\nthe bling ring,17,best movies on netflix,5,,\r\nthe bling ring,18,the bling ring full movie,5,,\r\nthe bling ring,19,the bling ring rotten tomatoes,5,,\r\nthe bling ring,20,the bling ring imdb,4,,\r\nthe bling ring,21,the bling ring 2011,4,,\r\nthe bling ring,22,sofia coppola,4,,\r\nthe bling ring,23,what happened to the bling ring,4,,\r\nthe bling ring,24,is the bling ring a true story,3,,\r\n,\r\n"
  },
  {
    "path": "004-EmailNotify/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "004-EmailNotify/README.md",
    "content": "# 监听玩客家平台币价变化，使用邮件通知\n\n本小项目主要体现邮箱的作用，如果利用微信绑定收信邮箱，便能实时知道自己想要知道的信息。 注意：发送邮箱的登录密码一般是在设置smtp中生成的授权码。\n\n- 在服务器上启动脚本，与连接无关：\n```\nnohup python main.py\n```\n"
  },
  {
    "path": "004-EmailNotify/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 玩客家 币价变化，邮件通知 api\n@Date       :2020/12/17\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport json\nimport smtplib\nimport time\nfrom email.mime.multipart import MIMEMultipart\nfrom email.mime.text import MIMEText\n\nimport requests\nfrom my_fake_useragent import UserAgent\n\nua = UserAgent()\nagent = ua.random()\n\n\nclass Email(object):\n    def __init__(self, from_user, pwd, to_other):\n        \"\"\"\n        :param from_user: 发送者邮箱号\n        :param pwd:       发送者邮箱密码（非登录密码）\n        :param to_other:  目标邮箱\n        \"\"\"\n        self.__from_user = from_user\n        self.__pwd = pwd\n        self.__to_other = to_other\n\n    def sendMsg(self, title=\"\", content=\"\"):\n        print(\"%s向%s发送邮件：标题{%s}\\t内容{%s}\" % (self.__from_user, self.__to_other, title, content))\n        retry = 0\n        while True:\n            print(\"【%s】开始发送，次数：%3d\" % (title, retry))\n            try:\n                msg = MIMEMultipart()\n                msg.attach(MIMEText(content, 'plain', 'utf-8'))\n                msg['Subject'] = title\n                msg['From'] = self.__from_user\n                s = smtplib.SMTP_SSL(\"smtp.qq.com\", 465)  # 通过SSL方式发送，服务器地址和端口\n                s.login(self.__from_user, self.__pwd)  # 登录邮箱\n                s.sendmail(self.__from_user, self.__to_other, msg.as_string())  # 开始发送\n                print(\"邮件发送成功，发送时间【%s】\" % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))))\n                break\n            except Exception as e:\n                print(\n                    \"邮件发送失败，5秒钟后重新发送，发送时间【%s】\" % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))))\n                print(e)\n                retry += 1\n                time.sleep(5)\n\n\nclass Coin(object):\n    def __init__(self, coin=\"\", diff=3.0, sleep=3, reload=1 * 60 * 60, email: Email = None):\n        self.__COIN = coin  # 币名称\n        self.__DIFF = diff  # 当trend的值幅度差为该设定值，邮箱进行通知\n        self.__SLEEP_TIME = sleep  # 每次获取的睡眠时间\n        self.__RELOAD_TIME = reload  # 每小时重复更新一次页面\n        self.__email = email\n        self.__RINGE_COUNT = int(self.__RELOAD_TIME / self.__SLEEP_TIME)\n        self.__is_release = False\n        self.__last_price = 0.0\n        self.__last_time = 0\n        self.__last_trend = 0.0\n        self.__is_first = True\n        self.__URL = \"https://app.wkj.pub/api/market/getMarketTradeJson\"  # 请求地址\n        self.__DATA = 'market=%s_bitcny&sellnum=10&buynum=10&donenum=10' % coin.lower()\n        self.__headers = {\"user-agent\": agent}\n\n    def start(self):\n        print(\"thread【%2s diff is %2s , sleep is %2d ,retry time is %5ld】\" % (\n            self.__COIN, str(self.__DIFF), self.__SLEEP_TIME, self.__RELOAD_TIME))\n        while True:\n            try:\n                time.sleep(self.__SLEEP_TIME)\n                if self.__is_release:\n                    print(\"%2s 已释放，退出！\" % self.__COIN)\n                    break\n                req = requests.post(self.__URL, data=self.__DATA, headers=self.__headers)\n                if req.status_code != 200:\n                    print(\"%2s 请求失败-%s\" % (self.__COIN, req.status_code))\n                    continue\n                data = json.loads(req.text)\n                # print(req.text)\n                price = float(data.get(\"data\").get(\"new_price\"))  # 当前价格\n                trend = float(data.get(\"data\").get(\"change\"))  # 跌涨幅 趋势\n                num = float(data.get(\"data\").get(\"doneOrders\")[0].get(\"num\"))  # 当前成交价\n                done_time = int(data.get(\"data\").get(\"doneOrders\")[0].get(\"time\"))  # 当前成交价\n                done_type = int(data.get(\"data\").get(\"doneOrders\")[0].get(\"type\"))  # 当前状态（1-buy，2-sell）\n                if price <= 0:\n                    print(\"%2s 价格为0，没有获取到数据，跳过\" % self.__COIN)\n                    continue\n                if done_time == self.__last_time:  # 没有最新成交的单\n                    print(\"【%2s …………】\" % self.__COIN)\n                    continue\n                status = \"buy\" if done_type == 1 else \"sell\"\n                print(\"【%2s \\t| %2s \\t| %5s \\t| %2s％ \\t| %2s】\" % (\n                    self.__COIN, status, str(price), trend, str(num)))\n                self.__last_price = price\n                self.__last_time = done_time\n                if self.__is_first:  # 第一次启动就不必了\n                    self.__is_first = False\n                    self.__last_trend = trend\n                    print(\"【%2s __is_first】\" % self.__COIN)\n                    continue\n                temp_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))\n                if self.__last_trend - trend > self.__DIFF:  # 跌了\n                    self.__last_trend = trend\n                    self.__email.sendMsg(\n                        \"【%2s跌:%s | %s％】\" % (self.__COIN, str(self.__last_price), str(trend)),\n                        str(\"【价格：%s，跌幅：%s％，时间：%s】\" % (\n                            str(self.__last_price), str(trend), temp_time)))\n                elif trend - self.__last_trend > self.__DIFF:  # 涨了\n                    self.__last_trend = trend\n                    self.__email.sendMsg(\n                        \"【%2s涨:%s | %s％】\" % (self.__COIN, str(self.__last_price), str(trend)),\n                        str(\"【价格：%s，涨幅：%s％，时间：%s】\" % (\n                            str(self.__last_price), str(trend), temp_time)))\n            except Exception as e:\n                print(\"wkj api【%2s】异常，5秒钟后重试!\" % self.__COIN)\n                print(e)\n                time.sleep(5)\n        print(\"【%2s 结束了】\" % self.__COIN)\n\n\nif __name__ == \"__main__\":\n    email = Email(from_user=\"aaa@qq.com\", pwd=\"bbb\", to_other='ccc@qq.com')\n    eth = Coin(coin=\"ETH\", diff=2, sleep=5, reload=2 * 60 * 60, email=email)\n    eth.start()"
  },
  {
    "path": "005-PaidSource/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "005-PaidSource/005-PaidSource.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\" />\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "005-PaidSource/README.md",
    "content": "# 这些脚本你肯定会有用到的\n\n### 操作已打开的chrome浏览器\n\n场景：某些情况我们获取怎么都获取不到cookie，但我们可以使用先在浏览器上登录，然后进行自动化操作。\n\n操作指南：\n\n```shell\n需要以该方式启动的浏览器：\nwin: chrome.exe --remote-debugging-port=9222\nmac：/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome  --remote-debugging-port=9222&\n```\n\n实现脚本：[chrome.py](./chrome.py)\n\n### excel表的常规操作\n\n场景：word文档生活使用就不用多说了，学会定能给生活带来很大的便利。\n\n操作指南：使用 pandas 开源库实现。\n\n实现脚本：① 考勤统计实现 [kaoqin.py](./kaoqin.py) 。②从excel表取数据翻译后重新写入[gtransfer.py](./gtransfer.py)\n\n### 用ffmpeg批量修改视频的md5值\n\n场景：短视频搬运专用。\n\n操作指南：需要安装ffmpeg环境。\n\n实现脚本：[ff_video.py](./ff_video.py)\n\n### 文件相关操作：json读写、文件子目录文件获取、html转word等\n\n场景：文件的一些操作。\n\n操作指南：略。\n\n实现脚本：[file_util.py](./file_util.py)\n\n### 其他站点爬虫与解析\n\n场景：注意学会BeautifulSoup解析，取属性值等。\n\n操作指南：略。\n\n实现脚本：[other_site.py](./other_site.py)"
  },
  {
    "path": "005-PaidSource/__init__.py",
    "content": ""
  },
  {
    "path": "005-PaidSource/chrome.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 用已经打开的chrome浏览器进行自动化操作。\n在某些应用场景我们获取怎么都获取不到cookie，但我们可以使用先在浏览器上登录，然后进行自动化操作。\n这里实现book118.com网站自动化操作。\n@Date       :2022/1/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport asyncio\nimport random\nimport time\n\nimport aiohttp\nimport requests\nfrom bs4 import BeautifulSoup\nfrom pyppeteer import launcher\n\nimport file_util as futls\nfrom v2ray_pool import Net\n\nloop = asyncio.get_event_loop()\n\n\nasync def get_cookie(page):\n    \"\"\"\n    获取cookie\n    :param:page page对象\n    :return:cookies 处理后的cookie\n    \"\"\"\n    cookie_list = await page.cookies()\n    cookies = \"\"\n    for cookie in cookie_list:\n        coo = \"{}={};\".format(cookie.get(\"name\"), cookie.get(\"value\"))\n        cookies += coo\n    return cookies\n\n\nasync def main():\n    async with aiohttp.ClientSession() as session:\n        try:\n            async with session.get(\"http://localhost:9222/json/version\") as response:\n                chrome = await response.json()\n                browser = await launcher.connect(\n                    defaultViewport=None,\n                    loop=loop,\n                    browserWSEndpoint=chrome['webSocketDebuggerUrl']\n                )\n        except aiohttp.ClientConnectorError:\n            print(\"start chrome --headless --remote-debugging-port=9222 --disable-gpu\")\n            return\n    # pages = await browser.pages()\n    page = await browser.newPage()  # \"通过 Browser 对象创建页面 Page 对象\"\n    await page.goto('https://max.book118.com/user_center_v1/doc/Doclist/trash.html')\n    table = await page.waitForSelector('#table')\n    print(table)\n    content = await page.content()  # 获取页面内容\n    futls.write(content, 'test/src.html')\n    agent = await browser.userAgent()\n    cookies = await get_cookie(page)\n    print('agent：[%s]' % agent)\n    print('cookies:[%s]' % cookies)\n    results = Book118.parse_page(content)\n    print(results)\n    print('需要操作的个数:[%d]' % len(results))\n    headers = {\n        'Cookie': cookies,\n        'User-Agent': agent\n    }\n    for result in results:\n        aids = result.get('aids')\n        title = result.get('title')\n        Book118.recycling(headers, aids, title)\n        time.sleep(random.randint(2, 5))\n        Book118.recycling_name(headers, aids)\n        time.sleep(random.randint(2, 5))\n\n\nclass Book118(Net):\n    '''https://max.book118.com/user_center_v1/doc/index/index.html#trash'''\n\n    @staticmethod\n    def recycling(headers, aids, title):\n        data = {\n            'aids': aids,\n            'is_optimization': 0,\n            'title': title,\n            'keywords': '',\n            'typeid': 481,\n            'dirid': 0,\n            'is_original': 0,\n            'needmoney': random.randint(3, 35),\n            'summary': ''\n        }\n        url = 'https://max.book118.com/user_center_v1/doc/Api/updateDocument/docListType/recycling'\n        r = requests.post(url=url, data=data, headers=headers, allow_redirects=False, verify=False, timeout=15,\n                          stream=True)\n        if r.status_code == 200:\n            print('修改[%s]成功' % title)\n        else:\n            print(r)\n            raise Exception('[%s]修改失败！' % title)\n\n    @staticmethod\n    def recycling_name(headers, aids):\n        data = {\n            'aids': aids,\n            'reason': '文件名已修复',\n            'status': 1\n        }\n        url = 'https://max.book118.com/user_center_v1/doc/Api/recoverDocument/docListType/recycling'\n        r = requests.post(url=url, data=data, headers=headers, allow_redirects=False, verify=False,\n                          timeout=15, stream=True)\n        if r.status_code == 200:\n            print('提交[%s]成功' % aids)\n        else:\n            print(r)\n            raise Exception('[%s]操作失败！' % aids)\n\n    @staticmethod\n    def load_page(url):\n        r = requests.get(url=url, allow_redirects=False, verify=False,\n                         timeout=15, stream=True)\n        r.encoding = r.apparent_encoding\n        print('url[%s], code[%d]' % (url, r.status_code))\n        if r.status_code == 200:\n            return r.text\n        return None\n\n    @staticmethod\n    def parse_page(content):\n        soup = BeautifulSoup(content, 'html.parser')\n        tbody = soup.find('tbody')\n        results = []\n        for tr in tbody.find_all('tr'):\n            if '文档名不规范' in tr.find('td', class_='col-delete-reason').text:\n                title: str = tr.get_attribute_list('data-title')[0]\n                if title.endswith('..docx'):\n                    title = title.replace('..docx', '')\n                    aids = tr.get_attribute_list('data-aid')[0]\n                    results.append({'aids': aids, 'title': title})\n        return results\n\n\nif __name__ == \"__main__\":\n    '''\n    注意：需要以该方式启动的浏览器：\n    win: chrome.exe --remote-debugging-port=9222\n    mac：/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome  --remote-debugging-port=9222&\n    '''\n    loop.run_until_complete(main())\n"
  },
  {
    "path": "005-PaidSource/ff_video.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 使用ffmpeg去掉最后一帧，改变md5。短视频搬运专用\n@Date       :2022/02/17\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\n\n\ndef cute_video(folder):\n    files = next(os.walk(folder))[2]  # 获取文件\n    for file in files:\n        file_path = os.path.join(folder, file)\n        shotname, extension = os.path.splitext(file)\n        if len(shotname) == 0 or len(extension) == 0:\n            continue\n        out_file = os.path.join(folder, 'out-{}{}'.format(shotname, extension))\n        # 获取时间。输入自己系统安装的ffmpeg，注意斜杠\n        time = os.popen(\n            r\"/usr/local/ffmpeg/bin/ffmpeg -i {} 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//\".format(\n                file_path)).read().replace('\\n', '').replace(' ', '')\n        if '.' in time:\n            match_time = time.split('.')[0]\n        else:\n            match_time = time\n        print(match_time)\n        ts = match_time.split(':')\n        sec = int(ts[0]) * 60 * 60 + int(ts[1]) * 60 + int(ts[2])\n        # 从0分0秒100毫秒开始截切（目的就是去头去尾）\n        os.popen(r\"/usr/local/ffmpeg/bin/ffmpeg -ss 0:00.100 -i {} -t {} -c:v copy -c:a copy {}\".format(file_path, sec,\n                                                                                                        out_file))\n\n\n# 主模块执行\nif __name__ == \"__main__\":\n    # path = os.path.dirname('/Users/Qincji/Downloads/ffmpeg/')\n    path = os.path.dirname('需要处理的目录')  # 目录下的所有视频\n    cute_video(path)\n"
  },
  {
    "path": "005-PaidSource/file_util.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 文件相关处理\n@Date       :2022/01/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport datetime\nimport json\nimport os\nimport re\nimport shutil\n\nimport cairosvg\nimport pandas as pd\nimport pypandoc  # 要安装pandoc\nfrom docx import Document\n\n\ndef file_name(file_dir):\n    results = []\n    for root, dirs, files in os.walk(file_dir):\n        # print(root)  # 当前目录路径\n        # print(dirs)  # 当前路径下所有子目录\n        # print(files)  # 当前路径下所有非目录子文件\n        results += files\n    return results\n\n\ndef deal_one_page():\n    fs = file_name('100条')\n    for f in fs:\n        try:\n            print('正在检测【%s】' % f)\n            shotname, extension = os.path.splitext('%s' % f)\n            print('正在检测【%s】' % shotname)\n            if '1篇' in shotname:\n                new_name = re.sub(r'1篇', '', f)\n                document = Document(r\"html/%s\" % f)\n                paragraphs = document.paragraphs\n                p = paragraphs[0]\n                p._element.getparent().remove(p._element)\n                document.save(r\"html/%s\" % new_name)\n                os.remove('html/%s' % f)\n        except Exception as e:\n            print(e)\n\n\ndef copy_doc():\n    fs = file_name('all')\n    i = 1\n    k = 1\n    temp_dir = '01'\n    os.makedirs('100条/%s' % temp_dir)\n    for f in fs:\n        try:\n            # print('正在检测【%s】' % f)\n            shotname, extension = os.path.splitext('%s' % f)\n            shutil.copyfile(r'all/%s' % f, r'100条/%s/%s' % (temp_dir, f))\n            if i % 100 == 0:\n                temp_dir = '0%d' % k if k < 10 else '%d' % k\n                k += 1\n                os.makedirs('100条/%s' % temp_dir)\n            i += 1\n        except Exception as e:\n            print(e)\n\n\n'''########文件处理相关#########'''\n\n\ndef html_cover_doc(in_path, out_path):\n    '''将html转化成功doc'''\n    path, file_name = os.path.split(out_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    pypandoc.convert_file(in_path, 'docx', outputfile=out_path)\n\n\ndef svg_cover_jpg(src, dst):\n    ''''\n    drawing = svg2rlg(\"drawing.svg\")\n    renderPDF.drawToFile(drawing, \"drawing.pdf\")\n    renderPM.drawToFile(drawing, \"fdrawing.png\", fmt=\"PNG\")\n    renderPM.drawToFile(drawing, \"drawing.jpg\", fmt=\"JPG\")\n    '''\n    path, file_name = os.path.split(dst)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    # drawing = svg2rlg(src)\n    # renderPM.drawToFile(drawing, dst, fmt=\"JPG\")\n    cairosvg.svg2png(url=src, write_to=dst)\n\n\ndef html_cover_excel(content, out_path):\n    '''将html转化成excel'''\n    path, file_name = os.path.split(out_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    tables = pd.read_html(content, encoding='utf-8')\n    writer = pd.ExcelWriter(out_path)\n    for i in range(len(tables)):\n        tables[i].to_excel(writer, sheet_name='表%d' % (i + 1))  # startrow\n    writer.save()  # 写入硬盘\n\n\ndef write_to_html(content, file_path):\n    '''将内容写入本地，自动加上head等信息'''\n    page = '''<!DOCTYPE html>\n                <html>\n                <head>\n                    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n                </head>\n                <body>'''\n    page += content\n    page += '''</body>\n    </html>'''\n    write(page, file_path)\n\n\ndef write_json(content, file_path):\n    '''写入json'''\n    path, file_name = os.path.split(file_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    with open(file_path, 'w') as f:\n        json.dump(content, f, ensure_ascii=False)\n        f.close()\n\n\ndef read_json(file_path):\n    '''读取json'''\n    with open(file_path, 'r') as f:\n        js_get = json.load(f)\n        f.close()\n    return js_get\n\n\ndef write(content, file_path):\n    '''写入txt文本内容'''\n    path, file_name = os.path.split(file_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    with open(file_path, 'w') as f:\n        f.write(content)\n        f.close()\n\n\ndef read(file_path) -> str:\n    '''读取txt文本内容'''\n    content = None\n    try:\n        with open(file_path, 'r') as f:\n            content = f.read()\n            f.close()\n    except Exception as e:\n        print(e)\n    return content\n\n\ndef get_next_folder(dst, day_diff, folder, max_size):\n    '''遍历目录文件，直到文件夹不存在或者数目达到最大（max_size）时，返回路径'''\n    while True:\n        day_time = (datetime.date.today() + datetime.timedelta(days=day_diff)).strftime('%Y-%m-%d')  # 下一天的目录继续遍历\n        folder_path = os.path.join(dst, day_time, folder)\n        if os.path.exists(folder_path):  # 已存在目录\n            size = len(next(os.walk(folder_path))[2])\n            if size >= max_size:  # 该下一个目录了\n                day_diff += 1\n                continue\n        else:\n            os.makedirs(folder_path)\n        return day_diff, folder_path\n\n\nif __name__ == '__main__':\n    pass\n"
  },
  {
    "path": "005-PaidSource/gsearch.py",
    "content": "import re\nimport time\nimport os\nfrom urllib.parse import quote_plus\n\nimport chardet\nimport requests_html\nimport pypandoc  # 要安装pandoc\n\nfrom v2ray_pool import Net\nfrom bs4 import BeautifulSoup\nimport googlesearch as ggs\nimport os\nimport random\nimport sys\nimport time\nimport ssl\n\nBLACK_DOMAIN = ['www.google.gf', 'www.google.io', 'www.google.com.lc']\nDOMAIN = 'www.google.com'\n\n\nclass GSearch(Net):\n    def search_page(self, url, pause=3):\n        \"\"\"\n        Google search\n        :param query: Keyword\n        :param language: Language\n        :return: result\n        \"\"\"\n        time.sleep(random.randint(1, pause))\n        try:\n            r = self.request_en(url)\n            print('resp code=%d' % r.status_code)\n            if r.status_code == 200:\n                charset = chardet.detect(r.content)\n                content = r.content.decode(charset['encoding'])\n                return content\n            elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n                location = r.headers['Location']\n                time.sleep(random.randint(1, pause))\n                return self.search_page(location)\n            # elif r.status_code == 429 or r.status_code == 443:\n            #     time.sleep(3)\n            #     return search_page(url)\n            return None\n        except Exception as e:\n            print(e)\n            return None\n\n    def parse_html(self, html):\n        soup = BeautifulSoup(html, 'html.parser')  # 声明BeautifulSoup对象\n        # find = soup.find('p')  # 使用find方法查到第一个p标签\n        # print('----->>>>%s' % str(find.text))\n        p_s = soup.find_all('p')\n        results = []\n        for p in p_s:\n            if p.find('img'):  # 不要带有图片的标签\n                continue\n            if p.find('a'):  # 不要带有链接的标签\n                continue\n            content = str(p)\n            if \"文章来源\" in content:\n                print('过滤[文章来源]>>>>>%s' % content)\n                continue\n            if \"来源：\" in content:\n                print('过滤[来源：]>>>>>%s' % content)\n                continue\n            if len(p.text.replace('\\n', '').strip()) < 1:  # 过滤空内容\n                # print('过滤[空字符]>>>>>！')\n                continue\n            results.append(content)\n        results.append('<p></br></p>')  # 隔一下\n        return results\n        # return re.findall(r'(<p.*?</p>)', html, re.DOTALL)\n\n    def get_html(self, url):\n        session = requests_html.HTMLSession()\n        html = session.get(url)\n        html.encoding = html.apparent_encoding\n        return html.text\n\n    def conver_to_doc(self, in_name, out_name):\n        try:\n            pypandoc.convert_file('%s.html' % in_name, 'docx', outputfile=\"doc/%s.docx\" % out_name)\n            os.remove('%s.html' % in_name)\n        except Exception as e:\n            print(e)\n\n    def download_and_merge_page(self, urls, name):\n        try:\n            page = ['''<!DOCTYPE html>\n                <html>\n                <head>\n                    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n                </head>\n            ''']\n            k = 0\n            size = random.randint(3, 5)  # 每次合并成功5篇即可\n            for i in range(len(urls)):\n                if k >= size:\n                    break\n                try:\n                    temp = self.parse_html(self.get_html(urls[i]))\n                except Exception as e:\n                    print(e)\n                    continue\n                if len(temp) < 3:  # 篇幅太短\n                    continue\n                page.append(\n                    '<p style=\"text-align: left; font-family: 宋体; font-size: 16px; line-height: 1.75; margin-bottom: 10px;\"><strong>第%d篇：</strong></p>' % (\n                            k + 1))  # 加入标题\n                page += temp\n                page.append('\\n')\n                k += 1\n            page.append('</html>')\n            with open(\"%s.html\" % name, mode=\"w\") as f:  # 写入文件\n                for p in page:\n                    f.write(p)\n            return k\n        except Exception as e:\n            print(e)\n            return 0\n\n    def get_full_urls(self, html):\n        a_s = re.findall(r'<a.*?</a>', html, re.DOTALL)\n        results = []\n        for a in a_s:\n            try:\n                # print(a)\n                # url = re.findall(r'/url\\?q=(.*?\\.html)', a, re.DOTALL)[0]\n                url: str = re.findall(r'(http[s]{0,1}://.*?\\.html)', a, re.DOTALL)[0]\n                # title = re.findall(r'<span.*?>(.*?)</span>', a, re.DOTALL)[0] #会有问题\n                # print('{\"url\":\"%s\",\"title\":\"%s\"}' % (url, title))\n                if 'google.com' in url:\n                    continue\n                if url in results:\n                    continue\n                # 过来同一个网站的\n                domain = re.findall('http[s]{0,1}://(.*?)/', url, re.DOTALL)[0]\n                # 含有--的\n                if '-' in domain:\n                    continue\n                # www.sz.gov.cn，'.'超过4个时绝对不行的，像：bbs.jrj.ex3.http.80.ipv6.luzhai.gov.cn\n                if domain.count('.') > 4:\n                    continue\n                for u in results:\n                    if domain in u:\n                        continue\n                results.append(url)\n            except Exception as e:\n                # print(e)\n                pass\n        return results\n\n    def get_full_titles(self, html):\n        results = []\n        soup = BeautifulSoup(html, \"html.parser\")\n        results = []\n        for a in soup.find_all(name='a'):\n\n            try:\n                h3 = a.find(name='h3')\n                if h3 and h3.has_attr('div'):\n                    div = h3.find(name='div')\n                    results.append(div.getText())\n                else:\n                    div = a.find(name='span')\n                    results.append(div.getText())\n\n            except Exception as e:\n                print(e)\n        return results\n\n    def format_common_url(self, search, domain='www.google.com', start=0):\n        url = 'https://{domain}/search?q={search}&start={start}'\n        url = url.format(domain=domain, search=quote_plus(search), start=start)\n        return url\n\n    def format_full_url(self, domain, as_q='', as_epq='', as_oq='', as_eq='', as_nlo='', as_nhi='', lr='', cr='',\n                        as_qdr='',\n                        as_sitesearch='',\n                        as_filetype='', tbs='', start=0, num=10):\n        \"\"\"\n        https://www.google.com/advanced_search\n        https://www.google.com/search?as_q=%E8%A1%A3%E6%9C%8D+%E8%A3%A4%E5%AD%90+%E6%9C%8D%E8%A3%85+%E9%A5%B0%E5%93%81+%E7%8F%A0%E5%AE%9D+%E9%93%B6%E9%A5%B0&as_epq=%E5%AE%98%E7%BD%91&as_oq=%E6%9C%8D%E8%A3%85+or+%E9%85%8D%E9%A5%B0&as_eq=%E9%9E%8B%E5%AD%90&as_nlo=20&as_nhi=1000&lr=lang_zh-CN&cr=countryCN&as_qdr=m&as_sitesearch=.com&as_occt=body&safe=active&as_filetype=&tbs=\n        allintext: 衣服 裤子 服装 饰品 珠宝 银饰 服装 OR or OR 配饰 \"官网\" -鞋子 site:.com 20..1000\n        :param domain: 域名：google.com\n        :param as_q:  输入重要字词： 砀山鸭梨\n        :param as_epq: 用引号将需要完全匹配的字词引起： \"鸭梨\"\n        :param as_oq: 在所需字词之间添加 OR： 批发 OR 特价\n        :param as_eq: 在不需要的字词前添加一个减号： -山大、-\"刺梨\"\n        :param as_nlo: 起点，在数字之间加上两个句号并添加度量单位：0..35 斤、300..500 元、2010..2011 年\n        :param as_nhi: 终点，在数字之间加上两个句号并添加度量单位：0..35 斤、300..500 元、2010..2011 年\n        :param lr: 查找使用您所选语言的网页。\n        :param cr: 查找在特定地区发布的网页。\n        :param as_qdr: 查找在指定时间内更新的网页。\n        :param as_sitesearch: 搜索某个网站（例如 wikipedia.org ），或将搜索结果限制为特定的域名类型(例如 .edu、.org 或 .gov)\n        :param as_filetype: 查找采用您指定格式的网页。如：filetype:pdf\n        :param tbs: 查找可自己随意使用的网页。\n        :param start: 第几页，如 90：表示从第9页开始，每一页10条\n        :param num: 每一页的条数\n        :return:\n        \"\"\"\n        url = 'https://{domain}/search?as_q={as_q}&as_epq={as_epq}&as_oq={as_oq}&as_eq={as_eq}&as_nlo={as_nlo}&as_nhi={as_nhi}&lr={lr}&cr={cr}&as_qdr={as_qdr}&as_sitesearch={as_sitesearch}&as_occt=body&safe=active&as_filetype={as_filetype}&tbs={tbs}&start={start}&num={num}'\n        url = url.format(domain=domain, as_q=quote_plus(as_q), as_epq=quote_plus(as_epq), as_oq=quote_plus(as_oq),\n                         as_eq=quote_plus(as_eq), as_nlo=as_nlo, as_nhi=as_nhi, lr=lr, cr=cr, as_qdr=as_qdr,\n                         as_sitesearch=as_sitesearch, start=start, num=num, tbs=tbs, as_filetype=as_filetype)\n        return url\n\n\nif __name__ == '__main__':\n    url = 'http://www.sz.gov.cn/cn/zjsz/nj/content/post_1356218.html'\n    domain: str = re.findall('http[s]{0,1}://(.*?)/', url, re.DOTALL)[0]\n    print(domain.count('.'))\n    print(domain)\n"
  },
  {
    "path": "005-PaidSource/gtransfer.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 实现从excel文件获取关键词进行翻译后写入新文件\n@Date       :2021/10/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport json\nimport os\nimport os.path\nimport random\nimport time\n\nimport chardet\nimport pandas as pd\n\nimport file_util as futls\nimport v2ray_util as utils\nfrom v2ray_pool import Net\n\nBLACK_DOMAIN = ['www.google.gf', 'www.google.io', 'www.google.com.lc']\nDOMAIN = 'www.google.com'\n\n\nclass GTransfer(Net):\n    def search_page(self, url, pause=3):\n        \"\"\"\n        Google search\n        :param query: Keyword\n        :param language: Language\n        :return: result\n        \"\"\"\n        time.sleep(random.randint(1, pause))\n        try:\n            r = self.request_en(url)\n            print('resp code=%d' % r.status_code)\n            if r.status_code == 200:\n                charset = chardet.detect(r.content)\n                content = r.content.decode(charset['encoding'])\n                return content\n            elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n                location = r.headers['Location']\n                time.sleep(random.randint(1, pause))\n                return self.search_page(location)\n            return None\n        except Exception as e:\n            print(e)\n            return None\n\n    def transfer(self, content):\n        # url = 'http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=' + content\n        url = 'http://translate.google.cn/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=en&tl=zh-CN&q=' + content\n        try:\n            cache = futls.read_json('data/cache.json')\n            for c in cache:\n                if content in c:\n                    print('已存在，跳过：{}'.format(content))\n                    return c.get(content)\n        except Exception as e:\n            pass\n        try:\n            result = self.search_page(url)\n            trans = json.loads(result)['sentences'][0]['trans']\n            # 解析获取翻译后的数据\n            # print(result)\n            print(trans)\n            self.local_cache.append({content: trans})\n            futls.write_json(self.local_cache, 'data/cache.json')\n            # 写入数据吗？下次直接缓存取\n        except Exception as e:\n            print(e)\n            utils.restart_v2ray()\n            return self.transfer(content)\n        return trans\n\n    def init_param(self, file_name):\n        utils.restart_v2ray()\n        self.local_cache = []\n        # 第一次加载本地的（已翻译的就不再翻译了）\n        try:\n            cache = futls.read_json('data/cache.json')\n            for c in cache:\n                self.local_cache.append(c)\n        except Exception as e:\n            pass\n        csv_file = os.path.join('data', file_name)\n        csv_out = os.path.join('data', 'out_' + file_name)\n        df = pd.read_excel(csv_file, sheet_name='CompetitorWords')\n        # 代表取出第一行至最后一行，代表取出第四列至最后一列。\n        datas = df.values\n        size = len(df)\n        print('总共有{}行数据'.format(size))\n        titles, titles_zh, keys1, keys2, keys3, pros = [], [], [], [], [], []\n        for col in range(0, size):\n            t = datas[col][0]\n            titles.append(t)\n            keys1.append(datas[col][1])\n            keys2.append(datas[col][2])\n            keys3.append(datas[col][3])\n            pros.append(datas[col][4])\n            titles_zh.append(self.transfer(t))\n            print('总共{}，现在到{}'.format(size, col + 1))\n        df_write = pd.DataFrame(\n            {'标题': titles, '中文标题': titles_zh, '关键词1': keys1, '关键词2': keys2, '关键词3': keys3, '橱窗产品': pros})\n        df_write.to_excel(csv_out, index=False)\n        utils.kill_all_v2ray()\n\n\n# http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=what\n\n\nif __name__ == '__main__':\n    g = GTransfer()\n    g.init_param('xxx.xls')\n    # utils.search_node()\n"
  },
  {
    "path": "005-PaidSource/kaoqin.py",
    "content": "\"\"\"\n@Description: excel表的常规操作，这里实现统计考勤\n@Date       :2022/02/21\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport pandas as pd\nimport calendar\nfrom pandas._libs.tslibs.timestamps import Timestamp\n\n\ndef get_days(year, month):  # 获取输出日期的列明\n    dates = calendar.monthrange(year, month)\n    week = dates[0]  # 1号那天是星期几\n    days = dates[1]  # 总共的天数\n    print(dates)\n    index_time = []\n    for day in range(1, days):\n        index_time.append('{}-{}-{}  星期{}'.format(year, month, day, (week + day) % 7))\n    print(index_time)\n    return index_time\n\n\ndef parse_excel(csv_file, out_file, names, dates):\n    df = pd.read_excel(csv_file, sheet_name='Sheet')  # 从文件和表格名称读取\n    datas = df.values\n    size = len(df)\n    print('总共有{}行数据'.format(size))\n    results = {}\n    for name in names:  # 我是根据名字统计\n        results.update({name: ['' for x in range(len(dates))]})  # 默认生成每个日期的空格\n    for col in range(0, size):\n        s_name = datas[col][2]  # 打印一下就知道去的那是哪里列的值了\n        t_time: Timestamp = datas[col][6]  # 我这里是时间戳，用type(datas[col][6])打印类型可知\n        if s_name not in names:\n            continue\n        # 获取这天是哪一天的，name_datas是哪个人对应的列表数据\n        d, h, m, name_datas = t_time.day, t_time.hour, t_time.minute, results.get(s_name)\n        # 早上 9:00前打卡，下午18:00后打卡，取一天最早和最晚的一次即可，门禁可能有很多数据\n        tt = '2022-1-{} {}:{}'.format(d, h, m)\n        old = name_datas[d - 1]  # 下标\n        if len(old) < 5:  # 空的\n            name_datas[d - 1] = '{} 早 {};'.format(tt, '' if h < 9 else '异常')  # 上班打卡\n        else:\n            # 去除第一个：\n            first = old.split(';')[0]\n            last = '{} 晚 {}'.format(tt, '' if h >= 18 else '异常')\n            name_datas[d - 1] = '{};{}'.format(first, last)\n    print(results)\n    df_write = pd.DataFrame(results, index=dates)\n    df_write.to_excel(out_file, index=True)  # 写入输出表格数据\n\n\nif __name__ == '__main__':\n    names = ['x1', 'x2', 'x3', 'x4', 'x5']  # 要统计那些人\n    parse_excel('data/一月考勤.xls', 'data/out_kaoqin.xls', names, get_days(2022, 1))\n"
  },
  {
    "path": "005-PaidSource/keywords.py",
    "content": "import json\nimport random\nimport re\nimport time\nfrom urllib.parse import quote_plus\n\nfrom v2ray_pool import Net\n\nimport chardet\nimport requests\nimport urllib3\nfrom bs4 import BeautifulSoup\nfrom my_fake_useragent import UserAgent\n\n\nclass Keywords(Net):\n    '''url：https://www.5118.com/ciku/index#129'''\n\n    def get_keys_by_net(self) -> []:\n        try:\n            r = self.request(r'https://www.5118.com/ciku/index#129')\n            if r.status_code != 200:\n                return None\n            r.encoding = r.apparent_encoding\n            # <span>法律</span><br>\n            soup = BeautifulSoup(r.text, \"html.parser\")\n            results = []\n            for a in soup.find_all(name='a'):\n                results += re.findall(r'<span>(.*?)</span><br', str(a.get_text), re.DOTALL)\n            return results\n        except Exception as e:\n            print(e)\n            return None\n\n    def get_keys_by_local(self) -> []:\n        with open('test/key_tag.json', 'r') as f:\n            js_get = json.load(f)\n            f.close()\n        return js_get\n\n    def get_titles_by_local(self) -> []:\n        with open('test/key_title.json', 'r') as f:\n            js_get = json.load(f)\n            f.close()\n        return js_get\n\n    def get_titles_by_net(self, key):\n        '''通过网盘搜索检查出\n        https://www.alipanso.com/search.html?page=1&keyword=%E7%90%86%E8%B4%A2&search_folder_or_file=2&is_search_folder_content=1&is_search_path_title=1&category=doc&file_extension=doc&search_model=1\n        '''\n        results = []\n        try:\n            time.sleep(random.randint(1, 4))\n            r = self.request_en(\n                r'https://www.alipanso.com/search.html?page=1&keyword=%s&search_folder_or_file=2&is_search_folder_content=1&is_search_path_title=1&category=doc&file_extension=doc&search_model=1' % key)\n            if r.status_code != 200:\n                print(r.status_code)\n                return None\n            r.encoding = r.apparent_encoding\n            soup = BeautifulSoup(r.text, \"html.parser\")\n            for a in soup.find_all(name='a'):\n                ts = re.findall(r'(.*?).doc', str(a.get_text()).replace('\\n', ''), re.DOTALL)\n                for t in ts:\n                    if '公众号' in t or '【' in t or '[' in t or ',' in t or '，' in t or ')' in t or '）' in t or t in results:\n                        continue\n                    if len(t) < 4:\n                        continue\n                    results.append(t)\n            return results\n        except Exception as e:\n            print(e)\n            return None\n\n\ndef test():\n    js = ['a', 'b', 'c']\n    with open('test/key_tag.json', 'w') as f:\n        json.dump(js, f)\n        f.close()\n    with open('test/key_tag.json', 'r') as f:\n        js_get = json.load(f)\n        f.close()\n    print(js_get)\n\n\nif __name__ == \"__main__\":\n    # test()\n    keys = Keywords().get_keys_by_net()\n    print(keys)\n    with open('test/key_tag.json', 'w') as f:\n        json.dump(keys, f, ensure_ascii=False)\n        f.close()\n"
  },
  {
    "path": "005-PaidSource/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 关键词获取\n@Date       :2021/09/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n# from amazon import run_api\nimport json\nimport re\n\nimport yagooglesearch\n\nimport v2ray_util as utils\nfrom gsearch import GSearch\nfrom keywords import Keywords\n\n\ndef start_task():\n    kd = Keywords()\n    keywords = kd.get_titles_by_local()\n    name = 'temp'\n    gs = GSearch()\n    i = 0\n    is_need_start = True\n    while i < len(keywords):\n        if is_need_start:\n            utils.restart_v2ray()\n            gs.update_agent()\n        is_need_start = True\n        key = keywords[i]\n        query = 'site:gov.cn filetype:html \"%s\"' % key\n        client = yagooglesearch.SearchClient(\n            query,\n            tbs=\"li:1\",\n            max_search_result_urls_to_return=100,\n            http_429_cool_off_time_in_minutes=49,\n            http_429_cool_off_factor=1.5,\n            proxy=\"socks5h://127.0.0.1:1080\",\n            verbosity=5,\n        )\n        client.assign_random_user_agent()\n        try:\n            page_urls = client.search()\n        except Exception:\n            continue\n        new_urls = []\n        for u1 in page_urls:\n            domain: str = re.findall('http[s]{0,1}://(.*?)/', u1, re.DOTALL)[0]\n            # 含有--的\n            if '-' in domain:\n                continue\n            # www.sz.gov.cn，'.'超过4个时绝对不行的，像：bbs.jrj.ex3.http.80.ipv6.luzhai.gov.cn\n            if domain.count('.') > 4:\n                continue\n            for u2 in new_urls:\n                if domain in u2:\n                    continue\n                new_urls.append(u2)\n        print('过滤器链接数：%d, 过滤后链接数：%d' % (len(page_urls), len(new_urls)))\n        page_size = len(new_urls)\n        if page_size == 0:\n            print('[%s]获取文章链接失败！' % key)\n            continue\n        if not gs.download_and_merge_page(page_urls, name):  # 合并文章\n            print('下载或者合并失败')\n            continue\n        doc_name = '%d篇%s' % (page_size, key)\n        gs.conver_to_doc(name, doc_name)\n        is_need_start = False\n        i += 1\n\n    utils.kill_all_v2ray()\n\n\ndef start_proxy_task():\n    kd = Keywords()\n    keywords: [] = kd.get_titles_by_local()\n    name = 'temp'\n    gs = GSearch()\n    key_s = []\n    key_s.pop()\n\n\ndef start_task2():\n    kd = Keywords()\n    keywords: [] = kd.get_titles_by_local()\n    name = 'temp'\n    gs = GSearch()\n    i = 0\n    is_need_start = True\n    key_size = len(keywords)\n    key = keywords.pop()\n    while i < key_size:\n        if is_need_start:\n            utils.restart_v2ray()\n            gs.update_agent()\n        else:\n            key = keywords.pop()\n        is_need_start = True\n        # key_url = gs.format_full_url(domain='google.com', as_sitesearch='.gov.cn', as_filetype='html', as_epq=key,\n        #                              lr='lang_zh-CN', cr='countryCN')\n        # key_url = gs.format_full_url(domain='search.iwiki.uk', as_sitesearch='gov.cn', as_filetype='html', as_epq=key,\n        #                              lr='lang_zh-CN', cr='countryCN')\n        # key_url = gs.format_common_url('site:gov.cn filetype:html %s' % key, domain='search.iwiki.uk')\n        key_url = gs.format_common_url('site:gov.cn intitle:%s' % key, domain='www.google.com')\n        print(key_url)\n        content = gs.search_page(key_url)\n        if content is None:\n            print('[%s]搜索失败，进行重试！' % key)\n            continue\n        with open('test/test_search.html', 'w') as f:\n            f.write(content)\n            f.close()\n        page_urls = gs.get_full_urls(content)  # 获取文章的url\n        page_size = len(page_urls)\n        if page_size == 0:\n            print('[%s]没有内容，下一个...' % key)\n        else:\n            size = gs.download_and_merge_page(page_urls, name)\n            if size == 0:  # 合并文章\n                print('下载或者合并失败，跳过！')\n            else:\n                doc_name = '%d篇%s' % (size, key)\n                gs.conver_to_doc(name, doc_name)\n                print('生成[%s]文章成功！！！' % doc_name)\n        is_need_start = False\n        i += 1\n        # 重新覆盖本地关键词\n        with open('test/key_title.json', 'w') as f:\n            json.dump(keywords, f, ensure_ascii=False)\n            f.close()\n\n    utils.kill_all_v2ray()\n\n\ndef test_titles():\n    kd = Keywords()\n    keywords = kd.get_titles_by_local()\n    print('总共需要加载%d个关键词' % len(keywords))\n\n\ndef test_task():\n    kd = Keywords()\n    keywords = kd.get_keys_by_local()\n    print('总共需要加载%d个关键词' % len(keywords))\n    # keywords = ['股市基金']\n    i = 0\n    search_keys = []\n    utils.restart_v2ray()  # 第一次用固定agent\n    is_need_start = False\n    while i < len(keywords):\n        if is_need_start:\n            utils.restart_v2ray()\n            # kd.update_agent()\n        is_need_start = True\n        key = keywords[i]\n        print('开始搜索：%s' % key)\n        titles = kd.get_titles_by_net(key)\n        if titles is None:\n            print('[%s]获取关键词标题失败！' % key)\n            continue\n        print(titles)\n        for t in titles:\n            if t not in search_keys:\n                search_keys.append(t)\n        # 每次都要更新一次\n        with open('test/key_title.json', 'w') as f:\n            json.dump(search_keys, f, ensure_ascii=False)\n            f.close()\n        is_need_start = False\n        i += 1\n\n    utils.kill_all_v2ray()\n\n\ndef test_get_title():\n    with open('search_page.html', 'r') as f:\n        page = f.read()\n        f.close()\n    gs = GSearch()\n    titles = gs.get_full_titles(page)  # 获取文章的标题\n    print(titles)\n\n\nif __name__ == \"__main__\":\n    # utils.restart_v2ray()\n    utils.search_node()\n    # utils.kill_all_v2ray()\n"
  },
  {
    "path": "005-PaidSource/other_site.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 获取其他站点信息爬虫\n@Date       :2022/1/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\nimport random\nimport re\nimport time\n\nimport requests\nfrom bs4 import BeautifulSoup\n\nimport file_util as futls\nimport v2ray_util as utils\nfrom v2ray_pool import Net\n\n\nclass Cncic(Net):\n    '''中华全国商业中心：https://www.cncic.org/'''\n\n    def start_task(self):\n        utils.restart_v2ray()\n        cncic = Cncic()\n        keys = [{'cat': 92, 'name': '专题分析报告'}, {'cat': 95, 'name': '政策法规'}, {'cat': 8, 'name': '月度分析'},\n                {'cat': 10, 'name': '黄金周分析'}, {'cat': 16, 'name': '零售百强'}, {'cat': 94, 'name': '市场观察'}, ]\n        success_datas = []\n        for key in keys:\n            cat = key.get('cat')\n            name = key.get('name')\n            datas = cncic.load_list(cat)\n            while len(datas) == 0:\n                utils.restart_v2ray()\n                datas = cncic.load_list(cat)\n            success_datas.append({'name': name, 'data': datas})\n            futls.write_json(success_datas, 'data/cncic/keys.json')  # 每次保存到本地\n\n        success_datas = futls.read_json('data/cncic/keys.json')\n        key_size = len(success_datas)\n        is_need_start = False\n        key = None\n        for i in range(key_size):\n            if is_need_start:\n                utils.restart_v2ray()\n            else:\n                key = success_datas.pop()\n            if key is None:\n                key = success_datas.pop()\n            is_need_start = True\n            folder = key.get('name')\n            datas = key.get('data')\n            for data in datas:\n                try:\n                    load_page = cncic.load_page(data.get('url'))\n                except Exception as e:\n                    print(e)\n                    continue\n                title, content = cncic.parse_page(load_page)\n                html_path = 'data/html/cncic/%s/%s.html' % (folder, title)\n                doc_path = 'data/doc/cncic/%s/%s.docx' % (folder, title)\n                futls.write_to_html(content, html_path)\n                try:\n                    futls.html_cover_doc(html_path, doc_path)\n                except Exception as e:\n                    print(e)\n            futls.write_json(success_datas, 'data/cncic/keys.json')  # 更新本地数据库\n            is_need_start = False\n            i += 1\n        utils.kill_all_v2ray()\n\n    def load_list(self, cat, paged=1) -> []:\n        results = []\n        while True:\n            url = 'https://www.cncic.org/?cat=%d&paged=%d' % (cat, paged)\n            try:\n                page = self.load_page(url)\n                results += self.parse_list(page)\n                paged += 1\n                time.sleep(random.randint(3, 6))\n            except Exception as e:\n                print(e)\n                break\n        return results\n\n    def load_page(self, url):\n        '''加载页面，如：https://www.cncic.org/?p=3823'''\n        r = self.request_zh(url)\n        r.encoding = r.apparent_encoding\n        print('Cncic[%s] code[%d]' % (url, r.status_code))\n        if r.status_code == 200:\n            return r.text\n        elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n            location = r.headers['Location']\n            time.sleep(1)\n            return self.load_page(location)\n        return None\n\n    def parse_page(self, page):\n        '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n        soup = BeautifulSoup(page, 'html.parser')\n        article = soup.find('article')\n        header = article.find('header')\n        title = header.text.replace('\\n', '').replace(' ', '')\n        content = article.find('div', class_='single-content')\n        result = str(header)\n        result += str(content)\n        return title, result\n\n    def parse_list(self, page):\n        '''解析列表（如：https://www.cncic.org/?cat=92）页面，返回标题、连接、日期'''\n        soup = BeautifulSoup(page, 'html.parser')\n        main = soup.find('main')\n        articles = main.find_all('article')\n        results = []\n        for article in articles:\n            header = article.find('header')\n            url = header.find('a').get_attribute_list('href')[0]\n            title = header.text.replace('\\n', '').replace(' ', '')\n            date = article.find('span', class_='date').text\n            results.append({'url': url, 'title': title, 'date': date})\n        return results\n\n\nclass Ceicdata(Net):\n    '''https://www.ceicdata.com/'''\n\n    def start_task_1(self):\n        cd = Ceicdata()\n        utils.restart_v2ray()\n        success_datas = futls.read_json('data/keys/ceicdata.json')\n        key_size = len(success_datas)\n        print(key_size)\n        is_need_start = False\n        key = None\n        for i in range(key_size):\n            if is_need_start:\n                utils.restart_v2ray()\n                cd.update_agent()\n            else:\n                key = success_datas.pop()\n            is_need_start = True\n            if key is None:\n                key = success_datas.pop()\n            title = key.get('title')\n            url = key.get('url')\n            try:\n                page = cd.load_page(url)\n            except Exception as e:\n                page = None\n                print(e)\n            if page is None:\n                continue\n            html_path = 'data/html/ceicdata/%s.html' % title\n            content = cd.parse_page_1(page)\n            futls.write_to_html(content, html_path)\n            futls.html_cover_excel(html_path, 'data/doc/ceicdata/%s.xlsx' % title)\n            futls.write_json(success_datas, 'data/keys/ceicdata.json')  # 更新本地数据库\n            is_need_start = False\n            i += 1\n        utils.kill_all_v2ray()\n\n    @staticmethod\n    def start_task2():\n        utils.restart_v2ray()\n        cd = Ceicdata()\n        keys_path = 'data/keys/ceicdata.json'\n        keys = futls.read_json(keys_path)\n        if not keys:\n            url = 'https://www.ceicdata.com/zh-hans/country/china'\n            page = cd.load_page(url)\n            if page is None:\n                raise Exception('获取页面失败')\n            keys = cd.parse_main_2(page)\n            if len(keys) == 0:\n                raise Exception('获取链接失败')\n            futls.write_json(keys, keys_path)\n        key_size = len(keys)\n        print('下载数量[%d]' % key_size)\n        is_need_start = False\n        key = None\n        for i in range(key_size):\n            if is_need_start:\n                utils.restart_v2ray()\n                cd.update_agent()\n            else:\n                key = keys.pop()\n            is_need_start = True\n            if key is None:\n                key = keys.pop()\n            url = key.get('url')\n            try:\n                page = cd.load_page(url)\n            except Exception as e:\n                page = None\n                print(e)\n            if page is None:\n                continue\n            try:\n                title, content = cd.parse_page_2(page)\n            except Exception as e:\n                print(e)\n                continue\n            html_path = 'data/html/ceicdata2/%s.html' % title\n            futls.write_to_html(content, html_path)\n            futls.html_cover_doc(html_path, 'data/doc/ceicdata/%s.docx' % title)\n            futls.write_json(keys, keys_path)  # 更新本地数据库\n            is_need_start = False\n            i += 1\n        utils.kill_all_v2ray()\n\n    def load_page(self, url):\n        '''加载页面，如：https://www.cncic.org/?p=3823'''\n        r = self.request_en(url)\n        # r = self.request(url)\n        r.encoding = r.apparent_encoding\n        print('Cncic[%s] code[%d]' % (url, r.status_code))\n        if r.status_code == 200:\n            return r.text\n        elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n            location = r.headers['Location']\n            time.sleep(1)\n            return self.load_page(location)\n        return None\n\n    def parse_main_1(self, page):\n        '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n        soup = BeautifulSoup(page, 'html.parser')\n        main = soup.find('main')\n        lists = main.find('div', class_='indicators-lists')\n        results = []\n        for a in lists.find_all('a'):\n            # https://www.ceicdata.com/zh-hans/indicator/nominal-gdp\n            title = a.text.replace(' ', '')\n            url = 'https://www.ceicdata.com' + a.get_attribute_list('href')[0].replace(' ', '')\n            results.append({'title': title, 'url': url})\n        return results\n\n    def parse_main_2(self, page):\n        '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n        soup = BeautifulSoup(page, 'html.parser')\n        main = soup.find('main')\n        results = []\n        for tbody in main.find_all('tbody'):\n            for a in tbody.find_all('a'):\n                # https://www.ceicdata.com/zh-hans/indicator/nominal-gdp\n                title = a.text.replace(' ', '')\n                url = 'https://www.ceicdata.com' + a.get_attribute_list('href')[0].replace(' ', '')\n                results.append({'title': title, 'url': url})\n        return results\n\n    def parse_page_1(self, page):\n        '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n        soup = BeautifulSoup(page, 'html.parser')\n        main = soup.find('main')\n        clearfix = main.find('div', class_='clearfix')\n        h1 = clearfix.find('h1')\n        h2 = clearfix.find('h2')\n        tables = clearfix.find_all('table')\n        content = str(h1) + str(tables[0]) + str(h2) + str(tables[1])\n        return content\n\n    def parse_page_2(self, page):\n        '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n        soup = BeautifulSoup(page, 'html.parser')\n        main = soup.find('main')\n        left = main.find('div', id='left-col-7')\n        title = left.find('span', class_='c-purple').text.replace(' ', '').replace('\\n', '')\n        left.find('div', id='breadcrumb').decompose()  # 移除节点\n        for ele in left.find_all('div', class_='hide'):\n            ele.decompose()  # 移除节点\n        for ele in left.find_all('div', class_='div-chart-btns'):\n            ele.decompose()  # 移除节点\n        for ele in left.find_all('div', class_='table-buy'):\n            ele.decompose()  # 移除节点\n        for ele in left.find_all('div', class_='div-bgr-2'):\n            if '查看价格选项' in str(ele):\n                ele.decompose()  # 移除节点\n        for ele in left.find_all('h4'):\n            if '购买' in str(ele):\n                ele.decompose()  # 移除节点\n        for ele in left.find_all('button'):\n            if '加载更多' in str(ele):\n                ele.decompose()  # 移除节点\n        for ele in left.find_all('div', class_='div-bgr-1'):\n            if '详细了解我们' in str(ele):\n                ele.decompose()  # 移除节点\n        i = 1\n        for img in left.find_all('img'):\n            src = str(img.get('src'))\n            path = '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/data/img/%d.svg' % i\n            dst = '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/data/img/%d.png' % i\n            if 'www.ceicdata.com' in src:\n                print('下载图片url[%s]' % src)\n                r = self.request_zh(src)\n                # r = self.request(src)\n                if r.status_code == 200:\n                    with open(path, 'wb') as f:\n                        f.write(r.content)\n                    futls.svg_cover_jpg(path, dst)  # 将svg转换成jpg\n                    img['src'] = dst\n                    i += 1\n                else:\n                    raise Exception('下载图片失败！')\n        rs = re.sub(r'href=\".*?\"', '', str(left))  # 移除href\n        return title, rs\n\n\nclass Cnnic(Net):\n    '''http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/ , 注意：因为文件过大，使用别人代理下载，固定代理'''\n\n    @staticmethod\n    def start_task():\n        cnnic = Cnnic()\n        keys_path = 'data/keys/cnnic.json'\n        all_keys = futls.read_json(keys_path)\n        if not all_keys:\n            all_keys = []\n            for i in range(7):\n                if i == 0:\n                    url = 'http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/index.htm'\n                else:\n                    url = 'http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/index_%d.htm' % i\n                page = cnnic.load_page(url)\n                if page:\n                    futls.write(page, 'test/src.html')\n                all_keys += cnnic.parse_page(page)\n                futls.write_json(all_keys, keys_path)\n        size = len(all_keys)\n        print('将要下载数量[%d]' % size)\n        for i in range(size):\n            key = all_keys.pop()\n            name = key.get('title')\n            url = key.get('url')\n            path = 'data/doc/cnnic/%s.pdf' % name\n            cnnic.download(url, path)\n            futls.write_json(all_keys, keys_path)\n            print('已下载[%d] | 还剩[%d]' % (i + 1, size - i - 1))\n\n    def load_page(self, url):\n        time.sleep(3)\n        # r = self.request_en(url)\n        # r = self.request(url)\n        proxies = {'http': 'http://11.0.222.4:80', 'https': 'http://11.0.222.4:80'}\n        r = requests.get(url=url, headers=self._headers, allow_redirects=False, verify=False,\n                         proxies=proxies, timeout=15)\n        r.encoding = r.apparent_encoding\n        print('Cnnic[%s] code[%d]' % (url, r.status_code))\n        if r.status_code == 200:\n            return r.text\n        elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n            location = r.headers['Location']\n            time.sleep(1)\n            return self.load_page(location)\n        return None\n\n    def parse_page(self, page):\n        '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n        soup = BeautifulSoup(page, 'html.parser')\n        content = soup.find('div', class_='content')\n        results = []\n        for li in content.find_all('li'):\n            a = li.find('a')\n            date = li.find('div', class_='date').text[0:4]  # 只要年份\n            title = a.text.replace('\\n', '').replace(' ', '')\n            # http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/hlwtjbg/202109/P020210915523670981527.pdf\n            #                                      ./hlwtjbg/202109/P020210915523670981527.pdf\n            url = 'http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/' + str(a.get('href')).replace('./', '')\n            if not '年' in title:\n                title = '%s年发布%s' % (date, title)\n            results.append({'title': title, 'url': url})\n        return results\n\n    def download(self, url, path):\n        if os.path.exists(path):\n            os.remove(path)\n        proxies = {'http': 'http://11.0.222.4:80', 'https': 'http://11.0.222.4:80'}\n        r = requests.get(url=url, headers=self._headers, allow_redirects=False, verify=False,\n                         proxies=proxies, timeout=15, stream=True)\n        i = 0\n        print('name[%s]|code[%d]' % (path, r.status_code))\n        with open(path, \"wb\") as pdf:\n            for chunk in r.iter_content(chunk_size=1024):\n                if chunk:\n                    i += 1\n                    if i % 30 == 0:\n                        print('.', end='')\n                    pdf.write(chunk)\n            pdf.close()\n        time.sleep(random.randint(3, 12))\n\n\nclass Othersite(Net):\n    def __init__(self):\n        super(Othersite, self).__init__()\n        self.dir = 'Othersite'\n\n    @staticmethod\n    def start_task():\n        reqs = [{'title': 'xxx', 'url': 'https://www.xxx.com/wedding/engagement-rings.html'}]\n        # utils.restart_v2ray()\n        sl = Othersite()\n        datas = futls.read_json(os.path.join('Othersite', 'page_urls.json'))\n        if datas is None:\n            datas = []\n        for req in reqs:\n            title = req.get('title')\n            url = req.get('url')\n            has_write = False\n            for data in datas:\n                if title in data.get('title'):\n                    has_write = True\n                    break\n            if has_write:  # 已经请求过了\n                print('页面连接 [%s]已存在，跳过！' % title)\n                continue\n            start = 1\n            page_urs = []\n            while True:\n                if start != 1:\n                    temp = url + '?p=' + str(start)\n                else:\n                    temp = url\n                try:\n                    page = sl.load_page(temp)\n                except Exception as e:\n                    print(e)\n                    print('--------000')\n                    utils.restart_v2ray()\n                    continue\n                futls.write(page, 'test/src.html')\n                has_next, results = sl.parse_list(page)\n                print('has_next: {} | {}'.format(has_next, results))\n                page_urs += results\n                if not has_next:\n                    break\n                start += 1\n                # page = futls.read('test/src.html')\n            # print(sl.parse_details(page))\n            datas.append({'title': title, 'urls': page_urs})\n            futls.write_json(datas, os.path.join('Othersite', 'page_urls.json'))\n        all_results = []  # 总数据表\n        size = len(datas)\n        alls_local = futls.read_json(os.path.join('Othersite', 'all.json'))\n        for i in range(size):\n            data = datas.pop()\n            title = data.get('title')\n            page_urs = data.get('urls')\n            has_write_all = False\n            # for local in alls_local:\n            #     if title in local.get('title'):\n            #         has_write_all = True\n            #         break\n            # if has_write_all:\n            #     print('[%s]已下载，跳过！' % title)\n            #     continue\n            sl.dir = os.path.join('Othersite', title)\n            url_size = len(page_urs)\n            print('下载数量[%d]' % url_size)\n            is_need_start = False\n            url = None\n            results = []\n            for i in range(url_size):\n                if is_need_start:\n                    utils.restart_v2ray()\n                    sl.update_agent()\n                else:\n                    url = page_urs.pop()\n                is_need_start = True\n                if url is None:\n                    url = page_urs.pop()\n                # 本地是否也已经存在\n                sku1 = url[url.rfind('-') + 1:].replace('.html', '').upper()\n                if os.path.exists(os.path.join(sl.dir, sku1)):\n                    is_need_start = False\n                    print('ksu [%s]已存在，跳过！' % sku1)\n                    continue\n                try:\n                    page = sl.load_page(url)\n                    sku = sl.parse_details(page)\n                    results.append(sku)\n                    is_need_start = False\n                except Exception as e:\n                    print(e)\n                    print('--------333')\n            all_results.append({'title': title, 'skus': results})\n            futls.write_json(all_results, os.path.join('Othersite', 'all.json'))\n            futls.write_json(datas, os.path.join('Othersite', 'page_urls.json'))\n        utils.kill_all_v2ray()\n\n    def load_page(self, url):\n        '''加载页面，如：https://www.cncic.org/?p=3823'''\n        time.sleep(random.randint(3, 8))\n        r = self.request_en(url)\n        # r = self.request(url)\n        r.encoding = r.apparent_encoding\n        print('Othersite code[%d] |url [%s] ' % (r.status_code, url))\n        if r.status_code == 200:\n            return r.text\n        elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n            location = r.headers['Location']\n            time.sleep(1)\n            return self.load_page(location)\n        return None\n\n    def parse_home(self, page):\n        soup = BeautifulSoup(page, 'html.parser')\n        nav = soup.find('nav')\n        results = []\n        for a in nav.find_all('a'):\n            results.append({'title': a.text, 'url': str(a.get('href'))})\n        return results\n\n    def parse_list(self, page):\n        soup = BeautifulSoup(page, 'html.parser')\n        ol = soup.find('ol')\n        # 判断是否还有下一页\n        next = False\n        pages = soup.find('div', class_='pages')\n        if pages:\n            pages_n = pages.find('li', class_='pages-item-next')\n            if pages_n:\n                next = True\n        results = []\n        for a in ol.find_all('a'):\n            results.append(str(a.get('href')))\n        return next, results\n\n    def parse_details(self, page):\n        soup = BeautifulSoup(page, 'html.parser')\n        # content = soup.find('div', class_='content')\n        # main = content.find('main')\n        right = soup.find('div', class_='product-info-main')\n        title = right.find('h1').text.replace('Othersite ', '')\n        sku = right.find('div', class_='value').text\n        try:\n            price = right.find('span', id='price-saved').find('span').text\n        except Exception:\n            print('没有折扣，继续找..')\n            price = right.find('span', class_='special-price').find('span', class_='price').text\n        # 下载图片\n        layout = soup.find('amp-layout')\n        carousel = layout.find('amp-carousel')\n        imgs = []\n        i = 0\n        content = '{}\\n{}'.format(sku, title)\n        for img in carousel.find_all('amp-img'):\n            src = str(img.get('src'))\n            imgs.append(src)\n            path = self.download_img(src, sku, i)\n            content = content + '\\n' + path\n            i += 1\n        futls.write(content, os.path.join(self.dir, sku, '{}.txt'.format(sku)))\n        return {'sku': sku, 'title': title, 'price': price, 'imgs': imgs}\n\n    def download_img(self, src, sku, i):\n        path = os.path.join(self.dir, sku, '{}-{}.jpg'.format(sku, i))\n        pre_path, file_name = os.path.split(path)\n        if pre_path and not os.path.exists(pre_path):\n            os.makedirs(pre_path)\n        time.sleep(random.randint(1, 2))\n        r = self.request_en(src)\n        if r.status_code == 200:\n            with open(path, 'wb') as f:\n                f.write(r.content)\n        else:\n            raise Exception('下载图片失败！')\n        return path\n\n\nif __name__ == \"__main__\":\n    pass\n"
  },
  {
    "path": "005-PaidSource/v2ray_pool/__init__.py",
    "content": "# 运行时路径。并非__init__.py的路径\nimport os\nimport sys\n\nBASE_DIR = \"../002-V2rayPool\"\nif os.path.exists(BASE_DIR):\n    sys.path.append(BASE_DIR)\n\nfrom core import utils\nfrom core.conf import Config\nfrom core.client import Creator\nfrom db.db_main import DBManage\nfrom base.net_proxy import Net"
  },
  {
    "path": "005-PaidSource/v2ray_pool/_db-checked.txt",
    "content": "ss://YWVzLTI1Ni1nY206cEtFVzhKUEJ5VFZUTHRN@134.195.196.68:443#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%202,134.195.196.68,美国\nss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@134.195.196.81:9101#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%206,134.195.196.81,美国\nss://YWVzLTI1Ni1nY206WEtGS2wyclVMaklwNzQ@134.195.196.187:8009#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%201,134.195.196.187,美国\nvmess://ew0KICAidiI6ICIyIiwNCiAgInBzIiA6Iue/u+WimeWFmmZhbnFpYW5nZGFuZy5jb20iLCIiIDogIkBTU1JTVUItVjE1LeS7mOi0ueaOqOiNkDpzdW8ueXQvc3Nyc3ViIiwNCiAgImFkZCI6ICI0Mi4xOTMuNDguNjQiLA0KICAicG9ydCI6ICI1MDAwMiIsDQogICJpZCI6ICI0MTgwNDhhZi1hMjkzLTRiOTktOWIwYy05OGNhMzU4MGRkMjQiLA0KICAiYWlkIjogIjY0IiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiNDIuMTkzLjQ4LjY0IiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiINCn0=,42.193.48.64,中国  上海 上海市 电信"
  },
  {
    "path": "005-PaidSource/v2ray_pool/_db-uncheck.txt",
    "content": ""
  },
  {
    "path": "005-PaidSource/v2ray_util.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 管理v2ray_pool的工具\n@Date       :2022/1/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport time\n\nfrom v2ray_pool import utils, Config, DBManage\n\n\ndef search_node():\n    # 如果有系统全局代理，可不需要开启v2ray_core代理，GoogleTrend(proxies=False)\n    utils.kill_all_v2ray()\n    Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64')  # v2ray内核存放路径\n    Config.set_v2ray_node_path(\n        '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool')  # 保存获取到节点的路径\n    proxy_url = 'ss://YWVzLTI1Ni1nY206WTZSOXBBdHZ4eHptR0M@134.195.196.3:3306#github.com/freefq%20-%20%E5%8C%97%E7%BE%8E%E5%9C%B0%E5%8C%BA%20%201'\n    dbm = DBManage()\n    dbm.init()  # 必须初始化\n    if dbm.check_url_single(proxy_url):\n        urls = dbm.load_urls_by_net(proxy_url=proxy_url)\n        dbm.check_and_save(urls, append=False)\n    # dbm.load_urls_and_save_auto()\n    # urls = dbm.load_unchecked_urls_by_local()\n    # dbm.check_and_save(urls, append=False)\n    utils.kill_all_v2ray()\n\n\ndef restart_v2ray(isSysOn=False):\n    utils.kill_all_v2ray()\n    Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64')  # v2ray内核存放路径\n    Config.set_v2ray_node_path(\n        '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool')  # 保存获取到节点的路径\n    dbm = DBManage()\n    dbm.init()  # 必须初始化\n    while 1:\n        if dbm.start_random_v2ray_by_local(isSysOn=isSysOn):\n            break\n        else:\n            print(\"启动失败，进行重试！\")\n            time.sleep(1)\n\n\ndef kill_all_v2ray():\n    utils.kill_all_v2ray()\n"
  },
  {
    "path": "006-TikTok/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "006-TikTok/006-TikTok.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\" />\n    <orderEntry type=\"jdk\" jdkName=\"Python 3.9\" jdkType=\"Python SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "006-TikTok/README.md",
    "content": "# App自动化\n\n## 效果\n\n抖音和tiktok能自动刷App评论。\n\n## 实现\n1. 使用android手机，能与电脑正常使用adb连接\n2. 使用[uiautomator2](https://github.com/openatx/uiautomator2) 开源库。    \n3. 借助：weditor 来获取元素（xpath等）。\n\n\n## seo\n```\n1、重量副词词汇\nhttps://gist.github.com/mkulakowski2/4289437\n\n2、查看网站排名\nhttps://www.sdwebseo.com/googlerankcheck/\n\n3、看看google浏览器中我的排名，如：\nhttps://www.google.com/search?q=giant stuffed animal caterpillar&num=10&gl=US\nhttps://www.google.com/search?q=4 foot stuffed panda bear&num=100&gl=US\n\n4、获取外链\n帮记者一个小忙，获取优质的外链：http://app.helpareporter.com/Pitches\nhttps://www.seoactionblog.com/haro-link-building-strategy/\n\n```\n\n> 其他：注意需要把电脑要把代理关掉"
  },
  {
    "path": "006-TikTok/__init__.py",
    "content": ""
  },
  {
    "path": "006-TikTok/dy_review.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 抖音app刷评论\n@Date       :2021/12/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport random\nimport time\n\nimport uiautomator2 as u2\n\nd = u2.connect()\nd.implicitly_wait(80)\n\n\ndef review_douyin():  # 评论\n    d.press(\"home\")\n    d.app_start('com.ss.android.ugc.aweme', stop=True)\n    time.sleep(1)\n    d(resourceId=\"com.ss.android.ugc.aweme:id/foj\").click()  # 点击搜索\n    time.sleep(1)\n    d(resourceId=\"com.ss.android.ugc.aweme:id/et_search_kw\").click()  # 点击输入框，预防键盘弹不起来\n    keys = ['元宵节创意视频']  # , '情人节', '搞笑视频', '真人动漫特效'\n    comments = ['[比心]', '[强壮]', '[击掌]', '[给力]', '[爱心]', '[派对]', '[不看]', '[炸弹]', '[憨笑]', '[悠闲]', '[嘿哈]', '[西瓜]', '[咖啡]',\n                '[太阳]', '[月亮]', '[发]', '[红包]', '[拳头]', '[勾引]', '[胜利]', '[抱拳]', '[左边]', '[送心]', '[来看我]', '[来看我]',\n                '[来看我]', '[灵机一动]', '[耶]', '[色]', '[震惊]', '[小鼓掌]', '[发呆]', '[偷笑]', '[石化]', '[思考]', '[笑哭]', '[奸笑]',\n                '[坏笑]', '[得意]', '[钱]', '[亲亲]', '[愉快]', '[玫瑰]', '[赞]', '[鼓掌]', '[感谢]', '[666]', '[胡瓜]', '[啤酒]', '[飞吻]',\n                '[紫薇别走]', '[听歌]', '[绝望的凝视]', '[不失礼貌的微笑]', '[吐舌]', '[呆无辜]', '[看]', '[熊吉]', '[黑脸]', '[吃瓜群众]', '[呲牙]',\n                '[绿帽子]', '[摸头]', '[皱眉]', '[OK]', '[碰拳]', '[强壮]', '[比心]', '[吐彩虹]', '[奋斗]', '[敲打]', '[惊喜]', '[如花]', '[强]',\n                '[做鬼脸]', '[尬笑]', '[红脸]', '牛啊', '牛啊牛', 'nb', '666', '赞一个', '赞', '棒', '学到了', '1', '已阅', '板凳',\n                '插一楼：变戏法的亮手帕', '插一楼：狗吃豆腐脑', '插一楼：癞蛤蟆打伞', '插一楼：离了水晶宫的龙', '插一楼：盲人聊天', '插一楼：五百钱分两下', '插一楼：盲公戴眼镜',\n                '插一楼：王八倒立', '插一楼：癞蛤蟆背小手', '插一楼：韩湘子吹笛', '插一楼：剥了皮的蛤蟆', '插一楼：马蜂蜇秃子', '插一楼：冷水烫鸡', '插一楼：老孔雀开屏', '插一楼：大姑娘养的',\n                '插一楼：三角坟地', '插一楼：蜡人玩火', '插一楼：发了霉的葡萄', '插一楼：厥着看天', '插一楼：种地不出苗', '插一楼：老虎头上的苍蝇', '插一楼：雷婆找龙王谈心',\n                '插一楼：菩萨的胸怀', '插一楼：牛屎虫搬家', '插一楼：变戏法的拿块布', '插一楼：老虎上吊', '插一楼：王八', '插一楼：老虎吃田螺', '插一楼：大肚子踩钢丝', '插一楼：耗子腰粗',\n                '插一楼：乌龟的', '插一楼：神仙放屁', '插一楼：麻油煎豆腐', '插一楼：汽车坏了方向盘', '插一楼：病床上摘牡丹', '插一楼：芝麻地里撒黄豆', '插一楼：打开棺材喊捉贼',\n                '插一楼：卤煮寒鸭子', '插一楼：鲤鱼找鲤鱼，鲫鱼找鲫鱼', '插一楼：癞蛤蟆插羽毛', '插一楼：烂伞遮日', '插一楼：老虎头上的苍蝇', '插一楼：三角坟地', '插一楼：卖布兼卖盐',\n                '插一楼：耗子腰粗', '插一楼：老孔雀开屏', '插一楼：筐中捉鳖', '插一楼：拐子进医院', '插一楼：茅房里打灯笼', '插一楼：癞蛤蟆背小手', '插一楼：肉骨头吹喇叭',\n                '插一楼：老鼠进棺材', '插一楼：种地不出苗', '插一楼：病床上摘牡丹', '插一楼：裤裆里摸黄泥巴', '插一楼：狗拿耗子', '插一楼：铁匠铺的料', '插一楼：高梁撒在粟地里',\n                '插一楼：茅厕里题诗', '插一楼：痰盂里放屁', '插一楼：老母猪打喷嚏', '插一楼：厕所里点灯', '插一楼：棺材铺的买卖', '插一楼：老公鸡着火', '插一楼：乌龟翻筋斗',\n                '插一楼：被窝里的跳蚤', '插一楼：赶着牛车拉大粪', '插一楼：老太婆上鸡窝', '插一楼：狗背上贴膏药', '插一楼：狗咬瓦片', '插一楼：哪吒下凡', '插一楼：二十一天不出鸡',\n                '插一楼：鞭炮两头点', '插一楼：抱黄连敲门', '插一楼：猫儿踏破油瓶盖', '插一楼：和尚念经', '插一楼：裁缝不带尺', '插一楼：上山钓鱼', '插一楼：狗长犄角',\n                '插一楼：带着存折进棺材', '插一楼：豁子拜师', '插一楼：宁来看棋', '插一楼：盲公戴眼镜', '插一楼：南来的燕，北来的风', '插一楼：杯水车薪', '插一楼：玉皇大帝放屁',\n                '插一楼：给刺儿头理发', '插一楼：九月的甘蔗', '插一楼：两只公牛打架', '插一楼：百川归海', '插一楼：挨打的乌龟', '插一楼：和尚挖墙洞', '插一楼：八月十五蒸年糕',\n                '插一楼：毒蛇钻进竹筒里', '插一楼：苍蝇叮菩萨', '插一楼：白布进染缸', '插一楼：粪堆上开花', '插一楼：癞蛤蟆上蒸笼',\n                '插楼：沙漠里钓鱼', '插楼：青㭎树雕菩萨', '插楼：看鸭勿上棚', '插楼：下大雨前刮大风', '插楼：在看羊的狗', '插楼：耍大刀里唱小生', '插楼：罗锅上山', '插楼：大车不拉',\n                '插楼：瞎子白瞪眼', '插楼：铁拐的葫芦', '插楼：苣荬菜炖鲇鱼', '插楼：旅馆里的蚊子', '插楼：石刻底下的冰瘤子', '插楼：吃稀饭摆脑壳', '插楼：叫化子背不起', '插楼：火车拉大粪',\n                '插楼：寿星玩琵琶', '插楼：六月的腊肉', '插楼：夜叉骂街', '插楼：孩儿的脊梁', '插楼：长了个钱串子脑袋', '插楼：现场看乒乓球比赛', '插楼：寡妇梦丈夫', '插楼：马背上放屁',\n                '插楼：落雨出太阳', '插楼：猴子捡生姜', '插楼：啄木鸟屙薄屎', '插楼：鸡毛扔火里', '插楼：油火腿子被蛇咬', '插楼：属秦椒的', '插楼：千亩地里一棵草', '插楼：药铺倒了',\n                '插楼：黄连水做饭', '插楼：卸架的黄烟叶儿', '插楼：螺蛳壳里赛跑', '插楼：躲了和尚躲不了庙', '插楼：驴槽子里面伸出一颗头来', '插楼：老妈妈吃火锅', '插楼：阎王的脸',\n                '插楼：吃粮勿管事', '插楼：脚跟拴石头', '插楼：麻秸秆儿打狼', '插楼：阎王7粑子', '插楼：画上的美女', '插楼：团鱼下滚汤', '插楼：孔夫子的脸', '插楼：曹操贪慕小乔',\n                '插楼：蒙住眼睛走路', '插楼：炒菜不放盐', '插楼：三月里的桃花', '插楼：老鼠吃面饽', '插楼：粥锅里煮铁球', '插楼：戴起眼镜喝滚茶', '插楼：吃香油唱曲子', '插楼：过冬的咸菜缸',\n                '插楼：三个小鬼没抓住', '插楼：对着坛子放屁', '插楼：赤骨肋受棒', '插楼：百灵鸟唱歌', '插楼：雨过天晴放干雷', '插楼：拄着拐棍上炭窑', '插楼：搁着料吃草', '插楼：王八碰桥桩',\n                '插楼：水上油', '插楼：偷鸡不得摸了一只鸭子', '插楼：黄瓜熬白瓜', '插楼：海瑞的棺材', '插楼：蛤蟆翻田坎', '插楼：乌龟进砂锅', '插楼：夜壶出烟', '插楼：李逵骂宋江',\n                '插楼：小孩买个花棒槌', '插楼：漏网之虾', '插楼：一口吹灭火焰山', '插楼：冷水调浆']\n    for key in keys:\n        # 不能搜索search\n        d(resourceId=\"com.ss.android.ugc.aweme:id/et_search_kw\").clear_text()  # 清除历史\n        d(resourceId=\"com.ss.android.ugc.aweme:id/et_search_kw\").set_text(key)  # 输入\n        time.sleep(1)\n        d(text='搜索', className='android.widget.TextView').click()  # 点击搜索\n        # d(resourceId=\"\tcom.ss.android.ugc.aweme:id/d0t\").click()  # 点击搜索\n        time.sleep(1)\n        d(text='视频', className='android.widget.Button').click()  # 点击视频，然后点击第一条\n        d.xpath(\n            '//*[@resource-id=\"com.ss.android.ugc.aweme:id/gw1\"]/android.widget.FrameLayout[1]').click()\n        stop, index = random.randint(15, 30), 0\n        while index < stop:  # 随机刷几十条\n            print('总共有：{}条 | 现在到：{}条'.format(stop, index))\n            try:\n                time.sleep(random.randint(3, 12))  # 随机停顿1~5秒\n                d(resourceId=\"com.ss.android.ugc.aweme:id/b2b\").click()  # 点击评论按钮\n            except Exception as e:\n                print(e)\n            try:\n                time.sleep(random.randint(1, 2))  # 随机停顿1~2秒\n                d(resourceId=\"com.ss.android.ugc.aweme:id/b1y\").click()  # 点击弹出键盘\n            except Exception as e:\n                print(e)\n            time.sleep(random.randint(1, 2))  # 随机停顿1~2秒\n            try:\n                d(resourceId=\"com.ss.android.ugc.aweme:id/b1y\").set_text(\n                    comments[random.randint(0, len(comments) - 1)])  # 输入\n            except Exception as e:\n                print(e)\n            try:\n                time.sleep(random.randint(1, 2))  # 随机停顿1~2秒\n                d(resourceId=\"com.ss.android.ugc.aweme:id/b1r\").click()  # 发送\n            except Exception as e:\n                print(e)\n            try:\n                time.sleep(random.randint(1, 2))  # 随机停顿1~2秒\n                d(resourceId=\"com.ss.android.ugc.aweme:id/back_btn\").click()  # 关闭\n            except Exception as e:\n                print(e)\n            try:\n                time.sleep(random.randint(5, 15))  # 随机停顿1~5秒\n                d.swipe_ext(\"up\")  # 上划，下一个视频\n            except Exception as e:\n                print(e)\n            index += 1\n        d(resourceId=\"com.ss.android.ugc.aweme:id/back_btn\").click()  # 返回搜索\n\n\n'''\n教程：\n1. 使用 weditor 来查看元素\n2. 当找不到resourceId时，先用d(text='画动漫人物', className='android.widget.EditText').info找个resourceId，再使用。因为输入框内容会变化，所有不能直接用。\n'''\n\nif __name__ == \"__main__\":\n    review_douyin()\n"
  },
  {
    "path": "006-TikTok/file_util.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 文件相关处理\n@Date       :2022/01/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport datetime\nimport json\nimport os\nimport re\nimport shutil\n\nimport cairosvg\nimport pandas as pd\nimport pypandoc  # 要安装pandoc\nfrom docx import Document\n\n\ndef file_name(file_dir):\n    results = []\n    for root, dirs, files in os.walk(file_dir):\n        # print(root)  # 当前目录路径\n        # print(dirs)  # 当前路径下所有子目录\n        # print(files)  # 当前路径下所有非目录子文件\n        results += files\n    return results\n\n\ndef deal_one_page():\n    fs = file_name('100条')\n    for f in fs:\n        try:\n            print('正在检测【%s】' % f)\n            shotname, extension = os.path.splitext('%s' % f)\n            print('正在检测【%s】' % shotname)\n            if '1篇' in shotname:\n                new_name = re.sub(r'1篇', '', f)\n                document = Document(r\"html/%s\" % f)\n                paragraphs = document.paragraphs\n                p = paragraphs[0]\n                p._element.getparent().remove(p._element)\n                document.save(r\"html/%s\" % new_name)\n                os.remove('html/%s' % f)\n        except Exception as e:\n            print(e)\n\n\ndef copy_doc():\n    fs = file_name('all')\n    i = 1\n    k = 1\n    temp_dir = '01'\n    os.makedirs('100条/%s' % temp_dir)\n    for f in fs:\n        try:\n            # print('正在检测【%s】' % f)\n            shotname, extension = os.path.splitext('%s' % f)\n            shutil.copyfile(r'all/%s' % f, r'100条/%s/%s' % (temp_dir, f))\n            if i % 100 == 0:\n                temp_dir = '0%d' % k if k < 10 else '%d' % k\n                k += 1\n                os.makedirs('100条/%s' % temp_dir)\n            i += 1\n        except Exception as e:\n            print(e)\n\n\n'''########文件处理相关#########'''\n\n\ndef html_cover_doc(in_path, out_path):\n    '''将html转化成功doc'''\n    path, file_name = os.path.split(out_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    pypandoc.convert_file(in_path, 'docx', outputfile=out_path)\n\n\ndef svg_cover_jpg(src, dst):\n    ''''\n    drawing = svg2rlg(\"drawing.svg\")\n    renderPDF.drawToFile(drawing, \"drawing.pdf\")\n    renderPM.drawToFile(drawing, \"fdrawing.png\", fmt=\"PNG\")\n    renderPM.drawToFile(drawing, \"drawing.jpg\", fmt=\"JPG\")\n    '''\n    path, file_name = os.path.split(dst)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    # drawing = svg2rlg(src)\n    # renderPM.drawToFile(drawing, dst, fmt=\"JPG\")\n    cairosvg.svg2png(url=src, write_to=dst)\n\n\ndef html_cover_excel(content, out_path):\n    '''将html转化成excel'''\n    path, file_name = os.path.split(out_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    tables = pd.read_html(content, encoding='utf-8')\n    writer = pd.ExcelWriter(out_path)\n    for i in range(len(tables)):\n        tables[i].to_excel(writer, sheet_name='表%d' % (i + 1))  # startrow\n    writer.save()  # 写入硬盘\n\n\ndef write_to_html(content, file_path):\n    '''将内容写入本地，自动加上head等信息'''\n    page = '''<!DOCTYPE html>\n                <html>\n                <head>\n                    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n                </head>\n                <body>'''\n    page += content\n    page += '''</body>\n    </html>'''\n    write(page, file_path)\n\n\ndef write_json(content, file_path):\n    '''写入json'''\n    path, file_name = os.path.split(file_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    with open(file_path, 'w') as f:\n        json.dump(content, f, ensure_ascii=False)\n        f.close()\n\n\ndef read_json(file_path):\n    '''读取json'''\n    with open(file_path, 'r') as f:\n        js_get = json.load(f)\n        f.close()\n    return js_get\n\n\ndef write(content, file_path):\n    '''写入txt文本内容'''\n    path, file_name = os.path.split(file_path)\n    if path and not os.path.exists(path):\n        os.makedirs(path)\n    with open(file_path, 'w') as f:\n        f.write(content)\n        f.close()\n\n\ndef read(file_path) -> str:\n    '''读取txt文本内容'''\n    content = None\n    try:\n        with open(file_path, 'r') as f:\n            content = f.read()\n            f.close()\n    except Exception as e:\n        print(e)\n    return content\n\n\ndef get_next_folder(dst, day_diff, folder, max_size):\n    '''遍历目录文件，直到文件夹不存在或者数目达到最大（max_size）时，返回路径'''\n    while True:\n        day_time = (datetime.date.today() + datetime.timedelta(days=day_diff)).strftime('%Y-%m-%d')  # 下一天的目录继续遍历\n        folder_path = os.path.join(dst, day_time, folder)\n        if os.path.exists(folder_path):  # 已存在目录\n            size = len(next(os.walk(folder_path))[2])\n            if size >= max_size:  # 该下一个目录了\n                day_diff += 1\n                continue\n        else:\n            os.makedirs(folder_path)\n        return day_diff, folder_path\n\n\nif __name__ == '__main__':\n    pass\n"
  },
  {
    "path": "006-TikTok/google_transfer_by_excel.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 实现从excel文件获取关键词进行翻译后写入新文件\n@Date       :2021/10/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nimport json\nimport os\nimport os.path\nimport random\nimport time\n\nimport chardet\nimport pandas as pd\nimport requests\n\nimport file_util as futls\nfrom my_fake_useragent import UserAgent\n\n\nclass GTransfer(object):\n    def __init__(self, file):\n        self.file = file\n        self._ua = UserAgent()\n        self._agent = self._ua.random()  # 随机生成的agent\n        self.USER_AGENT = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0\"\n        self._headers = {\"user-agent\": self.USER_AGENT, 'Connection': 'close'}\n\n    def request(self, url, allow_redirects=False, verify=False, proxies=None, timeout=8):\n        \"\"\"最终的请求实现\"\"\"\n        requests.packages.urllib3.disable_warnings()\n        if proxies:\n            return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify,\n                                proxies=proxies, timeout=timeout)\n        else:\n            return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify,\n                                timeout=timeout)\n\n    def search_page(self, url, pause=3):\n        \"\"\"\n        Google search\n        :param query: Keyword\n        :param language: Language\n        :return: result\n        \"\"\"\n        time.sleep(random.randint(1, pause))\n        try:\n            r = self.request(url)\n            print('resp code=%d' % r.status_code)\n            if r.status_code == 200:\n                charset = chardet.detect(r.content)\n                content = r.content.decode(charset['encoding'])\n                return content\n            elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303:\n                location = r.headers['Location']\n                time.sleep(random.randint(1, pause))\n                return self.search_page(location)\n            return None\n        except Exception as e:\n            print(e)\n            return None\n\n    def transfer(self, content):\n        # url = 'http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=' + content\n        url = 'http://translate.google.cn/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=zh-CN&tl=en&q=' + content\n        try:\n            cache = futls.read_json('lib/cache.json')\n            for c in cache:\n                if content in c:\n                    print('已存在，跳过：{}'.format(content))\n                    return c.get(content)\n        except Exception as e:\n            pass\n        try:\n            result = self.search_page(url)\n            trans = json.loads(result)['sentences'][0]['trans']\n            # 解析获取翻译后的数据\n            # print(result)\n            print(trans)\n            self.local_cache.append({content: trans})\n            futls.write_json(self.local_cache, self.file)\n            # 写入数据吗？下次直接缓存取\n        except Exception as e:\n            print(e)\n            return self.transfer(content)\n        return trans\n\n    def transfer_list(self, lists):\n        self.local_cache = []\n        for c in lists:\n            self.transfer(c)\n\n\n# http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=what\n\n\nif __name__ == '__main__':\n    files = next(os.walk('xxx/folder'))[2]\n    results = []\n    for f in files:\n        name, ext = os.path.splitext(f)\n        if len(name) > 0 and len(ext):\n            results.append(name)\n    g = GTransfer('xxx/en.json')\n    g.transfer_list(results)\n"
  },
  {
    "path": "006-TikTok/img_2_webp.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: WordPress 站点 将图片转 webp 格式，实现内容重组\n@Date       :2022/10/12\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\n\nimport os\n\n\ndef get_contain_pics(folder, content):\n    files = next(os.walk(folder))[2]\n    results = []\n    for f in files:\n        name, ext = os.path.splitext(f)\n        if len(name) < 1 or len(ext) < 1 or '.json' == str(ext) or '.txt' == str(ext):  # 非视频文件\n            continue\n        if content in f:\n            results.append(f)\n    return results\n\n\ndef convert_pic_to_webp(folder, sku, pic_files, t, key1, key2, key3, youtube_num):\n    dst_folder = os.path.join('data', sku)\n    desc_links = []\n    desc_links.append(\n        '<div align=\"center\"><iframe width=\"370\" height=\"658\" src=\"https://www.youtube.com/embed/{}\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>\\n'.format(\n            youtube_num))\n    desc_links.append(\n        '<div align=\"center\"><a href=\"https://www.youtube.com/channel/UCm2yBXcNJUjYDMl3yE8Ib7w\">more video with youtube >></a></div>\\n')\n    if not os.path.exists(dst_folder):\n        os.makedirs(dst_folder)\n    # TODO - 上传日期需要注意 月份\n    full_url = '<img class=\"aligncenter size-full wp-image-1376\" src=\"https://www.foryoutoy.com/wp-content/uploads/2022/05/{}\" alt=\"{}\" width=\"{}\" height=\"{}\" /> \\n'\n    tail = '''\n<h2>key1 Design Philosophy</h2>\n<strong>1st: Ideal Size </strong>\nkey1 for girls are great companions, are filled with pp cotton. key2 are many sizes to choose from.This soft friend is irresistible! Anirollz got their name from being key3 shaped like rolls, and they are pure squishy fun.\n\n<strong>2nd: Fine Workmanship </strong>\nThe soft fleece fabric, exquisite embroidery, special color, and full filling of various parts. Therefore, the bear stuffed animal funny plush toy look more lovely and texture. So silky smooth plush toys, you'll never want to let go!\n\n<strong>3rd: Wide Range Of Uses </strong>\nkey2 can meet different needs. For example, put on the bed, car, sofa for holding, lying down, leaning. The cuddle key1 is soft and comfortable. Therefore, you can use this key1 as your sleeping partner!\n\n<strong>4th: Perfect Gift </strong>\nOur key1 will be loved by kids and adults like. For instance, It's a great gift for family or friends at Christmas, Thanksgiving, birthday, graduation or Valentine's day.\n\n<strong>5th: Develop Empathy And Social Skills </strong>\nWith a cuddling, comforting plush toy companion. It also helps to develop empathy and social skills, and not only stimulates children's imagination and interest in the animal world. And by taking care of their key2 for kids, they build bridges with their friends in the future.in addition, plush toys are the perfect companions for kids three and older and add a unique decorative touch to any nursery, bedroom, or playroom!\n\n<h2>key2 Buyer notice</h2>\n<strong>1st: About hair removal</strong>\nDid you notice hair loss after receiving the goods? Don't worry, Our giant stuffed animal toys don't shed hair themselves.Because it is a new product, it will have a little floating hair and filling fiber, after you get it, tap it a few times, shake it a few times. it will remove the floating hair.\n\n<strong>2nd: About size</strong>\nAll products are measured in kind, and the key2 itself is soft and will have a certain error (about 3 cm).\n\n<strong>3rd: About packaging</strong>\nWe exhaust part of the air for packaging, because key1 itself is relatively large. Only after taking the time to transport can the product be better protected, so when you receive the product, it is slightly off\nthe description. Small or a little wrinkled, please don't be surprised. because this is a normal phenomenon, shake a few times after opening, under the sun expose for half an hour and recover immediately!\n\n<h2>Our guarantee</h2>\n<h3>Insurance</h3>\nEach order includes real-time tracking details and insurance.\n<h3>Order And Safe</h3>\nWe reps ready to help and answer any questions, you have within a 24 hour time frame, 7 days a week. We use state-of-the-art SSL Secure encryption to keep your personal and financial information 100% protected.\n\n<h2>Other aspects</h2>\nwe’re constantly reinventing and reimagining our key3. If you have feedback you would like to share with us, <a href=\"/contact\">please contact us</a>! We genuinely listen to your suggestions when reviewing our existing toys as well as new ones in development. And are you looking for wholesale plush doll for your school or church carnival? We have the perfect mix of darling stuffed toys for carnival prizes all sold at wholesale prices! We have everything from small stuffed animals such as key1 & dogs, fish, turtles, apes, owls, sharks, & bugs to big stuffed doll such as our large stuffed toy frog and our lovable, key3 and puppy dog!\n\n<img class=\"aligncenter size-full wp-image-1043\" src=\"https://www.foryoutoy.com/wp-content/uploads/2022/04/shoppingtell-3.webp\" alt=\"large stuffed animals for adults and kids\" width=\"470\" height=\"113\" />    \n    '''\n    tail = tail.replace('key1', key1).replace('key2', key2).replace('key3', key3)\n    for i in range(0, len(pic_files)):\n        f = pic_files[i]\n        src = os.path.join(folder, f)\n        # dst_name = '{}-m-{}{}'.format(sku, i + 1, ext)\n        dst_name = '{}-{}-{}.webp'.format(sku, t, i + 1)  # 更新至webp\n        dst = os.path.join(dst_folder, dst_name)\n        from PIL import Image\n        im = Image.open(src)  # 读入文件\n        print(im.size)\n        width = 500\n        if im.size[0] > width:\n            h = int(width * im.size[1] / im.size[0])\n            print(h)\n            im.thumbnail((width, h), Image.ANTIALIAS)  # 重新设置图片大小\n        im.save(dst)  # 保存\n        print(dst)\n        if i < 1:\n            alt_key = key3\n        elif i < 3:\n            alt_key = key2\n        else:\n            alt_key = key1\n        if t == 'd':\n            size = Image.open(dst).size\n            desc_links.append(full_url.format(dst_name, alt_key, size[0], size[1]))\n    if t == 'd':\n        with open(os.path.join('data/desc-html', '{}.txt'.format(sku)), mode='w', encoding='utf-8') as f:\n            for link in desc_links:\n                f.write(link)\n                f.write('\\n')\n            f.write(tail)\n        f.close()\n\n\ndef deal_sku(folder, sku, key1, key2, key3, youtube_num):\n    folder = os.path.join(folder, sku)\n    main_pics = sorted(get_contain_pics(folder, '主图'))\n    desc_pics = sorted(get_contain_pics(folder, '详情'))\n    convert_pic_to_webp(folder, sku, main_pics, 'm', key1, key2, key3, youtube_num)\n    convert_pic_to_webp(folder, sku, desc_pics, 'd', key1, key2, key3, youtube_num)\n\n\ndef deal_sku_list():\n    folder = 'xxx/SKU/'\n    deal_sku(folder, 'FYTA032DX', 'elephant plush toy', 'stuffed animal elephant', 'elephant stuffed toy', '4kB0ZJBmMkU')  # 奶瓶大象\n\n\ndef convert_2_webp(src, dst):\n    from PIL import Image\n    im = Image.open(src)  # 读入文件\n    print(im.size)\n    # width = 500\n    # if im.size[0] > width:\n    #     h = int(width * im.size[1] / im.size[0])\n    #     print(h)\n    #     im.thumbnail((width, h), Image.ANTIALIAS)  # 重新设置图片大小\n    im.save(dst)  # 保存\n\n\ndef convert_folder(src_folder, dst_folder):\n    files = next(os.walk(src_folder))[2]\n    for f in files:\n        if str(f).startswith('.'):\n            continue\n        name, ext = os.path.splitext(f)\n        convert_2_webp(os.path.join(src_folder, f), os.path.join(dst_folder, '{}.webp'.format(name)))\n\n\n'''\n操作说明：\n一：使用Fatkun工具下载图片后进行刷选\n1、主图主要4张\n2、详情图看情况删除一些，最好保留产品尺寸相关说明的图，正常不会超过10张\n二：使用本脚本进行进图压缩以及格式转换（压缩了10倍左右）\n1.本脚本将图片宽带缩小至500，高度等比例缩小\n2.本脚本将jpg、png格式转换成webp格式\n三：新建产品编辑\n1.本脚本生成描述图片连接，直接拷贝过去\n2.将变量尺寸属性值数字后面加上cm\n3.两种产品，定价范围\nA. 长条（如：FYTA004BR，长条兔子），属性名称：Long\n70cm  18~21\n90cm  35~40\n110cm 60~70\n130cm 90~100\n150cm 120~130\nB. 圆形（如：FYTA001SP，奶瓶猪），属性名称：Height\n35cm  30~35\n45cm  55~59\n55cm  80~88\n65cm  120~130\n75cm  160~180\n'''\nif __name__ == '__main__':\n    # deal_sku_list()\n    convert_folder('xxx/png', 'xxx/webp')\n\n#Best Stuffed Pig Toys With A Hat"
  },
  {
    "path": "006-TikTok/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: tiktok 相关开源库\n@Date       :2021/12/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n# import TikTokApi\nfrom TikTokAPI import TikTokAPI\n\n# https://github.com/avilash/TikTokAPI-Python\n\nif __name__ == \"__main__\":\n    pass\n"
  },
  {
    "path": "006-TikTok/post_autotk.py",
    "content": "\"\"\"\n@Description: 生成autotk.app(https://github.com/xhunmon/autotk)中post发送内容解析，\n@Date       :2022/3/20\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport datetime\nimport os\nimport random\nimport re\nimport time\n\nimport file_util as futls\n\n\ndef get_real_files(folder):\n    '''\n    获取真实文件，去掉(.xxx)等文隐藏文件\n    :param files:\n    :return:\n    '''\n    files = next(os.walk(folder))[2]\n    results = []\n    for f in files:\n        name, ext = os.path.splitext(f)\n        if len(name) < 1 or len(ext) < 1 or '.json' == str(ext) or '.txt' == str(ext):  # 非视频文件\n            print('非视频文件：{}'.format(f))\n            continue\n        results.append(f)\n    return results\n\n\ndef format_num(num):\n    if num < 10:\n        return '00{}'.format(num)\n    elif num < 100:\n        return '0{}'.format(num)\n    else:\n        return '{}'.format(num)\n\n\ndef split_en_title(spl, content):\n    if spl in content:\n        temps = content.split(spl)\n        coverDesc, title = \"\", \"\"\n        for temp in temps:\n            if len(temp) < 35:\n                coverDesc = temp\n            else:\n                title = temp\n        # print('------->过滤《{}》-> {} | {}'.format(spl, coverDesc, title))\n        return coverDesc, title\n    return None, None\n\n\ndef chose_cover_title(transfer: str):  # 截取合适的标题和封面内容\n    # 大于5个字符才开始截取\n    trans = re.findall(r'(.{5,}?\\?|.{5,}?\\.|.{5,}?#|.{5,}?!|.{5,}?$)', transfer)\n    title, cover = '', ''\n    for tran in trans:\n        tran = tran.replace('#', '').strip()\n        t_size = len(tran)\n        if len(title) < t_size:  # title取最长的\n            title = tran\n        # cover 优先取10~35个字符的，然后取靠拢的值\n        if len(cover) == 0:\n            cover = tran\n        elif 35 > t_size > 10:\n            cover = tran\n        elif 10 > t_size > len(cover):\n            cover = tran\n        elif 35 < t_size < len(cover):\n            cover = tran\n    return cover, title\n\n\ndef match_info(pre, files, transfers):\n    file_name, cover, title, transfer = None, None, None, None\n    for v in files:  # 文件名称\n        if str(v).startswith(pre):\n            file_name = str(v)\n            break\n    if not file_name:\n        raise RuntimeError('没有找到匹配的文件：{}'.format(pre))\n    for k in transfers:  # 翻译后当前数据\n        if str(k).startswith(pre):\n            transfer = str(k)\n            break\n    if not transfer:\n        raise RuntimeError('没有找到匹配的文件：{}'.format(pre))\n    # 处理标题\n    transfer = transfer[3:].replace('-', '').strip()\n    cover, title = chose_cover_title(transfer)\n    if not cover or not title:\n        raise RuntimeError('获取数据异常：{} | {} | {}'.format(file_name, cover, title))\n    return file_name, cover, title\n\n\ndef format_time_by_str(date):  # str转换为时间戳\n    time_array = time.strptime(date, \"%Y-%m-%d %H:%M:%S\")\n    return int(time.mktime(time_array))\n\n\ndef format_time_by_stamp(stamp):  # 时间戳转换为时间\n    return str(datetime.datetime.fromtimestamp(stamp))\n\n\ndef random_tag(tags):\n    num = random.randint(1, 2)\n    if len(tags) < num:\n        num = len(tags)\n    results = []\n    rsp = ''\n    while True:\n        if len(results) == num:\n            break\n        tag = random.choice(tags)\n        if tag in results:\n            continue\n        results.append(tag)\n        rsp += '#{} '.format(tag)\n    return rsp\n\n\ndef write_title_one(file_name, folder):\n    datas = get_real_files(folder)\n    with open(file_name, 'a', encoding='utf-8') as txt_f:\n        for f in datas:\n            name, ext = os.path.splitext(f)\n            txt_f.write('{}\\n'.format(name))\n        # txt_f.write('AAAAAAAA\\n')\n\n\ndef write_title():\n    file_name = 'lib/title.txt'\n    write_title_one(file_name, 'xxx/bzhsrc/')\n\n\ndef read_title_one(file_name):\n    datas = []\n    with open(file_name, 'r', encoding='utf-8') as txt_f:\n        while True:\n            txt = txt_f.readline().replace('\\n', '')\n            if not txt:\n                break\n            if len(txt) > 5:\n                datas.append(txt)\n    return datas\n\n\ndef del_content(content, dels):\n    for d in dels:\n        content = content.replace(d, '')\n    return content\n\n\ndef is_number(s):\n    try:\n        int(s)\n        return True\n    except ValueError:\n        pass\n    return False\n\n\ndef rename_files(src_folder):  # 重新生成序列\n    dels = ['#热࿆门', '#࿆热࿆门', '#热门', '“', '”',\n            '-', '-', '[', ']', '《', '》', '/', ':',\n            ' ']\n    files = get_real_files(src_folder)\n    index = 1\n    for f in files:\n        src = os.path.join(src_folder, f)\n        new_name: str = del_content(f, dels)\n        num = new_name[0:3]\n        if is_number(num):  # 如果有序号\n            new_name = '{}-{}'.format(num, new_name[3:])  # 有序号时使用\n        else:\n            new_name = '{}-{}'.format(format_num(index), new_name)\n        dst = os.path.join(src_folder, new_name)\n        print(new_name)\n        os.rename(src, dst)\n        index += 1\n    print('总共有[{}]个!'.format(len(files)))\n\n\ndef start_post(userId, src_folder, date_start, title_tags, transfers, musics, dst='post.txt', index=1, space_start=30,\n               space_end=40,\n               num_start=3, num_end=6, half_time=7):\n    '''\n    开始生成post.txt文件\n    :param userId: 用户id\n    :param src_folder: 视频目录\n    :param date_start: 第一个启动的时间，格式如：'2022-3-23 19:05:00'\n    :param title_tags:\n    :param transfers:\n    :param musics:\n    :param index: 起点位置\n    :param space_start: 下一个随机间隔开始发送的视频时间（分钟）\n    :param space_end: 下一个随机间隔结束发送的视频时间（分钟）\n    :param num_start: 每天随机发几个开始区间\n    :param num_end: 每天随机发几个结束区间\n    :param half_time（小时）: 保证一半在12点，一半在6点后\n    :return:\n    '''\n    posts = []\n    stamp = format_time_by_str(date_start)\n    cover_tags = [\"Vector\", \"Glitch\", \"Tint\", \"News\", \"Retro\", \"Skew\", \"Pill\", \"Pop\"]  # \"Standard\" --》有问题\n    files = get_real_files(src_folder)\n    import copy\n    temp_files: list = copy.deepcopy(files)\n    size = len(files)\n    while index <= size:\n        stamp_1 = stamp\n        num = random.randint(num_start, num_end)\n        half_num = int(num / 2)\n        for j in range(0, num):\n            if index > size:\n                print('已经处理完了：{}'.format(index))\n                break\n            stamp_1 += random.randint(1, 30) * 60\n            if j == half_num:  # 在一半的时候加上秒\n                stamp_1 += half_time * 60 * 60\n            date = format_time_by_stamp(stamp_1)\n            sound = random.choice(musics)\n            # pre = format_num(index)\n            pre = temp_files.pop()[0:3]\n            file, coverDesc, title1 = match_info(pre, files, transfers)\n            # TODO- #foryoutoy 玩具需要\n            title = '{} {} #foryoutoy'.format(title1, random_tag(title_tags))\n            coverTag = random.choice(cover_tags)\n            coverPic = random.randint(1, 3)\n            post = {\"date\": date, \"sound\": sound, \"title\": title, \"file\": file, \"userId\": userId,\n                    \"coverDesc\": coverDesc, \"coverTag\": coverTag, \"coverPic\": coverPic}\n            print(post)\n            posts.append(post)\n            stamp_1 += random.randint(space_start, space_end) * 60\n            index += 1\n        # print('----' * 5)\n        stamp += 24 * 60 * 60  # 下一天\n    futls.write_json(posts, dst)\n\n\ndef all_step():\n    # 1. 翻译文件，整理成txt\n    # 2. 标题标签tag，整理成json\n    # 3. 过滤无效信息，重命名文件\n    # 4. post\n    # 美国比英国晚5个小时\n    transfers = futls.read_2_list('lib/furrytoy/fanyi_en.txt')  # 英国 19~21,最多8个视频\n    title_tags = futls.read_json('lib/furrytoy/tags.json')\n    musics = futls.read_json('lib/musics.json')\n    dst = 'lib/furrytoy/post.json'\n    start_post('kif_nee2022', 'xxx/fyt_toy/', '2022-5-8 10:00:00', transfers=transfers,\n               title_tags=title_tags, musics=musics, dst=dst, space_start=70,\n               space_end=83,\n               num_start=2, num_end=2, half_time=8)  # 每天1~2个，12小时*60\n\n\nif __name__ == '__main__':\n    # write_title()\n    # all_step()\n    rename_files('xxx/beizhihui')  # 去掉冗余信息\n"
  },
  {
    "path": "006-TikTok/tikstar.py",
    "content": "\"\"\"\n@Description: 解析www.tikstar.com网站相关内容，获取tags\n@Date       :2021/12/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nfrom bs4 import BeautifulSoup\nimport file_util as futil\n\n\ndef parse_tags(page):\n    '''解析页面，返回标题和文章页面内容，如果生成文章则还需要组装'''\n    soup = BeautifulSoup(page, 'html.parser')\n    trs = soup.find('tbody').find_all('tr')\n    result = []\n    for tr in trs:\n        tds = tr.find_all('td')\n        tag_name = tds[0].find('h3').text.replace('\\n', '').replace(' ', '')\n        video_num = tds[1].text.replace('\\n', '').replace(' ', '')\n        views = tds[2].text.replace('\\n', '').replace(' ', '')\n        result.append('标签：{} 视频数：{} 观看数：{}'.format(tag_name, video_num, views))\n    return result\n\n\nif __name__ == '__main__':\n    html = futil.read('tags.html')\n    result = parse_tags(html)\n    print(result)\n    futil.write_json(result, 'shoes.json')\n"
  },
  {
    "path": "006-TikTok/tt_review.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: tiktok app（版本：22.8.2）刷评论脚本\n@Date       :2021/12/22\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport random\nimport time\nfrom datetime import datetime\n\nimport file_util as futil\n\nimport uiautomator2 as u2\n\n'''\nhttps://github.com/openatx/uiautomator2\n运行pip3 install -U uiautomator2 安装uiautomator2\n运行python3 -m uiautomator2 init安装包含httprpc服务的apk到安卓手机\nuiautomator2操作：https://python.iitter.com/other/35522.html\n借助：weditor 来获取元素，双击找控件id\n(注意电脑要把代理关掉)\n'''\n\nd = u2.connect()\nprint(d.info)\nd.implicitly_wait(20)\n\ncomments = ['good job', 'good', 'look me', 'crazy', 'emm...', 'I wish you a happy new year',\n            'May you be happy, lucky and happy.', 'I wish you a happy new year and good luck!',\n            'to put people and life above everything else', 'heroes in harm’s way', 'spirited', ' behind wave',\n            'mythical creatures', 'dagongren, which refers to people who work for others', 'involution',\n            'Versailles literature', 'Look at my', 'Too good', 'To learn', 'learned', 'Thank you', 'I got it.',\n            '666', 'nice', 'Well done', 'Look at my', 'Wonderful', 'Mine is not bad either.', 'Kudos', 'like u',\n            'lean it', 'well...', '😊', 'My god!', 'Me too', 'I see', 'Come on', 'See you', 'Allow me', 'Have fun',\n            'I\\'m home', 'Bless you!', 'Follow me', 'Good luck!', 'Bottoms up!', 'Guess what?', 'Keep it up!',\n            'Time is up', 'I like it!', 'That\\'s neat', 'Let\\'s face it.', 'Let\\'s get started', 'Is that so',\n            'That\\'s something', 'Do you really mean it', 'Mind you', 'I am behind you', 'That depends',\n            'What\\'s up today?', 'Cut it out', 'What did you say', 'Knock it off', '[angel]', '[astonish]',\n            '[awkward]', '[blink]', '[complacent]', '[cool]', '[cool][cute]', '[cool][cool]', '[cool][cool][cool]',\n            '[cry]', '[cute]', '[cute][cute]', '[cute][cute][cute]', '[disdain]', '[drool]',\n            '[embarrassed]', '[evil]', '[excited]', '[facewithrollingeyes]', '[flushed]', '[funnyface]', '[greedy]',\n            '[happy]', '[hehe]', '[joyful]', '[laugh]', '[laughwithtears]', '[loveface]', '[lovely]', '[nap]',\n            '[pride]', '[proud]', '[rage]', '[scream]', '[shock]', '[shout]', '[slap]', '[smile]', '[smileface]',\n            '[speechless]', '[stun]', '[sulk]', '[surprised]', '[tears]', '[thinking]', '[weep]', '[wicked]',\n            '[wow]', '[wronged]', '[yummy]',\n            ' You kick ass.', ' You did a great job.', \" You're a really strong person.\", ' You read a lot.',\n            ' That was impressive.', ' Your work on that project was incredible.', ' Keep up the good work!',\n            \" We're so proud of you.\", ' How smart of you!', ' You have a real talent.',\n            ' Well, a good teacher makes good student.', ' I would like to compliment you on your diligence.',\n            \" We're proud of you.\", ' He has a long head.', ' You look great today.', \" You're beautiful/gorgeous.\",\n            ' You look so healthy.', ' You look like a million dollars.', ' You have a good taste.',\n            ' I am impressed.', ' You inspire me.', ' You are an amazing friend.', 'You are such a good friend.',\n            ' You have a good sense of humor.', \" You're really talented.\", \" You're so smart.\",\n            \" You've got a great personality.\", ' You are just perfect!', ' You are one of a kind.',\n            ' You make me want to be a better person.', 'brb', 'g2g', 'AMA', 'dbd', 'this look great',\n            'we’re so proud of you.', 'nice place.', 'nice going! ', 'emm...amazing!', 'ohh...unbelievable!',\n            'yeh,impressive.', 'terrific..', 'fantastic!', 'fabulous.', 'attractive..', 'hei...splendid.',\n            'ooh, remarkable', 'gorgeous', 'h.., glamorous', 'marvelous.', 'brilliant..', 'well...glorious',\n            'outstanding...', 'stunning!', 'appealing.', 'yeh,impressive[cool]', 'terrific[angel]', 'fantastic[cool]',\n            'fabulous[angel]', 'attractive[cool]', 'splendid[angel]', 'remarkable[cool]', 'gorgeous[angel]',\n            'glamorous[angel]', 'marvelous[cool]', 'brilliant[angel]', 'glorious[cool]', 'outstanding[angel]',\n            'stunning[cool]', 'appealing[angel]', 'Would you like me?[angel]', 'Do you like crafts?[angel]',\n            'I have a new creative work, welcome![thinking]']\n\nprint('总共有{}条随机评论！'.format(len(comments)))\n\n\ndef start_vpn():  # 启动代理app\n    d.press(\"home\")\n    # d.app_stop('com.v2ray.ang')\n    d.app_start('com.v2ray.ang')\n    if 'Not' in d(resourceId=\"com.v2ray.ang:id/tv_test_state\").get_text():\n        print_t('正在启动v2ray...')\n        d(resourceId='com.v2ray.ang:id/fab').click()\n    if 'Connected' in d(resourceId=\"com.v2ray.ang:id/tv_test_state\").get_text():\n        print_t('启动v2ray完成，正在测试速度...')\n        d(resourceId='com.v2ray.ang:id/layout_test').click()\n    while 'Testing' in d(resourceId=\"com.v2ray.ang:id/tv_test_state\").get_text():\n        time.sleep(1)\n    print_t(d(resourceId=\"com.v2ray.ang:id/tv_test_state\").get_text())\n\n\ndef review_forYou():\n    # d.press(\"home\")\n    # d.app_start('com.zhiliaoapp.musically', stop=True)\n    time.sleep(1)\n    stop, index = random.randint(40, 70), 0\n    while index < stop:  # 随机刷几十条\n        cur_comment = comments[random.randint(0, len(comments) - 1)]\n        print_t('foryou-总共有：{}条 | 现在到：{}条\\t评论：{}'.format(stop, index, cur_comment))\n        comment_foryou(cur_comment)\n        try:\n            time.sleep(random.randint(7, 35))  # 随机停顿1~5秒\n            d.swipe_ext(\"up\")  # 上划，下一个视频\n        except Exception as e:\n            print_t(e)\n        index += 1\n\n\ndef review_tiktok():  # 评论\n    keys = ['handmadecraft']  # '#homedecor #flowershower #handmadecraft'\n    d.press(\"home\")\n    d.app_start('com.zhiliaoapp.musically', stop=False)\n    time.sleep(1)\n    try:\n        d(text='Discover', className='android.widget.TextView').click()  # 点击发现\n        time.sleep(1)\n        d(resourceId=\"com.zhiliaoapp.musically:id/fbt\").click()  # 点击搜索\n    except Exception as e:\n        print_t(e)\n    for key in keys:\n        # 不能搜索search\n        try:\n            d(resourceId=\"com.zhiliaoapp.musically:id/b15\").clear_text()  # 清除历史\n            d(resourceId=\"com.zhiliaoapp.musically:id/b15\").set_text(key)  # 输入\n            d(resourceId=\"com.zhiliaoapp.musically:id/fap\").click()  # 点击搜索\n            d(resourceId=\"android:id/text1\", text=\"Videos\").click()  # 点击视频，然后点击第一条\n            d.xpath(\n                '//*[@resource-id=\"com.zhiliaoapp.musically:id/cfh\"]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]').click()\n        except Exception as e:\n            print_t(e)\n        stop, index = random.randint(35, 60), 0\n        while index < stop:  # 随机刷几十条\n            cur_comment = comments[random.randint(0, len(comments) - 1)]\n            print_t('{}-总共有：{}条 | 现在到：{}条\\t评论：{}'.format(key, stop, index, cur_comment))\n            comment(cur_comment)\n            try:\n                time.sleep(random.randint(5, 20))  # 随机停顿1~5秒\n                d.swipe_ext(\"up\")  # 上划，下一个视频\n            except Exception as e:\n                print_t(e)\n            index += 1\n        time.sleep(random.randint(int(0.5 * 60), 5 * 60))\n        d(resourceId=\"com.zhiliaoapp.musically:id/t4\").click()  # 返回搜索\n\n\ndef comment(content):\n    try:\n        time.sleep(random.randint(3, 10))  # 随机停顿1~5秒\n        d(resourceId=\"com.zhiliaoapp.musically:id/acm\").click()  # 点击评论按钮\n    except Exception as e:\n        print_t(e)\n    try:\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        e_ele = d(text='Add comment...', className='android.widget.EditText')\n        e_ele.click()  # 点击弹出键盘\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        e_ele.clear_text()\n        e_ele.set_text(content)  # 输入\n    except Exception as e:\n        print_t(e)\n    try:\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        d(resourceId=\"com.zhiliaoapp.musically:id/ad6\").click()  # 发送\n    except Exception as e:\n        print_t(e)\n    # 关闭系统键盘\n    try:\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        d(resourceId=\"com.zhiliaoapp.musically:id/t4\").click()  # 关闭评论\n    except Exception as e:\n        print_t(e)\n\n\ndef comment_foryou(content):\n    try:\n        time.sleep(random.randint(3, 10))  # 随机停顿1~5秒\n        d(resourceId=\"com.zhiliaoapp.musically:id/acm\").click()  # 点击评论按钮\n    except Exception as e:\n        print_t(e)\n    try:\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        e_ele = d(text='Add comment...', className='android.widget.EditText')\n        e_ele.click()  # 点击弹出键盘\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        e_ele.clear_text()\n        e_ele.set_text(content)  # 输入\n    except Exception as e:\n        print_t(e)\n    try:\n        time.sleep(random.randint(1, 3))  # 随机停顿1~2秒\n        d(resourceId=\"com.zhiliaoapp.musically:id/ad6\").click()  # 发送\n    except Exception as e:\n        print_t(e)\n    # 关闭系统键盘\n    try:\n        d.press(\"back\")  # 返回1，关闭系统键盘\n    except Exception as e:\n        print_t(e)\n\n\ndef print_t(content):\n    dt = datetime.now()\n    print(dt.strftime('%H:%M:%S') + '\\t' + str(content))\n\n\nif __name__ == \"__main__\":\n    # start_vpn()\n    # review_tiktok()\n    # review_forYou()\n    # comment('Look at my')\n    d(resourceId=\"com.zhiliaoapp.musically:id/afa\").click()\n"
  },
  {
    "path": "006-TikTok/v2ray_pool/__init__.py",
    "content": "# 运行时路径。并非__init__.py的路径\nimport os\nimport sys\n\nBASE_DIR = \"../002-V2rayPool\"\nif os.path.exists(BASE_DIR):\n    sys.path.append(BASE_DIR)\n\nfrom core import utils\nfrom core.conf import Config\nfrom db.db_main import DBManage"
  },
  {
    "path": "007-CutVideoAudio/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n"
  },
  {
    "path": "007-CutVideoAudio/007-CutVideoAudio.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\" />\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "007-CutVideoAudio/README.md",
    "content": "# FFmpeg批量剪切音视频\n\n[V2版本脚本](ff_util_v2.py)更加完善，但是未接入GUI\n\n本项目主要通过ffmpeg工具进行批量视频剪辑，随机剪辑，从而躲过自媒体平台的检查，从而达到一份视频多个账号运营。\n\n使用前提：**必须要安装ffmpeg程序**，安装过程请自行百度。\n\n\n下载地址：\n\nMacOS：[QincjiCut1.0.0-mac](https://github.com/xhunmon/PythonIsTools/releases/download/1.0.4/QincjiCut1.0.0.app.zip)  下载后解压后使用\n\nWindow：QincjiCut1.0.0-win （未打包）\n\n效果如图：\n\n![剪辑器截图](./doc/example.png)\n\n#主要知识点\n\n## python GUI(界面)\n\n本文使用tkinter GUI(界面)框架进行界面显示：[./ui.py](ui.py) ，[学习参考](https://www.cnblogs.com/shwee/p/9427975.html) 。\n\n## [pyinstaller](https://pyinstaller.readthedocs.io/en/stable/) 打包\n\n使用pyinstaller把python程序打包成window和mac可执行文件，主要命令如下：\n```shell\n#① ：生成xxx.spec文件；（去掉命令窗口-w）\npyinstaller -F -i res/logo.ico main.py  -w\n#②：修改xxx.spec，参考main.spec\n#③：再次进行打包，参考installer-mac.sh\npyinstaller -F -i res/logo.ico main.spec  -w\n```\n打包脚本与配置已放在 `doc` 目录下，需要拷贝出根目录进行打包。\n\n注意：\npyinstaller打包工具的版本与python版本、python所需第三方库以及操作系统会存在各种问题，所以需要看日志查找问题。例如：打包后运用，发现导入pyppeteer报错，通过降低版本后能正常使用：pip install pyppeteer==0.2.2\n\n## 项目\n本项目跟Downloader下载器基本相同，而ffmpeg命令则可以通过 [](https://qincji.gitee.io/2021/01/18/ffmpeg/18_command/)\n\n"
  },
  {
    "path": "007-CutVideoAudio/__init__.py",
    "content": ""
  },
  {
    "path": "007-CutVideoAudio/config.ini",
    "content": "# 常用配置模块\n[common]\n#软件使用截止日期\nexpired_time=2025/12/15 23:59:59\n\n#app的版本名称\nversion_name=1.0.0\n\n#app的版本号\nversion_code=1000"
  },
  {
    "path": "007-CutVideoAudio/doc/mac-sh/main.spec",
    "content": "# -*- mode: python ; coding: utf-8 -*-\n\n\nblock_cipher = None\n\n\na = Analysis(['main.py','type_enum.py','ui.py','utils.py','editors.py','ff_util.py','ff_cut.py'],\n             pathex=['.'],\n             binaries=[],\n             datas=[('res/logo.ico', 'images'),('config.ini', '.')],\n             hiddenimports=[],\n             hookspath=[],\n             hooksconfig={},\n             runtime_hooks=[],\n             excludes=[],\n             win_no_prefer_redirects=False,\n             win_private_assemblies=False,\n             cipher=block_cipher,\n             noarchive=False)\npyz = PYZ(a.pure, a.zipped_data,\n             cipher=block_cipher)\n\nexe = EXE(pyz,\n          a.scripts,\n          a.binaries,\n          a.zipfiles,\n          a.datas,  \n          [],\n          name='main',\n          debug=False,\n          bootloader_ignore_signals=False,\n          strip=False,\n          upx=True,\n          upx_exclude=[],\n          runtime_tmpdir=None,\n          console=False,\n          disable_windowed_traceback=False,\n          target_arch=None,\n          codesign_identity=None,\n          entitlements_file=None , icon='res/logo.ico')\napp = BUNDLE(exe,\n             name='QincjiCut.app',\n             icon='res/logo.ico',\n             bundle_identifier=None)"
  },
  {
    "path": "007-CutVideoAudio/doc/mac-sh/pyinstaller.sh",
    "content": "#!/bin/bash\n\npyinstaller -F -i res/logo.ico main.spec main.py  -w \\\n-p type_enum.py \\\n-p ui.py \\\n-p utils.py \\\n-p ff_util.py \\\n-p editors.py \\\n-p ff_cut.py"
  },
  {
    "path": "007-CutVideoAudio/editors.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:editing.py 所有视频类的基类，负责与UI界面的绑定\n@Date       :2022/03/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport time\nfrom threading import Lock\n\nimport requests\nfrom my_fake_useragent import UserAgent\n\nfrom type_enum import PrintType\nfrom utils import Config\n\nua = UserAgent(family='chrome')\n\n\nclass Editors(object):\n    func_ui_print = None\n    __mutex_total = Lock()\n    __mutex_success = Lock()\n    __mutex_failed = Lock()\n    __mutex_bmg = Lock()\n    __count_total = 0\n    __count_success = 0\n    __count_failed = 0\n    __count_bmg = 0\n    __beijing_time = 0  # 在线北京时间\n\n    def __init__(self):\n        self._headers = {'user-agent': ua.random()}\n        self.get_beijing_time()\n\n    @staticmethod\n    def print_hint():\n        \"\"\"显示初始提示信息\"\"\"\n        Editors.print_ui(\n            \"\"\"\n程序原理：\n\\t1、随机裁剪掉视频头尾；\n\\t2、随机改变视频帧率和比特率；\n\\t3、随机更换视频bgm；\n\\t4、保证每次输出不重复。\n使用说明：\n\\t1、必须安装FFmpeg；\n\\t2、最短bgm长度大于最大视频长度。\n            \"\"\"\n        )\n\n    def start(self, ffmpeg, video, music, dst):\n        \"\"\"业务逻辑由子类实现\"\"\"\n        pass\n\n    @staticmethod\n    def print_ui(txt):\n        \"\"\"在界面显示内容\"\"\"\n        Editors.print_all_ui(txt=txt)  # 打印日志\n\n    @staticmethod\n    def print_all_ui(txt, print_type: PrintType = PrintType.log):\n        \"\"\"通知ui中func_ui_print更新内容\"\"\"\n        if Editors.func_ui_print is not None:\n            Editors.func_ui_print(txt=txt, print_type=print_type)\n\n    @staticmethod\n    def get_beijing_time():\n        \"\"\"静态方法：获取在线的北京时间\"\"\"\n        if Editors.__beijing_time > 0:\n            return Editors.__beijing_time\n        try:\n            response = requests.get(url='http://www.beijing-time.org/t/time.asp', headers={'user-agent': ua.random()})\n            result = response.text\n            data = result.split(\"\\r\\n\")\n            year = data[1][len(\"nyear\") + 1: len(data[1]) - 1]\n            month = data[2][len(\"nmonth\") + 1: len(data[2]) - 1]\n            day = data[3][len(\"nday\") + 1: len(data[3]) - 1]\n            # wday = data[4][len(\"nwday\")+1 : len(data[4])-1]\n            hrs = data[5][len(\"nhrs\") + 1: len(data[5]) - 1]\n            minute = data[6][len(\"nmin\") + 1: len(data[6]) - 1]\n            sec = data[7][len(\"nsec\") + 1: len(data[7]) - 1]\n\n            beijinTimeStr = \"%s/%s/%s %s:%s:%s\" % (year, month, day, hrs, minute, sec)\n            beijinTime = time.strptime(beijinTimeStr, \"%Y/%m/%d %X\")\n            Editors.__beijing_time = int(time.mktime(beijinTime))\n        except:\n            pass\n        return Editors.__beijing_time\n\n    @staticmethod\n    def is_expired():\n        \"\"\"静态方法：判断是否已过期\"\"\"\n        if Editors.__beijing_time == 0:  # 还没获取到时间\n            return True\n        expired_time_str = time.strptime(Config.instance().get_expired_time(), \"%Y/%m/%d %X\")\n        expired_time_int = int(time.mktime(expired_time_str))\n        return Editors.__beijing_time > expired_time_int\n\n    @staticmethod\n    def add_total_count(count=1):\n        \"\"\"静态方法：添加总下载任务数\"\"\"\n        Editors.__mutex_total.acquire()\n        Editors.__count_total += count\n        Editors.__mutex_total.release()\n        Editors.print_all_ui(txt=\"视频总数：%d\" % Editors.__count_total, print_type=PrintType.total)\n\n    @staticmethod\n    def add_bgm_count(count=1):\n        \"\"\"静态方法：添加总下载任务数\"\"\"\n        Editors.__mutex_bmg.acquire()\n        Editors.__count_bmg += count\n        Editors.__mutex_bmg.release()\n        Editors.print_all_ui(txt=\"bmg数量：%d\" % Editors.__count_bmg, print_type=PrintType.bgm)\n\n    @staticmethod\n    def get_total_count():\n        \"\"\"静态方法：获取总下载任务数\"\"\"\n        return Editors.__count_total\n\n    @staticmethod\n    def get_bgm_count():\n        \"\"\"静态方法：获取正在下载任务数\"\"\"\n        return Editors.__count_bmg\n\n    @staticmethod\n    def add_success_count():\n        \"\"\"静态方法：添加下载成功任务数\"\"\"\n        Editors.__mutex_success.acquire()\n        Editors.__count_success += 1\n        Editors.__mutex_success.release()\n        # 成功一条，减正在下载的一条\n        Editors.print_all_ui(txt=\"已完成：%d\" % Editors.__count_success, print_type=PrintType.success)\n\n    @staticmethod\n    def get_success_count():\n        \"\"\"静态方法：获取下载成功任务数\"\"\"\n        return Editors.__count_success\n\n    @staticmethod\n    def add_failed_count():\n        \"\"\"静态方法：添加下载失败任务数\"\"\"\n        Editors.__mutex_failed.acquire()\n        Editors.__count_failed += 1\n        Editors.__mutex_failed.release()\n        # 失败一条，减正在下载的一条\n        Editors.print_all_ui(txt=\"已失败：%d\" % Editors.__count_failed, print_type=PrintType.failed)\n\n    @staticmethod\n    def get_failed_count():\n        \"\"\"静态方法：获取下载失败任务数\"\"\"\n        return Editors.__count_failed\n"
  },
  {
    "path": "007-CutVideoAudio/ff_cut.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:editing.py 所有视频类的基类，负责与UI界面的绑定\n@Date       :2022/03/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os.path\nimport random\nimport shutil\nimport time\n\nfrom editors import Editors\nfrom ff_util import *\nfrom utils import *\n\n\nclass ListCut(Editors):\n    # 初始化\n    def __init__(self):\n        super().__init__()\n        self.headers = self._headers\n        # 抓获所有视频\n        self.end = False\n\n    def start(self, ffmpeg, video, music, dst):\n        Editors.print_ui(\"开始准备处理本地数据\")\n        if not os.path.exists(ffmpeg) or not os.path.exists(video) or not os.path.exists(music) or not os.path.exists(\n                dst):\n            Editors.print_ui(\"文件或者目录不存在！\")\n            return\n        dst_temp = os.path.join(dst, 'temp')\n        if not os.path.exists(dst_temp):\n            os.makedirs(dst_temp)\n        fps, bit = random.randint(25, 30), random.randint(1200, 1800)\n        start_v, end_v = random.randint(100, 500), random.randint(100, 500)\n        v_files, a_files = get_real_files(video), get_real_files(music)\n        index, size_v, size_a, start_time = 1, len(v_files), len(a_files), time.time()\n        Editors.add_total_count(size_v)\n        Editors.add_bgm_count(size_a)\n        Editors.print_ui(\"改系列帧率：{} | 比特率：{} | 截切开头：{} | 截切结尾：{}\".format(fps, bit, start_v, end_v))\n        time.sleep(3)\n        for i in range(0, size_v):\n            a_file = random.choice(a_files)\n            v_file = random.choice(v_files)\n            v_files.remove(v_file)\n            Editors.print_ui(\"{} 与 {} 开始合并！\".format(v_file, a_file))\n            try:\n                name_v, ext_v = os.path.splitext(v_file)\n                name_a, ext_a = os.path.splitext(a_file)\n                src_v = os.path.join(video, v_file)\n                dst_file = os.path.join(dst, '{}-{}{}'.format(format_num(index), name_v, ext_v))\n                dur_src_v = get_duration(ffmpeg, src_v)  # 毫秒\n                duration = dur_src_v - start_v - end_v\n                src_a = os.path.join(music, a_file)\n                dur_src_a = get_duration(ffmpeg, src_a)\n                random_a = (dur_src_a - duration) if dur_src_a > duration else 100\n                start_a = random.randint(0, random_a)  # 随机取音频\n                temp_file_v = os.path.join(dst_temp, 'temp{}'.format(ext_v))\n                temp_file_a = os.path.join(dst_temp, 'temp{}'.format(ext_a))\n                cut_video(ffmpeg, src_v, start_v, duration, fps, bit, temp_file_v)\n                cut_audio(ffmpeg, src_a, start_a, duration, temp_file_a)\n                muxer_va(ffmpeg, temp_file_v, temp_file_a, dst_file)\n                index += 1\n                Editors.add_success_count()\n            except Exception as e:\n                Editors.print_ui(str(e))\n                Editors.add_failed_count()\n                time.sleep(1)\n            use_time = time.time() - start_time\n            Editors.print_ui('已运行{}分{}秒...'.format(int(use_time / 60), int(use_time % 60)))\n            time.sleep(2)\n        shutil.rmtree(dst_temp)\n"
  },
  {
    "path": "007-CutVideoAudio/ff_util.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:ff_util.py ffmpeg截切命令工具\n@Date       :2022/03/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\n\n\ndef get_duration(ff_file, src):\n    '''\n    执行命令获取输出这样的：Duration: 00:00:31.63, start: 0.000000, bitrate: 1376 kb/s\n    :param ff_file: ffmpeg程序路径\n    :param src: 音视频文件\n    :return: 返回毫秒\n    '''\n    info = os.popen(r'{} -i \"{}\" 2>&1 | grep \"Duration\"'.format(ff_file, src)).read()\n    dur = info.split(',')[0].replace(' ', '').split(':')\n    h, m, ss = int(dur[1]) * 60 * 60 * 1000, int(dur[2]) * 60 * 1000, dur[3]\n    if '.' in ss:\n        s1 = ss.split('.')\n        s = int(s1[0]) * 1000 + int(s1[1]) * 10\n    else:\n        s = int(ss) * 1000\n    return h + m + s\n\n\ndef format_h_m_s(t):\n    if t < 10:\n        return '0{}'.format(t)\n    else:\n        return '{}'.format(t)\n\n\ndef format_ms(t):\n    if t < 10:\n        return '00{}'.format(t)\n    elif t < 100:\n        return '0{}'.format(t)\n    else:\n        return '{}'.format(t)\n\n\ndef format_duration_by_ms(ms):\n    '''\n    通过毫秒转化成 'xx:xx:xx.xxx' 格式\n    :param ms: 毫秒\n    :return:\n    '''\n    h = format_h_m_s(int(ms / 1000 / 60 / 60))\n    m = format_h_m_s(int(ms / 1000 / 60 % 60))\n    s = format_h_m_s(int(ms / 1000 % 60))\n    m_s = format_ms(int(ms % 1000))\n    return '{}:{}:{}.{}'.format(h, m, s, m_s)\n\n\ndef cut_audio(ff_file, src, start, dur, dst):\n    '''\n    裁剪一段音频进行输出\n    :param ff_file: ffmpeg程序路径\n    :param src: 要裁剪的文件路径，可以是视频文件\n    :param start: 开始裁剪点，单位毫秒开始\n    :param dur: 裁剪时长，单位秒\n    :param dst: 输出路径，包括后缀名\n    :return:\n    '''\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(\n        r'{} -i \"{}\" -vn -acodec copy -ss {} -t {} \"{}\"'.format(ff_file, src, format_duration_by_ms(start), dur, dst))\n\n\ndef cut_video(ff_file, src, start, dur, fps, bit, dst):\n    '''\n    裁剪一段视频进行输出， -ss xx:xx:xx.xxx\n    :param ff_file: ffmpeg程序路径\n    :param src: 要裁剪的文件路径，可以是视频文件\n    :param start: 开始裁剪点，单位毫秒开始\n    :param dur: 裁剪时长，单位秒\n    :param fps: 帧率，通常是25~30\n    :param bit: 比特率，通常是1600~2000即可\n    :param dst: 输出路径，包括后缀名\n    :return:\n    '''\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(\n        r'{} -i \"{}\" -ss {} -t {} -r {} -b:v {}K -an \"{}\"'.format(ff_file, src, format_duration_by_ms(start), dur, fps,\n                                                                  bit, dst))\n\n\ndef muxer_va(ff_file, src_v, src_a, dst):\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(r'{} -i \"{}\" -i \"{}\" -c:v copy -c:a aac -strict experimental \"{}\"'.format(ff_file, src_v, src_a, dst))\n"
  },
  {
    "path": "007-CutVideoAudio/ff_util_v2.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:ff_util.py ffmpeg截切命令工具\n@Date       :2022/03/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\nimport re\nimport time\nfrom datetime import datetime, timedelta\n\n\ndef get_va_infos(ff_file, src):\n    \"\"\"\n    获取视频的基本信息\n    @param ff_file: ffmpeg路径\n    @param src: 视频路径\n    @return: 结果：{'duration': '00:11:26.91', 'bitrate': '507', 'v_codec': 'h264', 'v_size': '1280x720', 'v_bitrate': '373', 'v_fps': '25', 'a_codec': 'aac', 'a_bitrate': '128'}\n    \"\"\"\n    cmd = r'{} -i \"{}\" -hide_banner 2>&1'.format(ff_file, src)\n    output = os.popen(cmd).read()\n    lines = output.splitlines()\n    result = {}\n    for line in lines:\n        if line.strip().startswith('Duration:'):\n            result['duration'] = line.split(',')[0].split(': ')[-1]\n            result['bitrate'] = line.split(',')[-1].strip().split(': ')[-1].split(' ')[0]\n        elif line.strip().startswith('Stream #0'):\n            line = re.sub(r'\\[.*?\\]', '', re.sub(r'\\(.*?\\)', '', line))\n            if 'Video' in line:\n                result['v_codec'] = line.split(',')[0].split(': ')[-1].strip()\n                result['v_size'] = line.split(',')[2].strip().split(' ')[0].strip()\n                result['v_bitrate'] = line.split(',')[3].strip().split(' ')[0].strip()\n                result['v_fps'] = line.split(',')[4].strip().split(' ')[0].strip()\n            elif 'Audio' in line:\n                result['a_codec'] = line.split(',')[0].split(': ')[-1].strip()\n                result['a_bitrate'] = line.split(',')[4].strip().split(' ')[0].strip()\n    print(result)\n    return result\n\n\ndef get_duration(ff_file, src):\n    '''\n    执行命令获取输出这样的：Duration: 00:00:31.63, start: 0.000000, bitrate: 1376 kb/s\n    :param ff_file: ffmpeg程序路径\n    :param src: 音视频文件\n    :return: 返回毫秒\n    '''\n    cmd = r'{} -i \"{}\" 2>&1 | grep \"Duration\"'.format(ff_file, src)\n    info = os.popen(cmd).read()\n    dur = info.split(',')[0].replace(' ', '').split(':')\n    h, m, ss = int(dur[1]) * 60 * 60 * 1000, int(dur[2]) * 60 * 1000, dur[3]\n    if '.' in ss:\n        s1 = ss.split('.')\n        s = int(s1[0]) * 1000 + int(s1[1]) * 10\n    else:\n        s = int(ss) * 1000\n    return h + m + s\n\n\ndef format_h_m_s(t):\n    return f'0{t}' if t < 10 else f'{t}'\n\n\ndef format_ms(t):\n    if t < 10:\n        return f'00{t}'\n    return f'0{t}' if t < 100 else f'{t % 1000}'\n\n\ndef format_to_time(ms):\n    '''\n     毫秒 --> 'xx:xx:xx.xxx'\n    :param ms: 毫秒\n    :return: 'xx:xx:xx.xxx'\n    '''\n    # t = timedelta(milliseconds=ms)\n    # return str(t)\n    h = format_h_m_s(int(ms / 1000 / 60 / 60))\n    m = format_h_m_s(int(ms / 1000 / 60 % 60))\n    s = format_h_m_s(int(ms / 1000 % 60))\n    m_s = format_ms(int(ms % 1000))\n    return '{}:{}:{}.{}'.format(h, m, s, m_s)\n\n\ndef format_to_ms(duration: str):\n    \"\"\"\n    'xx:xx:xx.xxx' --> 毫秒\n    @param duration: 时间长度'xx:xx:xx.xxx'\n    @return:  毫秒\n    \"\"\"\n    hms = duration.split(':')\n    s_str = hms[2]\n    ms_str = '0'\n    if '.' in s_str:\n        s_ms = s_str.split('.')\n        s_str = s_ms[0]\n        ms_str = s_ms[1]\n    h = int(hms[0]) * 1000 * 60 * 60\n    m = int(hms[1]) * 1000 * 60\n    s = int(s_str) * 1000\n    ms = int(ms_str)\n    return h + m + s + ms\n\n\ndef srt_to_ass(ff_file, src, dst):\n    os.system(f'{ff_file} -i {src} {dst}')\n\n\ndef cut_with_subtitle(ff_file, src, dst, srt, width, height, margin_v, font_size=50, dur_full: str = None,\n                      start='00:00:00.000', tail='00:00:00.000', fps=None,\n                      v_bit=None, a_bit=None):\n    \"\"\"\n    添加硬字幕：ffmpeg -i \"../output/733316.mp4\" -ss \"00:02:00\" -t 10 -r 23 -b:v 400K -c:v libx264 -b:a 38K -c:a aac -vf \"subtitles=../output/input.ass:force_style='PlayResX=1280,PlayResY=720,MarginV=80,Fontsize=50'\" ../output/ass.mp4\n    \"\"\"\n    t_start = time.time()\n    dur_ms = format_to_ms(dur_full) - format_to_ms(tail) - format_to_ms(start)\n    dur = format_to_time(dur_ms)\n\n    filename, ext = os.path.splitext(srt)\n    ass = f'{filename}.ass'\n    if os.path.exists(ass):\n        os.remove(ass)\n    ass_cmd = '{} -i \"{}\" \"{}\"'.format(ff_file, srt, ass)\n    print(ass_cmd)\n    os.system(ass_cmd)\n    # ffmpeg -i \"input.mp4\" -ss \"00:02:10.000\" -t 12397 -r 15 -b:v 500K -c:v libx264 -c:a aac\n    cmd = '{} -i \"{}\"'.format(ff_file, src)\n    cmd = '{} -ss \"{}\" -t {}'.format(cmd, start, int(format_to_ms(dur) / 1000))\n    if fps:  # 添加裁剪的fps\n        cmd = '{} -r {}'.format(cmd, fps)\n    # -c copy 不经过解码，会出现黑屏，因为有可能是P帧和B帧\n    if v_bit:  # 添加视频bitrate，并且指定用libx264进行编码（ffmpeg必须安装）\n        cmd = '{} -b:v {}K -c:v libx264'.format(cmd, v_bit)\n    if a_bit:  # 添加音频bitrate，并且指定用aac进行编码\n        cmd = '{} -b:a {}K -c:a aac'.format(cmd, a_bit)\n    # -vf \"subtitles=input.ass:force_style='PlayResX=1280,PlayResY=720,MarginV=70,Fontsize=50'\"\n    style = \"PlayResX={},PlayResY={},MarginV={},Fontsize={}\".format(width, height, margin_v, font_size)\n    sub_file = \"{}\".format(ass)\n    cmd = '''{} -vf \"subtitles={}:force_style='{}'\"'''.format(cmd, sub_file, style)\n    # cmd = f'{cmd} -vf \"subtitles={ass}:force_style=\"\"\"PlayResX={width},PlayResY={height},MarginV={margin_v},Fontsize={font_size}\"\"\"\"'\n    cmd = '{} {}'.format(cmd, dst)\n    print(cmd)\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(cmd)\n    os.remove(ass)\n    print('一共花了 {} 秒 进行裁剪并添加字幕 {}'.format(int(time.time() - t_start), src))\n\n\ndef cut_va_full(ff_file, src, dst, dur: str = None, start='00:00:00.000', fps=None, v_bit=None, a_bit=None,\n                copy_a=False):\n    \"\"\"\n    ffmpeg -i \"input.mp4\" -ss \"00:02:10.000\" -t 12397 -r 15 -b:v 500K -c:v libx264 -c:a aac \"凡人修仙传1重制版-国创-高清独家在线观看-bilibili-哔哩哔哩.mp4\"\n    其他所有的视频裁剪命令都需要通过这个实现\n    @param ff_file: ffmpeg路径\n    @param src:     输入路径\n    @param dst:     输出路径\n    @param dur:     裁剪长度，格式为'00:00:00.000'\n    @param start:   裁剪的起点，如果dur=None，表示需要对时间进行裁剪，只是转换格式罢了\n    @param fps:     帧率，通常视频15~18帧即可，动漫一般24帧\n    @param v_bit:   单独控制视频的比特率\n    @param a_bit:   单独控制音频的比特率\n    @param copy_a:  直接复制音频通道数据，当 a_bit=None方有效\n    \"\"\"\n    cmd = '{} -i \"{}\"'.format(ff_file, src)  # 输入文件\n    if dur is not None:  # 添加裁剪时间\n        cmd = '{} -ss \"{}\" -t {}'.format(cmd, start, int(format_to_ms(dur) / 1000))\n    if fps:  # 添加裁剪的fps\n        cmd = '{} -r {}'.format(cmd, fps)\n    # -c copy 不经过解码，会出现黑屏，因为有可能是P帧和B帧\n    if v_bit:  # 添加视频bitrate，并且指定用libx264进行编码（ffmpeg必须安装）\n        cmd = '{} -b:v {}K -c:v libx264'.format(cmd, v_bit)\n    if a_bit:  # 添加音频bitrate，并且指定用aac进行编码\n        cmd = '{} -b:a {}K -c:a aac'.format(cmd, a_bit)\n    elif copy_a:  # 是否完全复制音频\n        cmd = '{} -c:a copy'.format(cmd)\n    cmd = '{} \"{}\"'.format(cmd, dst)  # 添加输出\n    if os.path.exists(dst):\n        os.remove(dst)\n    t_start = time.time()\n    os.system(cmd)\n    print('一共花了 {} 秒 进行裁剪 {}'.format(int(time.time() - t_start), src))\n\n\ndef cut_va_tail(ff_file, src, dst, dur_full: str = None, start='00:00:00.000', tail='00:00:00.000', fps=None,\n                v_bit=None, a_bit=None, copy_a=False):\n    \"\"\"\n    裁剪头尾\n    \"\"\"\n    dur_ms = format_to_ms(dur_full) - format_to_ms(tail) - format_to_ms(start)\n    dur = format_to_time(dur_ms)\n    cut_va_full(ff_file, src, dst, dur, start, fps, v_bit, a_bit, copy_a)\n\n\ndef cut_audio(ff_file, src, start, dur, dst):\n    '''\n    裁剪一段音频进行输出\n    :param ff_file: ffmpeg程序路径\n    :param src: 要裁剪的文件路径，可以是视频文件\n    :param start: 开始裁剪点，单位毫秒开始\n    :param dur: 裁剪时长，单位秒\n    :param dst: 输出路径，包括后缀名\n    :return:\n    '''\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(\n        r'{} -i \"{}\" -vn -acodec copy -ss {} -t {} \"{}\"'.format(ff_file, src, format_to_time(start), dur, dst))\n\n\ndef cut_video(ff_file, src, start, dur, fps, bit, dst):\n    '''\n    裁剪一段视频进行输出， -ss xx:xx:xx.xxx\n    :param ff_file: ffmpeg程序路径\n    :param src: 要裁剪的文件路径，可以是视频文件\n    :param start: 开始裁剪点，单位毫秒开始\n    :param dur: 裁剪时长，单位秒\n    :param fps: 帧率，通常是25~30\n    :param bit: 比特率，通常是1600~2000即可\n    :param dst: 输出路径，包括后缀名\n    :return:\n    '''\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(\n        r'{} -i \"{}\" -ss {} -t {} -r {} -b:v {}K -an \"{}\"'.format(ff_file, src, format_to_time(start), dur, fps,\n                                                                  bit, dst))\n\n\ndef cut_va_dur(ff_file, src, dst, start=0, dur=0, fps=None, bit=None):\n    \"\"\"\n    根据头尾裁剪视频\n    :param ff_file: ffmpeg工具\n    :param src: 输入资源\n    :param dst: 输出文件\n    :param start:   起点\n    :param end: 终点\n    :param fps: 帧率\n    :param bit: 比特率\n    :return:\n    \"\"\"\n    length = get_duration(ff_file, src)\n    if start + dur > length:\n        print('裁剪比视频长')\n        return\n    if os.path.exists(dst):\n        os.remove(dst)\n\n    cmd = r'{} -i \"{}\"'.format(ff_file, src)\n    cmd = cmd + ' -ss {}'.format(format_to_time(start))\n    cmd = cmd + ' -t {}'.format(format_to_time(dur))\n    if fps:\n        cmd = cmd + ' -r {}'.format(fps)\n    if bit:\n        cmd = cmd + ' -b:v {}K'.format(bit)\n    cmd = cmd + ' -c:v libx264 \"{}\"'.format(dst)  # 使用x264解码后重新封装\n    # cmd = cmd+' -c copy {}'.format(dst)  #不经过解码，会出现黑屏\n    os.system(cmd)\n\n\ndef cut_va_start_end(ff_file, src, dst, start='00:00:00', end='00:00:00', dur='00:00:00', fps=None, bit=None):\n    dur = format_to_ms(dur) - format_to_ms(end)\n    cmd = f'{ff_file} -i \"{src}\" -ss {start} -t {dur} -c copy \"{dst}\"'\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(cmd)\n\n\ndef cut_va_end(ff_file, src, dst, start=0, end=0, fps=None, bit=None):\n    \"\"\"\n    根据头尾裁剪视频\n    :param ff_file: ffmpeg工具\n    :param src: 输入资源\n    :param dst: 输出文件\n    :param start:   起点\n    :param end: 终点\n    :param fps: 帧率\n    :param bit: 比特率\n    :return:\n    \"\"\"\n    length = get_duration(ff_file, src)\n    dur = length - (start + end)\n    if dur <= 0:\n        print('裁剪比视频长')\n        return\n    cut_va_dur(ff_file, src, dst, start, dur, fps, bit)\n\n\ndef muxer_va(ff_file, src_v, src_a, dst):\n    if os.path.exists(dst):\n        os.remove(dst)\n    os.system(r'{} -i \"{}\" -i \"{}\" -c:v copy -c:a aac -strict experimental \"{}\"'.format(ff_file, src_v, src_a, dst))\n"
  },
  {
    "path": "007-CutVideoAudio/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: 程序主入口\n@Date       :2022/03/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport os\nimport sys\n\nfrom ui import Ui\n\n# 主模块执行\nif __name__ == \"__main__\":\n    path = os.path.dirname(os.path.realpath(sys.argv[0]))\n    # ffmpeg = os.path.dirname('/usr/local/ffmpeg/bin/ffmpeg/')\n    # video = os.path.dirname('/Users/Qincji/Documents/zmt/handmade/')\n    # music = os.path.dirname('/Users/Qincji/Documents/zmt/music/')\n    # dst = os.path.dirname('/Users/Qincji/Downloads/ffmpeg/')\n    app = Ui()\n    # app.set_dir(ffmpeg, video, music, dst)\n    app.set_dir(path, path, path, path)\n    # to do\n    app.mainloop()\n"
  },
  {
    "path": "007-CutVideoAudio/type_enum.py",
    "content": "#!/usr/bin/env python\n# -*- coding:utf-8 -*-\n\"\"\"\n@Description:dy_download.py\n@Date       :2022/03/14\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\n\nfrom enum import Enum\n\n\nclass PrintType(Enum):\n    log = 1\n    total = 2\n    bgm = 3\n    success = 4\n    failed = 5\n"
  },
  {
    "path": "007-CutVideoAudio/ui.py",
    "content": "#!/usr/bin/env python\r\n# -*- coding:utf-8 -*-\r\n\"\"\"\r\n@Description: 用于GUI界面显示\r\n@Date       :2021/08/14\r\n@Author     :xhunmon\r\n@Mail       :xhunmon@gmail.com\r\n\"\"\"\r\nfrom tkinter import *\r\nfrom tkinter.filedialog import (askdirectory, askopenfilename)\r\n\r\nfrom editors import Editors\r\nfrom ff_cut import ListCut\r\nfrom type_enum import PrintType\r\nfrom utils import *\r\n\r\n\r\n# from PIL import Image, ImageTk\r\n\r\n\r\nclass Ui(Frame):\r\n    def __init__(self, master=None):\r\n        global bg_color\r\n        bg_color = '#373434'\r\n        Frame.__init__(self, master, bg=bg_color)\r\n        self.ui_width = 0\r\n        self.pack(expand=YES, fill=BOTH)\r\n        self.window_init()\r\n        self.createWidgets()\r\n\r\n    def window_init(self):\r\n        self.master.title(\r\n            '欢迎使用-音视频批量截切工具' + Config.instance().get_version_name() + '  如有疑问请联系：xhunmon@gmail.com')\r\n        self.master.bg = bg_color\r\n        width, height = self.master.maxsize()\r\n        # self.master.geometry(\"{}x{}\".format(width, height))\r\n        self.master.geometry(\"%dx%d+%d+%d\" % (width / 2, height / 2, width / 4, height / 4))\r\n        self.ui_width = width / 2\r\n\r\n    def createWidgets(self):\r\n        # fm1\r\n        self.fm1 = Frame(self, bg=bg_color)\r\n        self.fm1.pack(fill='y', pady=10)\r\n        # window没有原生PIL 64位支持\r\n        # load = Image.open('res/logo.png')\r\n        # load.thumbnail((38, 38), Image.ANTIALIAS)\r\n        # initIamge = ImageTk.PhotoImage(load)\r\n        # self.panel = Label(self.fm1, image=initIamge, bg=bg_color)\r\n        # self.panel.image = initIamge\r\n        # self.panel.pack(side=LEFT, fill='y', padx=5)\r\n        self.titleLabel = Label(self.fm1, text=\"视频批量剪辑，适合一个视频多账号发送\", font=('微软雅黑', 28), fg=\"white\", bg=bg_color)\r\n        self.titleLabel.pack(side=LEFT, fill='y')\r\n\r\n        # fm2\r\n        self.fm2 = Frame(self, bg=bg_color)\r\n        self.fm2.pack(side=TOP, fill=\"y\")\r\n        self.fm2_right = Frame(self.fm2, bg=bg_color)\r\n        self.fm2_right.pack(side=RIGHT, padx=0, pady=10, expand=YES, fill='y')\r\n        self.fm2_left = Frame(self.fm2, bg=bg_color)\r\n        self.fm2_left.pack(side=LEFT, padx=15, pady=10, expand=YES, fill='x')\r\n        self.fm2_left_ffmpeg = Frame(self.fm2_left, bg=bg_color)\r\n        self.fm2_left_souce_video = Frame(self.fm2_left, bg=bg_color)\r\n        self.fm2_left_souce_music = Frame(self.fm2_left, bg=bg_color)\r\n        self.fm2_left_dst = Frame(self.fm2_left, bg=bg_color)\r\n\r\n        self.downloadBtn = Button(self.fm2_right, text='开始', fg=\"#aaaaaa\", bg=bg_color,\r\n                                  font=('微软雅黑', 18), command=self.start_download)\r\n        self.downloadBtn.pack(side=RIGHT)\r\n\r\n        self.ffmFileEntry = Entry(self.fm2_left_ffmpeg, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1)\r\n        self.ffmFileEntry.config(insertbackground='#ffffff')\r\n        self.ffmFileBtn = Button(self.fm2_left_ffmpeg, text='选择FFmpeg程序：', bg=bg_color, fg='#aaaaaa',\r\n                                 font=('微软雅黑', 12), width='12', command=self.save_file)\r\n        self.ffmFileBtn.pack(side=LEFT)\r\n        self.ffmFileEntry.pack(side=LEFT, fill='y')\r\n        self.fm2_left_ffmpeg.pack(side=TOP, fill='x')\r\n\r\n        self.videoDirEntry = Entry(self.fm2_left_souce_video, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color,\r\n                                   bd=1)\r\n        self.videoDirEntry.config(insertbackground='#ffffff')\r\n        self.videoDirBtn = Button(self.fm2_left_souce_video, text='选择视频源目录：', bg=bg_color, fg='#aaaaaa',\r\n                                  font=('微软雅黑', 12), width='12', command=self.save_dir)\r\n        self.videoDirBtn.pack(side=LEFT)\r\n        self.videoDirEntry.pack(side=LEFT, fill='y')\r\n        self.fm2_left_souce_video.pack(side=TOP, fill='x')\r\n\r\n        self.musicDirEntry = Entry(self.fm2_left_souce_music, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color,\r\n                                   bd=1)\r\n        self.musicDirEntry.config(insertbackground='#ffffff')\r\n        self.musicDirBtn = Button(self.fm2_left_souce_music, text='选择bmg源目录：', bg=bg_color, fg='#aaaaaa',\r\n                                  font=('微软雅黑', 12), width='12', command=self.save_dir)\r\n        self.musicDirBtn.pack(side=LEFT)\r\n        self.musicDirEntry.pack(side=LEFT, fill='y')\r\n        self.fm2_left_souce_music.pack(side=TOP, fill='x')\r\n\r\n        self.dstEntry = Entry(self.fm2_left_dst, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1)\r\n        self.dstEntry.config(insertbackground='#ffffff')\r\n        self.dstBtn = Button(self.fm2_left_dst, text='选择输出目录：', bg=bg_color, fg='#aaaaaa',\r\n                             font=('微软雅黑', 12), width='12', command=self.download_url)\r\n        self.dstBtn.pack(side=LEFT)\r\n        self.dstEntry.pack(side=LEFT, fill='y')\r\n        self.fm2_left_dst.pack(side=TOP, pady=10, fill='x')\r\n\r\n        # fm3 任务数状态\r\n        self.fm3 = Frame(self, bg=bg_color, height=6)\r\n        self.fm3.pack(side=TOP, fill=\"x\")\r\n        self.totalLabel = Label(self.fm3, width=10, text=\"视频总数：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.totalLabel.pack(side=LEFT, fill='y', padx=20)\r\n        self.totalBgmLabel = Label(self.fm3, width=10, text=\"bgm总数：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.totalBgmLabel.pack(side=LEFT, fill='y', padx=20)\r\n        self.successLabel = Label(self.fm3, width=10, text=\"已完成：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.successLabel.pack(side=LEFT, fill='y', padx=20)\r\n        self.failLabel = Label(self.fm3, width=10, text=\"已失败：0\", font=('微软雅黑', 12), fg=\"white\", bg=bg_color)\r\n        self.failLabel.pack(side=LEFT, fill='y', padx=20)\r\n\r\n        # fm4\r\n        self.fm4 = Frame(self, bg=bg_color)\r\n        self.fm4.pack(side=TOP, expand=YES, fill=\"both\")\r\n        self.logLabel = Label(self.fm4, anchor='w', wraplength=self.ui_width - 40, text=\"\", font=('微软雅黑', 12),\r\n                              fg=\"white\",\r\n                              bg=bg_color)\r\n        self.logLabel.pack(side=TOP, fill='both', padx=20)\r\n\r\n        # 注册回调\r\n        Editors.func_ui_print = self.func_ui_print\r\n        # 判断是否有网络\r\n        if Editors.get_beijing_time() == 0:\r\n            self.output(\"获取数据异常，请检查您的网络！\")\r\n        else:\r\n            Editors.print_hint()\r\n\r\n    def save_dir(self):\r\n        path = askdirectory()\r\n        self.set_dir(path)\r\n\r\n    def save_file(self):\r\n        path = askopenfilename()\r\n        self.set_dir(path)\r\n\r\n    def set_dir(self, ffmpeg=None, video=None, music=None, dst=None):\r\n        if ffmpeg:\r\n            self.ffmFileEntry.delete(0, END)\r\n            self.ffmFileEntry.insert(0, ffmpeg)\r\n        if video:\r\n            self.videoDirEntry.delete(0, END)\r\n            self.videoDirEntry.insert(0, video)\r\n        if music:\r\n            self.musicDirEntry.delete(0, END)\r\n            self.musicDirEntry.insert(0, music)\r\n        if dst:\r\n            self.dstEntry.delete(0, END)\r\n            self.dstEntry.insert(0, dst)\r\n\r\n    def download_url(self):\r\n        ground_truth = ''\r\n        self.dstEntry.delete(0, END)\r\n        self.dstEntry.insert(0, ground_truth)\r\n\r\n    def output(self, txt):\r\n        self.logLabel.config(text=txt)\r\n\r\n    def func_ui_print(self, txt, print_type: PrintType = None):\r\n        if print_type == PrintType.log:\r\n            self.logLabel.config(text=txt)\r\n        elif print_type == PrintType.total:\r\n            self.totalLabel.config(text=txt)\r\n        elif print_type == PrintType.bgm:\r\n            self.totalBgmLabel.config(text=txt)\r\n        elif print_type == PrintType.success:\r\n            self.successLabel.config(text=txt)\r\n        elif print_type == PrintType.failed:\r\n            self.failLabel.config(text=txt)\r\n\r\n    def start_download(self):\r\n        # 判断是否有网络，去掉\r\n        # if Editors.get_beijing_time() == 0:\r\n        #     self.output(\"获取数据异常，请检查您的网络！\")\r\n        #     return\r\n        if Editors.is_expired():\r\n            self.output(\"授权证书已到期，请联系客服！\")\r\n            return\r\n        ffmpeg = self.ffmFileEntry.get()\r\n        video = self.videoDirEntry.get()\r\n        music = self.musicDirEntry.get()\r\n        dst = self.dstEntry.get()\r\n        editors: Editors = ListCut()\r\n        editor = threading.Thread(target=editors.start, args=(ffmpeg, video, music, dst))\r\n        editor.setDaemon(True)  # 设置守护进程，避免界面卡死\r\n        editor.start()\r\n"
  },
  {
    "path": "007-CutVideoAudio/utils.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:工具类\n@Date       :2021/08/16\n@Author     :xhunmon\n@Mail       :xhunmon@gmail.com\n\"\"\"\nimport configparser\nimport os\nimport re\nimport threading\n\n\ndef get_domain(url: str = None):\n    \"\"\"\n    获取链接地址的域名\n    :param url:\n    :return:\n    \"\"\"\n    # http://youtube.com/watch\n    return re.match(r\"(http://|https://).*?\\/\", url, re.DOTALL).group(0)\n\n\ndef get_real_files(folder):\n    '''\n    获取真实文件，去掉(.xxx)等文隐藏文件\n    :param files:\n    :return:\n    '''\n    files = next(os.walk(folder))[2]\n    results = []\n    for f in files:\n        name, ext = os.path.splitext(f)\n        if len(name) > 0 and len(ext):\n            results.append(f)\n    return results\n\n\ndef format_num(num):\n    if num < 10:\n        return '00{}'.format(num)\n    elif num < 100:\n        return '0{}'.format(num)\n    else:\n        return '{}'.format(num)\n\n\nclass Config(object):\n    \"\"\"\n    配置文件的单例类\n    \"\"\"\n    _instance_lock = threading.Lock()\n\n    def __init__(self):\n        parent_dir = os.path.dirname(os.path.abspath(__file__))\n        conf_path = os.path.join(parent_dir, 'config.ini')\n        self.conf = configparser.ConfigParser()\n        self.conf.read(conf_path, encoding=\"utf-8\")\n\n    @classmethod\n    def instance(cls, *args, **kwargs):\n        with Config._instance_lock:\n            if not hasattr(Config, \"_instance\"):\n                Config._instance = Config(*args, **kwargs)\n        return Config._instance\n\n    def get_expired_time(self):\n        return self.conf.get(\"common\", \"expired_time\")\n\n    def get_version_name(self):\n        return self.conf.get(\"common\", \"version_name\")\n\n    def get_version_code(self):\n        return self.conf.get(\"common\", \"version_code\")\n"
  },
  {
    "path": "008-ChatGPT-UI/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n.idea/\n"
  },
  {
    "path": "008-ChatGPT-UI/README.md",
    "content": "\n# GPT-UI 2.0 移动☞: [iMedia](https://github.com/xhunmon/iMedia)\n\n- UI大优化\n- 支持图片生成\n\n演示：\n\nhttps://user-images.githubusercontent.com/26396755/234748470-6203534e-9845-4a3b-a512-09c315670175.mp4"
  },
  {
    "path": "008-ChatGPT-UI/config.ini",
    "content": "[common]\nexpired_time=2025/12/15 23:59:59\n\ntitle=GPT-UI\n\nversion_name=v1.0.1--github/xhunmon\n\nversion_code=1010\n\nemail=xhunmon@126.com"
  },
  {
    "path": "008-ChatGPT-UI/config.json",
    "content": "{\n  \"key\": \"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\",\n  \"api_base\": \"\",\n  \"model\": \"gpt-3.5-turbo\",\n  \"stream\": true,\n  \"response\": true,\n  \"folder\": \"\",\n  \"repeat\": true,\n  \"proxy\": \"socks5://127.0.0.1:7890\"\n}"
  },
  {
    "path": "008-ChatGPT-UI/doc/config.json",
    "content": "{\n  \"api_base\": \"\",\n  \"key\": \"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\",\n  \"model\": \"gpt-3.5-turbo\",\n  \"stream\": true,\n  \"response\": true,\n  \"folder\": \"/Users/Qincji/Desktop/develop/py/opengpt/gptcli/doc/\",\n  \"repeat\": true,\n  \"proxy\": \"socks5://127.0.0.1:7890\"\n}"
  },
  {
    "path": "008-ChatGPT-UI/doc/pyinstaller.sh",
    "content": "#!/bin/bash\n\n\npyinstaller --windowed --name GPT-UI --add-data \"config.ini:.\"  --icon logo.ico main.py gpt.py utils.py\n\n#if use --onefile, the build file is small, but star very slow.\n#pyinstaller --onefile --windowed --name GPT-UI --add-data \"config.ini:.\"  --icon logo.ico main.py gpt.py utils.py\n"
  },
  {
    "path": "008-ChatGPT-UI/gpt.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description:gpt.py\n@Date       :2023/03/31\n@Author     :xhunmon\n@Mail       :xhunmon@126.com\n\"\"\"\n\nimport time\nfrom datetime import datetime\n\nfrom utils import *\n\n\nclass Gpt(object):\n    func_ui_print = None\n\n    def __init__(self, config: Config):\n        self.session = []\n        self.api_prompt = []\n        self.update_config(config)\n        self.content = \"\"\n        self.is_change = False\n        self.is_finish = True\n        gpt_t = threading.Thread(target=self.start)\n        gpt_t.setDaemon(True)\n        gpt_t.start()\n\n    def update_config(self, config: Config):\n        self.cfg = config\n        self.api_key = self.cfg.api_key\n        self.api_base = self.cfg.api_base\n        self.api_model = self.cfg.model\n        self.api_stream = self.cfg.stream\n        self.api_response = self.cfg.response\n        self.proxy = self.cfg.proxy\n        openai.api_key = self.api_key\n        if self.api_base:\n            openai.api_base = self.api_base\n        openai.proxy = self.proxy\n\n    def start(self):\n        while True:\n            if self.is_finish:\n                while not self.is_change:\n                    time.sleep(0.3)\n                self.print(\"\\nMY:\\n{}\".format(self.content))\n                self.print(\"\\nGPT:\\n\")\n                self.is_change = False\n                self.is_finish = False\n                self.handle_input(self.content)\n            time.sleep(1)\n\n    def print(self, content):\n        Gpt.func_ui_print(content)\n\n    def query_openai_stream(self, data: dict) -> str:\n        messages = []\n        messages.extend(self.api_prompt)\n        messages.extend(data)\n        answer = \"\"\n        try:\n            response = openai.ChatCompletion.create(\n                model=self.api_model,\n                messages=messages,\n                stream=True)\n            for part in response:\n                finish_reason = part[\"choices\"][0][\"finish_reason\"]\n                if \"content\" in part[\"choices\"][0][\"delta\"]:\n                    content = part[\"choices\"][0][\"delta\"][\"content\"]\n                    answer += content\n                    self.print(content)\n                elif finish_reason:\n                    pass\n\n        except KeyboardInterrupt:\n            self.print(\"Canceled\")\n        except openai.error.OpenAIError as e:\n            self.print(\"OpenAIError:{}\".format(e))\n            answer = \"\"\n        return answer\n\n    def content_change(self, content: str):\n        if not content:\n            return\n        if self.content != content:\n            self.content = content\n            self.is_change = True\n\n    def handle_input(self, content: str):\n        if not content:\n            return\n        self.is_finish = False\n        self.session.append({\"role\": \"user\", \"content\": content})\n        if self.api_stream:\n            answer = self.query_openai_stream(self.session)\n        else:\n            answer = self.query_openai(self.session)\n        if not answer:\n            self.session.pop()\n        elif self.api_response:\n            self.session.append({\"role\": \"assistant\", \"content\": answer})\n        if answer:\n            try:\n                if self.cfg.folder and not os.path.exists(self.cfg.folder):\n                    os.makedirs(self.cfg.folder)\n                wfile = os.path.join(self.cfg.folder, \"gpt.md\" if self.cfg.repeat else \"gpt_{}.md\".format(\n                    datetime.now().strftime(\"%Y%m%d%H%M:%S\")))\n                if self.cfg.repeat:\n                    with open(wfile, mode='a', encoding=\"utf-8\") as f:\n                        f.write(\"MY:\\n{}\\n\".format(content))\n                        f.write(\"\\nGPT:\\n{}\\n\\n\".format(answer))\n                        f.close()\n                else:\n                    with open(wfile, mode='w', encoding=\"utf-8\") as f:\n                        f.write(\"MY:\\n{}\\n\".format(content))\n                        f.write(\"\\nGPT:{}\".format(answer))\n                        f.close()\n            except Exception as e:\n                self.print(\"Write error: {} \".format(e))\n        self.is_finish = True\n\n    def query_openai(self, data: dict) -> str:\n        messages = []\n        messages.extend(self.api_prompt)\n        messages.extend(data)\n        try:\n            response = openai.ChatCompletion.create(\n                model=self.api_model,\n                messages=messages\n            )\n            content = response[\"choices\"][0][\"message\"][\"content\"]\n            self.print(content)\n            return content\n        except openai.error.OpenAIError as e:\n            self.print(\"OpenAI error: {} \".format(e))\n        return \"\"\n"
  },
  {
    "path": "008-ChatGPT-UI/main.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: main\n@Date       :2023/03/31\n@Author     :xhunmon\n@Mail       :xhunmon@126.com\n\"\"\"\nimport sys\nimport tkinter as tk\nfrom tkinter.filedialog import *\n\nfrom gpt import *\nfrom utils import *\n\n\nclass EntryWithPlaceholder(tk.Entry):\n    def __init__(self, master=None, placeholder='', **kwargs):\n        super().__init__(master, **kwargs)\n        self.placeholder = placeholder\n        self.placeholder_color = 'grey'\n        self.default_fg_color = self['fg']\n        self.bind('<FocusIn>', self.on_focus_in)\n        self.bind('<FocusOut>', self.on_focus_out)\n        self.put_placeholder()\n\n    def put_placeholder(self):\n        self.insert(0, self.placeholder)\n        self['fg'] = self.placeholder_color\n\n    def remove_placeholder(self):\n        cur_value = self.get()\n        if cur_value == self.placeholder:\n            self.delete(0, tk.END)\n            self['fg'] = self.default_fg_color\n\n    def on_focus_in(self, event):\n        self.remove_placeholder()\n\n    def on_focus_out(self, event):\n        if not self.get():\n            self.put_placeholder()\n\n\nclass Application(tk.Frame):\n    def __init__(self, config: Config, master=None):\n        super().__init__(master)\n        self.cfg = config\n        self.gpt = None\n        self.repeat = False\n        self.master = master\n        self.master.title(ConfigIni.instance().get_title())\n        self.pack()\n        self.create_widgets()\n\n    def create_config(self):\n        row = tk.Frame(self)\n        row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)\n        button = tk.Button(row, text=\"Config\", width='7', command=self.click_config)\n        button.pack(side=tk.LEFT, padx=5, pady=5)\n        self.configEntry = EntryWithPlaceholder(row, placeholder=self.cfg.config_path, width=45)\n        self.configEntry.pack(side=tk.LEFT, padx=5, pady=5)\n        button = tk.Button(row, text=\"Create\", width='7', command=self.click_create)\n        button.pack(side=tk.LEFT, padx=5, pady=5)\n\n    def create_folder(self):\n        row = tk.Frame(self)\n        row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)\n        button = tk.Button(row, text=\"Folder\", width='7', command=self.click_folder)\n        button.pack(side=tk.LEFT, padx=5, pady=5)\n        self.folderEntry = EntryWithPlaceholder(row,\n                                                placeholder=self.cfg.folder if self.cfg.folder else f'{Config.pre_tips} chat output directory, default current',\n                                                width=50)\n        self.folderEntry.pack(side=tk.LEFT, padx=5, pady=5)\n\n    def create_key(self):\n        row = tk.Frame(self)\n        row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)\n        label = tk.Label(row, text=f\"Key: \", width='7')\n        label.pack(side=tk.LEFT)\n        self.keyEntry = EntryWithPlaceholder(row,\n                                             placeholder=self.cfg.api_key if self.cfg.api_key else f'{Config.pre_tips} input key id',\n                                             width=50)\n        self.keyEntry.pack(side=tk.LEFT, padx=5, pady=5)\n\n    def create_model(self):\n        row = tk.Frame(self)\n        row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)\n        label = tk.Label(row, text=f\"Model: \", width='7')\n        label.pack(side=tk.LEFT)\n        self.modelEntry = EntryWithPlaceholder(row,\n                                               placeholder=self.cfg.model if self.cfg.model else f'{Config.pre_tips} default gpt-3.5-turbo, or: gpt-4/gpt-4-32k',\n                                               width=50)\n        self.modelEntry.pack(side=tk.LEFT, padx=5, pady=5)\n\n    def create_proxy(self):\n        row = tk.Frame(self)\n        row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)\n        label = tk.Label(row, text=f\"Proxy: \", width='7')\n        label.pack(side=tk.LEFT)\n        self.proxyEntry = EntryWithPlaceholder(row,\n                                               placeholder=self.cfg.proxy if self.cfg.proxy else f'{Config.pre_tips} default empty, or http/https/socks4a/socks5',\n                                               width=50)\n        self.proxyEntry.pack(side=tk.LEFT, padx=5, pady=5)\n\n    def create_send(self):\n        row = tk.Frame(self)\n        row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)\n        self.sendEntry = EntryWithPlaceholder(row, placeholder=f'{Config.pre_tips} say something, then click send.',\n                                              width=55)\n        self.sendEntry.pack(side=tk.LEFT, padx=5, pady=5)\n        self.sendEntry.bind(\"<Return>\", self.on_return_key)\n        button = tk.Button(row, text=\"Send\", width='7', command=self.click_send)\n        button.pack(side=tk.LEFT, padx=5, pady=5)\n\n    def create_widgets(self):\n        self.create_config()\n        self.create_folder()\n        self.create_key()\n        self.create_model()\n        self.create_proxy()\n        self.create_send()\n        # bottom text\n        text_frame = tk.Frame(self)\n        text_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)\n        self.text = tk.Text(text_frame, wrap=tk.WORD, undo=True, font=(\"Helvetica\", 12))\n        self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)\n        scroll_bar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.text.yview)\n        scroll_bar.pack(side=tk.RIGHT, fill=tk.Y)\n        self.text.config(yscrollcommand=scroll_bar.set)\n\n        # email text\n        email_button = tk.Button(self, text=ConfigIni.instance().get_email())\n        email_button.pack(side=tk.LEFT, padx=5, pady=5)\n\n        # version text\n        version_button = tk.Button(self, text=ConfigIni.instance().get_version_name())\n        version_button.pack(side=tk.RIGHT, padx=5, pady=5)\n\n        # clear text\n        clear_button = tk.Button(self, text=\"clear\", width=10, command=self.clear)\n        clear_button.pack(side=tk.RIGHT, padx=5, pady=5)\n\n        # copy text\n        copy_button = tk.Button(self, text=\"copy\", width=10, command=self.copy)\n        copy_button.pack(side=tk.RIGHT, padx=5, pady=5)\n\n        Gpt.func_ui_print = self.func_ui_print\n\n    def refresh(self):\n        # self.set_entry(self.configEntry, self.cfg.default)\n        self.set_entry(self.folderEntry, self.cfg.folder)\n        self.set_entry(self.keyEntry, self.cfg.api_key)\n        self.set_entry(self.modelEntry, self.cfg.model)\n        self.set_entry(self.proxyEntry, self.cfg.proxy)\n\n    def func_ui_print(self, txt):\n        self.show_text(txt)\n\n    def click_config(self):\n        path = askopenfilename()\n        self.set_entry(self.configEntry, path)\n        if self.cfg.update(path):\n            self.refresh()\n        else:\n            self.show_text(\"update fail !\")\n\n    def click_create(self):\n        self.cfg.click_create()\n        self.show_text(\"create file :{} \".format(self.cfg.config_path))\n\n    def click_folder(self):\n        path = askdirectory()\n        self.set_entry(self.folderEntry, path)\n\n    def set_entry(self, entry: tk.Entry, content):\n        entry.delete(0, tk.END)\n        entry.insert(0, content)\n\n    def on_return_key(self, event):\n        self.click_send()\n\n    def click_send(self):\n        # config = self.configEntry.get()\n        self.cfg.update_by_content(self.keyEntry.get(), self.modelEntry.get(), self.folderEntry.get(),\n                                   self.proxyEntry.get())\n        content: str = self.sendEntry.get()\n        # self.show_text(\"me: {}\\n\".format(content))\n        if not self.gpt:\n            self.gpt: Gpt = Gpt(self.cfg)\n        else:\n            self.gpt.update_config(self.cfg)\n        self.gpt.content_change(content)\n\n    def show_text(self, content):\n        self.text.insert(tk.END, \"{}\".format(content))\n        self.text.yview_moveto(1.0)  # auto scroll to new\n\n    def clear(self):\n        self.text.delete(\"1.0\", \"end\")\n\n    def copy(self):\n        self.master.clipboard_clear()\n        self.master.clipboard_append(self.text.get(\"1.0\", tk.END))\n\n\nif __name__ == \"__main__\":\n    root = tk.Tk()\n    folder = os.path.dirname(os.path.realpath(sys.argv[0]))\n    app = Application(Config(folder), master=root)\n    app.mainloop()\n"
  },
  {
    "path": "008-ChatGPT-UI/requirements.txt",
    "content": "openai\nrequests[socks]\ntkinter"
  },
  {
    "path": "008-ChatGPT-UI/utils.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8 -*-\n\"\"\"\n@Description: tool\n@Date       :2023/03/31\n@Author     :xhunmon\n@Mail       :xhunmon@126.com\n\"\"\"\nimport configparser\nimport json\nimport os\nimport platform\nimport re\nimport threading\n\nimport openai\n\n\ndef get_domain(url: str = None):\n    # http://youtube.com/watch\n    return re.match(r\"(http://|https://).*?\\/\", url, re.DOTALL).group(0)\n\n\nclass ConfigIni(object):\n    _instance_lock = threading.Lock()\n\n    def __init__(self):\n        parent_dir = os.path.dirname(os.path.abspath(__file__))\n        conf_path = os.path.join(parent_dir, 'config.ini')\n        self.conf = configparser.ConfigParser()\n        self.conf.read(conf_path, encoding=\"utf-8\")\n\n    @classmethod\n    def instance(cls, *args, **kwargs):\n        with ConfigIni._instance_lock:\n            if not hasattr(ConfigIni, \"_instance\"):\n                ConfigIni._instance = ConfigIni(*args, **kwargs)\n        return ConfigIni._instance\n\n    def get_expired_time(self):\n        return self.conf.get(\"common\", \"expired_time\")\n\n    def get_version_name(self):\n        return self.conf.get(\"common\", \"version_name\")\n\n    def get_version_code(self):\n        return self.conf.get(\"common\", \"version_code\")\n\n    def get_title(self):\n        return self.conf.get(\"common\", \"title\")\n\n    def get_email(self):\n        return self.conf.get(\"common\", \"email\")\n\n\nclass Config:\n    sep = \"\"\n    pre_tips = \"Tips:\"\n    # baseDir = os.path.dirname(os.path.realpath(sys.argv[0]))\n    base_dir = ''\n    md_sep = '\\n\\n' + '-' * 10 + '\\n'\n    encodings = [\"utf8\", \"gbk\"]\n\n    api_key = \"\"\n    api_base = \"\"\n    model = \"\"\n    prompt = []\n    stream = True\n    response = False\n    proxy = \"\"\n    folder = \"\"\n    config_path = \"\"\n    repeat = True\n\n    def __init__(self, dir: str) -> None:\n        self.base_dir = dir\n        if platform.system() == 'Darwin':  # MacOS：use pyinstaller pack issue.\n            if '/Contents/MacOS' in dir:  # ./GPT-UI.app/Contents/MacOS/ --> ./\n                app_path = dir.rsplit('/Contents/MacOS')[0]\n                self.base_dir = app_path[:app_path.rindex('/')]\n        self.config_path = os.path.join(self.base_dir, \"config.json\")\n        self.cfg = {}\n        self.load(self.config_path)\n\n    def load(self, file):\n        if not os.path.exists(file):\n            return\n        with open(file, \"r\") as f:\n            self.cfg = json.load(f)\n        c = self.cfg\n        self.api_key = c.get(\"api_key\", c.get(\"key\", openai.api_key))  # compatible with history key\n        self.api_base = c.get(\"api_base\", openai.api_base)\n        self.model = c.get(\"model\", \"gpt-3.5-turbo\")\n        self.stream = c.get(\"stream\", True)\n        self.response = c.get(\"response\", False)\n        self.proxy = c.get(\"proxy\", \"\")\n        self.folder = c.get(\"folder\", self.base_dir)\n        self.repeat = c.get(\"repeat\", True)\n\n    def get(self, key, default=None):\n        return self.cfg.get(key, default)\n\n    def click_create(self):\n        results = {\n            \"key\": \"\",\n            \"api_base\": \"\",\n            \"model\": \"gpt-3.5-turbo\",\n            \"stream\": True,\n            \"response\": True,\n            \"folder\": \"\",\n            \"repeat\": False,\n            \"proxy\": \"\",\n            \"prompt\": []\n        }\n        self.write_json(results, self.config_path)\n\n    def write_json(self, content, file_path):\n        path, file_name = os.path.split(file_path)\n        if path and not os.path.exists(path):\n            os.makedirs(path)\n        with open(file_path, 'w') as f:\n            json.dump(content, f, ensure_ascii=False)\n            f.close()\n\n    def update(self, path: str):\n        if not path.endswith(\".json\"):\n            return False\n        if path and not os.path.exists(path):\n            return False\n        self.load(path)\n        return True\n\n    def update_by_content(self, key: str = None, model: str = None, folder: str = None, proxy: str = None):\n        if key and len(key.strip()) > 0 and not key.startswith(Config.pre_tips):\n            self.api_key = key\n        else:\n            self.api_key = ''\n        if model and len(model.strip()) > 0 and not model.startswith(Config.pre_tips):\n            self.model = model\n        else:\n            self.model = 'gpt-3.5-turbo'\n        if folder and len(folder.strip()) > 0 and not folder.startswith(Config.pre_tips):\n            self.folder = folder\n        else:\n            self.folder = self.base_dir\n        if proxy.startswith(Config.pre_tips):\n            self.proxy = None\n        else:\n            self.proxy = proxy if len(proxy.strip()) > 0 else None\n"
  },
  {
    "path": "009-Translate/README.md",
    "content": "# 多平台免费翻译神器\n\n## 1. 输入框翻译\n\n直接在输入框输入内容，选择目标语言，翻译平台即可。\n\n\n## 2.文件翻译\n\n- 1.支持txt翻译，但是文本内容不宜过大\n\n- 2.支持html翻译\n\n- 3.定制版srt字幕文件翻译，自定义修改[load_srt.py](load_srt.py)\n\n- 4.其他文本文件也是可以的，但是肯定有bug\n\n\n## 3.打包应用程序\n\n可以自行打包 exe和Mac平台的app。打包脚本参考[doc/pyinstall.sh](doc/pyinstaller.sh)，注意window平台的路径反斜杠更改。\n\n\n> 主要引入：translators"
  },
  {
    "path": "009-Translate/asset/ch.ini",
    "content": "[Main]\nTitle = Super86翻译\nDescription = 免费多平台多语言翻译...\nEnableProxy = 使用代理\nRun = 翻译\nClear = 清空\nCopy = 复制\nFile = 选择文件\nSettings = 设置\nExit = 退出\nVersion = 版本\nBusiness = 联系我\nEmail = 邮箱：\nRedBook = 小红书：\n\n[Settings]\nTitle = 设置\nProxy = 代理\nProxyEnable = 启用\nProxyDesc = 支持http/https/socks5\nTheme = 主题\nThemeDesc = 留空使用默认主题\nFullTranslate = 开启全翻译（未验证）\nAdvanced = 开启高级功能\nRestart = 立刻重启，使所有修改生效\nOk = 确认修改\nCancel = 取消\nReset = 恢复默认\n\n[Loading]\nContent = 加载中...\nCancel = 关闭\n\n[Language]\nLanguageCH = 简体中文\nLanguageCHINESE_CHT = 繁体中文\nLanguageEN = 英文\nLanguageJAPAN = 日文\nLanguageKOREAN = 韩文\nLanguageAR = 阿拉伯文\nLanguageFRENCH = 法文\nLanguageGERMAN = 德文\nLanguageRU = 俄罗斯文\nLanguageES = 西班牙文\nLanguagePT = 葡萄牙文\nLanguageIT = 意大利文\nLanguageAF = 南非荷兰文\nLanguageAZ = 阿塞拜疆文\nLanguageBS = 波斯尼亚文\nLanguageCS = 捷克文\nLanguageCY = 威尔士文\nLanguageDA = 丹麦文\nLanguageDE = 德文\nLanguageET = 爱沙尼亚文\nLanguageFR = 法文\nLanguageGA = 爱尔兰文\nLanguageHR = 克罗地亚文\nLanguageHU = 匈牙利文\nLanguageID = 印尼文\nLanguageIS = 冰岛文\nLanguageKU = 库尔德文\nLanguageLA = 拉丁文\nLanguageLT = 立陶宛文\nLanguageLV = 拉脱维亚文\nLanguageMI = 毛利文\nLanguageMS = 马来文\nLanguageMT = 马耳他文\nLanguageNL = 荷兰文\nLanguageNO = 挪威文\nLanguageOC = 欧西坦文\nLanguagePI = 巴利文\nLanguagePL = 波兰文\nLanguageRO = 罗马尼亚文\nLanguageRS_LATIN = 塞尔维亚文(latin)\nLanguageSK = 斯洛伐克文\nLanguageSL = 斯洛文尼亚文\nLanguageSQ = 阿尔巴尼亚文\nLanguageSV = 瑞典文\nLanguageSW = 西瓦希里文\nLanguageTL = 塔加洛文\nLanguageTR = 土耳其文\nLanguageUZ = 乌兹别克文\nLanguageVI = 越南文\nLanguageLATIN = 拉丁文\nLanguageFA = 波斯文\nLanguageUG = 维吾尔文\nLanguageUR = 乌尔都文\nLanguageRS_CYRILLIC = 塞尔维亚文(cyrillic)\nLanguageBE = 白俄罗斯文\nLanguageBG = 保加利亚文\nLanguageUK = 乌克兰文\nLanguageMN = 蒙古文\nLanguageABQ = 阿巴扎文\nLanguageADY = 阿迪赫文\nLanguageKBD = 卡巴尔达文\nLanguageAVA = 阿瓦尔文\nLanguageDAR = 达尔瓦文\nLanguageINH = 因古什文\nLanguageCHE = 车臣文\nLanguageLBE = 拉克文\nLanguageLEZ = 莱兹甘文\nLanguageTAB = 塔巴萨兰文\nLanguageCYRILLIC = 西里尔文\nLanguageHI = 印地文\nLanguageMR = 马拉地文\nLanguageNE = 尼泊尔文\nLanguageBH = 比尔哈文\nLanguageMAI = 迈蒂利文\nLanguageANG = 昂加文\nLanguageBHO = 孟加拉文\nLanguageMAH = 摩揭陀文\nLanguageSCK = 那格浦尔文\nLanguageNEW = 尼瓦尔文\nLanguageGOM = 保加利亚文\nLanguageSA = 沙特阿拉伯文\nLanguageBGC = 哈里亚纳文\nLanguageDEVANAGARI = 德瓦那加里文\nLanguageTA = 泰米尔文\nLanguageKN = 卡纳达文\nLanguageTE = 泰卢固文\nLanguageKA = 卡纳达文"
  },
  {
    "path": "009-Translate/asset/config.ini",
    "content": "[Config]\nLoading = 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\nLogo = iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ4IDc5LjE2NDAzNiwgMjAxOS8wOC8xMy0wMTowNjo1NyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIxLjAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDA1M0ExN0REQzE1MTFFRDkzOTVGQzQ4OTU1NERGRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDA1M0ExN0VEQzE1MTFFRDkzOTVGQzQ4OTU1NERGRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MDUzQTE3QkRDMTUxMUVEOTM5NUZDNDg5NTU0REZFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0MDUzQTE3Q0RDMTUxMUVEOTM5NUZDNDg5NTU0REZFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PojFojMAACEJSURBVHja7F0H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\nVersion = 1.2.0\nDate = 2023.4.23\nEmail = xhunmon@126.com\nRedBook = Super86"
  },
  {
    "path": "009-Translate/asset/en.ini",
    "content": "[Main]\nTitle = Super86 Translator\nDescription = Free multi-platform and multi-language translation software...\nEnableProxy = enable proxy\nRun = Run\nClear = Clear\nCopy = Copy\nFile = File\nSettings = Settings\nExit = Exit\nVersion = Version\nBusiness = Contact Me\nEmail = Email:\nRedBook = RedBook:\n\n[Settings]\nTitle = Settings\nProxy = Proxy\nProxyEnable = Enable\nProxyDesc = support http/https/socks5\nTheme = Theme\nThemeDesc = Leave blank to use global default\nFullTranslate = full translation (Not verified)\nAdvanced = Use Advanced Interface\nRestart = Restart app now\nOk = Ok\nCancel = Cancel\nReset = Reset\n\n[Loading]\nContent = Loading...\nCancel = Close\n\n\n[Language]\nLanguageCH = Simplified Chinese\nLanguageCHINESE_CHT = Traditional Chinese\nLanguageEN = English\nLanguageJAPAN = Japanese\nLanguageKOREAN = Korean\nLanguageAR = Arabic\nLanguageFRENCH = French\nLanguageGERMAN = German\nLanguageRU = Russian\nLanguageES = Spanish\nLanguagePT = Portuguese\nLanguageIT = Italian\nLanguageAF = Afrikaans\nLanguageAZ = Azerbaijani\nLanguageBS = Bosnian\nLanguageCS = Czech\nLanguageCY = Welsh\nLanguageDA = Danish\nLanguageDE = German\nLanguageET = Estonian\nLanguageFR = French\nLanguageGA = Irish\nLanguageHR = Croatian\nLanguageHU = Hungarian\nLanguageID = Indonesian\nLanguageIS = Icelandic\nLanguageKU = Kurdish\nLanguageLA = Latin\nLanguageLT = Lithuanian\nLanguageLV = Latvian\nLanguageMI = Maori\nLanguageMS = Malay\nLanguageMT = Maltese\nLanguageNL = Dutch\nLanguageNO = Norwegian\nLanguageOC = Occitan\nLanguagePI = Pali\nLanguagePL = Polish\nLanguageRO = Romanian\nLanguageRS_LATIN = Serbian(latin)\nLanguageSK = Slovak\nLanguageSL = Slovenian\nLanguageSQ = Albanian\nLanguageSV = Swedish\nLanguageSW = Swahili\nLanguageTL = Tagalog\nLanguageTR = Turkish\nLanguageUZ = Uzbek\nLanguageVI = Vietnamese\nLanguageLATIN = Latin\nLanguageFA = Persian\nLanguageUR = Urdu\nLanguageRS_CYRILLIC = Serbian(cyrillic)\nLanguageBE = Belarusian\nLanguageBG = Bulgarian\nLanguageUK = Ukranian\nLanguageMN = Mongolian\nLanguageABQ = Abaza\nLanguageADY = Adyghe\nLanguageKBD = Kabardian\nLanguageAVA = Avar\nLanguageDAR = Dargwa\nLanguageINH = Ingush\nLanguageCHE = Chechen\nLanguageLBE = Lak\nLanguageLEZ = Lezghian\nLanguageTAB = Tabassaran\nLanguageCYRILLIC = Cyrillic\nLanguageHI = Hindi\nLanguageMR = Marathi\nLanguageNE = Nepali\nLanguageBH = Bihari\nLanguageMAI = Maithili\nLanguageANG = Angika\nLanguageBHO = Bhojpuri\nLanguageMAH = Magahi\nLanguageSCK = Nagpur\nLanguageNEW = Newari\nLanguageGOM = Goan Konkani\nLanguageSA = Saudi Arabia\nLanguageBGC = Haryanvi\nLanguageDEVANAGARI = Devanagari\nLanguageTA = Tamil\nLanguageKN = Kannada\nLanguageUG = Uyghur\nLanguageTE = Telugu\nLanguageKA = Kannada"
  },
  {
    "path": "009-Translate/asset/language.json",
    "content": "[\n  {\n    \"en_name\": \"Chinese(简体)\",\n    \"id_name\": \"zh-CHS\",\n    \"zh_name\": \"简体\",\n    \"google\": \"zh-CN\",\n    \"yandex\": \"zh\",\n    \"bing\": \"zh-Hans\",\n    \"baidu\": \"zh\",\n    \"alibaba\": \"zh\",\n    \"tencent\": \"zh\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"zh\",\n    \"caiyun\": \"zh\",\n    \"argos\": \"zh\"\n  },\n  {\n    \"en_name\": \"Chinese(繁体)\",\n    \"id_name\": \"zh-CHT\",\n    \"zh_name\": \"繁体\",\n    \"google\": \"zh-TW\",\n    \"yandex\": \"\",\n    \"bing\": \"zh-Hant\",\n    \"baidu\": \"cht\",\n    \"alibaba\": \"zh-TW\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"cnt\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(文言文)\",\n    \"id_name\": \"wyw\",\n    \"zh_name\": \"文言文\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(粤语)\",\n    \"id_name\": \"yue\",\n    \"zh_name\": \"粤语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(内蒙语)\",\n    \"id_name\": \"mn\",\n    \"zh_name\": \"内蒙语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(维吾尔语)\",\n    \"id_name\": \"uy\",\n    \"zh_name\": \"维吾尔语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(藏语)\",\n    \"id_name\": \"ti\",\n    \"zh_name\": \"藏语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(白苗文)\",\n    \"id_name\": \"mww\",\n    \"zh_name\": \"白苗文\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"Chinese(彝语)\",\n    \"id_name\": \"ii\",\n    \"zh_name\": \"彝语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"english\",\n    \"id_name\": \"en\",\n    \"zh_name\": \"英语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"arabic\",\n    \"id_name\": \"ar\",\n    \"zh_name\": \"阿拉伯语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"ara\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"russian\",\n    \"id_name\": \"ru\",\n    \"zh_name\": \"俄语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"french\",\n    \"id_name\": \"fr\",\n    \"zh_name\": \"法语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"fra\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"german\",\n    \"id_name\": \"de\",\n    \"zh_name\": \"德语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"spanish\",\n    \"id_name\": \"es\",\n    \"zh_name\": \"西班牙语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"spa\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"portuguese\",\n    \"id_name\": \"pt\",\n    \"zh_name\": \"葡萄牙语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"pt\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"italian\",\n    \"id_name\": \"it\",\n    \"zh_name\": \"意大利语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"japanese\",\n    \"id_name\": \"ja\",\n    \"zh_name\": \"日语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"jp\",\n    \"alibaba\": \"\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"Y\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"korean\",\n    \"id_name\": \"ko\",\n    \"zh_name\": \"韩语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"kor\",\n    \"alibaba\": \"\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"greek\",\n    \"id_name\": \"el\",\n    \"zh_name\": \"希腊语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"dutch\",\n    \"id_name\": \"nl\",\n    \"zh_name\": \"荷兰语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"hindi\",\n    \"id_name\": \"hi\",\n    \"zh_name\": \"北印度语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"turkish\",\n    \"id_name\": \"tr\",\n    \"zh_name\": \"土耳其语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"malay\",\n    \"id_name\": \"ms\",\n    \"zh_name\": \"马来西亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"thai\",\n    \"id_name\": \"th\",\n    \"zh_name\": \"泰国语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"vietnamese\",\n    \"id_name\": \"vi\",\n    \"zh_name\": \"越南语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"vie\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"indonesian\",\n    \"id_name\": \"id\",\n    \"zh_name\": \"印度尼西亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"Y\",\n    \"tencent\": \"Y\",\n    \"youdao\": \"Y\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"hebrew\",\n    \"id_name\": \"he\",\n    \"zh_name\": \"希伯来语\",\n    \"google\": \"iw\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"polish\",\n    \"id_name\": \"pl\",\n    \"zh_name\": \"波兰语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"Y\"\n  },\n  {\n    \"en_name\": \"mongolian\",\n    \"id_name\": \"mn\",\n    \"zh_name\": \"蒙古语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"czech\",\n    \"id_name\": \"cs\",\n    \"zh_name\": \"捷克语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"hungarian\",\n    \"id_name\": \"hu\",\n    \"zh_name\": \"匈牙利语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"Y\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"estonian\",\n    \"id_name\": \"et\",\n    \"zh_name\": \"爱沙尼亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"est\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"bulgarian\",\n    \"id_name\": \"bg\",\n    \"zh_name\": \"保加利亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"bul\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"danish\",\n    \"id_name\": \"da\",\n    \"zh_name\": \"丹麦语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"dan\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"finnish\",\n    \"id_name\": \"fi\",\n    \"zh_name\": \"芬兰语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"fin\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"romanian\",\n    \"id_name\": \"ro\",\n    \"zh_name\": \"罗马尼亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"rom\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"swedish\",\n    \"id_name\": \"sv\",\n    \"zh_name\": \"瑞典语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"swe\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"slovenian\",\n    \"id_name\": \"sl\",\n    \"zh_name\": \"斯洛文尼亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"slo\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"persian\",\n    \"id_name\": \"fa\",\n    \"zh_name\": \"波斯语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"bosnian\",\n    \"id_name\": \"bs\",\n    \"zh_name\": \"波斯尼亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"bs-Latn\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"bs-Latn\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"serbian\",\n    \"id_name\": \"sr\",\n    \"zh_name\": \"塞尔维亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"sr-Latn\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"sr-Latn\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"fijian\",\n    \"id_name\": \"fj\",\n    \"zh_name\": \"斐济语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"filipino\",\n    \"id_name\": \"tl\",\n    \"zh_name\": \"菲律宾语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"fil\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"fil\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"haitiancreole\",\n    \"id_name\": \"ht\",\n    \"zh_name\": \"海地克里奥尔语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"catalan\",\n    \"id_name\": \"ca\",\n    \"zh_name\": \"加泰罗尼亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"croatian\",\n    \"id_name\": \"hr\",\n    \"zh_name\": \"克罗地亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"latvian\",\n    \"id_name\": \"lv\",\n    \"zh_name\": \"拉脱维亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"lithuanian\",\n    \"id_name\": \"lt\",\n    \"zh_name\": \"立陶宛语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"urdu\",\n    \"id_name\": \"ur\",\n    \"zh_name\": \"乌尔都语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"ukrainian\",\n    \"id_name\": \"uk\",\n    \"zh_name\": \"乌克兰语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"welsh\",\n    \"id_name\": \"cy\",\n    \"zh_name\": \"威尔士语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"tahiti\",\n    \"id_name\": \"ty\",\n    \"zh_name\": \"塔希提岛语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"tongan\",\n    \"id_name\": \"to\",\n    \"zh_name\": \"汤加语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"swahili\",\n    \"id_name\": \"sw\",\n    \"zh_name\": \"斯瓦希里语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"samoan\",\n    \"id_name\": \"sm\",\n    \"zh_name\": \"萨摩亚语\",\n    \"google\": \"Y\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"slovak\",\n    \"id_name\": \"sk\",\n    \"zh_name\": \"斯洛伐克\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"Y\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"afrikaans\",\n    \"id_name\": \"af\",\n    \"zh_name\": \"南非荷兰语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"norwegian\",\n    \"id_name\": \"no\",\n    \"zh_name\": \"挪威语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"bengali\",\n    \"id_name\": \"bn\",\n    \"zh_name\": \"孟加拉语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"bn-BD\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"malagasy\",\n    \"id_name\": \"mg\",\n    \"zh_name\": \"马达加斯加语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"maltese\",\n    \"id_name\": \"mt\",\n    \"zh_name\": \"马耳他语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"queretarootomi\",\n    \"id_name\": \"otq\",\n    \"zh_name\": \"克雷塔罗托米语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"klingon\",\n    \"id_name\": \"tlh\",\n    \"zh_name\": \"克林贡语\",\n    \"google\": \"\",\n    \"yandex\": \"\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"Y\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"gujarati\",\n    \"id_name\": \"gu\",\n    \"zh_name\": \"古吉拉特语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"tamil\",\n    \"id_name\": \"ta\",\n    \"zh_name\": \"泰米尔语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"telugu\",\n    \"id_name\": \"te\",\n    \"zh_name\": \"泰卢固语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"punjabi\",\n    \"id_name\": \"pa\",\n    \"zh_name\": \"旁遮普语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"amharic\",\n    \"id_name\": \"am\",\n    \"zh_name\": \"阿姆哈拉语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"azerbaijani\",\n    \"id_name\": \"az\",\n    \"zh_name\": \"阿塞拜疆语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"bashkir\",\n    \"id_name\": \"ba\",\n    \"zh_name\": \"巴什基尔语\",\n    \"google\": \"\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"belarusian\",\n    \"id_name\": \"be\",\n    \"zh_name\": \"白俄罗斯语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"cebuano\",\n    \"id_name\": \"ceb\",\n    \"zh_name\": \"切布亚诺\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"chuvash\",\n    \"id_name\": \"cv\",\n    \"zh_name\": \"楚瓦什语\",\n    \"google\": \"\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"esperanto\",\n    \"id_name\": \"eo\",\n    \"zh_name\": \"世界语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"basque\",\n    \"id_name\": \"eu\",\n    \"zh_name\": \"巴斯克语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"irish\",\n    \"id_name\": \"ga\",\n    \"zh_name\": \"爱尔兰语\",\n    \"google\": \"Y\",\n    \"yandex\": \"Y\",\n    \"bing\": \"Y\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  },\n  {\n    \"en_name\": \"emoji\",\n    \"id_name\": \"emj\",\n    \"zh_name\": \"表情\",\n    \"google\": \"\",\n    \"yandex\": \"Y\",\n    \"bing\": \"\",\n    \"baidu\": \"\",\n    \"alibaba\": \"\",\n    \"tencent\": \"\",\n    \"youdao\": \"\",\n    \"sogou\": \"\",\n    \"deepl\": \"\",\n    \"caiyun\": \"\",\n    \"argos\": \"\"\n  }\n]"
  },
  {
    "path": "009-Translate/config.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n@Author  : Xhunmon \n@Time    : 2023/4/23 08:51\n@FileName: config.py\n@desc: \n\"\"\"\nimport configparser\nimport json\nimport locale\nimport os\n\nfrom utils import *\n\n__version__ = '1.0.0'\n__email__ = 'xhunmon@126.com'\n__wechet__ = 'VABlog'\n\nlanguage, encoding = locale.getdefaultlocale()\n\n\ndef is_zh_language():\n    cache = get_cache(Key.LANGUAGE, None)\n    # if cache == 'zh_CN' or language == 'zh_CN':\n    #     return True\n    # else:\n    #     return False\n    return True\n\n\nclass IniConfig(object):\n    def __init__(self):\n        asset_path = os.path.join(os.path.dirname(__file__), 'asset')\n        language_path = os.path.join(asset_path, 'language.json')\n        config_path = os.path.join(asset_path, 'config.ini')\n        ch_ini = os.path.join(asset_path, 'ch.ini')\n        en_ini = os.path.join(asset_path, 'en.ini')\n        ini = ch_ini if is_zh_language() else en_ini\n        self.language = configparser.ConfigParser()\n        self.language.read(ini, encoding='utf-8')\n        self.cfg = configparser.ConfigParser()\n        self.cfg.read(config_path, encoding='utf-8')\n        with open(language_path, 'r') as f:\n            self.trl = json.load(f)\n            f.close()\n\n    def full(self, tag, name):\n        return self.language[tag][name]\n\n    def main(self, name):\n        return self.full('Main', name)\n\n    def settings(self, name):\n        return self.full('Settings', name)\n\n    def loading(self, name):\n        return self.full('Loading', name)\n\n    def language(self, name):\n        return self.full('Language', name)\n\n    def config(self, name):\n        return self.cfg['Config'][name]\n\n    def translate(self):\n        return self.trl\n\n\nconf = IniConfig()\n"
  },
  {
    "path": "009-Translate/core.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n@Author  : Xhunmon \n@Time    : 2023/4/22 22:29\n@FileName: core.py\n@desc: \n\"\"\"\nimport translators as ts\n\nfrom config import *\nfrom ui import LoadingWin\n\n\nclass Language(object):\n\n    def __init__(self, id_name, full_name, zh_name=None, google=True, yandex=True, bing=True, baidu=True, alibaba=True,\n                 tencent=True, youdao=True, sogou=True, deepl=True, caiyun=True, argos=True):\n        self.id_name = id_name\n        self.zh_name = zh_name if zh_name else full_name\n        self.full_name = full_name\n        self.google = google\n        self.yandex = yandex\n        self.bing = bing\n        self.baidu = baidu\n        self.alibaba = alibaba\n        self.tencent = tencent\n        self.youdao = youdao\n        self.sogou = sogou\n        self.deepl = deepl\n        self.caiyun = caiyun\n        self.argos = argos\n\n    def is_enable(self, translator: str):\n        return eval('self.' + translator)\n\n\nclass Translation(object):\n    def __init__(self):\n        self.is_full = get_cache(Key.FULL_TRANSLATE, False)  # 显示所有翻译平台和语言，但是未校验\n        self.is_zh = is_zh_language()  # 是否是中文\n        self.select_translator = 'baidu'\n        self.select_from_lang = '自动' if self.is_zh else 'auto'\n        self.select_to_lang = '英语' if self.is_zh else 'english'\n        self.languages = conf.translate()\n        # self.languages.append(Language('en', 'english', '英语'))\n        # self.languages.append(Language('zh', 'chinese', '中文'))\n        # self.languages.append(Language('ar', 'arabic', '阿拉伯语', deepl=False, caiyun=False))\n        # self.languages.append(Language('ru', 'russian', '俄语'))\n        # self.languages.append(Language('fr', 'french', '法语'))\n        # self.languages.append(Language('de', 'german', '德语', alibaba=False, caiyun=False))\n        # self.languages.append(Language('es', 'spanish', '西班牙语'))\n        # self.languages.append(Language('pt', 'portuguese', '葡萄牙语', caiyun=False))\n        # self.languages.append(Language('it', 'italian', '意大利语', caiyun=False))\n        # self.languages.append(Language('ja', 'japanese', '日本语', alibaba=False))\n        # self.languages.append(Language('ko', 'korean', '朝鲜语', alibaba=False, deepl=False, caiyun=False))\n        # self.languages.append(\n        #     Language('el', 'greek', '希腊语', alibaba=False, tencent=False, youdao=False, caiyun=False, argos=False))\n\n    def set_to_lang(self, lang):\n        if lang:\n            self.select_to_lang = lang\n\n    def set_from_lang(self, lang):\n        if lang:\n            self.select_from_lang = lang\n\n    def check_select_language(self):\n        languages = self.get_languages()\n        has_from = False\n        has_to = False\n        for lg in languages:  # lg为full_name\n            if lg == self.select_from_lang:\n                has_from = True\n            if lg == self.select_to_lang:\n                has_to = True\n        if not has_from:\n            self.select_from_lang = '自动' if self.is_zh else 'auto'\n        if not has_to:\n            self.select_to_lang = '英语' if self.is_zh else 'english'\n\n    def get_translators(self):\n        # 'google', 'yandex', 'bing', 'baidu', 'alibaba', 'tencent', 'youdao', 'sogou', 'deepl', 'caiyun', 'argos',\n        # 'apertium', 'cloudYi', 'elia', 'iciba', 'iflytek', 'iflyrec', 'itranslate', 'judic', 'languageWire',\n        # 'lingvanex', 'niutrans', 'mglip', 'modernMt', 'myMemory', 'papago', 'qqFanyi', 'qqTranSmart', 'reverso',\n        # 'sysTran', 'tilde', 'translateCom', 'translateMe', 'utibet', 'volcEngine', 'yeekit'\n        # {\"en_name\": \"Chinese(简体)\", \"id_name\": \"zh-CHS\", \"zh_name\": \"简体\", \"google\": \"zh-CN\", \"yandex\": \"zh\", \"bing\": \"zh-Hans\", \"baidu\": \"zh\", \"alibaba\": \"zh\", \"tencent\": \"zh\", \"youdao\": \"Y\", \"sogou\": \"Y\", \"deepl\": \"zh\", \"caiyun\": \"zh\", \"argos\": \"zh\"}\n        tors = list(self.languages[0].keys())\n        tors.remove('en_name')\n        tors.remove('id_name')\n        tors.remove('zh_name')\n        return tors\n\n    def get_languages(self):\n        support = []\n        for lg in self.languages:  # 取出字典\n            if lg[self.select_translator] == '':  # 不支持\n                continue\n            if self.is_zh:\n                support.append(lg['zh_name'])\n            else:\n                support.append(lg['en_name'])\n        return support\n\n    def choose_translator(self, tl):\n        self.select_translator = tl\n\n    def _search_id_name(self, is_from=True):\n        key = self.select_from_lang if is_from else self.select_to_lang\n        for lg in self.languages:  # 取出字典\n            if self.is_zh:  # 查找出 zh_name 对应的\n                if lg['zh_name'] == key:\n                    if lg[self.select_translator] == '':\n                        continue\n                    elif lg[self.select_translator] == 'Y':\n                        return lg['id_name']\n                    else:\n                        return lg[self.select_translator]\n            else:\n                if lg['en_name'] == key:\n                    if lg[self.select_translator] == '':\n                        continue\n                    elif lg[self.select_translator] == 'Y':\n                        return lg['id_name']\n                    else:\n                        return lg[self.select_translator]\n        return None\n\n    def get_id_name(self, is_from=True):\n        if is_from:\n            from_key = self.select_from_lang\n            if from_key == '自动' or from_key == 'auto':\n                return 'auto'\n            search = self._search_id_name(True)\n            return search if search else 'auto'\n        else:  # to_ 目标\n            search = self._search_id_name(False)\n            return search if search else 'en'  # 最后还是没有，默认英语\n\n    def translate(self, window, content, is_html=False, file_path: str = None):\n        try:\n            if file_path and file_path.endswith('.srt'):\n                from load_srt import Translator\n                import utils, os\n                filename, ext = os.path.splitext(file_path)\n                out_path = f'{filename}_output{ext}'\n                tl = Translator()\n                proxy = get_cache(Key.PROXY_INPUT, None) if get_cache(Key.PROXY_ENABLE, False) else None\n                if proxy:\n                    proxy_real = proxy.replace(' ', '').replace('\\n', '')\n                    proxy_spit = proxy_real.split('://')\n                    proxy_user = {proxy_spit[0]: proxy_real}\n                    tl.proxy_user = proxy_user\n                else:\n                    tl.proxy_user = None\n                tl.translators = {self.select_translator: 3}\n                tl.translate_file(file_path, out_path, self.get_id_name(is_from=True), self.get_id_name(is_from=False))\n                window['OUT_TEXT'].update(f'输出文件：{out_path}')\n            else:\n                # ts.preaccelerate()\n                # 是否使用了代理 export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890\n                proxy = get_cache(Key.PROXY_INPUT, None) if get_cache(Key.PROXY_ENABLE, False) else None\n                proxy_user = None\n                if proxy:\n                    proxy_real = proxy.replace(' ', '').replace('\\n', '')\n                    proxy_spit = proxy_real.split('://')\n                    proxy_user = {proxy_spit[0]: proxy_real}\n                if is_html:\n                    # result = ts.translate_html(content, translator=self.select_translator,\n                    #                            from_language=self.get_id_name(is_from=True),\n                    #                            to_language=self.get_id_name(is_from=False), proxies=proxy_user)\n                    result = ts.translate_html(content, translator=self.select_translator,\n                                               from_language=self.get_id_name(is_from=True),\n                                               to_language=self.get_id_name(is_from=False), proxies=proxy_user,\n                                               if_ignore_empty_query=True, if_show_time_stat=True)\n                else:\n                    result = ts.translate_text(content, translator=self.select_translator,\n                                               from_language=self.get_id_name(is_from=True),\n                                               to_language=self.get_id_name(is_from=False), proxies=proxy_user)\n                if file_path is not None:\n                    import utils, os\n                    filename, ext = os.path.splitext(file_path)\n                    out_path = f'{filename}_output{ext}'\n                    utils.write(result, out_path)\n                    window['OUT_TEXT'].update(f'输出文件：{out_path}')\n                else:\n                    window['OUT_TEXT'].update(result)\n        except Exception as e:\n            result = str(e)\n        LoadingWin.is_loading = False\n"
  },
  {
    "path": "009-Translate/doc/pyinstaller.sh",
    "content": "#!/bin/bash\n\n\n#pyinstaller --windowed --name GPT-UI --add-data \"config.ini:.\"  --icon logo.ico main.py gpt.py utils.py\n\npyinstaller --windowed --name Super86翻译 --add-data \"asset/config.ini:asset\" --add-data  \"asset/ch.ini:asset\" --add-data \"asset/en.ini:asset\" --add-data \"asset/language.json:asset\"  --icon asset/logo.png main.py config.py core.py utils.py ui.py load_srt.py\n\n#https://blog.csdn.net/COCO56/article/details/117452383\n#if use --onefile, the build file is small, but star very slow.\n#pyinstaller --onefile --windowed --name GPT-UI --add-data \"config.ini:.\"  --icon logo.ico main.py gpt.py utils.py.py\n\n\npyinstaller --windowed --name Super86翻译 --add-data \"asset/config.ini:asset\" --add-data  \"asset/ch.ini:asset\" --add-data \"asset/en.ini:asset\" --add-data \"asset/language.json:asset\"  --icon asset/logo.png main.py config.py core.py utils.py ui.py load_srt.py -p package --paths /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages"
  },
  {
    "path": "009-Translate/load_srt.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n@Author  : Xhunmon \n@Time    : 2023/4/18 14:47\n@FileName: load_srt.py\n@desc: 从本地加载srt字幕文件\n\"\"\"\nimport os\nimport threading\nimport time\n\n\nclass Tag:\n    def __init__(self):\n        self.num = None\n        self.duration = None\n        self.msg = ''\n\n\nclass Translator(object):\n    STATUS_SUCCESS = 1\n    STATUS_FAIL = -1\n\n    def __init__(self):\n        # TODO --> 一定要对应自己的代理，可以为None\n        self.proxy_user = {\"socks5\": \"socks5://127.0.0.1:7890\"}\n        # 翻译平台，deepl尝试3次，失败后，用google尝试2次，再失败后，用百度尝试3次，当所有尝试都失败了，表示该次翻译失败\n        self.translators = {\"deepl\": 3, \"google\": 2, \"baidu\": 3}\n        # 是否支持翻译\n        self.__supports = ['.srt', '.txt']\n        # 需翻译的语音，可以为自动：auto\n        self.__from_language = 'zh'\n        # 目标语言\n        self.__to_language = 'en'\n        # 一次性翻译的字符串数组长度\n        self.__one_length = 1024\n        # 某些场景需要休眠时间，再进行尝试\n        self.__user_sleep = 2\n        # 监听的事件\n        self.__listener = None\n        # 需要被翻译的文件路径\n        self.__list = []\n\n    def __callback(self, status, src, dst, msg=None):\n        \"\"\"\n        翻译结果回调\n        :param status: 1成功，2失败\n        :param src: 返回需要翻译的路径\n        :param dst: 返回翻译后的文件路径\n        :param msg: 返回提示信息\n        \"\"\"\n        if self.__listener:\n            self.__listener(status, src=src, dst=dst, msg=msg)\n\n    def __support_file(self, src: str):\n        \"\"\"\n        判断源文件是否支持被翻译\n        :param src: 文件路径\n        :return:\n        \"\"\"\n        for item in self.__supports:\n            if src.endswith(item):\n                return True\n        return False\n\n    def __deal_file(self):\n        \"\"\"\n        从队列取出文件，进行相关判断和操作\n        :return: （是否成功，源文件路径，目标文件路径，操作信息，临时文件路径）\n        \"\"\"\n        item = self.__list.pop(0)\n        src, dst = item[0], item[1]\n        temp = None\n        if not os.path.exists(src):\n            return False, src, dst, \"源文件不存在\", temp\n        if not self.__support_file(src):\n            return False, src, dst, \"源文件格式不支持\", temp\n        # try:\n        #     folder = os.path.dirname(src)\n        #     filename, ext = os.path.splitext(src)\n        #     if dst is None:\n        #         dst = os.path.join(folder, '{}_out{}'.format(filename, ext))\n        #     else:\n        #         folder = os.path.dirname(dst)\n        #         if not os.path.exists(folder):\n        #             os.mkdir(folder)\n        #     temp = os.path.join(folder, '{}_temp{}'.format(filename, ext))\n        # except Exception as e:\n        #     return False, src, dst, str(e), temp\n        if dst is None or dst == '':\n            folder = os.path.dirname(src)\n            filename, ext = os.path.splitext(src)\n            dst = os.path.join(folder, '{}_out{}'.format(filename, ext))\n        if os.path.exists(dst):\n            os.remove(dst)\n        return True, src, dst, '成功', temp\n\n    def __parse_srt(self, src: str):\n        \"\"\"\n        获取srt文件内容，一行一行的\n        :param src: 路径\n        :return: [Line,Line...]\n        \"\"\"\n        i = 0\n        # 遇到空一行方为一组\n        tags = []\n        tag = Tag()\n        with open(src, 'r', encoding=\"utf-8\") as f:\n            for line in f:\n                line = line.strip().replace('\\n', '')\n                if line == '':  # 结束了\n                    tags.append(tag)\n                    i = 0\n                    tag = Tag()\n                    continue\n                if i == 0:\n                    tag.num = line\n                elif i == 1:\n                    tag.duration = line\n                else:\n                    tag.msg += line\n                i += 1\n        return tags\n\n    def __parse_file(self, src: str):\n        \"\"\"\n        一行一行获取文件内容\n        :param src: 文件路径\n        :return: 返回[Line, Line...]\n        \"\"\"\n        if src.endswith('.srt'):\n            return self.__parse_srt(src)\n        return []\n\n    def __merge_content(self, tags):\n        \"\"\"\n        合并需要翻译的内容，为了避免请求其次太多，将整个文件的内容进行分组翻译\n        :param tags:\n        :return:\n        \"\"\"\n        result = []\n        item = ''\n        for tag in tags:\n            if item == '':\n                item = tag.msg\n            else:\n                item = item + '\\n' + tag.msg\n            if len(item) > self.__one_length:  # 开启下一组\n                result.append(item)\n                item = ''\n        if item != '':  # 最后一组数据\n            result.append(item)\n        return result\n\n    def __request_item(self, content):\n        \"\"\"\n        真正的请求网络进行翻译\n        :param content: 翻译内容\n        :return: 是否成功，翻译后的内容\n        \"\"\"\n        for key, value in self.translators.items():\n            for i in range(1, value + 1):  # 每个失败后尝试的次数\n                try:\n                    print('使用 {} 翻译，进行次数{}'.format(key, i))\n                    import translators as ts\n                    item = ts.translate_text(content, translator=key, from_language=self.__from_language,\n                                             to_language=self.__to_language, proxies=self.proxy_user)\n                    return True, item\n                except Exception as e:\n                    print(e)\n                    time.sleep(self.__user_sleep)\n\n        return False, '翻译失败'\n\n    def __request_list(self, contents):\n        \"\"\"\n        拆分列表中的数据\n        :param contents:\n        :return:\n        \"\"\"\n        results = []\n        for content in contents:\n            success, item = self.__request_item(content)\n            if not success:\n                return False, results\n            for x in item.split('\\n'):\n                results.append(x)\n        return True, results\n\n    def __merge_file(self, tags, items, dst):\n        with open(dst, 'w', encoding=\"utf-8\") as f:\n            for tag in tags:\n                f.write(f'{tag.num}\\n')\n                f.write(f'{tag.duration}\\n')\n                f.write(f'{items.pop(0)}\\n')\n                f.write('\\n')\n\n    def __translate(self):\n        \"\"\"\n        子线程不断监听翻译文件，进行翻译\n        \"\"\"\n        success, src, dst, msg, temp = self.__deal_file()\n        if not success:\n            self.__callback(Translator.STATUS_FAIL, src=src, dst=dst, msg=msg)\n            return\n        # 解析得到一行行数据\n        tags = self.__parse_file(src)\n        if len(tags) <= 0:\n            self.__callback(Translator.STATUS_FAIL, src=src, dst=dst, msg=\"解析文件内容失败\")\n            return\n            # 将一行行待翻译的文件进行合并，减少翻译次数\n        contents = self.__merge_content(tags)\n        success, results = self.__request_list(contents)\n        if not success:\n            # # 缓存已翻译的数据\n            # if len(result) > 0:\n            #     self.__merge_file(tags, result, temp)\n            self.__callback(Translator.STATUS_FAIL, src=src, dst=dst, msg='翻译失败')\n            return\n        self.__merge_file(tags, results, dst)\n        self.__callback(Translator.STATUS_SUCCESS, src=src, dst=dst, msg=\"成功\")\n\n    def add_callback(self, listener):\n        \"\"\"\n        添加监听器，\n        :param listener: 监听器设计模式如：method_listener(status,**kwargs)\n        :return:\n        \"\"\"\n        self.__listener = listener\n\n    def translate_file(self, src, dst=None, from_lang='zh', to_lang='en'):\n        \"\"\"\n        对外只需要知道传入的文件即可，其余全部在本翻译器处理，较少参数传递\n        @param dst: 必传，需要进行翻译的文件。\n        @param src: 如果不传，自动根据src所在的目录生成同后缀名的文件\n        @param from_lang: 从什么语言翻译\n        @param to_lang: 翻译目标语言\n        \"\"\"\n        self.__from_language = from_lang\n        self.__to_language = to_lang\n        item = (src, dst)\n        self.__list.append(item)\n        # self.__event.set()  # 唤醒线程\n        self.__translate()\n"
  },
  {
    "path": "009-Translate/main.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n@Author  : Xhunmon \n@Time    : 2023/4/21 23:03\n@FileName: guiv2.py\n@desc: \n\"\"\"\nimport PySimpleGUI as sg\n\nfrom core import *\nfrom ui import *\nfrom ui import settings_show\nfrom utils import *\n\n\nclass MainWin(object):\n    def __init__(self):\n        self.tl = Translation()\n        self.lw = LoadingWin()\n\n    def advanced_ui(self, window):\n        # 高级才显示的UI\n        advanced_mode = get_cache(Key.ADVANCED_MODE, False)\n        window[Key.PROXY_ENABLE].update(visible=advanced_mode)\n\n    def make_window(self):\n        \"\"\"\n        Creates the main window\n        :return: The main window object\n        :rtype: (sg.Window)\n        \"\"\"\n        sg.theme(get_theme())\n\n        left_col = sg.Column([\n            [sg.Multiline(size=(50, 25), write_only=True, expand_x=True, expand_y=True, key='IN_TEXT',\n                          reroute_stdout=True,\n                          echo_stdout_stderr=True, reroute_cprint=True)],\n            [sg.B(conf.main(Key.M_RUN)), sg.B(conf.main(Key.M_CLEAR)), sg.B(conf.main(Key.M_COPY)),\n             sg.Input('', key=\"_FILEBROWSE_\", enable_events=True, visible=False),\n             sg.FileBrowse(conf.main(Key.M_FILE), file_types=(('ALL files', '.*')),\n                           target='_FILEBROWSE_')],\n            [sg.CB(conf.main('EnableProxy'), default=get_cache(Key.PROXY_ENABLE, False), font='_ 12',\n                   enable_events=True, k=Key.PROXY_ENABLE)]], element_justification='l', expand_x=True, expand_y=True)\n\n        right_col = [\n            [sg.Multiline(size=(70, 25), write_only=True, expand_x=True, expand_y=True, key='OUT_TEXT',\n                          reroute_stdout=True,\n                          echo_stdout_stderr=True, reroute_cprint=True)],\n            [sg.B(conf.main(Key.M_SETTINGS)), sg.Button(conf.main(Key.M_EXIT))],\n            [sg.T(conf.main('Version') + conf.config('Version'))]\n        ]\n        operation_at_bottom = sg.pin(sg.Column([[sg.T(conf.main('Business'), font='Default 12', pad=(0, 0)),\n                                                 sg.T(conf.main('Email') + conf.config('Email') + '  ',\n                                                      font='Default 12', pad=(0, 0)),\n                                                 sg.T(conf.main('RedBook') + conf.config('RedBook') + '  ',\n                                                      font='Default 12',\n                                                      pad=(0, 0))]],\n                                               pad=(0, 0), k='-OPTIONS BOTTOM-', expand_x=True, expand_y=False),\n                                     expand_x=True, expand_y=False)\n        self.tl.select_translator = get_cache('PLATFORM_TYPES', 'baidu')\n        self.tl.set_from_lang(get_cache('INPUT_TYPES'))\n        self.tl.set_to_lang(get_cache('OUTPUT_TYPES'))\n        self.tl.check_select_language()\n        lgs1 = self.tl.get_languages()\n        lgs1.insert(0, '自动') if is_zh_language() else lgs1.insert(0, 'auto')\n        choose_type_at_top = sg.pin(\n            # sg.user_settings_get_entry('INPUT_TYPES', lgs1)\n            sg.Column([[sg.Combo(values=lgs1, default_value=self.tl.select_from_lang, size=(50, 30),\n                                 key='IN_TYPE', enable_events=True, readonly=True),\n                        # sg.Combo(sg.user_settings_get_entry('OUTPUT_TYPES', lgs2),\n                        sg.Combo(values=self.tl.get_languages(), default_value=self.tl.select_to_lang,\n                                 size=(50, 30), key='OUT_TYPE', enable_events=True, readonly=True),\n                        # sg.Combo(sg.user_settings_get_entry('PLATFORM_TYPES', tls),\n                        sg.Combo(values=self.tl.get_translators(), default_value=self.tl.select_translator,\n                                 size=(20, 30), key='PLATFORM_TYPE', enable_events=True, readonly=True)\n                        ]], pad=(0, 0), k='-FOLDER CHOOSE-'))\n\n        # ----- Full layout -----\n\n        layout = [\n            [sg.Text(conf.main('Description'), font='Any 15', pad=(0, 5))],\n            [choose_type_at_top],\n            [sg.Pane(\n                [sg.Column([[left_col]], element_justification='l', expand_x=True, expand_y=True),\n                 sg.Column(right_col, element_justification='c', expand_x=True, expand_y=True)], orientation='h',\n                relief=sg.RELIEF_SUNKEN, expand_x=True, expand_y=True, k='-PANE-')],\n            [operation_at_bottom, sg.Sizegrip()]]\n\n        # --------------------------------- Create Window ---------------------------------\n        window = sg.Window(conf.main('Title'), layout, finalize=True, resizable=True, use_default_focus=False)\n        window.set_min_size(window.size)\n\n        window.bind('<F1>', 'Exit')\n        # window.bind(\"<Enter>\", 'Enter')\n        window['IN_TEXT'].bind('<Return>', ' Return')\n        self.advanced_ui(window)\n\n        window.bring_to_front()\n        return window\n\n    def show(self):\n        \"\"\"\n            The main program that contains the event loop.\n            It will call the make_window function to create the window.\n            \"\"\"\n        global python_only\n        # icon = sg.EMOJI_BASE64_HAPPY_WINK\n        icon = conf.config('Logo').encode('utf-8')\n        # icon = os.path.join(os.path.dirname(__file__), 'doc', 'logo.ico')\n        # sg.user_settings_filename('psgdemos.json')\n        sg.set_options(icon=icon)\n        window = self.make_window()\n        window.force_focus()\n        counter = 0\n\n        while True:\n            event, values = window.read()\n            # print(event, values)\n\n            counter += 1\n            if event in (sg.WINDOW_CLOSED, conf.main(Key.M_EXIT)):\n                break\n            elif event == conf.main(Key.M_SETTINGS):\n                change, restart = settings_show()\n                if change:  # settings 可能更改的内容：主题，代理，高级\n                    if restart:\n                        window.close()\n                        window = self.make_window()\n                    else:\n                        self.advanced_ui(window)\n\n            elif event == conf.main(Key.M_RUN) or event == 'IN_TEXT Return':  # IN_TEXT 的回车键监听, 需要从input获取到数据，然后进行翻译\n                sg.threading.Thread(target=self.tl.translate, args=(window, window['IN_TEXT'].get(), False),\n                                    daemon=True).start()\n                loading_show()\n                # result = ts.translate_text(text, translator='deepl', from_language='zh', to_language='en')\n                # window['OUT_TEXT'].update(result)\n                # pw.close()\n            elif event == conf.main(Key.M_CLEAR):\n                window['IN_TEXT'].update('')\n                window['OUT_TEXT'].update('')\n            elif event == conf.main(Key.M_COPY):\n                sg.clipboard_set(window['OUT_TEXT'].get())\n            elif event == '_FILEBROWSE_':\n                file_path: str = values['_FILEBROWSE_']\n                # 不判断了，能解析就用\n                # if file_path.endswith('.txt') or file_path.endswith('.html'):\n                content = read(file_path)\n                window['IN_TEXT'].update(file_path)\n                sg.threading.Thread(target=self.tl.translate,\n                                    args=(window, content, file_path.endswith('.html'), file_path), daemon=True).start()\n                loading_show()\n            elif event == 'IN_TYPE':  # 输入\n                type1 = values['IN_TYPE']\n                self.tl.select_from_lang = type1\n                get_cache('INPUT_TYPES', type1)\n            elif event == 'OUT_TYPE':  # 输出\n                type1 = values['OUT_TYPE']\n                self.tl.select_to_lang = type1\n                save_cache('OUTPUT_TYPES', type1)\n            elif event == 'PLATFORM_TYPE':  # 选择平台后，更新输入输出的可选\n                type1 = values['PLATFORM_TYPE']\n                self.tl.select_translator = type1\n                save_cache('PLATFORM_TYPES', type1)\n                window.close()\n                window = self.make_window()\n            elif event == 'Version':\n                sg.popup_scrolled(sg.get_versions(), keep_on_top=True, non_blocking=True)\n            elif event == Key.PROXY_ENABLE:  # 高级时在本窗口开启或关闭代理，但是需要在设置中设置代理地址\n                proxy_enable = values[Key.PROXY_ENABLE]\n                save_cache(Key.PROXY_ENABLE, proxy_enable)\n                window[Key.PROXY_ENABLE].update(value=proxy_enable)\n        window.close()\n\n\nif __name__ == '__main__':\n    MainWin().show()\n"
  },
  {
    "path": "009-Translate/tran_test.py",
    "content": "#!/usr/bin/python3.9\n# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/5/18 14:38\n@Author  : xhunmon\n@Email   : xhunmon@126.com\n@File    : tran_test.py\n@Desc    : \n\"\"\"\nimport re\n\nfrom load_srt import Translator\nimport time\nfrom utils import *\n\n\ndef t_test(srt_source):\n    sub_start = '00:02:00'\n    v_start = '00:00:17'\n\n\ndef translate_callback(status, **kwargs):\n    print(status)\n    print(kwargs[\"src\"])\n    print(kwargs[\"dst\"])\n    print(kwargs[\"msg\"])\n\n\nif __name__ == '__main__':\n    tl = Translator()\n    tl.add_callback(translate_callback)\n    try:\n        tl.translate_file(\"output/test.srt\", \"output/test_out.srt\")\n    except Exception as e:\n        print(e)\n"
  },
  {
    "path": "009-Translate/ui.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n@Author  : Xhunmon \n@Time    : 2023/4/22 22:31\n@FileName: ui.py\n@desc: \n\"\"\"\nfrom config import *\n\n\ndef loading_show():\n    LoadingWin.is_loading = True\n    layout = [[sg.Text(conf.loading('Content'), font='ANY 15')],\n              [sg.Image(data=conf.config('Loading').encode('utf-8'), key='_IMAGE_')],\n              [sg.Button(conf.loading('Cancel'))]\n              ]\n    window = sg.Window('').Layout(layout)\n    while LoadingWin.is_loading:  # Event Loop\n        event, values = window.Read(timeout=25)\n        if event in (None, 'Exit', conf.loading('Cancel')):\n            break\n        window.Element('_IMAGE_').UpdateAnimation(conf.config('Loading').encode('utf-8'), time_between_frames=50)\n    window.close()\n\n\nclass LoadingWin(object):\n    \"\"\"\n    loading dialog\n    \"\"\"\n    is_loading = True\n\n\ndef settings_show():\n    \"\"\"\n    Show the settings window.\n    This is where the folder paths and program paths are set.\n    Returns True if settings were changed\n\n    :return: True if settings were changed\n    :rtype: (bool)\n    \"\"\"\n    global_theme = get_theme()\n    proxy_enable = get_cache(Key.PROXY_ENABLE, False)\n    translate_enable = get_cache(Key.FULL_TRANSLATE, False)\n    restart_enable = get_cache(Key.RESTART_WINDOW, True)\n\n    layout = [\n        [sg.T('  ', font='_ 16', size=(40, 1))],\n        [sg.T(conf.settings('Proxy'), font='_ 16')],\n        [sg.CB(conf.settings('ProxyEnable'), default=proxy_enable, enable_events=True, k=Key.PROXY_ENABLE,\n               font='_ 12')],\n        [sg.Column([[sg.Input(size=(38, 1), default_text=get_cache(Key.PROXY_INPUT, ''), key=Key.PROXY_INPUT),\n                     sg.T(conf.settings('ProxyDesc'), font='_ 12')]],\n                   expand_x=True, expand_y=True, k=Key.PROXY_LAYOUT, visible=proxy_enable)],\n        [sg.T(conf.settings('Theme'), font='_ 16')],\n        [sg.T(conf.settings('ThemeDesc'), font='_ 11'), sg.T(global_theme, font='_ 13')],\n        [sg.Combo([''] + sg.theme_list(), get_cache(Key.THEME, ''), readonly=True, k=Key.THEME)],\n        [sg.CB(conf.settings('Advanced'), default=get_cache(Key.ADVANCED_MODE, True), font='_ 12',\n               k=Key.ADVANCED_MODE)],\n        [sg.CB(conf.settings('FullTranslate'), visible=False, default=translate_enable, font='_ 12',\n               k=Key.ADVANCED_MODE)],\n        [sg.CB(conf.settings('Restart'), default=restart_enable, enable_events=True, k=Key.RESTART_WINDOW,\n               font='_ 12')],\n        [sg.B(conf.settings('Ok'), bind_return_key=True), sg.B(conf.settings('Cancel')), sg.B(conf.settings('Reset'))],\n    ]\n\n    window = sg.Window(conf.settings('Title'), layout, finalize=True)\n    settings_changed = False\n\n    while True:\n        event, values = window.read()\n        if event in (conf.settings('Cancel'), sg.WIN_CLOSED):\n            break\n        if event == conf.settings('Ok'):\n            save_cache(Key.THEME, values[Key.THEME])\n            save_cache(Key.ADVANCED_MODE, values[Key.ADVANCED_MODE])\n            save_cache(Key.PROXY_ENABLE, proxy_enable)\n            save_cache(Key.FULL_TRANSLATE, translate_enable)\n            save_cache(Key.PROXY_INPUT, window[Key.PROXY_INPUT].get())\n            save_cache(Key.RESTART_WINDOW, window[Key.RESTART_WINDOW].get())\n            settings_changed = True\n            break\n        elif event == conf.settings('Reset'):  # 恢复所有默认设置\n            save_cache(Key.THEME, '')\n            save_cache(Key.ADVANCED_MODE, False)\n            save_cache(Key.PROXY_ENABLE, False)\n            save_cache(Key.FULL_TRANSLATE, False)\n            save_cache(Key.RESTART_WINDOW, True)\n            save_cache(Key.PROXY_INPUT, '')\n            settings_changed = True\n            break\n        elif event == Key.PROXY_ENABLE:\n            proxy_enable = values[Key.PROXY_ENABLE]\n            window[Key.PROXY_ENABLE].update(value=proxy_enable)\n            window[Key.PROXY_LAYOUT].update(visible=proxy_enable)\n        elif event == Key.FULL_TRANSLATE:\n            translate_enable = values[Key.FULL_TRANSLATE]\n            window[Key.FULL_TRANSLATE].update(value=translate_enable)\n        elif event == Key.RESTART_WINDOW:\n            restart_enable = values[Key.RESTART_WINDOW]\n            window[Key.RESTART_WINDOW].update(value=restart_enable)\n\n    window.close()\n    return settings_changed, restart_enable\n"
  },
  {
    "path": "009-Translate/utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\n@Author  : Xhunmon \n@Time    : 2023/4/22 21:54\n@FileName: utils.py\n@desc: \n\"\"\"\nimport PySimpleGUI as sg\n\n\ndef get_cache(key: str, default=None):\n    \"\"\"\n    获取本地的数据\n    :param key:\n    :param default:\n    :return:\n    \"\"\"\n    cache = sg.user_settings_get_entry(key, default)\n    if cache is None or cache == '':\n        return default\n    return cache\n\n\ndef save_cache(key: str, value):\n    \"\"\"\n    将数据保存到本地\n    :param key:\n    :param value:\n    :return:\n    \"\"\"\n    sg.user_settings_set_entry(key, value)\n\n\ndef read(file_path) -> str:\n    '''读取txt文本内容'''\n    content = None\n    try:\n        with open(file_path, 'r', encoding='utf-8') as f:\n            content = f.read()\n            f.close()\n    except Exception as e:\n        print(e)\n    return content\n\n\ndef write(content, file_path):\n    '''写入txt文本内容'''\n    try:\n        with open(file_path, mode='w', encoding='utf-8') as f:\n            f.write(content)\n    except Exception as e:\n        print(e)\n\n\ndef get_theme():\n    \"\"\"\n    Get the theme to use for the program\n    Value is in this program's user settings. If none set, then use PySimpleGUI's global default theme\n    :return: The theme\n    :rtype: str\n    \"\"\"\n    theme = get_cache(Key.THEME, '')\n    if theme == '':\n        theme = sg.OFFICIAL_PYSIMPLEGUI_THEME  # 默认主题\n    return theme\n\n\nclass Key:\n    \"\"\"\n    统一管理本地字符串\n    \"\"\"\n    PROXY_ENABLE = 'proxy_enable'  # settings\n    PROXY_LAYOUT = 'proxy_layout'  # settings\n    PROXY_INPUT = 'proxy_input'  # settings\n    THEME = 'theme'  # settings\n    RESTART_WINDOW = 'restart_window'  # settings\n    ADVANCED_MODE = 'advanced_mode'  # settings\n    FULL_TRANSLATE = 'full_translate'  # settings\n    LANGUAGE = 'language'  # config\n\n    # main\n    M_CLEAR = 'Clear'\n    M_RUN = 'Run'\n    M_COPY = 'Copy'\n    M_FILE = 'File'\n    M_SETTINGS = 'Settings'\n    M_EXIT = 'Exit'\n"
  },
  {
    "path": "010-YouTubeUpload/README.md",
    "content": "# YouTube uploader 自动上传\n\n因为使用API上传是有限制的，每天只能上传6条。因此得使用浏览器操作\n\n## 前提\n\n1. Chome浏览器，同时需要配置内核，可参考以下文章\n\n> selenium复用已打开浏览器：https://developer.aliyun.com/article/1121356\n> 下载chromedriver：https://chromedriver.storage.googleapis.com/index.html?path=112.0.5615.49/\n> 下载安装:https://blog.51cto.com/u_15295315/3042408\n\n\n2. 通过配置启动debug模式浏览器，然后登录\n\n\n## 备注\n\n脚本目前还不完善，因此有bug，因比较忙，不保证能更新。 "
  },
  {
    "path": "010-YouTubeUpload/main.py",
    "content": "#!/usr/bin/python3.9\n# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/5/22 11:44\n@Author  : xhunmon\n@Email   : xhunmon@126.com\n@File    : main.py\n@Desc    : 使用案例\n\"\"\"\nimport sys\n\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\nfrom youtube import *\n\nif __name__ == '__main__':\n    meta = {\n        \"title\": '标题',\n        \"description\": \"描述内容\",\n        \"tags\": ['标签1', '标签2', '标签3'],\n        \"edit\": False,\n        \"playlist_title\": '播放列表1',\n        \"schedule\": \"\"\n    }\n\n    executable_path = \"chromedriver\"\n    options = Options()\n    # TODO - 以下配置是为了打开现有的浏览器，公用cookie，安全有效，需要提起配置。\n    if sys.platform == 'linux':\n        print(\"Current OS is Linux.\")\n    elif sys.platform == 'darwin':\n        print(\"Current OS is mac OS.\")\n        executable_path = '/usr/local/bin/chromedriver'\n        options.debugger_address = 'localhost:9222'\n    elif sys.platform == 'win32':\n        print(\"Current OS is Windows.\")\n\n    __g_browser = webdriver.Chrome(executable_path=executable_path, chrome_options=options)\n    __g_browser.implicitly_wait(10)  # 设置查找运算智能等待超时时间\n\n    yt = YtbUploader()\n    yt.upload(__g_browser, 'xxx/xxx.mp4', options, None)\n\n# See PyCharm help at https://www.jetbrains.com/help/pycharm/\n"
  },
  {
    "path": "010-YouTubeUpload/tst.py",
    "content": "#!/usr/bin/python3.9\n# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/5/22 11:44\n@Author  : xhunmon\n@Email   : xhunmon@126.com\n@File    : tst.py\n@Desc    : \n\"\"\"\n"
  },
  {
    "path": "010-YouTubeUpload/youtube.py",
    "content": "#!/usr/bin/python3.9\n# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/5/9 09:10\n@Author  : xhunmon\n@Email   : xhunmon@126.com\n@File    : youtube.py\n@Desc    :\n1. 关闭所有浏览器：\n2. 命令格式：浏览器名称 --remote-debugging-port=端口号\n例：\nwindows：chrome --remote-debugging-port=9222\nmac：/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222\n\"\"\"\n\nimport logging\nimport platform\nimport time\nfrom datetime import datetime\nfrom typing import Optional, Tuple\n\nfrom selenium.webdriver import Chrome\nfrom selenium.webdriver.chrome.options import Options\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n\nlogging.basicConfig()\n\n\nclass YtbConst:\n    \"\"\"A class for storing constants for YoutubeUploader class\"\"\"\n    YOUTUBE_URL = 'https://www.youtube.com'\n    YOUTUBE_STUDIO_URL = 'https://studio.youtube.com'\n    YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload'\n    USER_WAITING_TIME = 1\n    VIDEO_TITLE = 'title'\n    VIDEO_DESCRIPTION = 'description'\n    VIDEO_EDIT = 'edit'\n    VIDEO_TAGS = 'tags'\n    TEXTBOX_ID = 'textbox'\n    TEXT_INPUT = 'text-input'\n    RADIO_LABEL = 'radioLabel'\n    UPLOADING_STATUS_CONTAINER = '//*[@id=\"dialog\"]/div[2]/div/ytcp-video-upload-progress/span'\n    NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK'\n\n    UPLOAD_DIALOG = '//ytcp-uploads-dialog'\n    ADVANCED_BUTTON_ID = 'toggle-button'\n    TAGS_CONTAINER_ID = 'tags-container'\n\n    TAGS_INPUT = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-ve/ytcp-video-metadata-editor/div/ytcp-video-metadata-editor-advanced/div[4]/ytcp-form-input-container/div[1]/div/ytcp-free-text-chip-bar/ytcp-chip-bar/div/input'\n    NEXT_BUTTON = '//*[@id=\"next-button\"]/div'\n    PUBLIC_BUTTON = '//*[@id=\"done-button\"]/div'\n    VIDEO_URL_CONTAINER = \"//span[@class='video-url-fadeable style-scope ytcp-video-info']\"\n    VIDEO_URL_ELEMENT = '//*[@id=\"share-url\"]'\n    HREF = 'href'\n    ERROR_CONTAINER = '//*[@id=\"error-message\"]'\n    VIDEO_NOT_FOUND_ERROR = 'Could not find video_id'\n    DONE_BUTTON = 'done-button'\n    INPUT_FILE_VIDEO = \"//input[@type='file']\"\n    INPUT_FILE_THUMBNAIL = \"//input[@id='file-loader']\"\n\n    # Playlist\n    VIDEO_PLAYLIST = 'playlist_title'\n    PL_DROPDOWN_CLASS = 'ytcp-video-metadata-playlists'\n    PL_SEARCH_INPUT_ID = 'search-input'\n    PL_ITEMS_CONTAINER_ID = 'items'\n    PL_ITEM_CONTAINER = '//span[text()=\"{}\"]'\n    PL_NEW_BUTTON_CLASS = 'new-playlist-button'\n    PL_CREATE_PLAYLIST_CONTAINER_XPATH = '//*[@id=\"text-item-0\"]/ytcp-ve'\n    PL_CREATE_BUTTON_XPATH = '//*[@id=\"create-button\"]/div'\n    PL_DONE_BUTTON_CLASS = 'done-button'\n\n    # Schedule 发布时间\n    VIDEO_SCHEDULE = 'schedule'\n    SCHEDULE_CONTAINER_ID = 'schedule-radio-button'\n    SCHEDULE_DATE_ID = 'datepicker-trigger'\n    SCHEDULE_DATE_TEXTBOX = '/html/body/ytcp-date-picker/tp-yt-paper-dialog/div/form/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input'\n    SCHEDULE_TIME = \"/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-review/div[2]/div[1]/ytcp-video-visibility-select/div[3]/ytcp-visibility-scheduler/div[1]/ytcp-datetime-picker/div/div[2]/form/ytcp-form-input-container/div[1]/div/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input\"\n\n\nclass YtbUploader:\n    \"\"\"\n    YouTube上传\n    \"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"\n\n        @param video_path:    视频路径\n        @param metadata_json: 字典数据\n                            meta = {\n                                \"title\": \"标题\",\n                                \"description\": \"描述\",\n                                \"tags\": [\"标签1\",\"标签2\"],\n                                \"edit\": False,\n                                \"playlist_title\": \"分栏标题\",\n                                \"schedule\": \"\"\n                        }\n        @param new_window:     在现有的的浏览器中打开新的页面，没做登录。需要配置先启动浏览器。\n        @param thumbnail_path:\n        \"\"\"\n        self.video_path = None\n        self.thumbnail_path = None\n        self.metadata_dict = None\n        self.browser: Chrome = None\n        self.logger = logging.getLogger(__name__)\n        self.logger.setLevel(logging.DEBUG)\n\n        self.is_mac = False\n        if not any(os_name in platform.platform() for os_name in [\"Windows\", \"Linux\"]):\n            self.is_mac = True\n\n    def __login(self):\n        self.browser.get(YtbConst.YOUTUBE_URL)\n        time.sleep(YtbConst.USER_WAITING_TIME)\n\n    def __clear_field(self, field):\n        field.click()\n        time.sleep(YtbConst.USER_WAITING_TIME)\n        if self.is_mac:\n            field.send_keys(Keys.COMMAND + 'a')\n        else:\n            field.send_keys(Keys.CONTROL + 'a')\n        time.sleep(YtbConst.USER_WAITING_TIME)\n        field.send_keys(Keys.BACKSPACE)\n\n    def __write_in_field(self, field, string, select_all=False):\n        if select_all:\n            self.__clear_field(field)\n        else:\n            field.click()\n            time.sleep(YtbConst.USER_WAITING_TIME)\n\n        field.send_keys(string)\n\n    def __get_video_id(self) -> Optional[str]:\n        video_id = None\n        try:\n            video_url_element = self.browser.find_element(By.XPATH, YtbConst.VIDEO_URL_ELEMENT)\n            video_id = video_url_element.get_attribute(\n                YtbConst.HREF).split('/')[-1]\n        except:\n            self.logger.warning(YtbConst.VIDEO_NOT_FOUND_ERROR)\n            pass\n        return video_id\n\n    def upload(self, browser, path, meta, thumbnail=None):\n        self.browser = browser\n        self.video_path = path\n        self.thumbnail_path = thumbnail\n        self.metadata_dict = meta\n        # self.__login()\n        return self.__upload()\n\n    def exit(self):\n        self.browser.close()\n\n    def __wait_loading(self):\n        count = 1\n        while count <= 10:\n            time.sleep(YtbConst.USER_WAITING_TIME)\n            try:\n                uploading_status_container = self.browser.find_element(By.XPATH,\n                                                                       YtbConst.UPLOADING_STATUS_CONTAINER)\n                uploading_progress = uploading_status_container.text\n                self.logger.debug('Upload video progress: {}'.format(uploading_progress))\n                if uploading_progress is None or len(uploading_progress) < 5:\n                    break\n            except:\n                self.logger.debug('Upload loading trying {}'.format(count))\n                count += 1\n\n    def __upload(self) -> Tuple[bool, Optional[str]]:\n        edit_mode = self.metadata_dict[YtbConst.VIDEO_EDIT]\n        if edit_mode:\n            self.browser.get(edit_mode)\n            time.sleep(YtbConst.USER_WAITING_TIME)\n        else:\n            # 切换到最新的开的页面，然后在该页面直接加载地址\n            self.browser.switch_to.window(self.browser.window_handles[0])\n            self.browser.get(YtbConst.YOUTUBE_URL)\n            time.sleep(YtbConst.USER_WAITING_TIME)\n            self.browser.get(YtbConst.YOUTUBE_UPLOAD_URL)\n            time.sleep(YtbConst.USER_WAITING_TIME)\n            absolute_video_path = self.video_path\n            self.browser.find_element(By.XPATH, YtbConst.INPUT_FILE_VIDEO).send_keys(\n                absolute_video_path)\n            self.logger.debug('Attached video {}'.format(self.video_path))\n\n            # Find status container\n            # self.__wait_loading()\n            time.sleep(YtbConst.USER_WAITING_TIME * 2)\n\n        if self.thumbnail_path is not None:\n            absolute_thumbnail_path = self.thumbnail_path\n            self.browser.find_element(By.XPATH, YtbConst.INPUT_FILE_THUMBNAIL).send_keys(\n                absolute_thumbnail_path)\n            change_display = \"document.getElementById('file-loader').style = 'display: block! important'\"\n            self.browser.execute_script(change_display)\n            self.logger.debug(\n                'Attached thumbnail {}'.format(self.thumbnail_path))\n\n        textboxs = self.browser.find_elements(By.ID, YtbConst.TEXTBOX_ID)\n        title_field, description_field = textboxs[0], textboxs[1]\n        # //*[@id=\"textbox\"]\n        self.__write_in_field(\n            title_field, self.metadata_dict[YtbConst.VIDEO_TITLE], select_all=True)\n        self.logger.debug('The video title was set to \\\"{}\\\"'.format(\n            self.metadata_dict[YtbConst.VIDEO_TITLE]))\n\n        video_description = self.metadata_dict[YtbConst.VIDEO_DESCRIPTION]\n        video_description = video_description.replace(\"\\n\", Keys.ENTER)\n        if video_description:\n            self.__write_in_field(description_field, video_description, select_all=True)\n            self.logger.debug('Description filled.')\n\n        kids_section = self.browser.find_element(By.NAME, YtbConst.NOT_MADE_FOR_KIDS_LABEL)\n        kids_section.location_once_scrolled_into_view\n        time.sleep(YtbConst.USER_WAITING_TIME)\n\n        self.browser.find_element(By.ID, YtbConst.RADIO_LABEL).click()\n        self.logger.debug('Selected \\\"{}\\\"'.format(YtbConst.NOT_MADE_FOR_KIDS_LABEL))\n\n        # Playlist\n        playlist = self.metadata_dict[YtbConst.VIDEO_PLAYLIST]\n        if playlist:\n            self.browser.find_element(By.CLASS_NAME, YtbConst.PL_DROPDOWN_CLASS).click()  # 点击播放列表\n            time.sleep(YtbConst.USER_WAITING_TIME)\n            self.logger.debug('Playlist xpath: \"{}\".'.format(YtbConst.PL_ITEM_CONTAINER.format(playlist)))\n            try:\n                playlist_item = self.browser.find_element(By.XPATH, YtbConst.PL_ITEM_CONTAINER.format(playlist))\n            except:\n                playlist_item = None\n            if playlist_item:\n                self.logger.debug('Playlist found.')\n                playlist_item.click()\n                time.sleep(YtbConst.USER_WAITING_TIME)\n            else:\n                self.logger.debug('Playlist not found. Creating')\n                # self.__clear_field(search_field)\n                time.sleep(YtbConst.USER_WAITING_TIME)\n\n                new_playlist_button = self.browser.find_element(By.CLASS_NAME, YtbConst.PL_NEW_BUTTON_CLASS)\n                new_playlist_button.click()\n\n                self.browser.find_element(By.XPATH, YtbConst.PL_CREATE_PLAYLIST_CONTAINER_XPATH).click()\n                playlist_title_textbox = self.browser.find_element(By.XPATH,\n                                                                   '/html/body/ytcp-playlist-creation-dialog/ytcp-dialog/tp-yt-paper-dialog/div[2]/div/ytcp-playlist-metadata-editor/div/div[1]/ytcp-social-suggestions-textbox/ytcp-form-input-container/div[1]/div[2]/div/ytcp-social-suggestion-input/div')\n                self.__write_in_field(playlist_title_textbox, playlist)\n\n                # //*[@id=\"textbox\"]\n                time.sleep(YtbConst.USER_WAITING_TIME)\n                # //*[@id=\"create-button\"]/div   //*[@id=\"dialog\"]/div[3]/div/ytcp-button[1]/div\n                create_playlist_button = self.browser.find_element(By.XPATH, YtbConst.PL_CREATE_BUTTON_XPATH)\n                create_playlist_button.click()\n                time.sleep(YtbConst.USER_WAITING_TIME)\n\n            time.sleep(YtbConst.USER_WAITING_TIME * 2)\n            done_button = self.browser.find_element(By.CLASS_NAME, YtbConst.PL_DONE_BUTTON_CLASS)\n            self.browser.execute_script(\"arguments[0].click();\", done_button)  # js注入实现点击\n            # done_button.click()\n\n        # Advanced options\n        time.sleep(YtbConst.USER_WAITING_TIME)\n        self.browser.find_element(By.ID, YtbConst.ADVANCED_BUTTON_ID).click()\n        self.logger.debug('Clicked MORE OPTIONS')\n        time.sleep(YtbConst.USER_WAITING_TIME)\n\n        # Tags\n        tags = self.metadata_dict[YtbConst.VIDEO_TAGS]\n        if tags:\n            # tags_container = self.browser.find_element(By.ID, Constant.TAGS_CONTAINER_ID)\n            self.browser.find_element(By.XPATH, '//*[@id=\"toggle-button\"]/div')  # 展开\n            tags_field = self.browser.find_element(By.XPATH, YtbConst.TAGS_INPUT)\n            self.__write_in_field(tags_field, ','.join(tags))\n            self.logger.debug('The tags were set to \\\"{}\\\"'.format(tags))\n\n        self.browser.find_element(By.XPATH, YtbConst.NEXT_BUTTON).click()\n        self.logger.debug('Clicked {} one'.format(YtbConst.NEXT_BUTTON))\n        time.sleep(YtbConst.USER_WAITING_TIME)\n        self.browser.find_element(By.XPATH, YtbConst.NEXT_BUTTON).click()\n        self.logger.debug('Clicked {} two'.format(YtbConst.NEXT_BUTTON))\n        time.sleep(YtbConst.USER_WAITING_TIME)\n        self.browser.find_element(By.XPATH, YtbConst.NEXT_BUTTON).click()\n        self.logger.debug('Clicked {} three'.format(YtbConst.NEXT_BUTTON))\n        time.sleep(YtbConst.USER_WAITING_TIME)\n\n        schedule = self.metadata_dict[YtbConst.VIDEO_SCHEDULE]\n        if schedule:  # 发布时间，暂不设置\n            upload_time_object = datetime.strptime(schedule, \"%m/%d/%Y, %H:%M\")\n            self.browser.find_element(By.ID, YtbConst.SCHEDULE_CONTAINER_ID).click()\n            self.browser.find_element(By.ID, YtbConst.SCHEDULE_DATE_ID).click()\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_DATE_TEXTBOX).clear()\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_DATE_TEXTBOX).send_keys(\n                datetime.strftime(upload_time_object, \"%b %e, %Y\"))\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_DATE_TEXTBOX).send_keys(Keys.ENTER)\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).click()\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).clear()\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).send_keys(\n                datetime.strftime(upload_time_object, \"%H:%M\"))\n            self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).send_keys(Keys.ENTER)\n            self.logger.debug(f\"Scheduled the video for {schedule}\")\n        else:\n            self.browser.find_element(By.XPATH, YtbConst.PUBLIC_BUTTON).click()\n            self.logger.debug('Made the video {}'.format(YtbConst.PUBLIC_BUTTON))\n\n        # Check status container and upload progress\n        self.__wait_loading()\n        self.logger.debug('Upload container gone.')\n\n        video_id = self.__get_video_id()\n\n        # done_button = self.browser.find_element(By.ID, Constant.DONE_BUTTON)\n        #\n        # # Catch such error as\n        # # \"File is a duplicate of a video you have already uploaded\"\n        # if done_button.get_attribute('aria-disabled') == 'true':\n        #     error_message = self.browser.find_element(By.XPATH, Constant.ERROR_CONTAINER).text\n        #     self.logger.error(error_message)\n        #     return False, None\n        #\n        # done_button.click()\n        self.logger.debug(\n            \"Published the video with video_id = {}\".format(video_id))\n        time.sleep(YtbConst.USER_WAITING_TIME)\n        # self.browser.get(Constant.YOUTUBE_URL)\n        return True, video_id\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "README.md",
    "content": "学习python最好的方式就是通过不断的实践！\n项目地址：[https://github.com/xhunmon/PythonIsTools](https://github.com/xhunmon/PythonIsTools) \n\n\n# 实践项目\n\n- [001-Downloader：抖音、快手音视频下载器](./001-Downloader) ——已完成\n\n- [002-v2ray代理池：爬取vmess、ss、trojan协议节点，进行校验，自更新](./002-V2rayPool) ——已完成\n\n- [003-Keywords：获取相关关键词以及其google趋势](./003-Keywords) ——已完成\n\n- [004-EmailNotify：监听虚拟币变化，使用邮箱通知](./004-EmailNotify) ——已完成\n\n- [005-PaidSource：这些脚本你肯定会有用到的](./005-PaidSource) ——已完成\n\n- [006-TikTok：App自动化](./006-TikTok) ——已完成\n\n- [007-CutVideoAudio：自媒体运营之视频剪辑，新增V2版本命令](./007-CutVideoAudio) ——已完成\n\n- [008-ChatGPT-UI：About A very useful ChatGPT 3.5/5 Tools with OpenAI API. 一个非常实用的聊天工具](https://github.com/xhunmon/iMedia) ——已完成\n\n- [009-多平台，多语言，支持文本、文件、srt字幕文件翻译](./009-Translate) ——已完成\n\n- [010-YouTube视频上传脚本](./010-YouTubeUpload) ——已完成\n\n----------\n\n##### 声明：本项目仅用于学习交流，禁止任何商业用途，违者自承担相应法律责任！\n"
  }
]