Repository: xhunmon/PythonIsTools Branch: main Commit: 6354d2d442a0 Files: 158 Total size: 555.3 KB Directory structure: gitextract_xtly8u7o/ ├── .gitignore ├── 001-Downloader/ │ ├── .gitignore │ ├── README.md │ ├── __init__.py │ ├── config.ini │ ├── doc/ │ │ ├── mac-sh/ │ │ │ ├── main.spec │ │ │ └── pyinstaller.sh │ │ └── win-sh/ │ │ ├── main.spec │ │ └── pyinstaller.sh │ ├── douyin/ │ │ └── dy_download.py │ ├── downloader.py │ ├── kuaishou/ │ │ └── ks_download.py │ ├── main.py │ ├── test/ │ │ ├── bilibili_video_download_v1.py │ │ ├── ff_video.py │ │ ├── test_pyinstaller.py │ │ ├── urls.txt │ │ └── xhs_download.py │ ├── type_enum.py │ ├── ui.py │ └── utils.py ├── 002-V2rayPool/ │ ├── .gitignore │ ├── 002-V2rayPool.iml │ ├── README.md │ ├── base/ │ │ └── net_proxy.py │ ├── core/ │ │ ├── client.py │ │ ├── conf.py │ │ ├── group.py │ │ ├── json_template/ │ │ │ ├── client.json │ │ │ ├── client_socks.json │ │ │ ├── client_ss.json │ │ │ ├── client_trojan.json │ │ │ ├── dyn_port.json │ │ │ ├── http.json │ │ │ ├── http2.json │ │ │ ├── kcp.json │ │ │ ├── mtproto.json │ │ │ ├── quic.json │ │ │ ├── server.json │ │ │ ├── socks.json │ │ │ ├── ss.json │ │ │ ├── stats_settings.json │ │ │ ├── tcp.json │ │ │ ├── vless.json │ │ │ └── ws.json │ │ ├── profile.py │ │ └── utils.py │ ├── db/ │ │ ├── db_main.py │ │ ├── local.py │ │ └── net.py │ ├── doc/ │ │ └── (参考用)config.json │ └── test_main.py ├── 003-Keywords/ │ ├── .gitignore │ ├── Necklace/ │ │ └── Necklace.xlsx │ ├── README.md │ ├── __init__.py │ ├── amazon/ │ │ ├── items.py │ │ ├── middlewares.py │ │ ├── pipelines.py │ │ ├── settings.py │ │ └── spiders/ │ │ ├── __init__.py │ │ ├── alibaba.py │ │ ├── amazon.py │ │ ├── checkip.py │ │ └── spiders.py │ ├── google.py │ ├── main.py │ ├── mypytrends/ │ │ ├── __init__.py │ │ ├── dailydata.py │ │ ├── exceptions.py │ │ ├── request.py │ │ └── test_trendReq.py │ ├── run_api.py │ ├── scrapy.cfg │ ├── test_souce1.xlsx │ ├── test_source.xlsx │ ├── v2ray_pool/ │ │ ├── __init__.py │ │ ├── _db-checked.txt │ │ └── _db-uncheck.txt │ ├── v2ray_util.py │ └── women-ring/ │ └── women ring.csv ├── 004-EmailNotify/ │ ├── .gitignore │ ├── README.md │ └── main.py ├── 005-PaidSource/ │ ├── .gitignore │ ├── 005-PaidSource.iml │ ├── README.md │ ├── __init__.py │ ├── chrome.py │ ├── ff_video.py │ ├── file_util.py │ ├── gsearch.py │ ├── gtransfer.py │ ├── kaoqin.py │ ├── keywords.py │ ├── main.py │ ├── other_site.py │ ├── v2ray_pool/ │ │ ├── __init__.py │ │ ├── _db-checked.txt │ │ └── _db-uncheck.txt │ └── v2ray_util.py ├── 006-TikTok/ │ ├── .gitignore │ ├── 006-TikTok.iml │ ├── README.md │ ├── __init__.py │ ├── dy_review.py │ ├── file_util.py │ ├── google_transfer_by_excel.py │ ├── img_2_webp.py │ ├── main.py │ ├── post_autotk.py │ ├── tikstar.py │ ├── tt_review.py │ └── v2ray_pool/ │ └── __init__.py ├── 007-CutVideoAudio/ │ ├── .gitignore │ ├── 007-CutVideoAudio.iml │ ├── README.md │ ├── __init__.py │ ├── config.ini │ ├── doc/ │ │ └── mac-sh/ │ │ ├── main.spec │ │ └── pyinstaller.sh │ ├── editors.py │ ├── ff_cut.py │ ├── ff_util.py │ ├── ff_util_v2.py │ ├── main.py │ ├── type_enum.py │ ├── ui.py │ └── utils.py ├── 008-ChatGPT-UI/ │ ├── .gitignore │ ├── README.md │ ├── config.ini │ ├── config.json │ ├── doc/ │ │ ├── config.json │ │ └── pyinstaller.sh │ ├── gpt.py │ ├── main.py │ ├── requirements.txt │ └── utils.py ├── 009-Translate/ │ ├── README.md │ ├── asset/ │ │ ├── ch.ini │ │ ├── config.ini │ │ ├── en.ini │ │ └── language.json │ ├── config.py │ ├── core.py │ ├── doc/ │ │ └── pyinstaller.sh │ ├── load_srt.py │ ├── main.py │ ├── tran_test.py │ ├── ui.py │ └── utils.py ├── 010-YouTubeUpload/ │ ├── README.md │ ├── main.py │ ├── tst.py │ └── youtube.py ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ **__pycache__/ **.idea **dist/ **build **__pycache__ ================================================ FILE: 001-Downloader/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 001-Downloader/README.md ================================================ # 资源下载器 本项目主要通过网络上开源的项目聚合成了一个跨平台的下载工具,可批量下载抖音、快手视音频资源。下载地址: MacOS:[Downloader1.0.3.app](https://github.com/xhunmon/PythonIsTools/releases/download/v1.0.3/Downloader1.0.3.app.zip) 下载后解压后使用 Window:[Downloader1.0.3.exe](https://github.com/xhunmon/PythonIsTools/releases/download/v1.0.3/Downloader1.0.3.exe.zip) 下载后解压后使用 效果如图: ![下载器截图](./doc/example.jpg) #主要知识点 ## python GUI(界面) 本文使用tkinter GUI(界面)框架进行界面显示:[./ui.py](ui.py) ,[学习参考](https://www.cnblogs.com/shwee/p/9427975.html) 。 ## [pyinstaller](https://pyinstaller.readthedocs.io/en/stable/) 打包 使用pyinstaller把python程序打包成window和mac可执行文件,主要命令如下: ```shell #① :生成xxx.spec文件;(去掉命令窗口-w) pyinstaller -F -i res/logo.ico main.py -w #②:修改xxx.spec,参考main.spec #③:再次进行打包,参考installer-mac.sh pyinstaller -F -i res/logo.ico main.spec -w ``` 打包脚本与配置已放在 `doc` 目录下,需要拷贝出根目录进行打包。 注意: pyinstaller打包工具的版本与python版本、python所需第三方库以及操作系统会存在各种问题,所以需要看日志查找问题。例如:打包后运用,发现导入pyppeteer报错,通过降低版本后能正常使用:pip install pyppeteer==0.2.2 ## 项目 项目代码结构非常简单,看ui.py和downloader.py就能知道大概。支持多线程任务下载。如果自己添加其他网站的资源下载,通过增加实现downloader.py和并且在ui.py中start_download增加入口判读即可无缝接入。 ================================================ FILE: 001-Downloader/__init__.py ================================================ ================================================ FILE: 001-Downloader/config.ini ================================================ # 常用配置模块 [common] #软件使用截止日期 expired_time=2025/12/15 23:59:59 #app的版本名称 version_name=1.0.4 #app的版本号 version_code=1040 ================================================ FILE: 001-Downloader/doc/mac-sh/main.spec ================================================ # -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis(['main.py','type_enum.py','ui.py','utils.py','downloader.py','douyin/dy_download.py'], pathex=['.'], binaries=[], datas=[('res/logo.ico', 'images'),('config.ini', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='main', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None , icon='res/logo.ico') app = BUNDLE(exe, name='Downloader.app', icon='res/logo.ico', bundle_identifier=None) ================================================ FILE: 001-Downloader/doc/mac-sh/pyinstaller.sh ================================================ #!/bin/bash pyinstaller -F -i res/logo.ico main.spec main.py -w \ -p type_enum.py \ -p ui.py \ -p utils.py \ -p downloader.py \ -p douyin/dy_download.py \ -p kuaishou/ks_download.py ================================================ FILE: 001-Downloader/doc/win-sh/main.spec ================================================ # -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis(['main.py','type_enum.py','ui.py','utils.py','downloader.py','douyin\\dy_download.py'], pathex=['.'], binaries=[], datas=[('res\\logo.ico', 'images'),('config.ini', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='main', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None , icon='res\\logo.ico') app = BUNDLE(exe, name='Downloader.exe', icon='res\\logo.ico', bundle_identifier=None) ================================================ FILE: 001-Downloader/doc/win-sh/pyinstaller.sh ================================================ #!/bin/bash pyinstaller -F -i res\\logo.ico -w main.spec main.py -p type_enum.py -p ui.py -p utils.py -p downloader.py -p douyin\\dy_download.py -p kuaishou\\ks_download.py ================================================ FILE: 001-Downloader/douyin/dy_download.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 抖音视频下载 @Date :2021/08/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import json import os import re import time import requests from downloader import Downloader class DouYin(Downloader): # 初始化 def __init__(self): super().__init__() self.headers = self._headers # 抓获所有视频 self.end = False def start(self, url, path): Downloader.print_ui("开始解析下载链接") # 读取保存路径 self.save = path # 读取下载视频个数 self.count = 10 # 读取下载是否下载音频 self.musicarg = True # 读取用户主页地址 self.user = '' # 读取单条 self.single = '' # 读取下载模式 #下载模式选择 like为点赞 post为发布 self.mode = 'post' # 保存用户名 self.nickname = '' if '/user/' in url: self.user = url else: self.single = url # 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 # try: # self.single = re.findall(r'(http.+?)\?extra_params', url)[0] # except: # self.single = url if len(self.single) > 0: self.count = 1 self.parse_single() else: self.judge_link() # 单条数据页面 def parse_single(self): url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', self.single)[ 0] r = requests.get(url=url) key = re.findall('video/(\d+)?', str(r.url))[0] jx_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={key}' # 官方接口 js = json.loads(requests.get(url=jx_url, headers=self.headers).text) detail = js['item_list'][0] # 作者信息 author_list = [] # 无水印视频链接 video_list = [] # 作品id aweme_id = [] # 作者id nickname = [] max_cursor = 0 author_list.append(str(detail['desc'])) video_list.append(str(detail['video']['play_addr']['url_list'][0]).replace('playwm', 'play')) aweme_id.append(str(detail['aweme_id'])) nickname.append(str(detail['author']['nickname'])) Downloader.print_ui('开始下载单个视频' + video_list[0]) self.videos_download(author_list, video_list, aweme_id, nickname, max_cursor) # 匹配粘贴的url地址 def Find(self, string): # findall() 查找匹配正则表达式的字符串 url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string) Downloader.print_ui('Find url: ' + url) return url # 判断个人主页api链接 def judge_link(self): user_url: str = self.user Downloader.print_ui('----为您下载多个视频----\r') key = re.findall('/user/(.*?)$', str(user_url))[0] if not key: key = user_url[28:83] Downloader.print_ui('----' + '用户的sec_id=' + key + '----\r') # 第一次访问页码 max_cursor = 0 # 构造第一次访问链接 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=' % ( self.mode, key, str(self.count), max_cursor) self.get_data(api_post_url, max_cursor) return api_post_url, max_cursor, key # 获取第一次api数据 def get_data(self, api_post_url, max_cursor): # 尝试次数 index = 0 # 存储api数据 result = [] while result == []: index += 1 Downloader.print_ui('----正在进行第 %d 次尝试----\r' % index) time.sleep(0.3) response = requests.get(url=api_post_url, headers=self.headers) html = json.loads(response.content.decode()) if self.end == False: # 下一页值 self.nickname = html['aweme_list'][0]['author']['nickname'] Downloader.print_ui('[ 用户 ]:' + str(self.nickname) + '\r') max_cursor = html['max_cursor'] result = html['aweme_list'] Downloader.print_ui('----抓获数据成功----\r') # 处理第一页视频信息 self.video_info(result, max_cursor) else: max_cursor = html['max_cursor'] self.next_data(max_cursor) # self.end = True Downloader.print_ui('----此页无数据,为您跳过----\r') return result, max_cursor # 下一页 def next_data(self, max_cursor): if self.count == 1: return user_url = self.user # 获取用户sec_uid # key = re.findall('/user/(.*?)\?', str(user_url))[0] key = re.findall('/user/(.*?)$', str(user_url))[0] if not key: key = user_url[28:83] # 构造下一次访问链接 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=' % ( self.mode, key, str(self.count), max_cursor) index = 0 result = [] while self.end == False: # 回到首页,则结束 if max_cursor == 0: self.end = True return index += 1 # Downloader.print_ui('----正在对' + max_cursor + '页进行第 %d 次尝试----\r' % index) Downloader.print_ui('----正在对{}页进行第 {} 次尝试----\r'.format(max_cursor, index)) time.sleep(3) response = requests.get(url=api_naxt_post_url, headers=self.headers) html = json.loads(response.content.decode()) if self.end == False: # 下一页值 max_cursor = html['max_cursor'] result = html['aweme_list'] Downloader.print_ui('----{}页抓获数据成功----\r'.format(max_cursor)) # 处理下一页视频信息 self.video_info(result, max_cursor) else: self.end = True Downloader.print_ui('----{}页抓获数据失败----\r'.format(max_cursor)) # sys.exit() # 处理视频信息 def video_info(self, result, max_cursor): # 作者信息 author_list = [] # 无水印视频链接 video_list = [] # 作品id aweme_id = [] # 作者id nickname = [] # 封面大图 # dynamic_cover = [] for i2 in range(len(result)): try: author_list.append(str(result[i2]['desc'])) video_list.append(str(result[i2]['video']['play_addr']['url_list'][0])) aweme_id.append(str(result[i2]['aweme_id'])) nickname.append(str(result[i2]['author']['nickname'])) # dynamic_cover.append(str(result[i2]['video']['dynamic_cover']['url_list'][0])) except Exception as error: # Downloader.print_ui2(error) pass self.videos_download(author_list, video_list, aweme_id, nickname, max_cursor) return self, author_list, video_list, aweme_id, nickname, max_cursor def videos_download(self, author_list, video_list, aweme_id, nickname, max_cursor): count = len(author_list) Downloader.add_total_count(count) for i in range(count): if count == 1: # 创建并检测下载目录是否存在 pre_save = os.path.join(self.save, "单条") else: pre_save = os.path.join(self.save, nickname[i]) try: os.makedirs(pre_save) except: pass Downloader.add_downloading_count() # try: # jx_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={aweme_id[i]}' # 官方接口 # js = json.loads(requests.get(url=jx_url, headers=self.headers).text) # music_url = str(js['item_list'][0]['music']['play_url']['url_list'][0]) # music_title = str(js['item_list'][0]['music']['author']) # if self.musicarg == "yes": # 保留音频 # music = requests.get(music_url) # 保存音频 # start = time.time() # 下载开始时间 # size = 0 # 初始化已下载大小 # chunk_size = 1024 # 每次下载的数据大小 # content_size = int(music.headers['content-length']) # 下载文件总大小 # if music.status_code == 200: # 判断是否响应成功 # Downloader.print_ui('[ 音频 ]:' + author_list[i] + '[文件 大小]:{size:.2f} MB'.format( # size=content_size / chunk_size / 1024)) # 开始下载,显示下载文件大小 # # m_url = pre_save + music_title + '-[' + author_list[i] + '].mp3' # m_url = os.path.join(pre_save, # nickname[i] + "-" + music_title + '-[' + author_list[i] + '].mp3') # Downloader.print_ui("路径:" + m_url) # with open(m_url, 'wb') as file: # 显示进度条 # for data in music.iter_content(chunk_size=chunk_size): # file.write(data) # size += len(data) # Downloader.print_ui('\r' + music_title + '\n[下载进度]:%s%.2f%%' % ( # '>' * int(size * 50 / content_size), float(size / content_size * 100))) # end = time.time() # 下载结束时间 # Downloader.print_ui('\n' + music_title + '\n[下载完成]:耗时: %.2f秒\n' % (end - start)) # 输出下载用时时间 # Downloader.add_success_count() # except Exception as error: # # Downloader.print_ui2(error) # Downloader.print_ui('该页音频没有' + str(self.count) + '个\r') # # Downloader.add_failed_count() # # break try: v_url = os.path.join(pre_save, nickname[i] + "-" + '[' + author_list[i] + '].mp4') # 如果本地已经有了就跳过 if os.path.exists(v_url): Downloader.print_ui('{}-已存在!'.format(v_url)) Downloader.add_success_count() continue video = requests.get(video_list[i], headers=self.headers) # 保存视频 start = time.time() # 下载开始时间 size = 0 # 初始化已下载大小 chunk_size = 100 # 每次下载的数据大小 content_size = int(video.headers['content-length']) # 下载文件总大小 if video.status_code == 200: # 判断是否响应成功 Downloader.print_ui( '[ 视频 ]:' + nickname[i] + '-' + author_list[i] + '[文件 大小]:{size:.2f} MB'.format( size=content_size / 1024 / 1024)) # 开始下载,显示下载文件大小 # v_url = os.path.join(pre_save, nickname[i] + "-" + '[' + author_list[i] + '].mp4') # v_url = pre_save + '[' + author_list[i] + '].mp4' Downloader.print_ui("路径:" + v_url) with open(v_url, 'wb') as file: # 显示进度条 for data in video.iter_content(chunk_size=chunk_size): file.write(data) size += len(data) Downloader.print_ui('\r' + author_list[i] + '\n[下载进度]:%s%.2f%%' % ( '>' * int(size * 50 / content_size), float(size / content_size * 100))) end = time.time() # 下载结束时间 Downloader.print_ui('\n' + author_list[i] + '\n[下载完成]:耗时: %.2f秒\n' % (end - start)) # 输出下载用时时间 Downloader.add_success_count() except Exception as error: # Downloader.print_ui2(error) Downloader.print_ui('该页视频没有' + str(count) + '个,已为您跳过\r') Downloader.add_failed_count() break self.next_data(max_cursor) ================================================ FILE: 001-Downloader/downloader.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:downloader.py 所有下载类的基类,负责与UI界面的绑定 @Date :2021/08/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import time from threading import Lock import requests from my_fake_useragent import UserAgent from type_enum import PrintType from utils import Config ua = UserAgent(family='chrome') class Downloader(object): func_ui_print = None __mutex_total = Lock() __mutex_success = Lock() __mutex_failed = Lock() __mutex_downloading = Lock() __count_total = 0 __count_success = 0 __count_failed = 0 __count_downloading = 0 __beijing_time = 0 # 在线北京时间 def __init__(self): self._headers = {'user-agent': ua.random()} self.get_beijing_time() @staticmethod def print_hint(): """显示初始提示信息""" Downloader.print_ui( """ 使用说明: 1、快手下载用户批量视频如:https://www.kuaishou.com/profile/xxx 2、快手下载单条视频如:https://www.kuaishou.com/short-video/xxx 3、抖音下载用户批量视频如:https://www.douyin.com/user/xxx 4、抖音下载单条视频如:https://www.douyin.com/video/xxx """ ) def start(self, url, path): """业务逻辑由子类实现""" pass @staticmethod def print_ui(txt): """在界面显示内容""" Downloader.print_all_ui(txt=txt) # 打印日志 @staticmethod def print_all_ui(txt, print_type: PrintType = PrintType.log): """通知ui中func_ui_print更新内容""" if Downloader.func_ui_print is not None: Downloader.func_ui_print(txt=txt, print_type=print_type) @staticmethod def get_beijing_time(): """静态方法:获取在线的北京时间""" if Downloader.__beijing_time > 0: return Downloader.__beijing_time try: response = requests.get(url='http://www.beijing-time.org/t/time.asp', headers={'user-agent': ua.random()}) result = response.text data = result.split("\r\n") year = data[1][len("nyear") + 1: len(data[1]) - 1] month = data[2][len("nmonth") + 1: len(data[2]) - 1] day = data[3][len("nday") + 1: len(data[3]) - 1] # wday = data[4][len("nwday")+1 : len(data[4])-1] hrs = data[5][len("nhrs") + 1: len(data[5]) - 1] minute = data[6][len("nmin") + 1: len(data[6]) - 1] sec = data[7][len("nsec") + 1: len(data[7]) - 1] beijinTimeStr = "%s/%s/%s %s:%s:%s" % (year, month, day, hrs, minute, sec) beijinTime = time.strptime(beijinTimeStr, "%Y/%m/%d %X") Downloader.__beijing_time = int(time.mktime(beijinTime)) except: pass return Downloader.__beijing_time @staticmethod def is_expired(): """静态方法:判断是否已过期""" if Downloader.__beijing_time == 0: # 还没获取到时间 return True expired_time_str = time.strptime(Config.instance().get_expired_time(), "%Y/%m/%d %X") expired_time_int = int(time.mktime(expired_time_str)) return Downloader.__beijing_time > expired_time_int @staticmethod def add_total_count(count=1): """静态方法:添加总下载任务数""" Downloader.__mutex_total.acquire() Downloader.__count_total += count Downloader.__mutex_total.release() Downloader.print_all_ui(txt="预计总数:%d" % Downloader.__count_total, print_type=PrintType.total) @staticmethod def get_total_count(): """静态方法:获取总下载任务数""" return Downloader.__count_total @staticmethod def add_downloading_count(): """静态方法:添加正在下载任务数""" Downloader.__mutex_downloading.acquire() Downloader.__count_downloading += 1 Downloader.__mutex_downloading.release() Downloader.print_all_ui(txt="正在下载:%d" % Downloader.__count_downloading, print_type=PrintType.downloading) @staticmethod def __sub_downloading_count(): """静态方法:减去正在下载任务数""" Downloader.__mutex_downloading.acquire() Downloader.__count_downloading -= 1 Downloader.__mutex_downloading.release() Downloader.print_all_ui(txt="正在下载:%d" % Downloader.__count_downloading, print_type=PrintType.downloading) @staticmethod def get_downloading_count(): """静态方法:获取正在下载任务数""" return Downloader.__count_downloading @staticmethod def add_success_count(): """静态方法:添加下载成功任务数""" Downloader.__mutex_success.acquire() Downloader.__count_success += 1 Downloader.__mutex_success.release() # 成功一条,减正在下载的一条 Downloader.__sub_downloading_count() Downloader.print_all_ui(txt="已完成:%d" % Downloader.__count_success, print_type=PrintType.success) @staticmethod def get_success_count(): """静态方法:获取下载成功任务数""" return Downloader.__count_success @staticmethod def add_failed_count(): """静态方法:添加下载失败任务数""" Downloader.__mutex_failed.acquire() Downloader.__count_failed += 1 Downloader.__mutex_failed.release() # 失败一条,减正在下载的一条 Downloader.__sub_downloading_count() Downloader.print_all_ui(txt="已失败:%d" % Downloader.__count_failed, print_type=PrintType.failed) @staticmethod def get_failed_count(): """静态方法:获取下载失败任务数""" return Downloader.__count_failed ================================================ FILE: 001-Downloader/kuaishou/ks_download.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 快手视频下载 @Date :2021/09/102 @Author :qincji @Mail :xhunmon@gmail.com """ import json import os import re import time import urllib import requests from downloader import Downloader requestUrl = 'https://video.kuaishou.com/graphql' class KuaiShou(Downloader): cookie = 'clientid=3; client_key=65890b29; kpf=PC_WEB; kpn=KUAISHOU_VISION; did=web_3e09c32da1db9d38c0122ffa25ad8b7d' # 初始化 def __init__(self): super().__init__() self.headers = self._headers # 抓获所有视频 self.end = False def set_cookie(self, c): """当cookie过期时,需要从外界出入""" KuaiShou.cookie = c def start(self, url, path): Downloader.print_ui("开始解析下载链接") # 读取保存路径 self.save = path if '/profile/' in url: self.parse_user(url) elif '/short-video/' in url: if 'trendingId' in url: self.parse_single_trendingId(urllib.parse.unquote(url, encoding="utf-8")) elif 'streamSource' in url: self.parse_single_streamSource(urllib.parse.unquote(url, encoding="utf-8")) else: Downloader.print_ui('该链接不支持下载') else: Downloader.print_ui('该链接不支持下载') # 单条数据页面 def parse_single_trendingId(self, url): try: Downloader.print_ui('----为您下载单个视频----\r') # userId = re.findall(r'/short-video/(.+?)\?', url)[0].strip() trendingId = re.findall(r'trendingId=(.+?)&', url)[0].strip() area = re.findall(r'area=(.+?)$', url)[0].strip() except: Downloader.print_ui('地址%s输入错误' % url) return links = [] try: result = self.post_single_trendingId(url, KuaiShou.cookie, trendingId, area) data = json.loads(result) feeds = data['data']['hotData']['feeds'] size = len(feeds) links.append(feeds) except Exception as e: Downloader.print_ui(str(e)) return if size < 1: Downloader.print_ui('解析地址%s异常' % url) return Downloader.add_total_count(size) for link in links: self.download(link) # 单条数据页面 def parse_single_streamSource(self, url): try: Downloader.print_ui('----为您下载单个视频----\r') userId = re.findall(r'/short-video/(.+?)\?', url)[0].strip() area = re.findall(r'area=(.+?)$', url)[0].strip() except: Downloader.print_ui('地址%s输入错误' % url) return links = [] try: result = self.post_single_streamSource(url, KuaiShou.cookie, userId, area) data = json.loads(result) feeds = [data['data']['visionVideoDetail'], ] size = len(feeds) links.append(feeds) except Exception as e: Downloader.print_ui(str(e)) return if size < 1: Downloader.print_ui('解析地址%s异常' % url) return Downloader.add_total_count(size) for link in links: self.download(link) # 判断个人主页api链接 def parse_user(self, url): # https://www.kuaishou.com/profile/3xcx5qwycxzxdre try: Downloader.print_ui('----为您下载多个视频----\r') userId = re.findall(r'/profile/(.+?)$', url)[0].strip() except: Downloader.print_ui('地址%s输入错误' % url) return pcursor = '' all_count = 0 links = [] while True: try: result = self.post_user(userId, KuaiShou.cookie, pcursor) data = json.loads(result) feeds = data['data']['visionProfilePhotoList']['feeds'] flen = len(feeds) pcursor = data['data']['visionProfilePhotoList']['pcursor'] if flen == 0: break all_count += flen links.append(feeds) except Exception as e: Downloader.print_ui(str(e)) break if len(links) < 1: Downloader.print_ui('解析地址%s异常' % url) return Downloader.add_total_count(all_count) for link in links: self.download(link) def post_single_trendingId(self, url, Cookie, trendingId, area): data = { "operationName": "hotVideoQuery", "variables": { "trendingId": trendingId, "page": "detail", "webPageArea": area }, "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" } headers = { 'Host': 'www.kuaishou.com', 'Connection': 'keep-alive', 'Content-Length': '1261', 'accept': '*/*', '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', 'content-type': 'application/json', 'Origin': 'https://www.kuaishou.com', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': url.encode(encoding='utf-8'), 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6' # 'Cookie': Cookie, } requests.packages.urllib3.disable_warnings() r = requests.post('https://www.kuaishou.com/graphql', data=json.dumps(data), headers=headers) r.encoding = r.apparent_encoding html = r.text return html def post_single_streamSource(self, url, Cookie, photoId, area): data = { "operationName": "visionVideoDetail", "variables": { "photoId": photoId, "page": "detail", "webPageArea": area }, "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" } headers = { 'Host': 'www.kuaishou.com', 'Connection': 'keep-alive', 'Content-Length': '1261', 'accept': '*/*', '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', 'content-type': 'application/json', 'Origin': 'https://www.kuaishou.com', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': url.encode(encoding='utf-8'), 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'Cookie': Cookie, } requests.packages.urllib3.disable_warnings() r = requests.post('https://www.kuaishou.com/graphql', data=json.dumps(data), headers=headers) r.encoding = r.apparent_encoding html = r.text return html def post_user(self, userId, Cookie, pcursor): data = {"operationName": "visionProfilePhotoList", "variables": {"userId": userId, "pcursor": pcursor, "page": "profile"}, "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"} failed = {'msg': 'failed...'} headers = { 'Host': 'video.kuaishou.com', 'Connection': 'keep-alive', 'Content-Length': '1261', 'accept': '*/*', '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', 'content-type': 'application/json', 'Origin': 'https://video.kuaishou.com', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://video.kuaishou.com/profile/' + userId, 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'Cookie': Cookie, } requests.packages.urllib3.disable_warnings() r = requests.post(requestUrl, data=json.dumps(data), headers=headers) r.encoding = 'UTF-8' html = r.text return html def progressbar(self, url, filepath, filename): if not os.path.exists(filepath): os.mkdir(filepath) start = time.time() response = requests.get(url, stream=True) size = 0 chunk_size = 1024 content_size = int(response.headers['content-length']) if response.status_code == 200: # print('Start download,[File size]:{size:.2f} MB'.format(size=content_size / chunk_size / 1024)) Downloader.print_ui(('%s Start download,[File size]:{size:.2f} MB' % filename).format( size=content_size / chunk_size / 1024)) filename = filename.replace("\n", "") # filepath = filepath + filename filepath = os.path.join(filepath, filename) try: with open(filepath, 'wb') as file: for data in response.iter_content(chunk_size=chunk_size): file.write(data) size += len(data) Downloader.print_ui('\r' + '%s[下载进度]:%s%.2f%%' % (filename, '>' * int(size * 50 / content_size), float(size / content_size * 100))) # print('\r' + '[下载进度]:%s%.2f%%' % ( # '>' * int(size * 50 / content_size), float(size / content_size * 100)), end=' ') end = time.time() # print('Download completed!,times: %.2f秒' % (end - start)) Downloader.print_ui('%s Download completed!,times: %.2f秒' % (filename, end - start)) except: Downloader.add_failed_count() Downloader.print_ui('%s [下载失败!!]' % filename) def download(self, feeds): author = '' for feed in feeds: try: Downloader.add_downloading_count() author = feed['author']['name'] filename = feed['photo']['caption'] + '.mp4' # filepath = self.save + '/' + author + '/' filepath = os.path.join(self.save, author) filename_path = os.path.join(filepath, filename) if not os.path.exists(filename_path): self.progressbar(feed['photo']['photoUrl'], filepath, filename) # print(filename + ",下载完成") Downloader.print_ui('%s--下载完成' % filename) Downloader.add_success_count() else: # print(filename + ",已存在,跳过") Downloader.print_ui('%s--已存在,跳过' % filename) Downloader.add_success_count() except: Downloader.add_failed_count() Downloader.print_ui('%s下载失败' % author) ================================================ FILE: 001-Downloader/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 程序主入口 @Date :2021/08/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os import sys from ui import Ui # 主模块执行 if __name__ == "__main__": path = os.path.dirname(os.path.realpath(sys.argv[0])) # path = os.path.dirname('/Users/Qincji/Documents/zmt/') app = Ui() app.set_dir(path) # to do app.mainloop() ================================================ FILE: 001-Downloader/test/bilibili_video_download_v1.py ================================================ # !/usr/bin/python # -*- coding:utf-8 -*- # time: 2019/04/17--08:12 __author__ = 'Henry' ''' 项目: B站视频下载 版本1: 加密API版,不需要加入cookie,直接即可下载1080p视频 20190422 - 增加多P视频单独下载其中一集的功能 ''' import requests, time, hashlib, urllib.request, re, json from moviepy.editor import * import os, sys # 访问API地址 def get_play_list(start_url, cid, quality): entropy = 'rbMCKn@KuamXWlPMoJGsKcbiJKUfkPF_8dABscJntvqhRSETg' appkey, sec = ''.join([chr(ord(i) + 2) for i in entropy[::-1]]).split(':') params = 'appkey=%s&cid=%s&otype=json&qn=%s&quality=%s&type=' % (appkey, cid, quality, quality) chksum = hashlib.md5(bytes(params + sec, 'utf8')).hexdigest() url_api = 'https://interface.bilibili.com/v2/playurl?%s&sign=%s' % (params, chksum) headers = { 'Referer': start_url, # 注意加上referer 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36' } # print(url_api) html = requests.get(url_api, headers=headers).json() # print(json.dumps(html)) video_list = [] for i in html['durl']: video_list.append(i['url']) # print(video_list) return video_list # 下载视频 ''' urllib.urlretrieve 的回调函数: def callbackfunc(blocknum, blocksize, totalsize): @blocknum: 已经下载的数据块 @blocksize: 数据块的大小 @totalsize: 远程文件的大小 ''' def Schedule_cmd(blocknum, blocksize, totalsize): speed = (blocknum * blocksize) / (time.time() - start_time) # speed_str = " Speed: %.2f" % speed speed_str = " Speed: %s" % format_size(speed) recv_size = blocknum * blocksize # 设置下载进度条 f = sys.stdout pervent = recv_size / totalsize percent_str = "%.2f%%" % (pervent * 100) n = round(pervent * 50) s = ('#' * n).ljust(50, '-') f.write(percent_str.ljust(8, ' ') + '[' + s + ']' + speed_str) f.flush() # time.sleep(0.1) f.write('\r') def Schedule(blocknum, blocksize, totalsize): speed = (blocknum * blocksize) / (time.time() - start_time) # speed_str = " Speed: %.2f" % speed speed_str = " Speed: %s" % format_size(speed) recv_size = blocknum * blocksize # 设置下载进度条 f = sys.stdout pervent = recv_size / totalsize percent_str = "%.2f%%" % (pervent * 100) n = round(pervent * 50) s = ('#' * n).ljust(50, '-') print(percent_str.ljust(6, ' ') + '-' + speed_str) f.flush() time.sleep(2) # print('\r') # 字节bytes转化K\M\G def format_size(bytes): try: bytes = float(bytes) kb = bytes / 1024 except: print("传入的字节格式不对") return "Error" if kb >= 1024: M = kb / 1024 if M >= 1024: G = M / 1024 return "%.3fG" % (G) else: return "%.3fM" % (M) else: return "%.3fK" % (kb) # 下载视频 def down_video(video_list, title, start_url, page): num = 1 print('[正在下载P{}段视频,请稍等...]:'.format(page) + title) currentVideoPath = os.path.join(sys.path[0], 'bilibili_video', title) # 当前目录作为下载目录 for i in video_list: opener = urllib.request.build_opener() # 请求头 opener.addheaders = [ # ('Host', 'upos-hz-mirrorks3.acgvideo.com'), #注意修改host,不用也行 ('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:56.0) Gecko/20100101 Firefox/56.0'), ('Accept', '*/*'), ('Accept-Language', 'en-US,en;q=0.5'), ('Accept-Encoding', 'gzip, deflate, br'), ('Range', 'bytes=0-'), # Range 的值要为 bytes=0- 才能下载完整视频 ('Referer', start_url), # 注意修改referer,必须要加的! ('Origin', 'https://www.bilibili.com'), ('Connection', 'keep-alive'), ] urllib.request.install_opener(opener) # 创建文件夹存放下载的视频 if not os.path.exists(currentVideoPath): os.makedirs(currentVideoPath) # 开始下载 if len(video_list) > 1: urllib.request.urlretrieve(url=i, filename=os.path.join(currentVideoPath, r'{}-{}.mp4'.format(title, num)), reporthook=Schedule_cmd) # 写成mp4也行 title + '-' + num + '.flv' else: urllib.request.urlretrieve(url=i, filename=os.path.join(currentVideoPath, r'{}.mp4'.format(title)), reporthook=Schedule_cmd) # 写成mp4也行 title + '-' + num + '.flv' num += 1 # 合并视频 def combine_video(video_list, title): currentVideoPath = os.path.join(sys.path[0], 'bilibili_video', title) # 当前目录作为下载目录 if not os.path.exists(currentVideoPath): os.makedirs(currentVideoPath) if len(video_list) >= 2: # 视频大于一段才要合并 print('[下载完成,正在合并视频...]:' + title) # 定义一个数组 L = [] # 访问 video 文件夹 (假设视频都放在这里面) root_dir = currentVideoPath # 遍历所有文件 for file in sorted(os.listdir(root_dir), key=lambda x: int(x[x.rindex("-") + 1:x.rindex(".")])): # 如果后缀名为 .mp4/.flv if os.path.splitext(file)[1] == '.flv': # 拼接成完整路径 filePath = os.path.join(root_dir, file) # 载入视频 video = VideoFileClip(filePath) # 添加到数组 L.append(video) # 拼接视频 final_clip = concatenate_videoclips(L) # 生成目标视频文件 final_clip.to_videofile(os.path.join(root_dir, r'{}.mp4'.format(title)), fps=24, remove_temp=False) print('[视频合并完成]' + title) else: # 视频只有一段则直接打印下载完成 print('[视频合并完成]:' + title) def getAid(Bid): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36' } url = "https://api.bilibili.com/x/web-interface/view?bvid=" + Bid print(url) r = requests.get(url, headers=headers) j = json.loads(r.text) # print(j["data"]["aid"]) print(j) return j["data"]["aid"] if __name__ == '__main__': # 用户输入av号或者视频链接地址 print('*' * 30 + 'B站视频下载小助手' + '*' * 30) start = input('请输入您要下载的B站av号、bv号或者视频链接地址:') if 'http' in start: if 'video/BV' in start: bv = re.findall(r'video/(.*?)\?', start)[0] start = str(getAid(bv)) print(start) if start.isdigit() == True: # 如果输入的是av号 # 获取cid的api, 传入aid即可 start_url = 'https://api.bilibili.com/x/web-interface/view?aid=' + start else: # https://www.bilibili.com/video/av46958874/?spm_id_from=333.334.b_63686965665f7265636f6d6d656e64.16 start_url = 'https://api.bilibili.com/x/web-interface/view?aid=' + re.search(r'/av(\d+)/*', start).group(1) # https://www.bilibili.com/video/BV1jL4y1e7Uz?t=7.2 # start_url = 'https://api.bilibili.com/x/web-interface/view?aid=' + re.findall(r'video/(.*?)\?', start)[0] print(start_url) # 视频质量 # # # quality = input('请输入您要下载视频的清晰度(1080p:80;720p:64;480p:32;360p:16)(填写80或64或32或16):') # 获取视频的cid,title headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36' } html = requests.get(start_url, headers=headers).json() data = html['data'] video_title = data["title"].replace(" ", "_") cid_list = [] if '?p=' in start: # 单独下载分P视频中的一集 p = re.search(r'\?p=(\d+)', start).group(1) cid_list.append(data['pages'][int(p) - 1]) else: # 如果p不存在就是全集下载 cid_list = data['pages'] # print(cid_list) for item in cid_list: cid = str(item['cid']) title = item['part'] if not title: title = video_title title = re.sub(r'[\/\\:*?"<>|]', '', title) # 替换为空的 print('[下载视频的cid]:' + cid) print('[下载视频的标题]:' + title) page = str(item['page']) start_url = start_url + "/?p=" + page video_list = get_play_list(start_url, cid, quality) start_time = time.time() down_video(video_list, title, start_url, page) combine_video(video_list, title) # 如果是windows系统,下载完成后打开下载目录 currentVideoPath = os.path.join(sys.path[0], 'bilibili_video') # 当前目录作为下载目录 if (sys.platform.startswith('win')): os.startfile(currentVideoPath) # 分P视频下载测试: https://www.bilibili.com/video/av19516333/ ================================================ FILE: 001-Downloader/test/ff_video.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: ffmpeg去掉最后一帧,改变md5 @Date :2022/02/17 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os def cute_video(folder): files = next(os.walk(folder))[2] # 获取文件 for file in files: file_path = os.path.join(folder, file) shotname, extension = os.path.splitext(file) if len(shotname) == 0 or len(extension) == 0: continue out_file = os.path.join(folder, 'out-{}{}'.format(shotname, extension)) # 获取时间。输入自己系统安装的ffmpeg,注意斜杠 time = os.popen( r"/usr/local/ffmpeg/bin/ffmpeg -i {} 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//".format( file_path)).read().replace('\n', '').replace(' ', '') if '.' in time: match_time = time.split('.')[0] else: match_time = time print(match_time) ts = match_time.split(':') sec = int(ts[0]) * 60 * 60 + int(ts[1]) * 60 + int(ts[2]) # 从0分0秒100毫秒开始截切(目的就是去头去尾) os.popen(r"/usr/local/ffmpeg/bin/ffmpeg -ss 0:00.100 -i {} -t {} -c:v copy -c:a copy {}".format(file_path, sec, out_file)) # 主模块执行 if __name__ == "__main__": # path = os.path.dirname('/Users/Qincji/Downloads/ffmpeg/') path = os.path.dirname('需要处理的目录') # 目录下的所有视频 cute_video(path) ================================================ FILE: 001-Downloader/test/test_pyinstaller.py ================================================ #!/usr/bin/env python # -*- coding:utf-8 -*- import os # 测试打包 # import pyppeteer # import sys # import asyncio # from urllib.parse import urlparse, urlunparse, urljoin # from concurrent.futures import ThreadPoolExecutor # from concurrent.futures._base import TimeoutError # from functools import partial # from typing import Set, Union, List, MutableMapping, Optional # # import requests # from pyquery import PyQuery # # from fake_useragent import UserAgent # from lxml.html.clean import Cleaner # import lxml # from lxml import etree # from lxml.html import HtmlElement # from lxml.html import tostring as lxml_html_tostring # from lxml.html.soupparser import fromstring as soup_parse # from parse import search as parse_search # from parse import findall, Result # from w3lib.encoding import html_to_unicode # from tkinter import * from tkinter.filedialog import (askdirectory) print('输入和粗了了') ================================================ FILE: 001-Downloader/test/urls.txt ================================================ ================================================ FILE: 001-Downloader/test/xhs_download.py ================================================ import os import random import time import requests from my_fake_useragent import UserAgent ua = UserAgent(family='chrome') pre_save = os.path.join(os.path.curdir, '0216') ''' ''' def download_url(url, index): try: headers = { 'Accept': '*/*', 'Accept-Encoding': 'identity;q=1, *;q=0', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Cookie': 'xhsTrackerId=6970aca9-a496-4f50-cf98-118929f063bf; timestamp2=2022021544322a4e45f1e1dec93beb82; timestamp2.sig=jk1cFo-zHueSZUpZRvlqyJwTFoA1y8ch9t76Bfy28_Q; solar.beaker.session.id=1644906492328060192125; xhsTracker=url=index&searchengine=google', 'Host': 'v.xiaohongshu.com', 'Pragma': 'no-cache', 'Referer': url, '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' } video = requests.get(url, headers=headers) # 保存视频 start = time.time() # 下载开始时间 size = 0 # 初始化已下载大小 chunk_size = 100 # 每次下载的数据大小 content_size = int(video.headers['content-length']) # 下载文件总大小 print(video.status_code) if video.status_code == 200: # 判断是否响应成功 print(str(index) + '[文件 大小]:{size:.2f} MB'.format(size=content_size / 1024 / 1024)) # 开始下载,显示下载文件大小 v_url = os.path.join(pre_save, '{}.mp4'.format(index)) # v_url = pre_save + '[' + author_list[i] + '].mp4' with open(v_url, 'wb') as file: # 显示进度条 for data in video.iter_content(chunk_size=chunk_size): file.write(data) size += len(data) # print('\r' + i + '\n[下载进度]:%s%.2f%%' % ( # '>' * int(size * 50 / content_size), float(size / content_size * 100))) end = time.time() # 下载结束时间 print('\n' + str(index) + '\n[下载完成]:耗时: %.2f秒\n' % (end - start)) # 输出下载用时时间 except Exception as error: # Downloader.print_ui2(error) print(error) print('该页视频没有' + str(index) + ',已为您跳过\r') if __name__ == '__main__': ls = [] if not os.path.exists(pre_save): os.makedirs(pre_save) with open('../xhs/urls.txt', 'r') as f: for line in f: if 'http' in line: ls.append(line.replace('\n', '').replace(' ', '')) size = len(ls) for i in range(0, size): url = ls[i] print('{}-{}'.format(i, url)) download_url(url, i) time.sleep(random.randint(5, 10)) ================================================ FILE: 001-Downloader/type_enum.py ================================================ #!/usr/bin/env python # -*- coding:utf-8 -*- """ @Description:dy_download.py @Date :2021/08/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ from enum import Enum class PrintType(Enum): log = 1 total = 2 downloading = 3 success = 4 failed = 5 ================================================ FILE: 001-Downloader/ui.py ================================================ #!/usr/bin/env python # -*- coding:utf-8 -*- """ @Description: 用于GUI界面显示 @Date :2021/08/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ from tkinter import * from tkinter.filedialog import (askdirectory) from douyin.dy_download import DouYin from downloader import Downloader from kuaishou.ks_download import KuaiShou from type_enum import PrintType from utils import * # from PIL import Image, ImageTk class Ui(Frame): def __init__(self, master=None): global bg_color bg_color = '#373434' Frame.__init__(self, master, bg=bg_color) self.ui_width = 0 self.pack(expand=YES, fill=BOTH) self.window_init() self.createWidgets() def window_init(self): self.master.title( '欢迎使用-自媒体资源下载器' + Config.instance().get_version_name() + ',本程序仅用于学习交流!如有疑问请联系:xhunmon@gmail.com') self.master.bg = bg_color width, height = self.master.maxsize() # self.master.geometry("{}x{}".format(width, height)) self.master.geometry("%dx%d+%d+%d" % (width / 2, height / 2, width / 4, height / 4)) self.ui_width = width / 2 def createWidgets(self): # fm1 self.fm1 = Frame(self, bg=bg_color) self.fm1.pack(fill='y', pady=10) # window没有原生PIL 64位支持 # load = Image.open('res/logo.png') # load.thumbnail((38, 38), Image.ANTIALIAS) # initIamge = ImageTk.PhotoImage(load) # self.panel = Label(self.fm1, image=initIamge, bg=bg_color) # self.panel.image = initIamge # self.panel.pack(side=LEFT, fill='y', padx=5) self.titleLabel = Label(self.fm1, text="资源下载器", font=('微软雅黑', 32), fg="white", bg=bg_color) self.titleLabel.pack(side=LEFT, fill='y') # fm2 self.fm2 = Frame(self, bg=bg_color) self.fm2.pack(side=TOP, fill="y") self.fm2_right = Frame(self.fm2, bg=bg_color) self.fm2_right.pack(side=RIGHT, padx=0, pady=10, expand=YES, fill='y') self.fm2_left = Frame(self.fm2, bg=bg_color) self.fm2_left.pack(side=LEFT, padx=15, pady=10, expand=YES, fill='x') self.fm2_left_top = Frame(self.fm2_left, bg=bg_color) self.fm2_left_bottom = Frame(self.fm2_left, bg=bg_color) self.downloadBtn = Button(self.fm2_right, text='开始下载', fg="#ffffff", bg=bg_color, font=('微软雅黑', 18), command=self.start_download) self.downloadBtn.pack(side=RIGHT) self.dirEntry = Entry(self.fm2_left_top, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1) self.dirEntry.config(insertbackground='#ffffff') # self.set_dir(os.path.dirname(os.path.realpath(sys.argv[0]))) self.dirBtn = Button(self.fm2_left_top, text='选择保存目录:', bg=bg_color, fg='#aaaaaa', font=('微软雅黑', 12), width='10', command=self.save_dir) self.dirBtn.pack(side=LEFT) self.dirEntry.pack(side=LEFT, fill='y') self.fm2_left_top.pack(side=TOP, fill='x') self.urlEntry = Entry(self.fm2_left_bottom, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1) self.urlEntry.config(insertbackground='#ffffff') self.urlButton = Button(self.fm2_left_bottom, text='清空下载地址:', bg=bg_color, fg='#aaaaaa', font=('微软雅黑', 12), width='10', command=self.download_url) self.urlButton.pack(side=LEFT) self.urlEntry.pack(side=LEFT, fill='y') self.fm2_left_bottom.pack(side=TOP, pady=10, fill='x') # fm3 任务数状态 self.fm3 = Frame(self, bg=bg_color, height=6) self.fm3.pack(side=TOP, fill="x") self.totalLabel = Label(self.fm3, width=10, text="预计总数:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.totalLabel.pack(side=LEFT, fill='y', padx=20) self.downloadingLabel = Label(self.fm3, width=10, text="正在下载:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.downloadingLabel.pack(side=LEFT, fill='y', padx=20) self.successLabel = Label(self.fm3, width=10, text="已完成:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.successLabel.pack(side=LEFT, fill='y', padx=20) self.failLabel = Label(self.fm3, width=10, text="已失败:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.failLabel.pack(side=LEFT, fill='y', padx=20) # fm4 self.fm4 = Frame(self, bg=bg_color) self.fm4.pack(side=TOP, expand=YES, fill="both") self.logLabel = Label(self.fm4, anchor='w', wraplength=self.ui_width - 40, text="", font=('微软雅黑', 12), fg="white", bg=bg_color) self.logLabel.pack(side=TOP, fill='both', padx=20) # 注册回调 Downloader.func_ui_print = self.func_ui_print # 判断是否有网络 if Downloader.get_beijing_time() == 0: self.output("获取数据异常,请检查您的网络!") else: Downloader.print_hint() def save_dir(self): path = askdirectory() self.set_dir(path) def set_dir(self, path): self.dirEntry.delete(0, END) self.dirEntry.insert(0, path) def download_url(self): ground_truth = '' self.urlEntry.delete(0, END) self.urlEntry.insert(0, ground_truth) def output(self, txt): self.logLabel.config(text=txt) def func_ui_print(self, txt, print_type: PrintType = None): if print_type == PrintType.log: self.logLabel.config(text=txt) elif print_type == PrintType.total: self.totalLabel.config(text=txt) elif print_type == PrintType.downloading: self.downloadingLabel.config(text=txt) elif print_type == PrintType.success: self.successLabel.config(text=txt) elif print_type == PrintType.failed: self.failLabel.config(text=txt) def start_download(self): # 判断是否有网络 if Downloader.get_beijing_time() == 0: self.output("获取数据异常,请检查您的网络!") return if Downloader.is_expired(): self.output("授权证书已到期,请联系客服!") return url = self.urlEntry.get() path = self.dirEntry.get() domain = get_domain(url) if "kwaicdn" in domain or "kuaishou" in domain: downloader: KuaiShou = KuaiShou() # downloader.set_cookie() else: downloader: Downloader = DouYin() downloader_t = threading.Thread(target=downloader.start, args=(url, path)) downloader_t.setDaemon(True) # 设置守护进程,避免界面卡死 downloader_t.start() ================================================ FILE: 001-Downloader/utils.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:工具类 @Date :2021/08/16 @Author :xhunmon @Mail :xhunmon@gmail.com """ import threading import configparser import os import re def get_domain(url: str = None): """ 获取链接地址的域名 :param url: :return: """ # http://youtube.com/watch return re.match(r"(http://|https://).*?\/", url, re.DOTALL).group(0) class Config(object): """ 配置文件的单例类 """ _instance_lock = threading.Lock() def __init__(self): parent_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(parent_dir, 'config.ini') self.conf = configparser.ConfigParser() self.conf.read(conf_path, encoding="utf-8") @classmethod def instance(cls, *args, **kwargs): with Config._instance_lock: if not hasattr(Config, "_instance"): Config._instance = Config(*args, **kwargs) return Config._instance def get_expired_time(self): return self.conf.get("common", "expired_time") def get_version_name(self): return self.conf.get("common", "version_name") def get_version_code(self): return self.conf.get("common", "version_code") ================================================ FILE: 002-V2rayPool/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 002-V2rayPool/002-V2rayPool.iml ================================================ ================================================ FILE: 002-V2rayPool/README.md ================================================ # v2ray节点代理池 学习python爬虫过程中,我们需要一些代理。而本项目则是通过收集网上公开的节点,来实现自己的代理请求的过程! ## v2ray是什么?如何使用? - [v2ray官方指导](https://www.v2ray.com/index.html) - [v2ray wiki](https://zh.wikipedia.org/wiki/V2Ray) ## v2ray客户端推荐 - [window:v2rayN](https://github.com/2dust/v2rayN/releases) - [macOS:V2rayU](https://github.com/yanue/V2rayU/releases) - [android:v2rayNG](https://github.com/2dust/v2rayNG/releases) - ios推荐Shadowrocket(俗称小火箭,注意不是shadowrocket VPN),但需要付费的,可以通过找"美区apple id共享2021小火箭",找共享的账号下载软件,但是会有风险,请一定要注意。 ## v2ray服务端推荐 有些应用场景会需要到用到专用的ip代理,如Tik Tok、亚马逊和Facebook等。这是,我们通过购买国外的服务器或者vps来搭建代理服务器,从而实现专有ip代理。 推荐使用[x-ui](https://github.com/vaxilu/x-ui) 进行非常简单的"一键式"搭建开源框架。 #本项目主要知识点 学习本项目需要先了解代理原理,以及v2ray实现的原理。 ## 实现思路 ![实现思路图](./doc/v2ray.jpg) ## v2ray内核使用 我们找的是v2ray节点,所以这些协议只能运行在v2ray特有程序中。因此,我们要找[v2ray内核](https://github.com/v2ray/v2ray-core/releases) 。 这里就以macOS系统举例说明: 1. 下载[v2ray-core-v4.31.0](https://github.com/v2fly/v2ray-core/releases/download/v4.31.0/v2ray-macos-64.zip) 2. 配置解压目录的路径: ```python Config.set_v2ray_core_path('xxx/v2ray-macos-64') ``` 3. 查看是否能正常启动: ```python client.Creator().v2ray_start('xxx') #如果需要开启全局代理 client.Creator().v2ray_start('xxx',True) ``` ## 2022-1-11检测可用测试节点(注意去掉后面","开始的内容): ```shell ss://YWVzLTI1Ni1nY206MWY2YWNhM2NlYmQyMWE0Y2Q1YTgwNzE4ZWQxNmI3NGNAMTIwLjIzMi4yMTQuMzY6NTAwMg#%F0%9F%87%B8%F0%9F%87%ACSingapore,8.25.96.100,美国 Level3 ss://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 ss://YWVzLTI1Ni1nY206UmV4bkJnVTdFVjVBRHhH@167.88.61.60:7002#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%203,167.88.61.60,美国加利福尼亚圣克拉拉 ss://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 trojan://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,加拿大安大略 ss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@172.99.190.87:9101#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2013,172.99.190.87,美国乔治亚亚特兰大 ss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@46.29.218.6:8091#github.com/freefq%20-%20%E6%8C%AA%E5%A8%81%20%2019,46.29.218.6,挪威 ``` 注意:本程序虽可跨平台,但因博主能力有限,只在macos系统操作过,无法在更多系统上去尝试和改进,望谅解! ------- 如有可用节点增加,请推荐给博主吧:xhunmon@gmail.com ================================================ FILE: 002-V2rayPool/base/net_proxy.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 网络代理请求的基类 @Date :2021/09/15 @Author :xhunmon @Mail :xhunmon@gmail.com """ import requests from my_fake_useragent import UserAgent class Net(object): """ 基类,封装常用接口 """ TIMEOUT = 8 def __init__(self, timeout=8): Net.TIMEOUT = timeout self._ua = UserAgent() self._agent = self._ua.random() # 随机生成的agent self.USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0" self._headers = {"user-agent": self.USER_AGENT, 'Connection': 'close'} # determine self._proxy = '127.0.0.1:1080' self._proxies_en = { 'http': 'socks5h://' + self._proxy, 'https': 'socks5h://' + self._proxy, } self._proxies_zh = { 'http': 'socks5://' + self._proxy, 'https': 'socks5://' + self._proxy, } def get_header(self, headers, key): key_lower = key.lower() headers_lower = {k.lower(): v for k, v in headers.items()} if (key_lower in headers_lower): return headers_lower[key_lower] else: return '' def update_agent(self): self.USER_AGENT = self._ua.random() # 随机生成的agent self._headers = {"user-agent": self.USER_AGENT, 'Connection': 'close'} def get_urls(self) -> []: """需子类实现""" pass def request(self, url, allow_redirects=False, verify=False, timeout=TIMEOUT): """普通请求""" return self.__request(url, allow_redirects=allow_redirects, verify=verify, timeout=timeout) def request_en(self, url, allow_redirects=False, verify=False, timeout=TIMEOUT): """国外网站请求,需要开代理""" return self.__request(url, allow_redirects=allow_redirects, verify=verify, proxies=self._proxies_en, timeout=timeout) def request_zh(self, url, allow_redirects=False, verify=False, timeout=TIMEOUT): """国内网站请求,需要开代理""" return self.__request(url, allow_redirects=allow_redirects, verify=verify, proxies=self._proxies_zh, timeout=timeout) def __request(self, url, allow_redirects=False, verify=False, proxies=None, timeout=TIMEOUT): """最终的请求实现""" requests.packages.urllib3.disable_warnings() if proxies: return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify, proxies=proxies, timeout=timeout) else: return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify, timeout=timeout) ================================================ FILE: 002-V2rayPool/core/client.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import base64 import json import os import urllib from core.conf import Config from core.group import Vmess, Vless, Socks, SS, Mtproto, Trojan, Group, Dyport from core.utils import ProtocolType import core.utils as util class ClientWriter: def __init__(self, group): self.config_factory = Config() # with open(self.config_factory.get_path('config_path'), 'r') as json_file: # self.config = json.load(json_file) self.write_path = self.config_factory.get_path("config_path") self.template_path = self.config_factory.json_path self.group = group self.node = group.node def load_template(self, template_name): ''' load special template ''' with open(self.template_path + "/" + template_name, 'r') as stream_file: template = json.load(stream_file) return template def transform(self): user_json = None if type(self.node) == Vmess: self.client_config = self.load_template('client.json') user_json = self.client_config["outbounds"][0]["settings"]["vnext"][0] user_json["users"][0]["id"] = self.node.password user_json["users"][0]["alterId"] = self.node.alter_id elif type(self.node) == Vless: self.client_config = self.load_template('client.json') user_json = self.client_config["outbounds"][0]["settings"]["vnext"][0] user_json["users"][0]["id"] = self.node.password del user_json["users"][0]["alterId"] del user_json["users"][0]["security"] user_json["users"][0]["encryption"] = self.node.encryption if self.node.flow: user_json["users"][0]["flow"] = self.node.flow self.client_config["outbounds"][0]["protocol"] = "vless" elif type(self.node) == Socks: self.client_config = self.load_template('client_socks.json') user_json = self.client_config["outbounds"][0]["settings"]["servers"][0] user_json["users"][0]["user"] = self.node.user_info user_json["users"][0]["pass"] = self.node.password elif type(self.node) == SS: self.client_config = self.load_template('client_ss.json') user_json = self.client_config["outbounds"][0]["settings"]["servers"][0] user_json["method"] = self.node.method user_json["password"] = self.node.password elif type(self.node) == Trojan: self.client_config = self.load_template('client_trojan.json') user_json = self.client_config["outbounds"][0]["settings"]["servers"][0] user_json["password"] = self.node.password elif type(self.node) == Mtproto: print("") print("MTProto protocol only use Telegram, and can't generate client json!") print("") exit(-1) try: if isinstance(self.group.port, int): user_json["port"] = self.group.port else: user_json["port"] = int(self.group.port) user_json["address"] = self.group.ip self.client_config["inbounds"][0]["listen"] = self.group.listen self.client_config["inbounds"][0]["port"] = self.group.i_port except: print('数据异常,启动失败') return # inbounds = self.client_config["inbounds"] # for inbound in inbounds: # inbound["listen"] = self.group.listen # if type(self.node) != SS: # self.client_config["outbounds"][0]["streamSettings"] = self.config["inbounds"][self.group.index][ # "streamSettings"] if self.group.tls == 'tls': self.client_config["outbounds"][0]["streamSettings"]["tlsSettings"] = {} elif self.group.tls == 'xtls': self.client_config["outbounds"][0]["streamSettings"]["xtlsSettings"]["serverName"] = self.group.ip del self.client_config["outbounds"][0]["streamSettings"]["xtlsSettings"]["certificates"] del self.client_config["outbounds"][0]["streamSettings"]["xtlsSettings"]["alpn"] del self.client_config["outbounds"][0]["mux"] def write(self): ''' 写客户端配置文件函数 ''' json_dump = json.dumps(self.client_config, indent=1) with open(self.write_path, 'w') as write_json_file: write_json_file.writelines(json_dump) # print("{0}({1})".format("save json success!", self.write_path)) class Creator(object): """ 生成代理json并启动 """ def __init__(self): self.__thread = None self.__main_pid = os.getpid() def parse_vmess(self, vmesslink): """返回:{'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': ''}""" if vmesslink.startswith(ProtocolType.VMESS): bs = vmesslink[len(ProtocolType.VMESS):] # paddings blen = len(bs) if blen % 4 > 0: bs += "=" * (4 - blen % 4) vms = base64.b64decode(bs).decode() return json.loads(vms) else: raise Exception("vmess link invalid") def parse_trojan(self, link): link = urllib.parse.unquote(link) trStr = link[link.find("//") + 2:] password = trStr[:trStr.find('@')] trStr = trStr[trStr.find('@') + 1:] sni = trStr[:trStr.find(':')] trStr = trStr[trStr.find(':') + 1:] port = trStr[:trStr.find('#')] name = trStr[trStr.find('#') + 1:] node = { "name": name, "server": sni, "port": port, "type": "trojan", "password": password, "sni": sni } return node def parse_ss(self, sslink): RETOBJ = { "v": "2", "ps": "", "add": "", "port": "", "id": "", "aid": "", "net": "shadowsocks", "type": "", "host": "", "path": "", "tls": "" } if sslink.startswith(ProtocolType.SS): info = sslink[len(ProtocolType.SS):] if info.rfind("#") > 0: info, _ps = info.split("#", 2) RETOBJ["ps"] = urllib.parse.unquote(_ps) if info.find("@") < 0: # old style link # paddings blen = len(info) if blen % 4 > 0: info += "=" * (4 - blen % 4) info = base64.b64decode(info).decode() atidx = info.rfind("@") method, password = info[:atidx].split(":", 2) addr, port = info[atidx + 1:].split(":", 2) else: atidx = info.rfind("@") addr, port = info[atidx + 1:].split(":", 2) info = info[:atidx] blen = len(info) if blen % 4 > 0: info += "=" * (4 - blen % 4) info = base64.b64decode(info).decode() method, password = info.split(":", 2) RETOBJ["add"] = addr RETOBJ["port"] = port RETOBJ["aid"] = method RETOBJ["id"] = password return RETOBJ def generateAndWrite(self, url: str): # listen="127.0.0.1", group = Group(None, 1024, end_port=None, tls="none", tfo="open", dyp=Dyport(), index=0) if url.startswith(ProtocolType.VMESS): _json = self.parse_vmess(url.strip()) node = Vmess(uuid=_json['id'], alter_id=int(_json['aid']), network=_json['net'], user_number=1, path=_json['path'] if 'path' in _json else None, host=_json['host'], header=None, email=None, quic=None) group.port = _json['port'] group.tls = _json['tls'] group.ip = _json['add'] group.protocol = node.__class__.__name__ group.node = node print(_json) elif url.startswith(ProtocolType.SS): _json = self.parse_ss(url.strip()) # {'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': ''} node = SS(0, _json['id'], _json['aid'], None) group.port = _json['port'] group.tls = _json['tls'] group.ip = _json['add'] group.protocol = node.__class__.__name__ group.node = node print(_json) elif url.startswith(ProtocolType.TROJAN): _json = self.parse_trojan(url.strip()) node = Trojan(0, _json['password'], None) group.port = _json['port'] group.tls = '' group.ip = _json['sni'] group.protocol = node.__class__.__name__ group.node = node print(_json) else: print('无效地址:%s' % url) return cw = ClientWriter(group) cw.transform() cw.write() def __kill_threading(self): pids = os.popen("ps aux |grep v2ray |awk '{print $2}'").read().split('\n') pid_all = [] for pid in pids: temp = pid.strip() if len(temp) > 1 and temp != 'PID' and temp != '0' and temp != str(self.__main_pid) and temp not in pid_all: pid_all.append(temp) for pid in pid_all: try: import subprocess # subprocess.check_output("kill %d" % int(pid)) a = os.popen("kill %d" % int(pid)).read() except Exception as e: pass util.sys_proxy_off() def __child_thread(self, url: str, isSysOn=False): self.generateAndWrite(url) # 执行就可,不需要知道结果 if Config.get_v2ray_core_path() is None: raise Exception('请先调用#Config.set_v2ray_core_path 设置路径') v2ray_path = os.path.join(Config.get_v2ray_core_path(), 'v2ray') config_path = os.path.join(Config.get_v2ray_core_path(), 'config.json') os.popen("%s -config %s >/dev/null 2>&1" % (v2ray_path, config_path)) print("%s -config %s >/dev/null 2>&1" % (v2ray_path, config_path)) if isSysOn: util.sys_v2ray_on() def v2ray_start(self, url: str, isSysOn=False): self.__kill_threading() self.__child_thread(url, isSysOn) def v2ray_start_with_log(self, url: str, isSysOn=False): try: self.v2ray_start(url, isSysOn) except Exception as e: print(e) print("启动异常:%s" % url) return False return True ================================================ FILE: 002-V2rayPool/core/conf.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import configparser import os class Config: __v2ray_core_path = None __v2ray_node_path = None def __init__(self): self.config = configparser.ConfigParser() parent_dir = os.path.dirname(os.path.abspath(__file__)) self.config_path = os.path.join(Config.__v2ray_core_path, 'config.json') self.json_path = os.path.join(parent_dir, 'json_template') # self.config.read(self.config_path) def get_path(self, key): # return self.config.get('path', key) return self.config_path def get_data(self, key): return self.config.get('data', key) def set_data(self, key, value): self.config.set('data', key, value) self.config.write(open(self.config_path, "w")) @staticmethod def set_v2ray_core_path(dir: str): """设置当前v2ray_core程序的目录""" Config.__v2ray_core_path = dir @staticmethod def get_v2ray_core_path(): """获取当前v2ray_core程序的目录""" return Config.__v2ray_core_path @staticmethod def set_v2ray_node_path(dir: str): """设置当前v2ray保存节点的目录""" Config.__v2ray_node_path = dir @staticmethod def get_v2ray_node_path(): """获取当前v2ray保存节点的目录""" return Config.__v2ray_node_path ================================================ FILE: 002-V2rayPool/core/group.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import base64 import json from urllib.parse import quote __author__ = 'qincji' class Dyport(object): def __init__(self, status=False, aid=0): self.status = status self.aid = aid class Quic(object): def __init__(self, security="none", key="", header="none"): self.security = security self.key = key self.header = header class User(object): def __init__(self, user_number, password, user_info=None): """ user_info可能是email, 也可能user_name, 具体取决于group的protocol password: id或者密码 """ self.__password = password self.user_info = user_info self.user_number = user_number @property def password(self): return self.__password class SS(User): def __init__(self, user_number, password, method, user_info): super(SS, self).__init__(user_number, password, user_info) self.method = method def __str__(self): if self.user_info: return "Email: {self.user_info}\nMethod: {self.method}\nPassword: {password}\n".format(self=self, password=self.password) else: return "Method: {self.method}\nPassword: {password}\n".format(self=self, password=self.password) def link(self, ip, port, tls): ss_origin_url = "{0}:{1}@{2}:{3}".format(self.method, self.password, ip, port) return "ss://{}".format(bytes.decode(base64.b64encode(bytes(ss_origin_url, 'utf-8')))) def stream(self): return "shadowsocks" class Trojan(User): def __init__(self, user_number, password, email): super(Trojan, self).__init__(user_number, password, email) def __str__(self): if self.user_info: return "Email: {self.user_info}\nPassword: {password}\n".format(self=self, password=self.password) else: return "Password: {password}\n".format(password=self.password) def link(self, ip, port, tls): return "trojan://{0}@{1}:{2}".format(self.password, ip, port) def stream(self): return "trojan" class Mtproto(User): def __str__(self): if self.user_info: return "Email: {}\nSecret: {}\n".format(self.user_info, self.password) else: return "Secret: {}\n".format(self.password) def link(self, ip, port, tls): return "tg://proxy?server={0}&port={1}&secret={2}".format(ip, port, self.password) def stream(self): return "mtproto" class Socks(User): def __str__(self): return "User: {0}\nPass: {1}\nUDP: true\n".format(self.user_info, self.password) def link(self, ip, port, tls): if tls == "tls": return "HTTPS Socks5 don't support telegram share link" else: return "tg://socks?server={0}&port={1}&user={2}&pass={3}".format(ip, port, self.user_info, self.password) def stream(self): return "socks" class Vless(User): def __init__(self, uuid, user_number, encryption=None, email=None, network=None, path=None, host=None, header=None, flow="", serviceName="", mode=""): super(Vless, self).__init__(user_number, uuid, email) self.encryption = encryption self.path = path self.host = host self.header = header self.network = network self.flow = flow self.serviceName = serviceName self.mode = mode def __str__(self): email = "" if self.user_info: email = "Email: {}".format(self.user_info) result = ''' {email} ID: {password} Encryption: {self.encryption} Network: {network} '''.format(self=self, password=self.password, email=email, network=self.stream()).strip() + "\n" return result def stream(self): if self.network == "ws": return "WebSocket host: {0}, path: {1}".format(self.host, self.path) elif self.network == "tcp": return "tcp" elif self.network == "grpc": return "grpc serviceName: {}, mode: {}".format(self.serviceName, self.mode) elif self.network == "kcp": result = "kcp" if self.header and self.header != 'none': result = "{} {}".format(result, self.header) if self.path != "": result = "{} seed: {}".format(result, self.path) return result def link(self, ip, port, tls): result_link = "vless://{s.password}@{ip}:{port}?encryption={s.encryption}".format(s=self, ip=ip, port=port) if tls == "tls": result_link += "&security=tls" elif tls == "xtls": result_link += "&security=xtls&flow={}".format(self.flow) if self.network == "ws": result_link += "&type=ws&host={0}&path={1}".format(self.host, quote(self.path)) elif self.network == "tcp": result_link += "&type=tcp" elif self.network == "grpc": result_link += "&type=grpc&serviceName={}&mode={}".format(self.serviceName, self.mode) elif self.network == "kcp": result_link += "&type=kcp&headerType={0}&seed={1}".format(self.header, self.path) return result_link class Vmess(User): def __init__(self, uuid, alter_id: int, network: str, user_number, *, path=None, host=None, header=None, email=None, quic=None): super(Vmess, self).__init__(user_number, uuid, email) self.alter_id = alter_id self.network = network self.path = path self.host = host self.header = header self.quic = quic if quic: self.header = quic.header self.host = quic.security self.path = quic.key def stream(self): network = "" if self.network == "quic": network = "Quic\n{}".format(self.quic) elif self.network == "h2": network = "HTTP/2 path: {}".format(self.path) elif self.network == "ws": network = "WebSocket host: {0}, path: {1}".format(self.host, self.path) elif self.network == "tcp": if self.host: network = "tcp host: {0}".format(self.host) else: network = "tcp" elif self.network == "kcp": network = "kcp" if self.header and self.header != 'none': network = "{} {}".format(network, self.header) if self.path != "": network = "{} seed: {}".format(network, self.path) return network def __str__(self): email = "" if self.user_info: email = "Email: {}".format(self.user_info) result = ''' {email} UUID: {uuid} Alter ID: {self.alter_id} Network: {network} '''.format(self=self, uuid=self.password, email=email, network=self.stream()).strip() + "\n" return result def link(self, ip, port, tls): json_dict = { "v": "2", "ps": "", "add": ip, "port": port, "aid": self.alter_id, "type": self.header, "net": self.network, "path": self.path, "host": self.host, "id": self.password, "tls": tls } json_data = json.dumps(json_dict) result_link = "vmess://{}".format(bytes.decode(base64.b64encode(bytes(json_data, 'utf-8')))) return result_link class Group(object): def __init__(self, ip, port: str, *, end_port=None, tfo=None, tls="none", i_port='1080', listen="0.0.0.0", dyp=Dyport(), index=0, tag='A'): self.ip = ip self.port = port self.end_port = end_port self.tag = tag self.node = None self.tfo = tfo self.tls = tls self.dyp = dyp self.protocol = None self.index = index self.listen = listen self.i_port = i_port # port = 101 # key = str(port) if isinstance(port, int) else port # print(key) # if isinstance(key, int): # print('key is int') # if isinstance(key, str): # print('key is str') ================================================ FILE: 002-V2rayPool/core/json_template/client.json ================================================ { "log": { "access": "", "error": "", "loglevel": "info" }, "inbounds": [ { "port": 1080, "listen": "0.0.0.0", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1", "clients": null }, "streamSettings": null }, { "listen": "127.0.0.1", "protocol": "http", "settings": { "timeout": 360 }, "port": "1087" } ], "outbounds": [ { "protocol": "vmess", "settings": { "vnext": [ { "address": "123.ocm", "port": 1234, "users": [ { "id": "cc4f8d5b-967b-4557-a4b6-bde92965bc27", "alterId": 0, "security": "aes-128-gcm" } ] } ] }, "streamSettings": { "security": "", "tlsSettings": {}, "wsSettings": {}, "httpSettings": {}, "network": "tcp", "kcpSettings": {}, "tcpSettings": {}, "quicSettings": {} }, "mux": { "enabled": true } }, { "protocol": "freedom", "settings": { "response": null }, "tag": "direct" } ], "dns": { "servers": [ "8.8.8.8", "8.8.4.4", "localhost" ] }, "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ { "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" }, { "type": "field", "domain": ["geosite:cn"], "outboundTag": "direct" }, { "type": "field", "domain": ["geoip:cn"], "outboundTag": "direct" } ] } } ================================================ FILE: 002-V2rayPool/core/json_template/client_socks.json ================================================ { "log": { "access": "", "error": "", "loglevel": "info" }, "inbounds": [ { "port": 1080, "listen": "0.0.0.0", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1", "clients": null }, "streamSettings": null }, { "listen": "127.0.0.1", "protocol": "http", "settings": { "timeout": 360 }, "port": "1087" } ], "outbounds": [ { "protocol": "socks", "settings": { "servers": [ { "address": "123.ocm", "port": 1234, "users": [ { "user": "hello", "pass": "3.1415" } ] } ] }, "streamSettings": { "security": "", "tlsSettings": {}, "wsSettings": {}, "httpSettings": {}, "network": "tcp", "kcpSettings": {}, "tcpSettings": {}, "quicSettings": {} }, "mux": { "enabled": true } }, { "protocol": "freedom", "settings": { "response": null }, "tag": "direct" } ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ { "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" }, { "type": "field", "domain": ["geosite:cn"], "outboundTag": "direct" }, { "type": "field", "domain": ["geoip:cn"], "outboundTag": "direct" } ] } } ================================================ FILE: 002-V2rayPool/core/json_template/client_ss.json ================================================ { "log": { "access": "", "error": "", "loglevel": "info" }, "inbounds": [ { "port": 1080, "listen": "0.0.0.0", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1", "clients": null }, "streamSettings": null }, { "listen": "127.0.0.1", "protocol": "http", "settings": { "timeout": 360 }, "port": "1087" } ], "outbounds": [ { "protocol": "shadowsocks", "settings": { "servers": [ { "address": "serveraddr.com", "method": "aes-128-gcm", "ota": false, "password": "sspasswd", "port": 1024 } ] } }, { "protocol": "freedom", "settings": { "response": null }, "tag": "direct" } ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ { "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" }, { "type": "field", "domain": ["geosite:cn"], "outboundTag": "direct" }, { "type": "field", "domain": ["geoip:cn"], "outboundTag": "direct" } ] } } ================================================ FILE: 002-V2rayPool/core/json_template/client_trojan.json ================================================ { "log": { "access": "", "error": "", "loglevel": "info" }, "inbounds": [ { "port": 1080, "listen": "0.0.0.0", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1", "clients": null }, "streamSettings": null }, { "listen": "127.0.0.1", "protocol": "http", "settings": { "timeout": 360 }, "port": "1087" } ], "outbounds": [ { "protocol": "trojan", "settings": { "servers": [ { "address": "serveraddr.com", "port": 443, "password": "passwd" } ] }, "streamSettings": { "security": "tls", "network": "tcp" } }, { "protocol": "freedom", "settings": { "response": null }, "tag": "direct" } ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ { "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" }, { "type": "field", "domain": ["geosite:cn"], "outboundTag": "direct" }, { "type": "field", "domain": ["geoip:cn"], "outboundTag": "direct" } ] } } ================================================ FILE: 002-V2rayPool/core/json_template/dyn_port.json ================================================ { "protocol": "vmess", "port": "10000-20000", "tag": "dynamicPort", "settings": { "default": { "alterId": 32 } }, "allocate": { "strategy": "random", "concurrency": 3, "refresh": 5 } } ================================================ FILE: 002-V2rayPool/core/json_template/http.json ================================================ { "network": "tcp", "security": "none", "tlsSettings": {}, "httpSettings": {}, "tcpSettings": { "header": { "type": "http", "request": { "version": "1.1", "method": "GET", "path": [ "/" ], "headers": { "Host": [ "" ], "User-Agent": [ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36", "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" ], "Accept-Encoding": [ "gzip, deflate" ], "Connection": [ "keep-alive" ], "Pragma": "no-cache" } }, "response": { "version": "1.1", "status": "200", "reason": "OK", "headers": { "Content-Type": [ "application/octet-stream", "video/mpeg" ], "Transfer-Encoding": [ "chunked" ], "Connection": [ "keep-alive" ], "Pragma": "no-cache" } } } }, "kcpSettings": {}, "wsSettings": {}, "quicSettings": {} } ================================================ FILE: 002-V2rayPool/core/json_template/http2.json ================================================ { "network": "h2", "security": "tls", "tlsSettings": {}, "tcpSettings": {}, "httpSettings": { "path": "/ray/" }, "kcpSettings": {}, "wsSettings": {}, "quicSettings": {} } ================================================ FILE: 002-V2rayPool/core/json_template/kcp.json ================================================ { "network": "kcp", "security": "none", "tlsSettings": {}, "tcpSettings": {}, "httpSettings": {}, "kcpSettings": { "mtu": 1350, "tti": 50, "uplinkCapacity": 100, "downlinkCapacity": 100, "congestion": false, "readBufferSize": 2, "writeBufferSize": 2, "header": { "type": "none" } }, "wsSettings": {}, "quicSettings": {} } ================================================ FILE: 002-V2rayPool/core/json_template/mtproto.json ================================================ { "mtproto-in": { "tag": "tg-in", "port": 5829, "protocol": "mtproto", "settings": { "users": [{"secret": "b0cbcef5a486d9636472ac27f8e11a9d"}] } }, "mtproto-out": { "tag": "tg-out", "protocol": "mtproto", "settings": {} }, "routing-bind": { "type": "field", "inboundTag": ["tg-in"], "outboundTag": "tg-out" } } ================================================ FILE: 002-V2rayPool/core/json_template/quic.json ================================================ { "network": "quic", "security": "none", "tlsSettings": {}, "tcpSettings": {}, "kcpSettings": {}, "httpSettings": {}, "wsSettings": {}, "quicSettings": { "security": "none", "key": "", "header": { "type": "none" } } } ================================================ FILE: 002-V2rayPool/core/json_template/server.json ================================================ { "log": { "access": "/var/log/v2ray/access.log", "error": "/var/log/v2ray/error.log", "loglevel": "info" }, "inbounds": [ { "port": 999999999, "protocol": "vmess", "settings": { "clients": [ { "id": "cc4f8d5b-967b-4557-a4b6-bde92965bc27", "alterId": 0 } ] }, "streamSettings": { "security": "none", "tlsSettings": {}, "wsSettings": {}, "httpSettings": {}, "network": "", "kcpSettings": {}, "tcpSettings": {}, "quicSettings": {} } } ], "outbounds": [ { "protocol": "freedom", "settings": {} }, { "protocol": "blackhole", "settings": {}, "tag": "blocked" } ], "routing": { "rules": [ { "type": "field", "ip": [ "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "::1/128", "fc00::/7", "fe80::/10" ], "outboundTag": "blocked" } ] } } ================================================ FILE: 002-V2rayPool/core/json_template/socks.json ================================================ { "auth": "password", "accounts": [ { "user": "hello", "pass": "socks" } ], "udp": true } ================================================ FILE: 002-V2rayPool/core/json_template/ss.json ================================================ { "port": 1024, "protocol": "shadowsocks", "settings": { "method": "aes-128-gcm", "password": "password", "network":"tcp,udp" } } ================================================ FILE: 002-V2rayPool/core/json_template/stats_settings.json ================================================ { "stats": {}, "api": { "services": [ "StatsService" ], "tag": "api" }, "policy": { "levels": { "0": { "statsUserDownlink": true, "statsUserUplink": true } }, "system": { "statsInboundUplink": true, "statsInboundDownlink": true } }, "routingRules": { "inboundTag": [ "api" ], "outboundTag": "api", "type": "field" }, "dokodemoDoor": { "listen": "127.0.0.1", "port": 10085, "protocol": "dokodemo-door", "settings": { "address": "127.0.0.1" }, "tag": "api" } } ================================================ FILE: 002-V2rayPool/core/json_template/tcp.json ================================================ { "network": "tcp", "security": "none", "tlsSettings": {}, "tcpSettings": {}, "kcpSettings": {}, "wsSettings": {}, "httpSettings": {}, "quicSettings": {}, "grpcSettings": {} } ================================================ FILE: 002-V2rayPool/core/json_template/vless.json ================================================ { "clients": [ { "id": "d4e321ea-e118-11ea-a265-42010a8c0002" } ], "decryption": "none", "fallbacks": [ { "dest": 80 } ] } ================================================ FILE: 002-V2rayPool/core/json_template/ws.json ================================================ { "network": "ws", "security": "none", "tlsSettings": {}, "tcpSettings": {}, "kcpSettings": {}, "httpSettings": {}, "wsSettings": { "path": "", "headers": { "Host": "" } }, "quicSettings": {} } ================================================ FILE: 002-V2rayPool/core/profile.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import json import os from conf import Config from group import SS, Socks, Vmess, Vless, Mtproto, Quic, Group, Dyport, Trojan class Stats: def __init__(self, status=False, door_port=0): self.status = status self.door_port = door_port def __str__(self): return "open" if self.status else "close" class Profile: def __init__(self): self.path = Config().get_path('config_path') self.group_list = [] self.stats = None self.ban_bt = False self.user_number = 0 self.network = "ipv4" self.modify_time = os.path.getmtime(self.path) self.read_json() def __str__(self): result = "" for group in self.group_list: result = "{}{}".format(result, group) result = result + "Tip: The same group's node protocol, port, tls are the same." return result def read_json(self): with open(self.path, 'r') as json_file: self.config = json.load(json_file) # 读取配置文件大框架 conf_inbounds = self.config["inbounds"] conf_rules = self.config["routing"]["rules"] stats = Stats() if "stats" in self.config: stats.status = True for inbound in conf_inbounds: if "protocol" in inbound and inbound["protocol"] == "dokodemo-door": stats.door_port = inbound["port"] break self.stats = stats for rule in conf_rules: if "protocol" in rule and "bittorrent" in rule["protocol"]: self.ban_bt = True # local_ip = get_ip() local_ip = '' if ":" in local_ip: self.network = "ipv6" group_ascii = 64 # before 'A' ascii code for index, json_part in enumerate(conf_inbounds): group = self.parse_group(json_part, index, local_ip) if group != None: group_ascii = group_ascii + 1 if group_ascii > 90: group.tag = str(group_ascii) else: group.tag = chr(group_ascii) self.group_list.append(group) del self.config def parse_group(self, part_json, group_index, local_ip): dyp, quic, end_port, tfo, header, tls, path, host, conf_ip, serviceName, mode = Dyport(), None, None, None, "", "", "", "", local_ip, "", "gun" protocol = part_json["protocol"] if protocol == 'dokodemo-door' or (protocol == "vmess" and "streamSettings" not in part_json): return conf_settings = part_json["settings"] port_info = str(part_json["port"]).split("-", 2) if "domain" in part_json and part_json["domain"]: conf_ip = part_json["domain"] if len(port_info) == 2: port, end_port = port_info else: port = port_info[0] if "detour" in conf_settings: dynamic_port_tag = conf_settings["detour"]["to"] for inbound in self.config["inbounds"]: if "tag" in inbound and inbound["tag"] == dynamic_port_tag: dyp.aid = inbound["settings"]["default"]["alterId"] dyp.status = True break if protocol in ("vmess", "vless", "socks", "trojan"): conf_stream = part_json["streamSettings"] tls = conf_stream["security"] if "sockopt" in conf_stream and "tcpFastOpen" in conf_stream["sockopt"]: tfo = "open" if conf_stream["sockopt"]["tcpFastOpen"] else "close" if "httpSettings" in conf_stream and conf_stream["httpSettings"]: path = conf_stream["httpSettings"]["path"] elif "wsSettings" in conf_stream and conf_stream["wsSettings"]: host = conf_stream["wsSettings"]["headers"]["Host"] path = conf_stream["wsSettings"]["path"] elif "tcpSettings" in conf_stream and conf_stream["tcpSettings"]: host = conf_stream["tcpSettings"]["header"]["request"]["headers"]["Host"] header = "http" if conf_stream["network"] == "kcp" and "header" in conf_stream["kcpSettings"]: header = conf_stream["kcpSettings"]["header"]["type"] if "seed" in conf_stream["kcpSettings"]: path = conf_stream["kcpSettings"]["seed"] if conf_stream["network"] == "quic" and conf_stream["quicSettings"]: quic_settings = conf_stream["quicSettings"] quic = Quic(quic_settings["security"], quic_settings["key"], quic_settings["header"]["type"]) if conf_stream["network"] == "grpc" and conf_stream["grpcSettings"]: serviceName = conf_stream["grpcSettings"]["serviceName"] if "multiMode" in conf_stream["grpcSettings"] and conf_stream["grpcSettings"]["multiMode"]: mode = "multi" group = Group(conf_ip, port, end_port=end_port, tls=tls, tfo=tfo, dyp=dyp, index=group_index) if protocol == "shadowsocks": self.user_number = self.user_number + 1 email = conf_settings["email"] if 'email' in conf_settings else '' ss = SS(self.user_number, conf_settings["password"], conf_settings["method"], email) group.node = ss group.protocol = ss.__class__.__name__ return group elif protocol in ("vmess", "vless", "trojan"): clients = conf_settings["clients"] elif protocol == "socks": clients = conf_settings["accounts"] elif protocol == "mtproto": clients = conf_settings["users"] for client in clients: email, node, flow = "", None, "" self.user_number = self.user_number + 1 if "email" in client and client["email"]: email = client["email"] if protocol == "vmess": node = Vmess(client["id"], client["alterId"], conf_stream["network"], self.user_number, path=path, host=host, header=header, email=email, quic=quic) elif protocol == "socks": node = Socks(self.user_number, client["pass"], user_info=client["user"]) elif protocol == "mtproto": node = Mtproto(self.user_number, client["secret"], user_info=email) elif protocol == "vless": if tls == "xtls": flow = client["flow"] node = Vless(client["id"], self.user_number, conf_settings["decryption"], email, conf_stream["network"], path, host, header, flow, serviceName, mode) elif protocol == "trojan": node = Trojan(self.user_number, client["password"], email) if not group.protocol: group.protocol = node.__class__.__name__ group.node = node return group ================================================ FILE: 002-V2rayPool/core/utils.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import random import re import os import socket import string import sys import termios import tty import urllib.request import signal import subprocess from enum import Enum, unique class ProtocolType(object): TROJAN = 'trojan://' SS = 'ss://' VMESS = 'vmess://' @unique class StreamType(Enum): TCP = 'tcp' TCP_HOST = 'tcp_host' SOCKS = 'socks' SS = 'ss' MTPROTO = 'mtproto' H2 = 'h2' WS = 'ws' QUIC = 'quic' KCP = 'kcp' KCP_UTP = 'utp' KCP_SRTP = 'srtp' KCP_DTLS = 'dtls' KCP_WECHAT = 'wechat' KCP_WG = 'wireguard' VLESS_KCP = 'vless_kcp' VLESS_UTP = 'vless_utp' VLESS_SRTP = 'vless_srtp' VLESS_DTLS = 'vless_dtls' VLESS_WECHAT = 'vless_wechat' VLESS_WG = 'vless_wireguard' VLESS_TCP = 'vless_tcp' VLESS_TLS = 'vless_tls' VLESS_WS = 'vless_ws' VLESS_GRPC = 'vless_grpc' VLESS_XTLS = 'vless_xtls' TROJAN = 'trojan' def header_type_list(): return ("none", "srtp", "utp", "wechat-video", "dtls", "wireguard") def ss_method(): return ("aes-256-gcm", "aes-128-gcm", "chacha20-poly1305") def xtls_flow(): return ("", "xtls-rprx-origin", "xtls-rprx-direct") def get_ip(): """ 获取本地ip """ my_ip = "" try: my_ip = urllib.request.urlopen('http://api.ipify.org').read() except Exception: my_ip = urllib.request.urlopen('http://icanhazip.com').read() return bytes.decode(my_ip).strip() def port_is_use(port): """ 判断端口是否占用 """ tcp_use, udp_use = False, False s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(3) tcp_use = s.connect_ex(('127.0.0.1', int(port))) == 0 try: u.bind(('127.0.0.1', int(port))) except: udp_use = True finally: u.close() return tcp_use or udp_use def random_port(start_port, end_port): while True: random_port = random.randint(start_port, end_port) if not port_is_use(random_port): return random_port def is_email(email): """ 判断是否是邮箱格式 """ str = r'^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}$' return re.match(str, email) def is_ipv4(ip): try: socket.inet_pton(socket.AF_INET, ip) except AttributeError: # no inet_pton here, sorry try: socket.inet_aton(ip) except socket.error: return False return ip.count('.') == 3 except socket.error: # not a valid ip return False return True def is_ipv6(ip): try: socket.inet_pton(socket.AF_INET6, ip) except socket.error: # not a valid ip return False return True def check_ip(ip): return is_ipv4(ip) or is_ipv6(ip) def bytes_2_human_readable(number_of_bytes, precision=1): """ 流量bytes转换为kb, mb, gb等单位 """ if number_of_bytes < 0: raise ValueError("!!! number_of_bytes can't be smaller than 0 !!!") step_to_greater_unit = 1024. number_of_bytes = float(number_of_bytes) unit = 'bytes' if (number_of_bytes / step_to_greater_unit) >= 1: number_of_bytes /= step_to_greater_unit unit = 'KB' if (number_of_bytes / step_to_greater_unit) >= 1: number_of_bytes /= step_to_greater_unit unit = 'MB' if (number_of_bytes / step_to_greater_unit) >= 1: number_of_bytes /= step_to_greater_unit unit = 'GB' if (number_of_bytes / step_to_greater_unit) >= 1: number_of_bytes /= step_to_greater_unit unit = 'TB' number_of_bytes = round(number_of_bytes, precision) return str(number_of_bytes) + ' ' + unit def random_email(): domain = ['163', 'qq', 'sina', '126', 'gmail', 'outlook', 'icloud'] core_email = "@{}.com".format(random.choice(domain)) return ''.join(random.sample(string.ascii_letters + string.digits, 8)) + core_email def readchar(prompt=""): if prompt: sys.stdout.write(prompt) sys.stdout.flush() fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) print(ch) return ch.strip() def kill_all_v2ray(): pids = os.popen("ps aux |grep v2ray |awk '{print $2}'").read().split('\n') for pid in pids: try: import subprocess # subprocess.check_output("kill %d" % int(pid)) a = os.popen("kill %d" % int(pid)).read() except Exception as e: pass sys_proxy_off() # netstat -nlp | grep :1080 | awk '{print $7}' | awk -F\" / \" '{ print $1 }' def kill_process_by_port(port): try: pids = os.popen("pgrep -f v2ray|xargs kill -9").read().split('\n') print(pids) except: pass def sys_proxy_on(proxy, port): ''''控制macOS系统代理''' os.system('networksetup -setwebproxy wi-fi %s %d' % (proxy, port)) # http os.system('networksetup -setsecurewebproxy wi-fi %s %d' % (proxy, port)) # https os.system('networksetup -setsocksfirewallproxy wi-fi %s %d' % (proxy, port)) # socks def sys_v2ray_on(): # proxy_on("127.0.0.1", 1080) '''端口要对应起v2ray开启的,具体要看写入config.json文件中inbounds节点部分''' os.system('networksetup -setwebproxy wi-fi 127.0.0.1 1087') os.system('networksetup -setsecurewebproxy wi-fi 127.0.0.1 1087') os.system('networksetup -setsocksfirewallproxy wi-fi 127.0.0.1 1080') def sys_proxy_off(): os.system('networksetup -setwebproxystate wi-fi off') os.system('networksetup -setsecurewebproxystate wi-fi off') os.system('networksetup -setsocksfirewallproxystate wi-fi off') ================================================ FILE: 002-V2rayPool/db/db_main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 数据相关接口封装 @Date :2021/08/30 @Author :xhunmon @Mail :xhunmon@gmail.com """ import random from core import client from db.local import DbLocal, DbEnable from db.net import * class DBManage(object): def init(self): self.dbLocal = DbLocal() self.check = PYCheck() self.dbEnable = DbEnable().get() def __add_urls_de_dup(self, all_urls: [], new_urls: []) -> []: """合并数组,并且去重,去空""" if not new_urls: # 为 [] 或者 None return all_urls for url in new_urls: temp = url.strip().replace('\n', '') if temp in all_urls or len(temp) < 20: continue all_urls.append(temp) def start_random_v2ray_by_local(self, isSysOn=False): """从本地随机启动一个可用的proxy""" urls = self.load_enable_urls_by_local() for url in urls: if client.Creator().v2ray_start_with_log(random.choice(urls), isSysOn) is False: time.sleep(1) continue time.sleep(2) ips = PYCheck().get_curren_ip() if not ips: print('无效地址:%s' % url) continue print('代理开启成功') time.sleep(1) return True return False def load_urls_and_save_auto(self): """首先通过不需要代理的网页获取节点,当代理有可用时,开启代理,获取需要代理获取的网页""" self.dbLocal.clear_local() all_urls = self.load_urls_by_not_proxy() proxy_url = None for url in all_urls: if client.Creator().v2ray_start_with_log(url) is False: time.sleep(1) continue time.sleep(2) ips = PYCheck().get_curren_ip() if not ips: print('无效地址:%s' % url) continue proxy_url = url break if proxy_url is None: raise Exception("无代理可用,退出!") print("获得可用代理地址:%s" % proxy_url) proxy_urls = self.load_urls_by_net_with_proxy(proxy_url=proxy_url) all_urls = all_urls + proxy_urls self.check_and_save(all_urls, append=False) def load_urls_by_not_proxy(self, save_local=True): all_urls = [] # 1. 先把不需要代理的先请求下来 self.__add_urls_de_dup(all_urls, PNTWGithubV2ray().get_urls()) print("获取https://hub.xn--gzu630h.xn--kpry57d/freefq/free后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNSsfree().get_urls()) print("获取https://view.ssfree.ru/后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNFreevpnX().get_urls()) print("获取https://freevpn-x.com/后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNGithubIwxf().get_urls()) print("获取https://github.com/iwxf/free-v2ray/blob/master/README.md 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNFreeV2ray().get_urls()) print("获取https://view.freev2ray.org/ 后数目:%d" % len(all_urls)) if save_local: # 保存到本地 self.dbLocal.save_urls(all_urls) return all_urls def load_urls_by_net_with_proxy(self, proxy_url=None, save_local=True): all_urls = [] if save_local: # 保存到本地 self.dbLocal.save_urls(all_urls) if not proxy_url: 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' creator = client.Creator() creator.v2ray_start(proxy_url) time.sleep(2) self.__add_urls_de_dup(all_urls, PYIvmess().get_urls()) print("获取https://t.me/s/ivmess 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PYFlyingboat().get_urls()) print("获取https://t.me/s/flyingboat 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PYFreevpnnet().get_urls()) print("获取https://www.freevpnnet.com/ 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PYMerlinblog().get_urls()) print("获取https://merlinblog.xyz/wiki/freess.html 后数目:%d" % len(all_urls)) # __add_urls_de_dup(all_urls, PYFreeFq().get_urls()) self.__add_urls_de_dup(all_urls, PYFreeFq().download_urls(f_day=1)) # 前2天的地址 print("获取从https://freefq.com/ 后数目:%d" % len(all_urls)) if save_local: # 保存到本地 self.dbLocal.save_urls(all_urls) return all_urls def load_urls_by_net(self, proxy_url=None, save_local=True, need_proxy=True): """ 通过网络获取最新的节点,但是需要代理 :param proxy_url: 代理url,默认的如果失效了则回去失败 :param save_local: 是否保存到本地 :param need_proxy: 如果程序本身就在外网跑,就不需要开启代理获取了 :return: """ all_urls = [] # 1. 先把不需要代理的先请求下来 self.__add_urls_de_dup(all_urls, PNTWGithubV2ray().get_urls()) print("获取https://hub.xn--gzu630h.xn--kpry57d/freefq/free后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNSsfree().get_urls()) print("获取https://view.ssfree.ru/后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNFreevpnX().get_urls()) print("获取https://freevpn-x.com/后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNGithubIwxf().get_urls()) print("获取https://github.com/iwxf/free-v2ray/blob/master/README.md 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PNFreeV2ray().get_urls()) print("获取https://view.freev2ray.org/ 后数目:%d" % len(all_urls)) print("准备开启代理获取...") if need_proxy: if not proxy_url: 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' # 需要代理 creator = client.Creator() creator.v2ray_start(proxy_url) time.sleep(2) self.__add_urls_de_dup(all_urls, PYIvmess().get_urls()) print("获取https://t.me/s/ivmess 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PYFlyingboat().get_urls()) print("获取https://t.me/s/flyingboat 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PYFreevpnnet().get_urls()) print("获取https://www.freevpnnet.com/ 后数目:%d" % len(all_urls)) self.__add_urls_de_dup(all_urls, PYMerlinblog().get_urls()) print("获取https://merlinblog.xyz/wiki/freess.html 后数目:%d" % len(all_urls)) # __add_urls_de_dup(all_urls, PYFreeFq().get_urls()) self.__add_urls_de_dup(all_urls, PYFreeFq().download_urls(f_day=1)) # 前2天的地址 print("获取从https://freefq.com/ 后数目:%d" % len(all_urls)) if save_local: # 保存到本地 self.dbLocal.save_urls(all_urls, append=False) return all_urls def load_unchecked_urls_by_local(self): """获取本地未校验过的url""" self.dbLocal.get_urls(False) urls = self.dbLocal.get_checked_urls() return urls def load_enable_urls_by_local(self): """获取已检测过的url""" return self.dbEnable.get_urls() def check_url_single(self, url: str): client.Creator().v2ray_start(url) time.sleep(2) ips = PYCheck().get_curren_ip() if not ips: print('地址无效!') return False print('检查地址结果:%s' % url) print(ips) return True def check_and_save(self, urls: [], append=True): """检测url是否可用,并且保存到本地""" if not append: self.dbEnable.clear_local() all_infos = self.dbEnable.get_infos() new_infos = [] size = len(urls) for i in range(size): try: if i % 30 == 0: # 每三十个更新一次 self.dbLocal.save_urls(urls=urls, append=False) # 更新剩下的 url = urls.pop() in_all = False for item in all_infos: if url in item: in_all = True break if in_all: print('取出地址已存在:%s' % url) continue if client.Creator().v2ray_start_with_log(url) is False: time.sleep(1) continue time.sleep(2) ips = PYCheck().get_curren_ip() if not ips: print('地址无效!') continue ip, add = str(ips[0]), ips[1] hase_item = False # 已存在 for item in all_infos: if ip in item: hase_item = True break if hase_item: print('ip=%s已存在!' % ip) continue info = r'%s,%s,%s' % (url.strip(), ip.strip(), add.strip().replace('\n', '')) print('%s!总共:%d |待检测:%d |可用:%d' % ('地址有效', size, len(urls), len(all_infos))) new_infos.append(info) all_infos.append(info) self.dbEnable.save_urls(new_infos) # 写入已通过的 new_infos.clear() except Exception as e: print(e) # 最后 print('%s!总共:%d |待检测:%d |可用:%d' % ('全部检测完毕!', size, len(urls), len(all_infos))) self.dbEnable.save_urls(new_infos) self.dbLocal.clear_local() ================================================ FILE: 002-V2rayPool/db/local.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 数据库相关类 @Date :2021/08/25 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os import shutil from concurrent.futures import ThreadPoolExecutor from threading import Lock from core.conf import Config class DbLocal(object): """ 加载本地文件的url,并进行检测是否合法 """ def __init__(self): path = Config.get_v2ray_node_path() parent_dir = os.path.dirname(os.path.abspath(__file__)) if path: parent_dir = path if not os.path.exists(parent_dir): os.mkdir(parent_dir) self.__save_path = os.path.join(parent_dir, '_db-uncheck.txt') self.__get_path = os.path.join(parent_dir, '_db-uncheck.txt') if not os.path.isfile(self.__save_path): open(self.__save_path, mode='w', encoding="utf-8").write('') if not os.path.isfile(self.__get_path): open(self.__get_path, mode='w', encoding="utf-8").write('') self.__reset() def __reset(self): """恢复默认状态""" self.__checked_urls = [] self.__load_urls = [] self.__checked_finish = False self.__checked_count = 0 def __check_url(self, url: str): """在异步中检测url是否合法等""" self.__checked_count += 1 # print('check_url: %s' % url) # 如果检测通过 temp = url.strip() if len(temp) > 10 and (temp.startswith('ss://') or temp.startswith('vmess://')): self.__checked_urls.append(temp) if len(self.__load_urls) == self.__checked_count: self.__checked_finish = True print('已经完成检测') def is_checked_finished(self) -> bool: """是否已经完成检测""" return self.__checked_finish def get_checked_urls(self) -> []: """已通过检测的url""" return self.__checked_urls def get_urls(self, is_check=True) -> bool: """ 开始加载本地的链接,异步处理结果。 :param get_path: 本地路径 :return: True:加载成功 """ self.__reset() get_path = self.__get_path if not os.path.isfile(get_path): return False try: with open(get_path, mode='r') as f: for url in f: if url and url not in self.__load_urls: self.__load_urls.append(url) except Exception as e: print(e) finally: f.close() size = len(self.__load_urls) if size == 0: print('本地无数据') return False if is_check: # 创建线程池,传入max_workers参数来设置线程池中最多能同时运行的线程数目 executor = ThreadPoolExecutor(max_workers=3) for url in self.__load_urls: executor.submit(self.__check_url, url).done() else: self.__checked_urls = self.__load_urls self.__checked_finish = True print("已加载数目:%d" % size) return True def clear_local(self): """通过写入空字符实现清除内容""" try: with open(self.__save_path, mode='w', encoding="utf-8") as f: f.write('') f.close() except Exception as e: print(e) def save_urls(self, urls, append=True): """ 保存节点到本地 :param urls: :param append: 添加到末尾 :return: """ if not urls: return all_url = [] if not append: # 清空之后再继续 self.clear_local() all_url = urls else: # 把本地的取出来,然后再进行去重 for url in urls: if url not in all_url: all_url.append(url) try: with open(self.__save_path, mode='r') as f: for url in f: if url and url not in all_url: all_url.append(url) except Exception as e: print(e) finally: f.close() size = len(all_url) print('目前本地总共%d条' % size) try: self.clear_local() with open(self.__save_path, mode='a', encoding="utf-8") as f: for url in all_url: url = url.strip().replace('\n', '') if len(url) > 20: f.write(url + '\n') f.close() except Exception as e: print(e) class DbEnable(object): """ 单例模式,提供可用的v2ray对象 """ _instance_lock = Lock() def __init__(self): self.__urls = [] self.__index = 0 self.__END = '.back' self.__mutex = Lock() self.__default_url = 'ss://YWVzLTI1Ni1nY206WWd1c0gyTVdBOFBXYzNwMlZEc1I3QVZ2@81.19.223.189:31764#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%208' self.__config_path = '' path = Config.get_v2ray_node_path() # 获取本地文件实例? parent_dir = os.path.dirname(os.path.abspath(__file__)) if path: parent_dir = path if not os.path.exists(parent_dir): os.mkdir(parent_dir) self.__path = os.path.join(parent_dir, '_db-checked.txt') if not os.path.isfile(self.__path): open(self.__path, mode='w', encoding="utf-8").write('') @classmethod def get(cls, *args, **kwargs): with DbEnable._instance_lock: if not hasattr(DbEnable, "_instance"): DbEnable._instance = DbEnable(*args, **kwargs) return DbEnable._instance def init(self, config_path, def_url=None): """ 初始化所需要参数 :param config_path: 配置文件路径 :param def_url: 默认使用的 v2ray链接 :return: """ if def_url is not None: self.__default_url = def_url if len(config_path) < len('(参考用)config.json'): return False if not os.path.isfile(config_path): return False self.__config_path = config_path return self.__back_config_json() def __back_config_json(self): """备份配置文件""" if not os.path.isfile(self.__config_path): return False back_path = self.__config_path + self.__END try: shutil.copy(self.__config_path, back_path) except Exception as e: print(e) return False return True def __restore_config_json(self): """恢复配置文件""" back_path = self.__config_path + self.__END if not os.path.isfile(back_path): return False try: if os.path.isfile(self.__config_path): os.remove(self.__config_path) shutil.copy(back_path, self.__config_path) except Exception as e: print(e) return False return True def add_url(self, url: str): self.__add(url) def add_urls(self, url: []): self.__adds(url) def select_by_order(self): """选择下一个代理,并且更新配置文件,让他起作用""" url = self.get_by_order() return True def get_by_order(self): """从队列中顺序获取一个v2ray协议实例""" def_url = self.__default_url if self.__check_and_enable(): def_url = self.__get(self.__index) return def_url def __check_and_enable(self): """检查是否已到结束,如果是,下一个从0开始""" if len(self.__urls) == 0: return False if len(self.__urls) <= self.__index + 1: self.__index = -1 self.__index += 1 return True def __add(self, url): self.__mutex.acquire() self.__urls.append(url) self.__mutex.release() def __adds(self, urls: []): self.__mutex.acquire() self.__urls.extend(urls) self.__mutex.release() def __get(self, index): self.__mutex.acquire() url = self.__urls[index] self.__mutex.release() return url def clear_local(self): """通过写入空字符实现清除内容""" try: with open(self.__path, mode='w', encoding="utf-8") as f: f.write('') f.close() except Exception as e: print(e) def get_urls(self) -> []: """获取截取后的url""" urls = [] try: with open(self.__path, mode='r') as f: for url in f: url = url.strip().replace('\n', '') if len(url) > 20: urls.append(url.split(',')[0]) except Exception as e: print(e) return urls def get_infos(self) -> []: """获取所有信息,包括ip和地址""" infos = [] try: with open(self.__path, mode='r') as f: for info in f: info = info.strip().replace('\n', '') if len(info) > 20: infos.append(info) except Exception as e: print(e) return infos def de_duplication(self): """去重""" infos = self.get_infos() new_infos = [] for info in infos: if len(info.strip()) <= 0: # 去空行 continue _in = False for n in new_infos: if info.split(',')[1] in n: _in = True break if not _in: new_infos.append(info) print('去重前数目:%d,去重后的数目:%d' % (len(infos), len(new_infos))) try: self.clear_local() with open(self.__path, mode='a', encoding="utf-8") as f: for url in new_infos: url = url.strip().replace('\n', '') if len(url) > 20: f.write(url + '\n') f.close() except Exception as e: print(e) def save_urls(self, urls, append=True): """ 保存节点到本地 :param urls: :param append: 添加到末尾 :return: """ if not urls: return all_url = [] if not append: # 清空之后再继续 self.clear_local() all_url = urls else: # 把本地的取出来,然后再进行去重 for url in urls: if url not in all_url: all_url.append(url) try: with open(self.__path, mode='r') as f: for url in f: if url and url not in all_url: all_url.append(url) except Exception as e: print(e) finally: f.close() size = len(all_url) print('目前本地总共已检测可用%d条' % size) try: self.clear_local() with open(self.__path, mode='a', encoding="utf-8") as f: for url in all_url: url = url.strip().replace('\n', '') if len(url) > 20: f.write(url + '\n') f.close() except Exception as e: print(e) ================================================ FILE: 002-V2rayPool/db/net.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 从网络获取 @Date :2021/08/30 @Author :xhunmon @Mail :xhunmon@gmail.com """ import json from base.net_proxy import Net import re import time import chardet def re_vmess_ss_trojan(pattern, html) -> []: """传入规则:r'xxx%sxxx', %s为固定的:(vmess://.+?|ss://.+?|trojan://.+?)""" results = re.findall(pattern % '(vmess://.+?|ss://.+?|trojan://.+?)', html, re.DOTALL) urls = [] for i in results: temp: str = i.strip() if len(temp) < 20 and temp in results: # 凭感觉 continue # 如果是换行的 if '\n' in temp: items = temp.split('\n') for j in items: temp_j: str = j.strip() if len(temp_j) < 20 and temp_j in results: # 凭感觉 continue value_j = temp_j.replace('\n', '') if len(value_j) > 10: urls.append(value_j) continue value = temp.replace('\n', '') if len(value) > 10: urls.append(value) return urls class PYCheck(Net): def get_curren_ip(self, url='https://ip.cn/api/index?ip=&type=0'): """获取内容""" try: r = self.request_zh(url) if r.status_code == 200: charset = chardet.detect(r.content) content = r.content.decode(charset['encoding']) r.encoding = r.apparent_encoding # {"rs":1,"code":0,"address":"德国 Hessen ","ip":"51.38.122.98","isDomain":0} results = json.loads(content) if not results: return None return [results['ip'], results['address']] elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(1) return self.get_curren_ip(location) except Exception as e: print(e) return None class PNFreeV2ray(Net): """ https://view.freev2ray.org/ """ def get_urls(self) -> []: try: r = self.request(r'https://view.freev2ray.org/') if r.status_code != 200: return None r.encoding = r.apparent_encoding return re_vmess_ss_trojan(r'"%s"', r.text) except Exception as e: print(e) return None class PNTWGithubV2ray(Net): """ # https://hub.xn--gzu630h.xn--kpry57d/freefq/free """ def get_urls(self) -> []: try: r = self.request(r'https://hub.xn--gzu630h.xn--kpry57d/freefq/free') if r.status_code != 200: return None r.encoding = r.apparent_encoding return re_vmess_ss_trojan(r'"%s"', r.text) except Exception as e: print(e) return None class PNSsfree(Net): """ https://view.ssfree.ru/ """ def get_urls(self) -> []: try: r = self.request(r'https://view.ssfree.ru/') if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text return re_vmess_ss_trojan(r'"%s"', html) except Exception as e: print(e) return None class PNFreevpnX(Net): """ https://freevpn-x.com/ """ def get_urls(self) -> []: """ 获取当前页面中文本的url :param date: 如:2021/08/29 :return: """ url2 = r'https://url.cr/api/user.ashx?do=freevpn&ip=127.0.0.1&uuid=C5E0C9BA-FECB-44ED-9BD8-90C55365E11B&_=%d' % ( time.time() * 1000) try: r = self.request(url2) if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text results = html.split('\n') urls = [] for i in results: temp = i.strip() if len(temp) > 10: urls.append(temp) return urls except Exception as e: print(e) return None class PNGithubIwxf(Net): """ https://github.com/iwxf/free-v2ray/blob/master/README.md """ def get_urls(self) -> []: """ 获取当前页面中文本的url :param date: 如:2021/08/29 :return: """ try: r = self.request(r'https://github.com/iwxf/free-v2ray/blob/master/README.md') if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text return re_vmess_ss_trojan(r'
%s
', html) except Exception as e: print(e) return None class PYFreevpnnet(Net): """ https://www.freevpnnet.com/ 需要代理 """ def get_urls(self) -> []: try: r = self.request_en(r'https://www.freevpnnet.com/') if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text return re_vmess_ss_trojan(r'>%s<', html) except Exception as e: print(e) return None class PYMerlinblog(Net): """ https://merlinblog.xyz/wiki/freess.html 需要代理 """ def get_urls(self) -> []: try: r = self.request_en(r'https://merlinblog.xyz/wiki/freess.html') if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text return re_vmess_ss_trojan('%s<', html) except Exception as e: print(e) return None class PYFlyingboat(Net): """ https://t.me/s/flyingboat 需要代理 """ def get_urls(self) -> []: try: r = self.request_en(r'https://t.me/s/flyingboat') if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text return re_vmess_ss_trojan('>%s<', html) except Exception as e: print(e) return None class PYIvmess(Net): """ https://t.me/s/ivmess 需要代理 """ def get_urls(self) -> []: try: r = self.request_en(r'https://t.me/s/ivmess') if r.status_code != 200: return None r.encoding = r.apparent_encoding html = r.text return re_vmess_ss_trojan('>%s<', html) except Exception as e: print(e) return None class PYFreeFq(Net): """ 从https://freefq.com/获取免费节点,规则:https://freefq.com/v2ray/2021/08/30/v2ray.html """ def __get_content_url(self, date) -> str: """ 获取当前页面中文本的url :param date: 如:2021/08/29 :return: """ try: r = self.request_en(r'https://freefq.com/v2ray/%s/v2ray.html' % date) if r.status_code != 200: return None r.encoding = r.apparent_encoding return self.__get_url_by_content_html(r.text) except Exception as e: print(e) return None def __get_url_by_content_html(self, html: str): """从文本中通过正则获取节点所在文本的url""" results = re.findall(r'.+?', html) tag = None pre = 'https://www.freefq.com/d/file/v2ray' for url in results: if pre in url: tag = url break if not tag: print('无法获取节点url:%s' % results) return None try: url = re.findall(r'%s.+?\.htm' % pre, tag)[0] except Exception as e: print(e) return None return url def __get_url_by_detail_html(self, html: str): """截取文本中的节点url""" # results = re.findall(r'(trojan://.+?|vmess://.+?|ss://.+?)
', html) results = re_vmess_ss_trojan(r'%s
', html) urls = [] for url in results: temp: str = url.strip() if len(temp) < 20 and temp in results: # 凭感觉 continue urls.append(url) return urls def __get_detail_urls(self, url: str) -> []: """获取内容""" try: r = self.request_en(url) if r.status_code != 200: return None r.encoding = r.apparent_encoding # return self.__get_url_by_detail_html(r.text) return re_vmess_ss_trojan(r'%s
', r.text) except Exception as e: print(e) return None def download_urls(self, f_day=1) -> []: """ 开始获取 :param f_day: 需要获取往前的天数 :return: """ DAY = 24 * 60 * 60 l_time = time.time() all_url = [] for day in range(1, f_day + 1): # 需要从前一天开始 temp_day = time.strftime("%Y/%m/%d", time.localtime(l_time - (day * DAY))) url = self.__get_content_url(temp_day) if not url: continue urls = self.__get_detail_urls(url) if not urls: # 如果是None或者[] continue for temp in urls: if temp not in all_url: all_url.append(temp) time.sleep(1) return all_url def get_urls(self) -> []: return self.download_urls() ================================================ FILE: 002-V2rayPool/doc/(参考用)config.json ================================================ { "log": { "access": "", "error": "", "loglevel": "info" }, "inbounds": [ { "port": "1080", "listen": "0.0.0.0", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1", "clients": null }, "streamSettings": null }, { "listen": "127.0.0.1", "protocol": "http", "settings": { "timeout": 360 }, "port": "1087" } ], "outbounds": [ { "protocol": "shadowsocks", "settings": { "servers": [ { "address": "167.88.61.60", "method": "aes-256-gcm", "ota": false, "password": "RexnBgU7EV5ADxG", "port": 7002 } ] } }, { "protocol": "freedom", "settings": { "response": null }, "tag": "direct" } ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ { "type": "field", "ip": [ "geoip:private" ], "outboundTag": "direct" }, { "type": "field", "domain": [ "geosite:cn" ], "outboundTag": "direct" }, { "type": "field", "domain": [ "geoip:cn" ], "outboundTag": "direct" } ] } } ================================================ FILE: 002-V2rayPool/test_main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 主要入口 @Date :2021/08/25 @Author :xhunmon @Mail :xhunmon@gmail.com """ from core import utils from core.conf import Config from db.db_main import * EXIT_NUM = 100 if __name__ == '__main__': utils.kill_all_v2ray() Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64') # v2ray内核存放路径 Config.set_v2ray_node_path('/Users/Qincji/Desktop/develop/py/project/PythonIsTools/002-V2rayPool') # 保存获取到节点的路径 proxy_url = 'vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIkBTU1JTVUItVjUyLeS7mOi0ueaOqOiNkDpzdW8ueXQvc3Nyc3ViIiwNCiAgImFkZCI6ICIxMTIuMzMuMzIuMTM2IiwNCiAgInBvcnQiOiAiMTAwMDMiLA0KICAiaWQiOiAiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwNCiAgImFpZCI6ICIxIiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiMTEyLjMzLjMyLjEzNiIsDQogICJwYXRoIjogIiIsDQogICJ0bHMiOiAiIiwNCiAgInNuaSI6ICIiDQp9' dbm = DBManage() dbm.init() # 必须初始化 if dbm.check_url_single(proxy_url): urls = dbm.load_urls_by_net(proxy_url=proxy_url) dbm.check_and_save(urls, append=False) # print(urls) # urls = dbm.load_unchecked_urls_by_local() # dbm.check_and_save(urls, append=False) # urls = dbm.load_enable_urls_by_local() # dbm.load_urls_and_save_auto() utils.kill_all_v2ray() ================================================ FILE: 003-Keywords/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 003-Keywords/README.md ================================================ # 通过google trends查找相关关键词,并且生成趋势 ## 效果 例子:通过`women ring`关键词查找出有1千个相关关键词:[women ring.csv](women-ring/women ring.csv) ![](women-ring/2.jpg) 以及其生成关键词趋势,如:[swarovski rings.jpg](women-ring/swarovski rings.jpg) ![](women-ring/1.jpg) ![](women-ring/3.jpg) ## 实现 1. 使用[pytrends](https://github.com/GeneralMills/pytrends) 开源库。 2. 使用[002-V2rayPool](../002-V2rayPool) 代理(可选择第三方代理)。 3. 实现入口请参照:[main.py](main.py) > 注:项目架构是使用[Scrapy](https://www.osgeo.cn/scrapy/intro/overview.html) 实现的,可实现amazon关键词查询等。 ================================================ FILE: 003-Keywords/__init__.py ================================================ ================================================ FILE: 003-Keywords/amazon/items.py ================================================ # Define here the models for your scraped items # # See documentation in: # https://docs.scrapy.org/en/latest/topics/items.html import scrapy class AmazonItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass ================================================ FILE: 003-Keywords/amazon/middlewares.py ================================================ # Define here the models for your spider middleware # # See documentation in: # https://docs.scrapy.org/en/latest/topics/spider-middleware.html import requests from scrapy import signals # useful for handling different item types with a single interface from scrapy.http import TextResponse class AmazonSpiderMiddleware: # Not all methods need to be defined. If a method is not defined, # scrapy acts as if the spider middleware does not modify the # passed objects. @classmethod def from_crawler(cls, crawler): # This method is used by Scrapy to create your spiders. s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s def process_spider_input(self, response, spider): # Called for each response that goes through the spider # middleware and into the spider. # Should return None or raise an exception. return None def process_spider_output(self, response, result, spider): # Called with the results returned from the Spider, after # it has processed the response. # Must return an iterable of Request, or item objects. for i in result: yield i def process_spider_exception(self, response, exception, spider): # Called when a spider or process_spider_input() method # (from other spider middleware) raises an exception. # Should return either None or an iterable of Request or item objects. pass def process_start_requests(self, start_requests, spider): # Called with the start requests of the spider, and works # similarly to the process_spider_output() method, except # that it doesn’t have a response associated. # Must return only requests (not items). for r in start_requests: yield r def spider_opened(self, spider): spider.logger.info('Spider opened: %s' % spider.name) class AmazonDownloaderMiddleware: # Not all methods need to be defined. If a method is not defined, # scrapy acts as if the downloader middleware does not modify the # passed objects. @classmethod def from_crawler(cls, crawler): # This method is used by Scrapy to create your spiders. s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s def process_request(self, request, spider): # Called for each request that goes through the downloader # middleware. # Must either: # - return None: continue processing this request # - or return a Response object # - or return a Request object # - or raise IgnoreRequest: process_exception() methods of # installed downloader middleware will be called return None def process_response(self, request, response, spider): # Called with the response returned from the downloader. # Must either; # - return a Response object # - return a Request object # - or raise IgnoreRequest return response def process_exception(self, request, exception, spider): # Called when a download handler or a process_request() # (from other downloader middleware) raises an exception. # Must either: # - return None: continue processing this exception # - return a Response object: stops process_exception() chain # - return a Request object: stops process_exception() chain pass def spider_opened(self, spider): spider.logger.info('Spider opened: %s' % spider.name) class AmazonProxyMiddleware(object): def process_request(self, request, spider): print('执行AmazonProxyMiddleware……') # Set the location of the proxy proxy_url = "socks5://127.0.0.1:1080" request.meta['proxy'] = proxy_url # 考虑socks代理,使用requests库进行请求 if proxy_url.startswith('socks'): url = request.url method = request.method headers = {key: request.headers[key] for key in request.headers} body = request.body cookies = request.cookies timeout = request.meta.get('download_timeout', 10) proxies = {'http': proxy_url, 'https': proxy_url} resp = requests.request(method, url, data=body, headers=headers, cookies=cookies, verify=False, timeout=timeout, proxies=proxies) resp.headers['content-encoding'] = None response = TextResponse(url=url, headers=resp.headers, body=resp.content, request=request, encoding=resp.encoding) return response return None ================================================ FILE: 003-Keywords/amazon/pipelines.py ================================================ # Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html # useful for handling different item types with a single interface from itemadapter import ItemAdapter class AmazonPipeline: def process_item(self, item, spider): return item ================================================ FILE: 003-Keywords/amazon/settings.py ================================================ # Scrapy settings for amazon project # # For simplicity, this file contains only settings considered important or # commonly used. You can find more settings consulting the documentation: # # https://docs.scrapy.org/en/latest/topics/settings.html # https://docs.scrapy.org/en/latest/topics/downloader-middleware.html # https://docs.scrapy.org/en/latest/topics/spider-middleware.html BOT_NAME = 'amazon' SPIDER_MODULES = ['amazon.spiders'] NEWSPIDER_MODULE = 'amazon.spiders' # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'amazon (+http://www.yourdomain.com)' # Obey robots.txt rules ROBOTSTXT_OBEY = False # Configure maximum concurrent requests performed by Scrapy (default: 16) #CONCURRENT_REQUESTS = 32 # Configure a delay for requests for the same website (default: 0) # See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs DOWNLOAD_DELAY = 2 # The download delay setting will honor only one of: #CONCURRENT_REQUESTS_PER_DOMAIN = 16 #CONCURRENT_REQUESTS_PER_IP = 16 # Disable cookies (enabled by default) #COOKIES_ENABLED = False # Disable Telnet Console (enabled by default) #TELNETCONSOLE_ENABLED = False # Override the default request headers: #DEFAULT_REQUEST_HEADERS = { # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', # 'Accept-Language': 'en', #} # Enable or disable spider middlewares # See https://docs.scrapy.org/en/latest/topics/spider-middleware.html #SPIDER_MIDDLEWARES = { # 'amazon.middlewares.AmazonSpiderMiddleware': 543, #} # Enable or disable downloader middlewares # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html # DOWNLOADER_MIDDLEWARES = { # # 'amazon.middlewares.AmazonDownloaderMiddleware': 543, # 'amazon.middlewares.AmazonProxyMiddleware': 543 # } # Enable or disable extensions # See https://docs.scrapy.org/en/latest/topics/extensions.html #EXTENSIONS = { # 'scrapy.extensions.telnet.TelnetConsole': None, #} # Configure item pipelines # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html #ITEM_PIPELINES = { # 'amazon.pipelines.AmazonPipeline': 300, #} # Enable and configure the AutoThrottle extension (disabled by default) # See https://docs.scrapy.org/en/latest/topics/autothrottle.html #AUTOTHROTTLE_ENABLED = True # The initial download delay #AUTOTHROTTLE_START_DELAY = 5 # The maximum download delay to be set in case of high latencies #AUTOTHROTTLE_MAX_DELAY = 60 # The average number of requests Scrapy should be sending in parallel to # each remote server #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # Enable showing throttling stats for every response received: #AUTOTHROTTLE_DEBUG = False # Enable and configure HTTP caching (disabled by default) # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings #HTTPCACHE_ENABLED = True #HTTPCACHE_EXPIRATION_SECS = 0 #HTTPCACHE_DIR = 'httpcache' #HTTPCACHE_IGNORE_HTTP_CODES = [] #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage' ================================================ FILE: 003-Keywords/amazon/spiders/__init__.py ================================================ # This package will contain the spiders of your Scrapy project # # Please refer to the documentation for information on how to create and manage # your spiders. ================================================ FILE: 003-Keywords/amazon/spiders/alibaba.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 亚马逊相关关键词获取 @Date :2021/09/24 @Author :xhunmon @Mail :xhunmon@gmail.com """ import re from urllib.parse import quote_plus import scrapy from my_fake_useragent import UserAgent class AlibabaSpider(scrapy.Spider): name = 'alibaba' allowed_domains = ['alibaba.com'] results = [] keywords = [] headers = { 'Host': 'www.alibaba.com', 'User-Agent': UserAgent().random() } def start_requests(self): print('alibaba start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://www.alibaba.com/trade/search?fsb=y&IndexArea=product_en&CatId=&SearchText={k}'.format( k=quote_plus(k)) for k in self.keywords] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('alibaba parse') with open('alibaba.html', mode='w') as f: f.write(response.text) ================================================ FILE: 003-Keywords/amazon/spiders/amazon.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 亚马逊相关关键词获取 @Date :2021/09/24 @Author :xhunmon @Mail :xhunmon@gmail.com """ import re from urllib.parse import quote_plus import scrapy from my_fake_useragent import UserAgent class AmazonSpider(scrapy.Spider): name = 'amazon' allowed_domains = ['amazon.com'] results = [] keywords = [] headers = { 'Host': 'www.amazon.com', 'User-Agent': UserAgent().random() } def start_requests(self): print('amazon start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://www.amazon.com/s?k={k}'.format(k=quote_plus(k)) for k in self.keywords] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('amazon parse') temps = re.findall(r'(.+?)', response.text, re.DOTALL) deal = [x.replace('\n', '').strip() for x in temps] print(deal) self.results += deal ================================================ FILE: 003-Keywords/amazon/spiders/checkip.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 检查当前代理是否起作用 @Date :2021/09/24 @Author :xhunmon @Mail :xhunmon@gmail.com """ import re import scrapy from my_fake_useragent import UserAgent class CheckIpSpider(scrapy.Spider): name = 'ip138' allowed_domains = ['ip138.com'] ips = None headers = { 'User-Agent': UserAgent().random() } def start_requests(self): print('CheckIpSpider start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://2021.ip138.com'] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('CheckIpSpider parse') results = re.findall(r'\[(.+?).+?:(.+?)\n

', response.text, re.DOTALL) print(results) self.ips = results ================================================ FILE: 003-Keywords/amazon/spiders/spiders.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 爬虫页面集合 @Date :2021/09/26 @Author :xhunmon @Mail :xhunmon@gmail.com """ import re from urllib.parse import quote_plus import scrapy from my_fake_useragent import UserAgent class CheckIpSpider(scrapy.Spider): """ 检查当前代理ip信息 """ name = 'ip138' allowed_domains = ['ip138.com'] ips = None headers = { 'User-Agent': UserAgent().random() } def start_requests(self): print('CheckIpSpider start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://2021.ip138.com'] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('CheckIpSpider parse') results = re.findall(r'\[(.+?).+?:(.+?)\n

', response.text, re.DOTALL) print(results) self.ips = results class AmazonSpider(scrapy.Spider): """ https://www.amazon.com/s?k=?? 亚马逊页面获取搜索词相关: """ name = 'amazon' allowed_domains = ['amazon.com'] results = [] keywords = [] headers = { 'Host': 'www.amazon.com', 'User-Agent': UserAgent().random() } def start_requests(self): print('amazon start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://www.amazon.com/s?k={k}'.format(k=quote_plus(k)) for k in self.keywords] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('amazon parse') temps = re.findall(r'(.+?)', response.text, re.DOTALL) deal = [x.replace('\n', '').strip() for x in temps] print(deal) self.results += deal class EtsySpider(scrapy.Spider): """需要连接外网 https://www.etsy.com/market/ """ name = 'etsy' allowed_domains = ['etsy.com'] results = [] keywords = [] headers = { 'User-Agent': UserAgent().random() } def start_requests(self): print('etsy start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://www.etsy.com/market/{k}'.format(k=quote_plus(k)) for k in self.keywords] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('etsy parse') with open('etsy.html', mode='w') as f: f.write(response.text) f.close() temps = re.findall(r'(.+?)', response.text, re.DOTALL) deal = [x.replace('\n', '').strip() for x in temps] print(deal) self.results += deal class LakesideSpider(scrapy.Spider): """一个商城网站 https://www.lakeside.com/browse/Clothing-Accessories """ name = 'lakeside' allowed_domains = ['lakeside.com'] results = [] keywords = [] headers = { 'User-Agent': UserAgent().random() } def start_requests(self): print('lakeside start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://www.lakeside.com/browse/{k}'.format(k=quote_plus(k)) for k in self.keywords] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('lakeside parse') with open('lakeside.html', mode='w') as f: f.write(response.text) f.close() # temps = re.findall(r'(.+?)', response.text, # re.DOTALL) # deal = [x.replace('\n', '').strip() for x in temps] # print(deal) # self.results += deal class WordtrackerSpider(scrapy.Spider): """ https://www.wordtracker.com/search?query=food%20bags """ name = 'wordtracker' allowed_domains = ['wordtracker.com'] results = [] keywords = [] headers = { 'User-Agent': UserAgent().random() } def start_requests(self): print('wordtracker start_requests') """ start_requests做为程序的入口,可以重写,自定义第一批请求 """ start_urls = ['https://www.wordtracker.com/search?query={k}'.format(k=quote_plus(k)) for k in self.keywords] # , meta={'proxy': 'socks5h://127.0.0.1:1080/'} for url in start_urls: yield scrapy.Request(url, headers=self.headers, callback=self.parse) def parse(self, response): print('wordtracker parse') with open('wordtracker.html', mode='w') as f: f.write(response.text) f.close() temps = re.findall(r'(.+?)', response.text, re.DOTALL) deal = [x.replace('\n', '').strip() for x in temps] print(deal) self.results += deal ================================================ FILE: 003-Keywords/google.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: google相关获取 @Date :2021/10/08 @Author :xhunmon @Mail :xhunmon@gmail.com """ import xlsxwriter import os.path import matplotlib.pyplot as plt from mypytrends.request import TrendReq class GoogleTrend(object): def __init__(self): self.data = {} self.max_column = 0 def search(self, keyword, path, hl='en-US', proxies=False, retries=2, timeframe='2019-10-01 2022-01-01'): if not os.path.exists(path): os.makedirs(path) csv_file = os.path.join(path, "%s.xlsx" % keyword) workbook = xlsxwriter.Workbook(csv_file) # 设置整个工作薄的格式 workbook.formats[0].set_align('vcenter') # 单元格垂直居中 # workbook.formats[0].set_text_wrap() # 自动换行 worksheet = workbook.add_worksheet('sheet1') i_row, i_column = 0, 0 first_row = ['keyword', 'no', 'top keyword', 'top range', 'rising keyword', 'rising range'] self.max_column = len(first_row) for i in range(self.max_column): worksheet.write(i_row, i, first_row[i]) i_row += 1 tr = self.__get_req(hl=hl, proxies=proxies, retries=retries) i_row = self.__search_trends(i_row, worksheet, path, keyword, timeframe, tr) first_data = self.__search_related_queries(keyword, timeframe, tr) tops = first_data[keyword]['top'] risings = first_data[keyword]['rising'] top_size = len(tops) rising_size = len(risings) max_len = top_size if top_size > rising_size else rising_size for i in range(max_len): top_key, top_range, rising_key, rising_range = None, None, None, None if i < top_size: top_key = tops[i]['keyword'] top_range = tops[i]['range'] if i < rising_size: rising_key = risings[i]['keyword'] rising_range = risings[i]['range'] max_datas = [keyword, i, top_key, top_range, rising_key, rising_range] for j in range(len(max_datas)): worksheet.write(i_row, j, max_datas[j]) i_row += 1 # self.__save_line(csv_file, [keyword, i, top_key, top_range, rising_key, rising_range]) # self.__save_line(csv_file, ['', '']) # 换行 i_row += 1 for top in tops: top_key = top['keyword'] try: i_row = self.__sub_search(top_key, i_row, worksheet, path, csv_file, timeframe, tr) # self.__save_line(csv_file, ['', '']) # 换行 i_row += 1 except: pass for rising in risings: rising_key = rising['keyword'] try: i_row = self.__sub_search(rising_key, i_row, worksheet, path, csv_file, timeframe, tr) # self.__save_line(csv_file, ['', '']) # 换行 i_row += 1 except: pass workbook.close() def __sub_search(self, i_row, worksheet, keyword, path, csv_file, timeframe, tr): i_row = self.__search_trends(i_row, worksheet, path, keyword, timeframe, tr) first_data = self.__search_related_queries(keyword, timeframe, tr) tops = first_data[keyword]['top'] risings = first_data[keyword]['rising'] top_size = len(tops) rising_size = len(risings) max_len = top_size if top_size > rising_size else rising_size for i in range(max_len): top_key, top_range, rising_key, rising_range = None, None, None, None if i < top_size: top_key = tops[i]['keyword'] top_range = tops[i]['range'] try: i_row = self.__search_trends(i_row, worksheet, path, top_key, timeframe, tr) except: pass if i < rising_size: rising_key = risings[i]['keyword'] rising_range = risings[i]['range'] try: i_row = self.__search_trends(i_row, worksheet, path, rising_key, timeframe, tr) except: pass # self.__save_line(csv_file, [keyword, i, top_key, top_range, rising_key, rising_range]) max_datas = [keyword, i, top_key, top_range, rising_key, rising_range] for j in range(len(max_datas)): worksheet.write(i_row, j, max_datas[j]) i_row += 1 return i_row def __search_related_queries(self, keyword, timeframe, tr: TrendReq) -> {}: tr.build_payload([keyword, ], cat=0, timeframe=timeframe, geo='', gprop='') related = tr.related_queries() r_value = [related[key] for key in related][0] r_top = r_value['top'] r_rising = r_value['rising'] tops = [] risings = [] print('---------top--------') for index, row in r_top.iterrows(): print(index, row["query"], row["value"]) tops.append({"keyword": row["query"], "range": row["value"]}) print('---------rising--------') for index, row in r_rising.iterrows(): print(index, row["query"], row["value"]) risings.append({"keyword": row["query"], "range": row["value"]}) return {keyword: {"top": tops, "rising": risings}} def __search_trends(self, i_row, worksheet, path, keyword, timeframe, tr: TrendReq): tr.build_payload([keyword, ], cat=0, timeframe=timeframe, geo='', gprop='') trends = tr.interest_over_time() x_data = [] y_data = [] year = '' month = '' temp_value = 0 count = 0 for time, row in trends.iterrows(): value = row[keyword] date = str(time) t = date.split(' ')[0] if ' ' in date else date y = t.split('-')[0] m = t.split('-')[1] if month != m and month != '': y_data.append(int(temp_value / count)) x_data.append(year[2:] + "-" + month) year = '' month = '' count = 0 temp_value = 0 continue year = y month = m count += 1 temp_value += value y_data.append(int(temp_value / count)) x_data.append(year[2:] + "-" + month) print(y_data) print(x_data) print('%s : %d - %d' % (keyword, len(y_data), len(x_data))) # self.__draw_graph(x_data, y_data, 'pci.jpg', keyword) img_path = os.path.join(path, "%s.jpg" % keyword) self.__draw_histogram(x_data, y_data, img_path, keyword) worksheet.insert_image(i_row - 1, self.max_column, img_path, {'x_scale': 0.2, 'y_scale': 0.2, 'object_position': 1}) i_row += 1 return i_row def __draw_histogram(self, x: [], y: [], path, title, x_name='date', y_name='trends'): plt.figure(dpi=60) plt.ylim(0, 100) plt.style.use('ggplot') plt.bar(x, y, label=title) # 显示图例(使绘制生效) plt.legend() # 横坐标名称 plt.xlabel(x_name) # 纵坐标名称 plt.ylabel(y_name) # 横坐标显示倒立 plt.xticks(rotation=90) # 保存图片到本地 plt.savefig(path) # 显示图片 # plt.show() def __draw_graph(self, x: [], y: [], path, title, x_name='date', y_name='trends'): plt.figure() '''绘制第一条数据线 1、节点为圆圈 2、线颜色为红色 3、标签名字为y1-data ''' plt.ylim(0, 100) plt.plot(x, y, marker='o', color='r', label=title) # 显示图例(使绘制生效) plt.legend() # 横坐标名称 plt.xlabel(x_name) # 纵坐标名称 plt.ylabel(y_name) # 横坐标显示倒立 plt.xticks(rotation=90) # 保存图片到本地 plt.savefig(path) # 显示图片 # plt.show() def __save_line(self, path, line, mode='a'): with open(path, mode=mode, encoding='utf-8', newline='') as csvfile: writer = csv.writer(csvfile) writer.writerow(line) csvfile.close() def __get_req(self, hl='en-US', proxies=False, retries=2) -> TrendReq: if proxies: return TrendReq(hl=hl, tz=360, timeout=(10, 35), proxies=['socks5h://127.0.0.1:1080', ], retries=retries, backoff_factor=0.1, requests_args={'verify': False}) else: return TrendReq(hl=hl, tz=360, timeout=(10, 35), retries=retries, backoff_factor=0.1, requests_args={'verify': False}) ================================================ FILE: 003-Keywords/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 关键词获取 @Date :2021/09/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ # from amazon import run_api import os import time import xlwt import run_api import v2ray_util as v2util from google import GoogleTrend from openpyxl import load_workbook def Write_Img(): import xlsxwriter book = xlsxwriter.Workbook('test_source.xlsx') sheet = book.get_worksheet_by_name('Sheet1') # sheet = book.add_worksheet('demo') # sheet.insert_image(0, 5, 'Necklace/Necklace.jpg', {'x_scale': 0.2, 'y_scale': 0.2, 'object_position': 1}) print(sheet) book.close() def read_xlsl(): import pandas as pd df = pd.read_excel('test_source.xlsx', sheet_name='Sheet1') data = df.values print(data) print('\n') df = pd.read_excel('test_souce1.xlsx', sheet_name='2021xuqiu') data = df.values print(data) if __name__ == "__main__": # v2util.restart_v2ray() # 获取amazon中相关词,代理需要看:middlewares.py, # results = run_api.crawl_amazon(['plastic packaging', ]) # 把通过google trends查出关键词相关词,以及其词的趋势图,如: # GoogleTrend().search('Necklace', 'Necklace', proxies=True, timeframe='2021-01-01 2022-01-01') # v2util.kill_all_v2ray() # Write_Img() read_xlsl() ================================================ FILE: 003-Keywords/mypytrends/__init__.py ================================================ ================================================ FILE: 003-Keywords/mypytrends/dailydata.py ================================================ from datetime import date, timedelta from functools import partial from time import sleep from calendar import monthrange import pandas as pd from mypytrends.exceptions import ResponseError from mypytrends.request import TrendReq def get_last_date_of_month(year: int, month: int) -> date: """Given a year and a month returns an instance of the date class containing the last day of the corresponding month. Source: https://stackoverflow.com/questions/42950/get-last-day-of-the-month-in-python """ return date(year, month, monthrange(year, month)[1]) def convert_dates_to_timeframe(start: date, stop: date) -> str: """Given two dates, returns a stringified version of the interval between the two dates which is used to retrieve data for a specific time frame from Google Trends. """ return f"{start.strftime('%Y-%m-%d')} {stop.strftime('%Y-%m-%d')}" def _fetch_data(pytrends, build_payload, timeframe: str) -> pd.DataFrame: """Attempts to fecth data and retries in case of a ResponseError.""" attempts, fetched = 0, False while not fetched: try: build_payload(timeframe=timeframe) except ResponseError as err: print(err) print(f'Trying again in {60 + 5 * attempts} seconds.') sleep(60 + 5 * attempts) attempts += 1 if attempts > 3: print('Failed after 3 attemps, abort fetching.') break else: fetched = True return pytrends.interest_over_time() def get_daily_data(word: str, start_year: int, start_mon: int, stop_year: int, stop_mon: int, geo: str = 'US', verbose: bool = True, wait_time: float = 5.0) -> pd.DataFrame: """Given a word, fetches daily search volume data from Google Trends and returns results in a pandas DataFrame. Details: Due to the way Google Trends scales and returns data, special care needs to be taken to make the daily data comparable over different months. To do that, we download daily data on a month by month basis, and also monthly data. The monthly data is downloaded in one go, so that the monthly values are comparable amongst themselves and can be used to scale the daily data. The daily data is scaled by multiplying the daily value by the monthly search volume divided by 100. For a more detailed explanation see http://bit.ly/trendsscaling Args: word (str): Word to fetch daily data for. start_year (int): the start year start_mon (int): start 1st day of the month stop_year (int): the end year stop_mon (int): end at the last day of the month geo (str): geolocation verbose (bool): If True, then prints the word and current time frame we are fecthing the data for. Returns: complete (pd.DataFrame): Contains 4 columns. The column named after the word argument contains the daily search volume already scaled and comparable through time. The column f'{word}_unscaled' is the original daily data fetched month by month, and it is not comparable across different months (but is comparable within a month). The column f'{word}_monthly' contains the original monthly data fetched at once. The values in this column have been backfilled so that there are no NaN present. The column 'scale' contains the scale used to obtain the scaled daily data. """ # Set up start and stop dates start_date = date(start_year, start_mon, 1) stop_date = get_last_date_of_month(stop_year, stop_mon) # Start pytrends for US region pytrends = TrendReq(hl='en-US', tz=360) # Initialize build_payload with the word we need data for build_payload = partial(pytrends.build_payload, kw_list=[word], cat=0, geo=geo, gprop='') # Obtain monthly data for all months in years [start_year, stop_year] monthly = _fetch_data(pytrends, build_payload, convert_dates_to_timeframe(start_date, stop_date)) # Get daily data, month by month results = {} # if a timeout or too many requests error occur we need to adjust wait time current = start_date while current < stop_date: last_date_of_month = get_last_date_of_month(current.year, current.month) timeframe = convert_dates_to_timeframe(current, last_date_of_month) if verbose: print(f'{word}:{timeframe}') results[current] = _fetch_data(pytrends, build_payload, timeframe) current = last_date_of_month + timedelta(days=1) sleep(wait_time) # don't go too fast or Google will send 429s daily = pd.concat(results.values()).drop(columns=['isPartial']) complete = daily.join(monthly, lsuffix='_unscaled', rsuffix='_monthly') # Scale daily data by monthly weights so the data is comparable complete[f'{word}_monthly'].ffill(inplace=True) # fill NaN values complete['scale'] = complete[f'{word}_monthly'] / 100 complete[word] = complete[f'{word}_unscaled'] * complete.scale return complete ================================================ FILE: 003-Keywords/mypytrends/exceptions.py ================================================ class ResponseError(Exception): """Something was wrong with the response from Google""" def __init__(self, message, response): super(Exception, self).__init__(message) # pass response so it can be handled upstream self.response = response ================================================ FILE: 003-Keywords/mypytrends/request.py ================================================ import json import sys import time from datetime import datetime, timedelta import pandas as pd import requests from pandas.io.json._normalize import nested_to_record from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry from mypytrends import exceptions from urllib.parse import quote class TrendReq(object): """ Google Trends API """ GET_METHOD = 'get' POST_METHOD = 'post' GENERAL_URL = 'https://trends.google.com/trends/api/explore' INTEREST_OVER_TIME_URL = 'https://trends.google.com/trends/api/widgetdata/multiline' INTEREST_BY_REGION_URL = 'https://trends.google.com/trends/api/widgetdata/comparedgeo' RELATED_QUERIES_URL = 'https://trends.google.com/trends/api/widgetdata/relatedsearches' TRENDING_SEARCHES_URL = 'https://trends.google.com/trends/hottrends/visualize/internal/data' TOP_CHARTS_URL = 'https://trends.google.com/trends/api/topcharts' SUGGESTIONS_URL = 'https://trends.google.com/trends/api/autocomplete/' CATEGORIES_URL = 'https://trends.google.com/trends/api/explore/pickers/category' TODAY_SEARCHES_URL = 'https://trends.google.com/trends/api/dailytrends' ERROR_CODES = (500, 502, 504, 429) def __init__(self, hl='en-US', tz=360, geo='', timeout=(2, 5), proxies='', retries=0, backoff_factor=0, requests_args=None): """ Initialize default values for params """ # google rate limit self.google_rl = 'You have reached your quota limit. Please try again later.' self.results = None # set user defined options used globally self.tz = tz self.hl = hl self.geo = geo self.kw_list = list() self.timeout = timeout self.proxies = proxies # add a proxy option self.retries = retries self.backoff_factor = backoff_factor self.proxy_index = 0 self.requests_args = requests_args or {} self.cookies = self.GetGoogleCookie() # intialize widget payloads self.token_payload = dict() self.interest_over_time_widget = dict() self.interest_by_region_widget = dict() self.related_topics_widget_list = list() self.related_queries_widget_list = list() def GetGoogleCookie(self): """ Gets google cookie (used for each and every proxy; once on init otherwise) Removes proxy from the list on proxy error """ while True: if "proxies" in self.requests_args: try: return dict(filter(lambda i: i[0] == 'NID', requests.get( 'https://trends.google.com/?geo={geo}'.format( geo=self.hl[-2:]), timeout=self.timeout, **self.requests_args ).cookies.items())) except: continue else: if len(self.proxies) > 0: proxy = {'https': self.proxies[self.proxy_index]} else: proxy = '' try: return dict(filter(lambda i: i[0] == 'NID', requests.get( 'https://trends.google.com/?geo={geo}'.format( geo=self.hl[-2:]), timeout=self.timeout, proxies=proxy, **self.requests_args ).cookies.items())) except requests.exceptions.ProxyError: print('Proxy error. Changing IP') if len(self.proxies) > 1: self.proxies.remove(self.proxies[self.proxy_index]) else: print('No more proxies available. Bye!') raise continue def GetNewProxy(self): """ Increment proxy INDEX; zero on overflow """ if self.proxy_index < (len(self.proxies) - 1): self.proxy_index += 1 else: self.proxy_index = 0 def _get_data(self, url, method=GET_METHOD, trim_chars=0, **kwargs): """Send a request to Google and return the JSON response as a Python object :param url: the url to which the request will be sent :param method: the HTTP method ('get' or 'post') :param trim_chars: how many characters should be trimmed off the beginning of the content of the response before this is passed to the JSON parser :param kwargs: any extra key arguments passed to the request builder (usually query parameters or data) :return: """ s = requests.session() # Retries mechanism. Activated when one of statements >0 (best used for proxy) if self.retries > 0 or self.backoff_factor > 0: retry = Retry(total=self.retries, read=self.retries, connect=self.retries, backoff_factor=self.backoff_factor, status_forcelist=TrendReq.ERROR_CODES, method_whitelist=frozenset(['GET', 'POST'])) s.mount('https://', HTTPAdapter(max_retries=retry)) s.headers.update({'accept-language': self.hl}) if len(self.proxies) > 0: self.cookies = self.GetGoogleCookie() s.proxies.update({'https': self.proxies[self.proxy_index]}) if method == TrendReq.POST_METHOD: response = s.post(url, timeout=self.timeout, cookies=self.cookies, **kwargs, **self.requests_args) # DO NOT USE retries or backoff_factor here else: response = s.get(url, timeout=self.timeout, cookies=self.cookies, **kwargs, **self.requests_args) # DO NOT USE retries or backoff_factor here # check if the response contains json and throw an exception otherwise # Google mostly sends 'application/json' in the Content-Type header, # but occasionally it sends 'application/javascript # and sometimes even 'text/javascript if response.status_code == 200 and 'application/json' in \ response.headers['Content-Type'] or \ 'application/javascript' in response.headers['Content-Type'] or \ 'text/javascript' in response.headers['Content-Type']: # trim initial characters # some responses start with garbage characters, like ")]}'," # these have to be cleaned before being passed to the json parser content = response.text[trim_chars:] # parse json self.GetNewProxy() return json.loads(content) else: # error raise exceptions.ResponseError( 'The request failed: Google returned a ' 'response with code {0}.'.format(response.status_code), response=response) def build_payload(self, kw_list, cat=0, timeframe='today 5-y', geo='', gprop=''): """Create the payload for related queries, interest over time and interest by region""" if gprop not in ['', 'images', 'news', 'youtube', 'froogle']: raise ValueError('gprop must be empty (to indicate web), images, news, youtube, or froogle') self.kw_list = kw_list self.geo = geo or self.geo self.token_payload = { 'hl': self.hl, 'tz': self.tz, 'req': {'comparisonItem': [], 'category': cat, 'property': gprop} } # build out json for each keyword for kw in self.kw_list: keyword_payload = {'keyword': kw, 'time': timeframe, 'geo': self.geo} self.token_payload['req']['comparisonItem'].append(keyword_payload) # requests will mangle this if it is not a string self.token_payload['req'] = json.dumps(self.token_payload['req']) # get tokens self._tokens() return def _tokens(self): """Makes request to Google to get API tokens for interest over time, interest by region and related queries""" # make the request and parse the returned json widget_dicts = self._get_data( url=TrendReq.GENERAL_URL, method=TrendReq.GET_METHOD, params=self.token_payload, trim_chars=4, )['widgets'] # order of the json matters... first_region_token = True # clear self.related_queries_widget_list and self.related_topics_widget_list # of old keywords'widgets self.related_queries_widget_list[:] = [] self.related_topics_widget_list[:] = [] # assign requests for widget in widget_dicts: if widget['id'] == 'TIMESERIES': self.interest_over_time_widget = widget if widget['id'] == 'GEO_MAP' and first_region_token: self.interest_by_region_widget = widget first_region_token = False # response for each term, put into a list if 'RELATED_TOPICS' in widget['id']: self.related_topics_widget_list.append(widget) if 'RELATED_QUERIES' in widget['id']: self.related_queries_widget_list.append(widget) return def interest_over_time(self): """Request data from Google's Interest Over Time section and return a dataframe""" over_time_payload = { # convert to string as requests will mangle 'req': json.dumps(self.interest_over_time_widget['request']), 'token': self.interest_over_time_widget['token'], 'tz': self.tz } # make the request and parse the returned json req_json = self._get_data( url=TrendReq.INTEREST_OVER_TIME_URL, method=TrendReq.GET_METHOD, trim_chars=5, params=over_time_payload, ) df = pd.DataFrame(req_json['default']['timelineData']) if (df.empty): return df df['date'] = pd.to_datetime(df['time'].astype(dtype='float64'), unit='s') df = df.set_index(['date']).sort_index() # split list columns into seperate ones, remove brackets and split on comma result_df = df['value'].apply(lambda x: pd.Series( str(x).replace('[', '').replace(']', '').split(','))) # rename each column with its search term, relying on order that google provides... for idx, kw in enumerate(self.kw_list): # there is currently a bug with assigning columns that may be # parsed as a date in pandas: use explicit insert column method result_df.insert(len(result_df.columns), kw, result_df[idx].astype('int')) del result_df[idx] if 'isPartial' in df: # make other dataframe from isPartial key data # split list columns into seperate ones, remove brackets and split on comma df = df.fillna(False) result_df2 = df['isPartial'].apply(lambda x: pd.Series( str(x).replace('[', '').replace(']', '').split(','))) result_df2.columns = ['isPartial'] # Change to a bool type. result_df2.isPartial = result_df2.isPartial == 'True' # concatenate the two dataframes final = pd.concat([result_df, result_df2], axis=1) else: final = result_df final['isPartial'] = False return final def interest_by_region(self, resolution='COUNTRY', inc_low_vol=False, inc_geo_code=False): """Request data from Google's Interest by Region section and return a dataframe""" # make the request region_payload = dict() if self.geo == '': self.interest_by_region_widget['request'][ 'resolution'] = resolution elif self.geo == 'US' and resolution in ['DMA', 'CITY', 'REGION']: self.interest_by_region_widget['request'][ 'resolution'] = resolution self.interest_by_region_widget['request'][ 'includeLowSearchVolumeGeos'] = inc_low_vol # convert to string as requests will mangle region_payload['req'] = json.dumps( self.interest_by_region_widget['request']) region_payload['token'] = self.interest_by_region_widget['token'] region_payload['tz'] = self.tz # parse returned json req_json = self._get_data( url=TrendReq.INTEREST_BY_REGION_URL, method=TrendReq.GET_METHOD, trim_chars=5, params=region_payload, ) df = pd.DataFrame(req_json['default']['geoMapData']) if (df.empty): return df # rename the column with the search keyword df = df[['geoName', 'geoCode', 'value']].set_index( ['geoName']).sort_index() # split list columns into separate ones, remove brackets and split on comma result_df = df['value'].apply(lambda x: pd.Series( str(x).replace('[', '').replace(']', '').split(','))) if inc_geo_code: result_df['geoCode'] = df['geoCode'] # rename each column with its search term for idx, kw in enumerate(self.kw_list): result_df[kw] = result_df[idx].astype('int') del result_df[idx] return result_df def related_topics(self): """Request data from Google's Related Topics section and return a dictionary of dataframes If no top and/or rising related topics are found, the value for the key "top" and/or "rising" will be None """ # make the request related_payload = dict() result_dict = dict() for request_json in self.related_topics_widget_list: # ensure we know which keyword we are looking at rather than relying on order kw = request_json['request']['restriction'][ 'complexKeywordsRestriction']['keyword'][0]['value'] # convert to string as requests will mangle related_payload['req'] = json.dumps(request_json['request']) related_payload['token'] = request_json['token'] related_payload['tz'] = self.tz # parse the returned json req_json = self._get_data( url=TrendReq.RELATED_QUERIES_URL, method=TrendReq.GET_METHOD, trim_chars=5, params=related_payload, ) # top topics try: top_list = req_json['default']['rankedList'][0][ 'rankedKeyword'] df_top = pd.DataFrame( [nested_to_record(d, sep='_') for d in top_list]) except KeyError: # in case no top topics are found, the lines above will throw a KeyError df_top = None # rising topics try: rising_list = req_json['default']['rankedList'][1][ 'rankedKeyword'] df_rising = pd.DataFrame( [nested_to_record(d, sep='_') for d in rising_list]) except KeyError: # in case no rising topics are found, the lines above will throw a KeyError df_rising = None result_dict[kw] = {'rising': df_rising, 'top': df_top} return result_dict def related_queries(self): """Request data from Google's Related Queries section and return a dictionary of dataframes If no top and/or rising related queries are found, the value for the key "top" and/or "rising" will be None """ # make the request related_payload = dict() result_dict = dict() for request_json in self.related_queries_widget_list: # ensure we know which keyword we are looking at rather than relying on order kw = request_json['request']['restriction'][ 'complexKeywordsRestriction']['keyword'][0]['value'] # convert to string as requests will mangle related_payload['req'] = json.dumps(request_json['request']) related_payload['token'] = request_json['token'] related_payload['tz'] = self.tz # parse the returned json req_json = self._get_data( url=TrendReq.RELATED_QUERIES_URL, method=TrendReq.GET_METHOD, trim_chars=5, params=related_payload, ) # top queries try: top_df = pd.DataFrame( req_json['default']['rankedList'][0]['rankedKeyword']) top_df = top_df[['query', 'value']] except KeyError: # in case no top queries are found, the lines above will throw a KeyError top_df = None # rising queries try: rising_df = pd.DataFrame( req_json['default']['rankedList'][1]['rankedKeyword']) rising_df = rising_df[['query', 'value']] except KeyError: # in case no rising queries are found, the lines above will throw a KeyError rising_df = None result_dict[kw] = {'top': top_df, 'rising': rising_df} return result_dict def trending_searches(self, pn='united_states'): """Request data from Google's Hot Searches section and return a dataframe""" # make the request # forms become obsolete due to the new TRENDING_SEARCHES_URL # forms = {'ajax': 1, 'pn': pn, 'htd': '', 'htv': 'l'} req_json = self._get_data( url=TrendReq.TRENDING_SEARCHES_URL, method=TrendReq.GET_METHOD )[pn] result_df = pd.DataFrame(req_json) return result_df def today_searches(self, pn='US'): """Request data from Google Daily Trends section and returns a dataframe""" forms = {'ns': 15, 'geo': pn, 'tz': '-180', 'hl': 'en-US'} req_json = self._get_data( url=TrendReq.TODAY_SEARCHES_URL, method=TrendReq.GET_METHOD, trim_chars=5, params=forms, **self.requests_args )['default']['trendingSearchesDays'][0]['trendingSearches'] result_df = pd.DataFrame() # parse the returned json sub_df = pd.DataFrame() for trend in req_json: sub_df = sub_df.append(trend['title'], ignore_index=True) result_df = pd.concat([result_df, sub_df]) return result_df.iloc[:, -1] def top_charts(self, date, hl='en-US', tz=300, geo='GLOBAL'): """Request data from Google's Top Charts section and return a dataframe""" try: date = int(date) except: raise ValueError( 'The date must be a year with format YYYY. See https://github.com/GeneralMills/pytrends/issues/355') # create the payload chart_payload = {'hl': hl, 'tz': tz, 'date': date, 'geo': geo, 'isMobile': False} # make the request and parse the returned json req_json = self._get_data( url=TrendReq.TOP_CHARTS_URL, method=TrendReq.GET_METHOD, trim_chars=5, params=chart_payload, **self.requests_args ) try: df = pd.DataFrame(req_json['topCharts'][0]['listItems']) except IndexError: df = None return df def suggestions(self, keyword): """Request data from Google's Keyword Suggestion dropdown and return a dictionary""" # make the request kw_param = quote(keyword) parameters = {'hl': self.hl} req_json = self._get_data( url=TrendReq.SUGGESTIONS_URL + kw_param, params=parameters, method=TrendReq.GET_METHOD, trim_chars=5, **self.requests_args )['default']['topics'] return req_json def categories(self): """Request available categories data from Google's API and return a dictionary""" params = {'hl': self.hl} req_json = self._get_data( url=TrendReq.CATEGORIES_URL, params=params, method=TrendReq.GET_METHOD, trim_chars=5, **self.requests_args ) return req_json def get_historical_interest(self, keywords, year_start=2018, month_start=1, day_start=1, hour_start=0, year_end=2018, month_end=2, day_end=1, hour_end=0, cat=0, geo='', gprop='', sleep=0): """Gets historical hourly data for interest by chunking requests to 1 week at a time (which is what Google allows)""" # construct datetime objects - raises ValueError if invalid parameters initial_start_date = start_date = datetime(year_start, month_start, day_start, hour_start) end_date = datetime(year_end, month_end, day_end, hour_end) # the timeframe has to be in 1 week intervals or Google will reject it delta = timedelta(days=7) df = pd.DataFrame() date_iterator = start_date date_iterator += delta while True: # format date to comply with API call start_date_str = start_date.strftime('%Y-%m-%dT%H') date_iterator_str = date_iterator.strftime('%Y-%m-%dT%H') tf = start_date_str + ' ' + date_iterator_str try: self.build_payload(keywords, cat, tf, geo, gprop) week_df = self.interest_over_time() df = df.append(week_df) except Exception as e: print(e) pass start_date += delta date_iterator += delta if (date_iterator > end_date): # Run for 7 more days to get remaining data that would have been truncated if we stopped now # This is needed because google requires 7 days yet we may end up with a week result less than a full week start_date_str = start_date.strftime('%Y-%m-%dT%H') date_iterator_str = date_iterator.strftime('%Y-%m-%dT%H') tf = start_date_str + ' ' + date_iterator_str try: self.build_payload(keywords, cat, tf, geo, gprop) week_df = self.interest_over_time() df = df.append(week_df) except Exception as e: print(e) pass break # just in case you are rate-limited by Google. Recommended is 60 if you are. if sleep > 0: time.sleep(sleep) # Return the dataframe with results from our timeframe return df.loc[initial_start_date:end_date] ================================================ FILE: 003-Keywords/mypytrends/test_trendReq.py ================================================ from unittest import TestCase import pandas.api.types as ptypes from mypytrends.request import TrendReq class TestTrendReq(TestCase): def test__get_data(self): """Should use same values as in the documentation""" pytrend = TrendReq() self.assertEqual(pytrend.hl, 'en-US') self.assertEqual(pytrend.tz, 360) self.assertEqual(pytrend.geo, '') self.assertTrue(pytrend.cookies['NID']) def test_build_payload(self): """Should return the widgets to get data""" pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.token_payload) def test__tokens(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.related_queries_widget_list) def test_interest_over_time(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.interest_over_time()) def test_interest_over_time_images(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='images') self.assertIsNotNone(pytrend.interest_over_time()) def test_interest_over_time_news(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='news') self.assertIsNotNone(pytrend.interest_over_time()) def test_interest_over_time_youtube(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='youtube') self.assertIsNotNone(pytrend.interest_over_time()) def test_interest_over_time_froogle(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop='froogle') self.assertIsNotNone(pytrend.interest_over_time()) def test_interest_over_time_bad_gprop(self): pytrend = TrendReq() with self.assertRaises(ValueError): pytrend.build_payload(kw_list=['pizza', 'bagel'], gprop=' ') def test_interest_by_region(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.interest_by_region()) def test_related_topics(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.related_topics()) def test_related_queries(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.related_queries()) def test_trending_searches(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.trending_searches()) def test_top_charts(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.top_charts(date=2019)) def test_suggestions(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) self.assertIsNotNone(pytrend.suggestions(keyword='pizza')) def test_ispartial_dtype(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel']) df = pytrend.interest_over_time() assert ptypes.is_bool_dtype(df.isPartial) def test_ispartial_dtype_timeframe_all(self): pytrend = TrendReq() pytrend.build_payload(kw_list=['pizza', 'bagel'], timeframe='all') df = pytrend.interest_over_time() assert ptypes.is_bool_dtype(df.isPartial) ================================================ FILE: 003-Keywords/run_api.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 通过框架自带命令 启动命令脚本 @Date :2021/09/20 @Author :xhunmon @Mail :xhunmon@gmail.com """ from scrapy.crawler import CrawlerProcess from scrapy.utils.log import configure_logging from scrapy.utils.project import get_project_settings from amazon.spiders.alibaba import AlibabaSpider from amazon.spiders.amazon import AmazonSpider from amazon.spiders.checkip import CheckIpSpider settings = get_project_settings() configure_logging(settings) crawler = CrawlerProcess(settings) def check_ip(): spider = CheckIpSpider crawler.crawl(spider) crawler.start() return spider.ips def crawl_amazon(keywords: []): spider = AmazonSpider spider.keywords = keywords crawler.crawl(spider) crawler.start() return spider.results def crawl_alibaba(keywords: []): spider = AlibabaSpider spider.keywords = keywords crawler.crawl(spider) crawler.start() ================================================ FILE: 003-Keywords/scrapy.cfg ================================================ # Automatically created by: scrapy startproject # # For more information about the [deploy] section see: # https://scrapyd.readthedocs.io/en/latest/deploy.html [settings] default = amazon.settings [deploy] #url = http://localhost:6800/ project = amazon ================================================ FILE: 003-Keywords/v2ray_pool/__init__.py ================================================ # 运行时路径。并非__init__.py的路径 import os import sys BASE_DIR = "../002-V2rayPool" if os.path.exists(BASE_DIR): sys.path.append(BASE_DIR) from core import utils from core.conf import Config from core.client import Creator from db.db_main import DBManage from base.net_proxy import Net ================================================ FILE: 003-Keywords/v2ray_pool/_db-checked.txt ================================================ ss://YWVzLTI1Ni1nY206ZzVNZUQ2RnQzQ1dsSklkQDE0Mi4yMDIuNDguMTA4OjUwMDM#%E7%BE%8E%E5%9B%BD-2.48MB/s,142.202.48.108,加拿大 魁北克 魁北克 ss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3QDM4LjY4LjEzNC4xOTE6MjM3NQ#%E7%BE%8E%E5%9B%BD-1.81MB/s(Youtube:%E4%B8%8D%E8%89%AF%E6%9E%97),38.68.134.191,美国 新泽西 科进 ss://YWVzLTI1Ni1nY206ZzVNZUQ2RnQzQ1dsSklkQDE2OS4xOTcuMTQzLjE1Nzo1MDA0#%E7%BE%8E%E5%9B%BD-988.6KB/s(Youtube:%E4%B8%8D%E8%89%AF%E6%9E%97),169.197.143.157,美国 佐治亚 亚特兰大 ss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3QDM4LjEyMS40My43MToyMzc2#%E7%BE%8E%E5%9B%BD-2.82MB/s,38.121.43.71,美国 加利福尼亚 科进 ss://YWVzLTI1Ni1nY206Rm9PaUdsa0FBOXlQRUdQ@167.88.61.204:7307#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%203,167.88.61.204,瑞典 斯德哥尔摩 ss://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,美国 新泽西 科进 ss://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,美国 新泽西 科进 ss://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,美国 纽约 纽约 科进 ss://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,加拿大 魁北克 魁北克 ss://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,加拿大 魁北克 魁北克 ss://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,美国 科进 ss://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,美国 科进 trojan://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,加拿大 安大略 多伦多 ss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@145.239.1.137:8091#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2015,51.38.122.98,德国 Hessen trojan://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,俄罗斯 莫斯科 莫斯科 trojan://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,俄罗斯 莫斯科 莫斯科 trojan://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,俄罗斯 莫斯科 莫斯科 ss://YWVzLTI1Ni1nY206Y2RCSURWNDJEQ3duZklO@167.88.61.60:8118#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%2025,167.88.61.60,瑞典 斯德哥尔摩 ss://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,美国 佐治亚 亚特兰大 ss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@172.99.190.87:9101#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2030,172.99.190.87,美国 康涅狄格 ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpzRjQzWHQyZ09OcWNnRlg1NjM@141.95.0.26:826#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2031,54.38.217.138,美国 新泽西 trojan://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,加拿大 安大略 基奇纳 vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIkBTU1JTVUItVjI4LeS7mOi0ueaOqOiNkDpzdW8ueXQvc3Nyc3ViIiwNCiAgImFkZCI6ICI0Mi4xNTcuOC4xNjIiLA0KICAicG9ydCI6ICI1MDAwMiIsDQogICJpZCI6ICI0MTgwNDhhZi1hMjkzLTRiOTktOWIwYy05OGNhMzU4MGRkMjQiLA0KICAiYWlkIjogIjY0IiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiNDIuMTU3LjguMTYyIiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiINCn0=,42.157.8.162,中国 安徽省 合肥市 联通 vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogImh0dHBzOi8vZ2l0aHViLmNvbS9BbHZpbjk5OTkvbmV3LXBhYy93aWtpIOS/hOe9l+aWr2cyIiwNCiAgImFkZCI6ICIxOTUuMTMzLjUzLjg4IiwNCiAgInBvcnQiOiAiMjU5NjQiLA0KICAiaWQiOiAiYmE5M2U1NmMtNzdmOS0xMWVjLWFmNDMtZDJlOGI0YzNhNzVhIiwNCiAgImFpZCI6ICIwIiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiIiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiIsDQogICJhbHBuIjogIiINCn0=,195.133.53.88,俄罗斯 莫斯科 莫斯科 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzE4IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwMyIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiJhaWNvbzZkdS5jb20iLCJwYXRoIjoiL3dzIiwidGxzIjoiIiwic25pIjoiIn0=,114.43.130.6,中国 台湾省 中华电信 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzI1IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii9zL2QwYTg2OTIuZm0uYXBwbGUuY29tOjMyNjY3IiwidGxzIjoiIiwic25pIjoiIn0=,172.70.214.120,美国 加利福尼亚 旧金山 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzI2IiwiYWRkIjoic2hjdTAxLmlwbGMxODguY29tIiwicG9ydCI6IjEwMDA0IiwiaWQiOiI2NWNhYzU2ZC00MTU1LTQzYzgtYmFlMC1mMzY4Y2IyMWY3NzEiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6InRjcCIsInR5cGUiOiJub25lIiwiaG9zdCI6InNoY3UwMS5pcGxjMTg4LmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiIiLCJzbmkiOiIifQ==,210.71.214.218,中国 台湾省 中华电信 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzI4IiwiYWRkIjoiNDIuMTU3LjguNTIiLCJwb3J0IjoiNDg3MjciLCJpZCI6IjU3YWE1YWMzLWQxZDAtNGUyZi1iMzJlLTY0ODhkNWE3Y2I0NSIsImFpZCI6IjY0Iiwic2N5IjoiYXV0byIsIm5ldCI6InRjcCIsInR5cGUiOiJub25lIiwiaG9zdCI6IiIsInBhdGgiOiIiLCJ0bHMiOiIiLCJzbmkiOiIifQ==,42.157.8.52,中国 安徽省 合肥市 联通 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzMzIiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxNDYuNTYuMTc3LjM0IiwicGF0aCI6Ii92MnJheSIsInRscyI6IiIsInNuaSI6IiJ9,172.70.211.7,美国 加利福尼亚 旧金山 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM0IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii93cyIsInRscyI6IiIsInNuaSI6IiJ9,172.70.210.228,美国 加利福尼亚 旧金山 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM1IiwiYWRkIjoiNDIuMTkzLjQ4LjY0IiwicG9ydCI6IjUwMDAyIiwiaWQiOiI0MTgwNDhhZi1hMjkzLTRiOTktOWIwYy05OGNhMzU4MGRkMjQiLCJhaWQiOiI2NCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiI0Mi4xOTMuNDguNjQiLCJwYXRoIjoiIiwidGxzIjoiIiwic25pIjoiIn0=,42.193.48.64,中国 上海 上海市 电信 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM3IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii8iLCJ0bHMiOiIiLCJzbmkiOiIifQ==,172.70.206.182,美国 加利福尼亚 旧金山 vmess://eyJ2IjoiMiIsInBzIjoi57+75aKZ5YWaZmFucWlhbmdkYW5nLmNvbUAwMTIwX0NOXzM5IiwiYWRkIjoiMTEyLjMzLjMyLjEzNiIsInBvcnQiOiIxMDAwNCIsImlkIjoiNjVjYWM1NmQtNDE1NS00M2M4LWJhZTAtZjM2OGNiMjFmNzcxIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ0Y3AiLCJ0eXBlIjoibm9uZSIsImhvc3QiOiIxMTIuMzMuMzIuMTM2IiwicGF0aCI6Ii9zLzUzMTg0NDIuZm0uYXBwbGUuY29tOjU0MDgwIiwidGxzIjoiIiwic25pIjoiIn0=,172.69.33.158,美国 加利福尼亚 ================================================ FILE: 003-Keywords/v2ray_pool/_db-uncheck.txt ================================================ vmess://eyJ2IjogIjIiLCAicHMiOiAiZ2l0aHViLmNvbS9mcmVlZnEgLSBcdTViODlcdTVmYmRcdTc3MDFcdTgwNTRcdTkwMWEgMSIsICJhZGQiOiAiNDIuMTU3LjguMTYyIiwgInBvcnQiOiAiNTAwMDIiLCAiaWQiOiAiNDE4MDQ4YWYtYTI5My00Yjk5LTliMGMtOThjYTM1ODBkZDI0IiwgImFpZCI6ICI2NCIsICJzY3kiOiAiYXV0byIsICJuZXQiOiAidGNwIiwgInR5cGUiOiAibm9uZSIsICJob3N0IjogIiIsICJwYXRoIjogIiIsICJ0bHMiOiAiIiwgInNuaSI6ICIifQ==,42.157.8.162,中国 安徽省 合肥市 联通 ss://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,美国 科进 ss://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,美国 佐治亚 亚特兰大 trojan://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,加拿大 安大略 基奇纳 ss://YWVzLTI1Ni1nY206ZmFCQW9ENTRrODdVSkc3@46.29.218.6:2376#github.com/freefq%20-%20%E6%8C%AA%E5%A8%81%20%207,46.29.218.6,挪威 trojan://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,加拿大 安大略 基奇纳 ss://YWVzLTI1Ni1nY206ZTRGQ1dyZ3BramkzUVk@172.99.190.87:9101#github.com/freefq%20-%20%E7%BE%8E%E5%9B%BD%20%2012,172.99.190.87,美国 康涅狄格 trojan://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,俄罗斯 莫斯科 莫斯科 trojan://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,俄罗斯 莫斯科 莫斯科 trojan://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,加拿大 安大略 多伦多 ss://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,美国 ss://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,美国 科进 ss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@145.239.1.137:8091#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2029,51.38.122.98,德国 Hessen trojan://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,美国 加利福尼亚 trojan://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,俄罗斯 莫斯科 莫斯科 ss://YWVzLTI1Ni1nY206WTZSOXBBdHZ4eHptR0M@167.88.61.60:5601#github.com/freefq%20-%20%E7%91%9E%E5%85%B8%20%2034,167.88.61.60,瑞典 斯德哥尔摩 ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpzRjQzWHQyZ09OcWNnRlg1NjM@141.95.0.26:826#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%2037,54.38.217.138,美国 新泽西 trojan://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,美国 加利福尼亚 vmess://eyJ2IjogIjIiLCAicHMiOiAiZ2l0aHViLmNvbS9mcmVlZnEgLSBcdTRlMGFcdTZkNzdcdTVlMDJcdTVmOTBcdTZjNDdcdTUzM2FcdTgwNTRcdTkwMWFcdTZmMTVcdTZjYjNcdTZjZmVcdTY1NzBcdTYzNmVcdTRlMmRcdTVmYzMgNDAiLCAiYWRkIjogInNoY3UwMS5pcGxjMTg4LmNvbSIsICJwb3J0IjogIjEwMDA0IiwgImlkIjogIjY1Y2FjNTZkLTQxNTUtNDNjOC1iYWUwLWYzNjhjYjIxZjc3MSIsICJhaWQiOiAiMSIsICJzY3kiOiAiYXV0byIsICJuZXQiOiAidGNwIiwgInR5cGUiOiAibm9uZSIsICJob3N0IjogInNoY3UwMS5pcGxjMTg4LmNvbSIsICJwYXRoIjogIiIsICJ0bHMiOiAiIiwgInNuaSI6ICIifQ==,61.216.19.199,中国 台湾省 中华电信 ================================================ FILE: 003-Keywords/v2ray_util.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 管理v2ray_pool的工具 @Date :2022/1/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import time from v2ray_pool import utils, Config, DBManage def search_node(): # 如果有系统全局代理,可不需要开启v2ray_core代理,GoogleTrend(proxies=False) utils.kill_all_v2ray() Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64') # v2ray内核存放路径 Config.set_v2ray_node_path( '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool') # 保存获取到节点的路径 proxy_url = 'ss://YWVzLTI1Ni1nY206UENubkg2U1FTbmZvUzI3@145.239.1.137:8091#github.com/freefq%20-%20%E8%8B%B1%E5%9B%BD%20%207' dbm = DBManage() dbm.init() # 必须初始化 # if dbm.check_url_single(proxy_url): # urls = dbm.load_urls_by_net(proxy_url=proxy_url) # dbm.check_and_save(urls, append=False) dbm.load_urls_and_save_auto() # urls = dbm.load_unchecked_urls_by_local() # dbm.check_and_save(urls, append=False) utils.kill_all_v2ray() def restart_v2ray(isSysOn=False): utils.kill_all_v2ray() Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64') # v2ray内核存放路径 Config.set_v2ray_node_path( '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool') # 保存获取到节点的路径 dbm = DBManage() dbm.init() # 必须初始化 while 1: if dbm.start_random_v2ray_by_local(isSysOn=isSysOn): break else: print("启动失败,进行重试!") time.sleep(1) def kill_all_v2ray(): utils.kill_all_v2ray() ================================================ FILE: 003-Keywords/women-ring/women ring.csv ================================================ keyword,no,top keyword,top range,rising keyword,rising range women ring,0,ring for women,100,are men and women ring sizes the same,550 women ring,1,gold women ring,40,emerald rings for women,200 women ring,2,gold ring,40,ruby ring for women,200 women ring,3,ring for women gold,31,gucci rings women,200 women ring,4,women rings,31,cartier ring,190 women ring,5,rings,30,emerald ring for women,170 women ring,6,rings for women,24,thumb ring women,170 women ring,7,wedding ring women,20,cartier,170 women ring,8,wedding ring,20,macys,170 women ring,9,diamond ring,17,pinky rings for women,160 women ring,10,diamond,17,moonstone ring,160 women ring,11,diamond ring women,16,gold price today,150 women ring,12,women ring size,16,versace ring women,140 women ring,13,ring size,16,gold rate today,130 women ring,14,engagement,14,how to measure ring size,130 women ring,15,engagement ring women,14,cartier love ring,120 women ring,16,diamond ring for women,14,kay jewelers,110 women ring,17,engagement ring,13,thumb ring for women,110 women ring,18,wedding ring for women,13,opal ring for women,110 women ring,19,engagement ring for women,12,swarovski,110 women ring,20,silver ring women,11,diamond ring for women price,100 women ring,21,silver ring,11,louis vuitton,100 women ring,22,women ring design,11,the bling ring,100 women ring,23,ring design,11,how to measure ring size women,100 women ring,24,women ring finger,10,eternity ring for women,90 , ring for women,0,gold ring women,100,gold ring for women under 5000,3950 ring for women,1,ring for women gold,98,fendi ring,450 ring for women,2,gold ring,94,dior ring,350 ring for women,3,rings,70,silver rate today,300 ring for women,4,rings for women,69,opal rings for women,250 ring for women,5,diamond,49,ruby ring for women,250 ring for women,6,diamond ring for women,46,pinky rings for women,200 ring for women,7,diamond ring,44,emerald ring for women,200 ring for women,8,engagement ring,41,ruby rings for women,190 ring for women,9,wedding ring for women,41,eternity ring for women,150 ring for women,10,engagement,39,solitaire ring for women,150 ring for women,11,wedding ring,38,gold pendant,120 ring for women,12,engagement ring for women,36,diamond ring price for women,120 ring for women,13,ring size for women,28,pearl ring,120 ring for women,14,ring design,26,gold ring for women under 10000,110 ring for women,15,silver ring,26,thumb ring for women,110 ring for women,16,engagement rings,23,pearl ring for women,100 ring for women,17,silver ring for women,23,gucci ring,100 ring for women,18,ring design for women,23,custom rings for women,100 ring for women,19,engagement rings for women,23,kay jewelers,90 ring for women,20,ring finger for women,18,amethyst ring for women,90 ring for women,21,gold rings for women,18,emerald rings for women,80 ring for women,22,gold ring design,18,rose gold rings for women,80 ring for women,23,wedding rings,17,opal ring for women,70 ring for women,24,ring finger,17,types of rings for women,70 , gold women ring,0,ring for women gold,100,gold ring for women under 10000,36200 gold women ring,1,ring for women,98,gold price today,350 gold women ring,2,gold rings,24,gold ring for women under 5000,250 gold women ring,3,gold ring design,23,white gold engagement ring for women,130 gold women ring,4,gold rings women,23,ring design for men,110 gold women ring,5,women gold ring design,22,silver ring for women,80 gold women ring,6,women ring design,22,ring design for women,80 gold women ring,7,ring design,22,gold rate today,70 gold women ring,8,gold ring design for women,21,gold ring design for women,60 gold women ring,9,ring design for women,20,ring for women gold,50 gold women ring,10,gold rings for women,20,ring for women,50 gold women ring,11,rings for women,19,gold ring design,50 gold women ring,12,gold ring women price,19,gold ring women price,50 gold women ring,13,gold ring price,19,gold ring for girls,50 gold women ring,14,gold price,18,women ring design,50 gold women ring,15,gold ring for women price,15,white gold ring for women,50 gold women ring,16,diamond ring,12,women gold ring design,40 gold women ring,17,diamond ring for women,11,gold rate,40 gold women ring,18,ring designs for women,9,gold chain for women,40 gold women ring,19,gold ring designs for women,8,, gold women ring,20,gold engagement ring for women,7,, gold women ring,21,gold rate,7,, gold women ring,22,engagement ring for women,7,, gold women ring,23,gold ring for men,7,, gold women ring,24,silver ring for women,6,, , gold ring,0,diamond,100,nose ring designs in gold for female,750 gold ring,1,gold diamond ring,99,simple gold ring design for female,200 gold ring,2,diamond ring,97,how to measure ring size,170 gold ring,3,white gold ring,93,new gold ring design for female,150 gold ring,4,white gold,90,ear ring design in gold,150 gold ring,5,rose gold ring,81,2 gram gold ring price,130 gold ring,6,rose gold,79,boys gold ring design,130 gold ring,7,gold rings,77,3 gram gold ring,120 gold ring,8,gold ring price,77,today gold price,120 gold ring,9,gold price,77,2 gram gold ring,110 gold ring,10,ring design gold,77,gold ring price in bd,110 gold ring,11,ring design,76,nose ring gold design,100 gold ring,12,rings,74,gold engagement ring designs for female,90 gold ring,13,engagement ring gold,57,today gold rate,80 gold ring,14,engagement ring,56,bulgari ring gold,80 gold ring,15,gold ring men,47,white gold ring for women,80 gold ring,16,gold wedding ring,47,ear ring design gold,80 gold ring,17,wedding ring,46,3 gram gold ring price,70 gold ring,18,mens gold ring,40,ring designs for girls gold,70 gold ring,19,gold band ring,34,white gold nose ring,70 gold ring,20,ring for men gold,32,gold dome ring,70 gold ring,21,women gold ring,31,ring design for women,60 gold ring,22,14k gold ring,30,simple gold ring design,60 gold ring,23,yellow gold ring,30,couple gold ring design,60 gold ring,24,gold nose ring,27,ring light,60 , ring for women gold,0,rings for women,100,gold ring for women under 5000,400 ring for women gold,1,gold rings for women,96,gold rate today,200 ring for women gold,2,gold ring design for women,90,gold ring for women under 10000,200 ring for women gold,3,gold ring design,87,gold ring for girls,170 ring for women gold,4,ring design for women,82,white gold ring for women,60 ring for women gold,5,gold ring price for women,65,engagement ring for women,50 ring for women gold,6,gold price,65,gold ring design for women,50 ring for women gold,7,diamond ring,58,tanishq gold ring for women,50 ring for women gold,8,diamond ring for women,48,ring design for women,50 ring for women gold,9,engagement ring for women,41,gold ring design,50 ring for women gold,10,ring designs for women,38,, ring for women gold,11,gold ring designs for women,37,, ring for women gold,12,white gold ring for women,33,, ring for women gold,13,wedding ring for women,30,, ring for women gold,14,gold rate,29,, ring for women gold,15,white gold ring,29,, ring for women gold,16,gold ring for men,28,, ring for women gold,17,gold engagement rings for women,26,, ring for women gold,18,engagement rings for women,24,, ring for women gold,19,tanishq gold ring for women,24,, ring for women gold,20,tanishq,24,, ring for women gold,21,gold rate today,23,, ring for women gold,22,silver ring for women,22,, ring for women gold,23,gold earrings for women,22,, ring for women gold,24,gold chain,21,, , women rings,0,rings for women,100,diamond engagement rings for women with price,1750 women rings,1,engagement,25,turquoise rings for women,1300 women rings,2,engagement rings,25,adjustable rings for women,250 women rings,3,women engagement rings,25,gucci ring,250 women rings,4,wedding rings,24,personalized rings for women,200 women rings,5,wedding rings women,24,fendi rings women,200 women rings,6,engagement rings for women,24,eternity ring,200 women rings,7,ring,20,unique engagement rings for women,180 women rings,8,wedding rings for women,20,opal rings for women,170 women rings,9,gold rings women,20,emerald rings for women,160 women rings,10,gold rings,19,class rings for women,130 women rings,11,rings for women gold,16,chocolate diamond rings for women,130 women rings,12,ring for women,16,types of rings for women,130 women rings,13,diamond,13,rubber rings for women,130 women rings,14,women diamond rings,12,celtic rings for women,130 women rings,15,diamond rings for women,12,gucci ring women,110 women rings,16,diamond rings,12,silicone rings for women,110 women rings,17,silver rings,7,solitaire rings for women,110 women rings,18,silver rings women,7,silicone wedding rings for women,110 women rings,19,silver rings for women,6,simple engagement rings for women,100 women rings,20,engagement ring,5,gucci rings women,100 women rings,21,gold ring,5,tiffany rings for women,90 women rings,22,men rings,5,nose rings for women,90 women rings,23,jewelry,5,tiffany and co,90 women rings,24,pandora,5,eternity ring for women,90 , rings,0,lord of the rings,100,rolex rings ipo,34750 rings,1,the lord of the rings,98,rolex rings share price,11350 rings,2,engagement rings,65,floating rings weeping woods,9650 rings,3,ring,55,rolex rings ipo gmp,7050 rings,4,wedding rings,39,rolex rings ipo allotment status,5950 rings,5,gold rings,29,floating rings at weeping woods,5400 rings,6,diamond rings,25,rolex rings ipo allotment date,5350 rings,7,pandora rings,16,shang-chi and the legend of the ten rings,5000 rings,8,pandora,16,shang chi and the legend of the ten rings,2450 rings,9,silver rings,15,shang chi,2200 rings,10,mens rings,14,ten rings,1250 rings,11,onion rings,13,where to watch lord of the rings,550 rings,12,men rings,12,lord of the rings 4k,350 rings,13,promise rings,12,clay rings,350 rings,14,rings for women,10,air fryer onion rings,300 rings,15,diamond engagement rings,9,mejuri rings,250 rings,16,rings movie,8,onion rings in air fryer,200 rings,17,nose rings,8,akatsuki rings,200 rings,18,amazon rings,8,chunky rings,180 rings,19,rings for men,8,glamira rings,170 rings,20,ten rings,6,harry styles rings,160 rings,21,gold engagement rings,6,matching rings,120 rings,22,the hobbit,5,engagement rings for women,110 rings,23,cheap rings,5,pura vida rings,100 rings,24,lord of the rings cast,5,peach rings,90 , rings for women,0,engagement,100,amethyst rings for women,12700 rings for women,1,engagement rings,100,gold thumb rings for women,4800 rings for women,2,engagement rings for women,99,emerald rings for women,350 rings for women,3,wedding rings for women,78,key rings for women,300 rings for women,4,wedding rings,76,sapphire rings for women,300 rings for women,5,rings for women gold,70,chocolate diamond rings for women,250 rings for women,6,gold rings,67,turquoise rings for women,250 rings for women,7,ring,62,unique engagement rings for women,250 rings for women,8,ring for women,60,nose rings for women,200 rings for women,9,diamond rings,45,cartier rings for women,200 rings for women,10,diamond rings for women,43,louis vuitton,190 rings for women,11,diamond,42,engagement rings for women near me,170 rings for women,12,rings for women silver,27,real gold rings for women,160 rings for women,13,silver rings,26,sapphire rings,160 rings for women,14,engagement ring for women,19,opal rings for women,150 rings for women,15,engagement ring,18,engagement rings for men and women,150 rings for women,16,gold ring for women,17,eternity rings for women,150 rings for women,17,gold ring,16,anklets for women,150 rings for women,18,jewelry,16,diamond engagement rings for women with price,140 rings for women,19,pandora,16,james avery,130 rings for women,20,promise rings for women,15,ruby rings for women,120 rings for women,21,pandora rings for women,15,ruby ring for women,90 rings for women,22,pandora rings,14,adjustable rings for women,90 rings for women,23,promise rings,14,gemstone rings for women,90 rings for women,24,diamond ring,14,unique rings for women,90 , wedding ring women,0,wedding ring for women,100,wedding sets for women,90 wedding ring women,1,wedding rings,54,wedding ring sets,70 wedding ring women,2,rings for women,44,wedding ring sets for women,60 wedding ring women,3,wedding rings for women,43,engagement rings for women,50 wedding ring women,4,wedding bands,25,wedding dresses,40 wedding ring women,5,wedding ring sets,23,, wedding ring women,6,diamond ring for women,20,, wedding ring women,7,wedding sets for women,19,, wedding ring women,8,engagement rings,19,, wedding ring women,9,wedding bands for women,19,, wedding ring women,10,engagement rings for women,18,, wedding ring women,11,wedding ring sets for women,18,, wedding ring women,12,wedding band for women,17,, wedding ring women,13,women wedding ring set,12,, wedding ring women,14,wedding ring finger women,10,, wedding ring women,15,wedding ring finger,9,, wedding ring women,16,wedding ring set for women,8,, wedding ring women,17,wedding ring finger for women,5,, wedding ring women,18,wedding dresses,4,, wedding ring women,19,men and women wedding ring sets,2,, wedding ring women,20,kay jewelers,1,, , wedding ring,0,wedding rings,100,wedding ring sheathing,69850 wedding ring,1,rings,96,vanessa bryant wedding ring,10700 wedding ring,2,engagement ring,93,wedding ring sheating,2200 wedding ring,3,ring for wedding,79,alex lovell no wedding ring,2150 wedding ring,4,wedding band,76,andy murray wedding ring,300 wedding ring,5,gold wedding ring,58,hailey bieber wedding ring,300 wedding ring,6,the wedding ring,57,ariana grande wedding ring,250 wedding ring,7,diamond ring,56,lotr wedding ring,250 wedding ring,8,diamond wedding ring,55,kobe bryant wedding ring,190 wedding ring,9,wedding ring hand,50,couples wedding ring sets,150 wedding ring,10,ring finger,42,how many people should i invite to my wedding,130 wedding ring,11,wedding ring finger,42,finance wedding ring,100 wedding ring,12,mens wedding ring,36,which hand does wedding ring go on,100 wedding ring,13,mens ring,36,wedding ring tattoo ideas,100 wedding ring,14,wedding ring sets,35,his and her wedding ring sets,90 wedding ring,15,engagement rings,32,what hand does a wedding ring go on for a man,90 wedding ring,16,wedding ring bands,30,which hand does your wedding ring go on,90 wedding ring,17,wedding bands,30,outlander wedding ring,80 wedding ring,18,wedding ring set,27,do you wear your engagement ring on your wedding day,80 wedding ring,19,engagement ring and wedding ring,25,which finger does a wedding ring go on,80 wedding ring,20,men wedding ring,25,which hand does a wedding ring go on,80 wedding ring,21,wedding dress,24,opal engagement ring,80 wedding ring,22,wedding ring men,24,which hand do you wear a wedding ring on,80 wedding ring,23,what hand wedding ring,21,wedding showers,70 wedding ring,24,black wedding ring,17,what finger for wedding ring,70 , diamond ring,0,diamond engagement ring,100,lab grown diamonds,300 diamond ring,1,engagement ring,96,ban maxxis diamond ring 14,300 diamond ring,2,gold diamond ring,85,diamond hoop nose ring,130 diamond ring,3,rings,70,diamond ring price in bangladesh,120 diamond ring,4,diamond rings,67,salt and pepper diamond,110 diamond ring,5,diamond price,51,levian chocolate diamond ring,110 diamond ring,6,diamond ring price,49,diamond ring design for female,110 diamond ring,7,wedding ring,38,salt and pepper diamond ring,100 diamond ring,8,diamond wedding ring,37,diamond ring price in uae,100 diamond ring,9,diamond engagement rings,32,10 carat diamond ring price,90 diamond ring,10,engagement rings,31,diamond ring price in pakistan,70 diamond ring,11,diamond band ring,30,radiant cut diamond ring,70 diamond ring,12,black diamond,25,heart shape diamond ring,70 diamond ring,13,black diamond ring,25,radiant diamond ring,70 diamond ring,14,white gold diamond ring,22,3k diamond ring,70 diamond ring,15,emerald diamond ring,22,diamond ring for women,60 diamond ring,16,solitaire diamond ring,20,4 ct diamond ring,50 diamond ring,17,princess diamond ring,20,best way to clean diamond ring,40 diamond ring,18,solitaire ring,20,diamond ring for girls,40 diamond ring,19,2 carat diamond ring,19,2 carat emerald cut diamond ring,40 diamond ring,20,1 carat diamond,19,, diamond ring,21,1 carat diamond ring,18,, diamond ring,22,diamond sapphire ring,17,, diamond ring,23,princess cut ring,17,, diamond ring,24,oval diamond ring,17,, , diamond,0,diamond ring,100,diamond casino heist,22950 diamond,1,black diamond,78,pokemon brilliant diamond,12700 diamond,2,free diamond,73,diamond league 2021,10600 diamond,3,diamond free fire,60,free fire hack diamond 2020,9400 diamond,4,free fire,58,diamond jeje,7850 diamond,5,free fire free diamond,57,double diamond top up,7150 diamond,6,diamond painting,50,lil uzi vert diamond,6750 diamond,7,blue diamond,42,lil uzi vert,6000 diamond,8,diamond rings,41,lil uzi diamond,5050 diamond,9,diamond price,38,blaq diamond love letter,4100 diamond,10,diamond earrings,32,free fire redeem code,3100 diamond,11,diamonds,29,diamond pang,2900 diamond,12,diamond cut,28,avenues of the diamond,2700 diamond,13,neil diamond,26,dunia games free fire 70 diamond,1950 diamond,14,diamond white,26,diamond princess cruise ship,1850 diamond,15,diamond necklace,26,diamond and pearl remake,900 diamond,16,white diamond,25,free fire diamond hack generator,900 diamond,17,dustin diamond,22,pokemon diamond and pearl remake,900 diamond,18,diamond princess,21,dustin diamond,900 diamond,19,diamond ff,20,diamond painting wereld,700 diamond,20,kinemaster,20,ff diamond hack,700 diamond,21,kinemaster diamond,20,kinemaster diamond mod apk,700 diamond,22,pokemon diamond,19,kinemaster diamond apk download,650 diamond,23,diamond platnumz,18,generator diamond online free fire,650 diamond,24,minecraft diamond,16,diamond princess cruise,600 , diamond ring women,0,diamond ring for women,100,eternity ring for women,46600 diamond ring women,1,diamond rings,41,platinum ring for women,70 diamond ring women,2,diamond rings for women,36,, diamond ring women,3,rings for women,36,, diamond ring women,4,gold ring for women,23,, diamond ring women,5,gold diamond ring for women,21,, diamond ring women,6,engagement rings,17,, diamond ring women,7,diamond ring price,16,, diamond ring women,8,engagement rings for women,16,, diamond ring women,9,diamond ring for women price,13,, diamond ring women,10,wedding rings,13,, diamond ring women,11,wedding rings for women,9,, diamond ring women,12,gold rings for women,8,, diamond ring women,13,platinum ring for women,7,, diamond ring women,14,diamond ring for men,6,, diamond ring women,15,tanishq,6,, diamond ring women,16,tanishq diamond ring for women,5,, diamond ring women,17,black diamond ring,5,, diamond ring women,18,wedding bands for women,4,, diamond ring women,19,black diamond ring for women,4,, diamond ring women,20,diamond bands for women,3,, diamond ring women,21,white gold rings for women,2,, diamond ring women,22,eternity ring for women,1,, , women ring size,0,ring size for women,100,etsy,41000 women ring size,1,average women ring size,53,average women ring size,130 women ring size,2,average ring size for women,43,how to measure ring size women,100 women ring size,3,ring size chart,38,gold rings for women,100 women ring size,4,women ring size chart,37,average ring size for women,90 women ring size,5,rings for women,26,engagement rings for women,80 women ring size,6,ring size chart for women,26,how to measure ring size,80 women ring size,7,how to measure ring size,23,rings for women,60 women ring size,8,how to measure ring size women,22,, women ring size,9,how to measure ring size for women,6,, women ring size,10,ring sizes for women,5,, women ring size,11,wedding rings for women,5,, women ring size,12,engagement rings for women,4,, women ring size,13,how to find ring size,3,, women ring size,14,gold rings for women,3,, women ring size,15,etsy,2,, , ring size,0,ring size chart,100,what is the screen size of new fire-boltt ring smartwatch?,3700 ring size,1,measure ring size,60,how to figure out ring size at home,600 ring size,2,ring size mm,57,how to know the size of your ring finger,500 ring size,3,how to measure ring size,48,measuring ring size at home,500 ring size,4,uk ring size,45,how can you measure your ring size,400 ring size,5,rings,44,how to know the size of your ring,350 ring size,6,us ring size,39,fendi ring,350 ring size,7,size of ring,37,charmed aroma,250 ring size,8,ring size in mm,26,royal essence,250 ring size,9,pandora ring,25,ring size chart nz,250 ring size,10,pandora ring size,25,2.25 inches ring size,200 ring size,11,find ring size,24,how to.measure ring size,190 ring size,12,pandora,24,2.5 inches to mm,180 ring size,13,ring finger size,24,how do you measure your ring size,170 ring size,14,how to size a ring,23,how to find ring size at home,170 ring size,15,cm to ring size,22,size 8 ring in cm,140 ring size,16,ring size in cm,19,how to measure ring size women,140 ring size,17,mens ring size,18,how to know what size ring you are,140 ring size,18,average ring size,17,how do i measure my ring size,130 ring size,19,how to find ring size,16,how to know your ring size female,120 ring size,20,engagement ring,16,52 ring size in letters,110 ring size,21,men ring size,16,how to find your ring size at home,110 ring size,22,engagement ring size,16,how to work out ring size uk,110 ring size,23,ring sizes,15,us ring size to eu,100 ring size,24,cm to mm,14,how do i figure out my ring size,100 , engagement,0,ring engagement,100,bakhtawar bhutto engagement,4800 engagement,1,rings,99,himanshi khurana engagement,4600 engagement,2,engagement rings,96,ankita lokhande engagement,4300 engagement,3,diamond engagement ring,14,ankita lokhande,4150 engagement,4,diamond rings,13,engagement awc.odisha.gov.in,2950 engagement,5,diamond engagement rings,13,emma stone engagement ring,2700 engagement,6,employee engagement,11,hardik pandya engagement,1850 engagement,7,wedding ring,10,youth engagement for global action,1750 engagement,8,gold engagement rings,8,gwen stefani engagement ring,1450 engagement,9,engagement party,8,katrina kaif engagement,1250 engagement,10,engagement meaning,7,lily collins engagement ring,1200 engagement,11,engagement dress,7,aaron rodgers engagement,1000 engagement,12,instagram engagement,7,demi lovato engagement,800 engagement,13,wedding rings,6,instagram engagement rate calculator,300 engagement,14,engagement photos,6,engagement makeup look,300 engagement,15,rules of engagement,6,mia khalifa engagement,250 engagement,16,engagement wishes,5,engagement artinya,250 engagement,17,community engagement,5,happy engagement wishes,200 engagement,18,engagement rate,5,engagement cake design,190 engagement,19,engagement quotes,4,engagement rate calculator,180 engagement,20,engagement gifts,4,instagram engagement calculator,160 engagement,21,customer engagement,4,engagement anniversary wishes to husband,160 engagement,22,forfait sans engagement,4,engagement rings for couples,140 engagement,23,tiffany,4,engagement adalah,140 engagement,24,engagement rings for women,4,engagement anniversary wishes,140 , engagement ring women,0,engagement ring for women,100,white gold engagement ring for women,60 engagement ring women,1,women engagement rings,71,, engagement ring women,2,engagement rings,65,, engagement ring women,3,rings for women,59,, engagement ring women,4,engagement rings for women,55,, engagement ring women,5,diamond ring for women,26,, engagement ring women,6,wedding rings,22,, engagement ring women,7,diamond rings for women,18,, engagement ring women,8,diamond engagement rings for women,17,, engagement ring women,9,wedding rings for women,16,, engagement ring women,10,engagement ring finger,16,, engagement ring women,11,gold engagement rings for women,13,, engagement ring women,12,engagement ring finger for women,12,, engagement ring women,13,wedding bands,8,, engagement ring women,14,wedding bands for women,8,, engagement ring women,15,engagement ring sets for women,5,, engagement ring women,16,white gold engagement ring for women,5,, engagement ring women,17,wedding ring sets for women,4,, engagement ring women,18,kay jewelers,2,, , diamond ring for women,0,diamond rings,100,eternity ring for women,550 diamond ring for women,1,diamond rings for women,98,, diamond ring for women,2,rings for women,98,, diamond ring for women,3,gold diamond ring for women,74,, diamond ring for women,4,diamond ring price for women,46,, diamond ring for women,5,diamond ring price,46,, diamond ring for women,6,diamond engagement rings for women,46,, diamond ring for women,7,engagement rings for women,40,, diamond ring for women,8,engagement rings,39,, diamond ring for women,9,wedding rings for women,25,, diamond ring for women,10,tanishq diamond ring for women,21,, diamond ring for women,11,tanishq,19,, diamond ring for women,12,platinum diamond ring for women,15,, diamond ring for women,13,platinum ring for women,12,, diamond ring for women,14,black diamond ring for women,12,, diamond ring for women,15,diamond ring for men,12,, diamond ring for women,16,diamond earrings for women,10,, diamond ring for women,17,eternity ring for women,10,, diamond ring for women,18,wedding bands for women,9,, , engagement ring,0,engagement rings,100,patrick mahomes engagement ring,11650 engagement ring,1,rings,98,heather rae young engagement ring,11100 engagement ring,2,diamond engagement ring,87,shailene woodley engagement ring,5700 engagement ring,3,diamond ring,82,gwen stefani,2700 engagement ring,4,wedding ring,59,katie thurston engagement ring,1850 engagement ring,5,engagement ring gold,44,gwen stefani engagement ring,1550 engagement ring,6,diamond engagement rings,29,emma stone engagement ring,1350 engagement ring,7,halo engagement ring,19,demi lovato engagement ring,600 engagement ring,8,wedding rings,18,hidden halo engagement ring,550 engagement ring,9,wedding ring and engagement ring,18,engagement ring booking,450 engagement ring,10,engagement ring finger,18,1000 dollar engagement ring,300 engagement ring,11,engagement ring hand,17,gold engagement ring designs for female,110 engagement ring,12,tiffany,16,engagement ring platter,110 engagement ring,13,tiffany engagement ring,15,how much are you supposed to spend on an engagement ring,110 engagement ring,14,oval engagement ring,14,engagement ring designs for female,100 engagement ring,15,rose engagement ring,14,three stone oval engagement ring,100 engagement ring,16,sapphire engagement ring,13,which finger is for engagement ring,90 engagement ring,17,engagement ring set,13,engagement and wedding ring set,80 engagement ring,18,solitaire engagement ring,13,princess diana engagement ring,70 engagement ring,19,pear engagement ring,13,nikki bella engagement ring,70 engagement ring,20,engagement ring price,12,oval solitaire engagement ring,70 engagement ring,21,emerald engagement ring,12,engagement ring cost rule,70 engagement ring,22,rose gold engagement ring,12,do you wear your engagement ring on your wedding day,60 engagement ring,23,white gold engagement ring,11,how much is an engagement ring,60 engagement ring,24,engagement ring size,11,what finger does the engagement ring go on,60 , wedding ring for women,0,wedding rings for women,100,wedding ring set for women,110 wedding ring for women,1,wedding rings,98,wedding ring sets for women,80 wedding ring for women,2,rings for women,93,wedding ring sets,60 wedding ring for women,3,wedding sets for women,48,wedding sets for women,60 wedding ring for women,4,wedding ring sets,46,, wedding ring for women,5,wedding ring sets for women,45,, wedding ring for women,6,wedding bands for women,40,, wedding ring for women,7,wedding band for women,39,, wedding ring for women,8,engagement rings for women,39,, wedding ring for women,9,wedding ring set for women,20,, wedding ring for women,10,wedding ring for men,19,, wedding ring for women,11,wedding ring finger for women,15,, wedding ring for women,12,ring finger for women,14,, wedding ring for women,13,gold wedding bands for women,8,, wedding ring for women,14,best wedding rings for women,5,, , engagement ring for women,0,rings for women,100,unique engagement rings for women,300 engagement ring for women,1,engagement rings,97,wedding ring sets for women,70 engagement ring for women,2,engagement rings for women,92,diamond rings for women,60 engagement ring for women,3,diamond ring for women,43,diamond engagement rings for women,50 engagement ring for women,4,diamond rings for women,34,engagement rings for women,40 engagement ring for women,5,diamond engagement rings for women,33,engagement rings,40 engagement ring for women,6,wedding rings for women,31,, engagement ring for women,7,gold engagement rings for women,26,, engagement ring for women,8,engagement ring finger for women,18,, engagement ring for women,9,engagement ring finger,18,, engagement ring for women,10,wedding bands for women,10,, engagement ring for women,11,white gold engagement ring for women,6,, engagement ring for women,12,unique engagement rings for women,4,, engagement ring for women,13,wedding ring sets for women,4,, , silver ring,0,sterling silver,100,re8 silver ring,9950 silver ring,1,sterling silver ring,95,dior ring silver,900 silver ring,2,silver rings,68,dior ring,900 silver ring,3,rings,68,silver ring design for girl,400 silver ring,4,gold ring,61,fendi ring,350 silver ring,5,men silver ring,35,covetous silver serpent ring ds3,300 silver ring,6,men ring,34,sterling silver ring blanks,250 silver ring,7,mens silver ring,32,gold price today,250 silver ring,8,silver ring price,30,boy ring design silver,250 silver ring,9,diamond ring,30,silver price today,200 silver ring,10,diamond silver ring,30,evil eye ring silver,180 silver ring,11,silver price,29,sterling silver thumb ring,180 silver ring,12,diamond,29,boys ring design,170 silver ring,13,ring for men,24,gucci silver heart ring,170 silver ring,14,silver 925 ring,23,silver wishbone ring,160 silver ring,15,silver ring for men,23,sterling silver turtle ring,140 silver ring,16,ring for men silver,23,snake ring,140 silver ring,17,wedding ring silver,22,silver ring designs for men,130 silver ring,18,925 silver,22,silver ring designs for girls,130 silver ring,19,black silver ring,21,silver serpent ring ds3,120 silver ring,20,ring design,21,today gold rate,120 silver ring,21,silver ring design,20,vivienne westwood,120 silver ring,22,silver band ring,18,leg ring silver,110 silver ring,23,sterling silver rings,18,etsy uk,110 silver ring,24,pandora ring,18,wrap around ring silver,110 , women ring design,0,ring design for women,100,gold rate today,300 women ring design,1,women gold ring design,71,, women ring design,2,gold ring,70,, women ring design,3,gold ring design,66,, women ring design,4,gold ring for women design,64,, women ring design,5,gold ring for women,62,, women ring design,6,silver ring for women,6,, women ring design,7,ring design for men,5,, women ring design,8,gold ring design for men,4,, women ring design,9,gold rate today,3,, , ring design,0,gold,100,gold ring design 2020,70700 ring design,1,ring gold,98,latest ring design 2020,32500 ring design,2,gold ring design,97,"gold ring design for male under 10,000",9650 ring design,3,men ring design,17,blue stone ring design for female,5350 ring design,4,female ring design,15,silver ring design for girl,350 ring design,5,ring designs,15,umbrella ring design,350 ring design,6,ear ring design,15,girl finger ring design,300 ring design,7,ring design for female,14,ear ring design for girl,300 ring design,8,diamond ring,14,boy ring design silver,250 ring design,9,design diamond ring,14,gold ear ring design for girl,200 ring design,10,ear ring,13,today gold price,200 ring design,11,design engagement ring,13,ring ceremony,200 ring design,12,silver ring,13,simple gold ring design for female,200 ring design,13,engagement ring,13,big gold ring design for female,190 ring design,14,silver ring design,13,toe ring design,160 ring design,15,stone ring design,13,simple ring design for female,150 ring design,16,gold price,13,simple ring design for girl,140 ring design,17,new ring design,12,gold rate today,130 ring design,18,design of ring,12,small ear ring design,130 ring design,19,ring design for men,11,queen ring design,110 ring design,20,female gold ring design,11,latest ring design for girl,110 ring design,21,ring gold design female,11,boy ring design gold,100 ring design,22,gold ring design for female,11,chandi ring design for girl,100 ring design,23,women ring design,11,boys gold ring design,100 ring design,24,girl ring design,10,finger ring design for girl,100 , women ring finger,0,ring finger for women,100,gold ring for women,110 women ring finger,1,wedding ring finger women,33,gold finger ring for women,40 women ring finger,2,wedding ring,30,, women ring finger,3,wedding ring finger,30,, women ring finger,4,engagement ring finger for women,22,, women ring finger,5,wedding ring for women,20,, women ring finger,6,wedding ring finger for women,18,, women ring finger,7,gold ring for women,18,, women ring finger,8,gold finger ring for women,16,, women ring finger,9,ring finger for men,10,, , cartier ring,0,love ring,100,cartier 750 ring 52833a real or fake,18050 cartier ring,1,love ring cartier,97,gucci ghost ring,9100 cartier ring,2,cartier love,95,cartier 750 ring 52833a leve,6900 cartier ring,3,cartier gold ring,31,dhgate cartier ring,1450 cartier ring,4,gold ring,30,chaumet,500 cartier ring,5,cartier bracelet,28,cartier clash ring,400 cartier ring,6,diamond ring,26,clash de cartier ring,400 cartier ring,7,cartier diamond ring,24,cartier ring dupe amazon,350 cartier ring,8,rings,21,cartier dupe ring,350 cartier ring,9,cartier rings,21,dhgate,300 cartier ring,10,tiffany,20,panthère de cartier ring,300 cartier ring,11,tiffany ring,19,fendi ring,250 cartier ring,12,wedding ring,18,dior ring,250 cartier ring,13,engagement ring,18,dior,250 cartier ring,14,cartier engagement ring,18,cartier love ring dupe,200 cartier ring,15,cartier wedding ring,17,rings for women,200 cartier ring,16,cartier ring price,14,chanel bracelet,190 cartier ring,17,trinity cartier ring,14,cartier 750 ring 52833a,170 cartier ring,18,trinity ring,14,carters,150 cartier ring,19,cartier trinity,13,cartier friendship ring,150 cartier ring,20,trinity,13,christ,140 cartier ring,21,cartier band,12,cartier live ring,130 cartier ring,22,cartier love bracelet,12,cartier 750 ring,120 cartier ring,23,gucci,11,louis vuitton,110 cartier ring,24,gucci ring,11,gucci rings,110 , cartier,0,bracelet cartier,100,adam and cartier love island,5700 cartier,1,love cartier,72,cartier surjan,5550 cartier,2,cartier ring,71,adam and cartier,4850 cartier,3,cartier watch,60,cartier love island,2700 cartier,4,jacques cartier,42,gabbie cartier,1700 cartier,5,bracelet love cartier,36,lilou cartier,1400 cartier,6,santos cartier,27,pasha de cartier parfum,800 cartier,7,cartier tank,25,cartier skeleton watch,300 cartier,8,cartier glasses,23,กํา ไล cartier,200 cartier,9,cartier watches,20,cartier santos skeleton,170 cartier,10,cartier ring love,19,anel cartier,170 cartier,11,tiffany,17,cartier frames men,160 cartier,12,rolex,15,cartier bilezik,140 cartier,13,cartier panthere,14,bratara cartier,130 cartier,14,cartier bague,13,cartier crash,130 cartier,15,cartier necklace,12,van cleef & arpels,130 cartier,16,gucci,11,cartier bileklik altın,120 cartier,17,cartier rings,11,cartier brille,120 cartier,18,cartier pasha,11,pizzeria jacques cartier,110 cartier,19,cartier perfume,11,bague clou cartier,110 cartier,20,montre cartier,10,pulseira cartier,110 cartier,21,parfum cartier,10,bague cartier femme,100 cartier,22,louis vuitton,10,cartier trinity armband,100 cartier,23,cartier sunglasses,9,cartier ohrringe,90 cartier,24,chanel,9,cartier glasses men,90 , macys,0,macys near me,100,macys cyber monday 2019,15650 macys,1,macys sale,66,macys parade 2020,11400 macys,2,macys hours,59,macys closing stores 2020,4500 macys,3,macys furniture,59,macys fireworks 2020,2950 macys,4,nordstrom,53,macys parade 2019,1000 macys,5,kohls,46,is macys open,250 macys,6,macys shoes,46,is macys closing,250 macys,7,jcpenney,45,macys stock,150 macys,8,macys dresses,42,macys stock price,150 macys,9,macys coupon,42,coach outlet,130 macys,10,macys store,41,macys thanksgiving parade time,120 macys,11,insite macys,41,macys closing,110 macys,12,macys mens,40,macys outdoor furniture,110 macys,13,macys card,38,macys open,100 macys,14,target,37,bath and body works,100 macys,15,dillards,34,dillards near me,90 macys,16,macys credit,33,macys palm desert,90 macys,17,macys login,32,macys radley sectional,90 macys,18,macys credit card,27,macys pr,80 macys,19,macys home,26,macys el centro,80 macys,20,macys men,24,gap factory,80 macys,21,macys parade,24,macys guam,70 macys,22,macys boots,21,macys backstage,70 macys,23,macys online,20,macys bedding sale,70 macys,24,macys stock,19,macys returns,70 , moonstone ring,0,moonstone engagement ring,100,rainbow moonstone engagement ring,300 moonstone ring,1,moonstone rings,77,vintage moonstone engagement ring,300 moonstone ring,2,gold moonstone ring,49,moonstone promise ring,250 moonstone ring,3,moon ring,46,moon magic,140 moonstone ring,4,silver moonstone ring,42,moon ring,110 moonstone ring,5,moonstone diamond ring,35,moonstone wedding ring,100 moonstone ring,6,moonstone wedding ring,31,mens moonstone ring,80 moonstone ring,7,moonstone meaning,30,moon stone,80 moonstone ring,8,rainbow moonstone ring,27,raw moonstone ring,70 moonstone ring,9,etsy moonstone ring,25,moonstone ring uk,60 moonstone ring,10,etsy,24,rose gold moonstone ring,60 moonstone ring,11,moonstone ring meaning,24,promise rings,60 moonstone ring,12,moonstone ring uk,17,silver moonstone ring,40 moonstone ring,13,rose gold moonstone ring,17,moonstone rings,40 moonstone ring,14,moon stone,16,, moonstone ring,15,raw moonstone ring,13,, moonstone ring,16,vintage moonstone ring,13,, moonstone ring,17,mens moonstone ring,12,, moonstone ring,18,alexandrite,12,, moonstone ring,19,moonstone and diamond ring,11,, moonstone ring,20,june birthstone,11,, moonstone ring,21,opal rings,10,, moonstone ring,22,moonstone engagement ring meaning,7,, moonstone ring,23,moon magic,6,, moonstone ring,24,vintage moonstone engagement ring,5,, , gold price today,0,gold price in today,100,gold price in pakistan 2020 today,26000 gold price today,1,gold rate,31,gold price today siliguri,200 gold price today,2,gold rate today,30,gold price today in siliguri,200 gold price today,3,today gold price india,20,gold price today kota,180 gold price today,4,gold price in india today,17,gold price today jammu,120 gold price today,5,gold price in india,16,yes bank share price,110 gold price today,6,today price of gold,15,sbi share price,100 gold price today,7,price of gold,15,gold price today varanasi,100 gold price today,8,today silver price,14,24ct gold price today,90 gold price today,9,silver price,14,yes bank share,90 gold price today,10,gold price today delhi,12,gold price today in moradabad,90 gold price today,11,today gold price kolkata,9,gold price today jaipur,90 gold price today,12,today gold price in delhi,9,gold price today patna,90 gold price today,13,gold price in delhi,9,gold price today in kota,90 gold price today,14,today gold price hyderabad,8,gold price today amritsar,80 gold price today,15,today gold price 22k,7,gold price today kanpur,70 gold price today,16,today 22 carat gold price,7,gold price today in jammu,70 gold price today,17,22k gold price today,7,ril share price,70 gold price today,18,gold price today mumbai,6,gold price today 22k kolkata,70 gold price today,19,gold price mumbai,6,gold price today in meerut,60 gold price today,20,gold price today pakistan,6,gold price today bbsr,60 gold price today,21,today gold price in kolkata,6,24 carat gold price in ahmedabad today,60 gold price today,22,gold price today in hyderabad,6,gold price today jodhpur,60 gold price today,23,today gold price in hyderabad,6,gold price today in up,60 gold price today,24,gold price today ahmedabad,5,gold price today gwalior,50 , gold rate today,0,today gold price,100,gold rate in pakistan today 2020,22350 gold rate today,1,gold price,98,gold rate in kerala today 1gm,2900 gold rate today,2,gold rate today chennai,77,today gold rate in ap,150 gold rate today,3,today gold rate hyderabad,70,today gold rate mysore,100 gold rate today,4,gold rate today india,60,today gold rate in karnataka,90 gold rate today,5,gold rate today in chennai,57,today gold and silver rate in hyderabad,80 gold rate today,6,gold rate in chennai,54,gold rate today in latur,80 gold rate today,7,today bangalore gold rate,52,gold rate today bhopal,80 gold rate today,8,gold silver rate today,48,gold rate today in karnataka,80 gold rate today,9,silver rate today,48,svbc gold rate today,70 gold rate today,10,gold rate in india today,48,today gold rate in kakinada,60 gold rate today,11,gold rate in india,47,gold rate in mysore today,60 gold rate today,12,gold rate today in india,46,gold rate today kakinada,60 gold rate today,13,gold rate in hyderabad today,45,today gold rate hyderabad,60 gold rate today,14,gold rate mumbai today,44,gold silver rate today,50 gold rate today,15,gold rate in hyderabad,43,today gold rate in up,50 gold rate today,16,today gold rate delhi,42,gold rate today vizag,50 gold rate today,17,gold rate delhi today,42,today gold rate visakhapatnam,50 gold rate today,18,22 carat gold rate,35,gold rate today rajahmundry,50 gold rate today,19,today gold rate 22 carat,35,silver rate today,50 gold rate today,20,today gold rate in delhi,33,today gold rate vijayawada,40 gold rate today,21,gold rate today in bangalore,33,gold rate today nashik,40 gold rate today,22,gold rate today kerala,32,gold rate today lucknow,40 gold rate today,23,gold rate in bangalore,32,, gold rate today,24,gold rate in delhi,32,, , how to measure ring size,0,how to measure your ring size,100,how to measure my ring size at home,69350 how to measure ring size,1,how to measure for ring size,88,how to figure out ring size,25850 how to measure ring size,2,how to measure ring size at home,75,how to measure a ring size at home,500 how to measure ring size,3,cm to mm,68,how to measure ring size with string,450 how to measure ring size,4,rings,63,ring size guide,250 how to measure ring size,5,how to measure ring size in cm,57,how to measure bra size,200 how to measure ring size,6,how to measure ring size in mm,56,etsy,200 how to measure ring size,7,how to measure ring size uk,52,how do you measure ring size,200 how to measure ring size,8,how to measure ring finger size,50,pandora,180 how to measure ring size,9,ring size chart,45,how to measure ring size women,170 how to measure ring size,10,inches to mm,44,how to measure your ring size at home,160 how to measure ring size,11,how to measure ring size in inches,39,how to measure ring size uk,150 how to measure ring size,12,ring size in inches,37,how to measure ring size at home,140 how to measure ring size,13,how to measure ring size men,36,how to measure my ring size,140 how to measure ring size,14,how to measure ring size women,34,how to measure ring size for men,120 how to measure ring size,15,how to measure my ring size,33,mejuri,120 how to measure ring size,16,how to measure finger size for ring,23,how to measure finger size for ring,110 how to measure ring size,17,how to measure mens ring size,23,cm to mm,110 how to measure ring size,18,how to measure ring size with tape measure,22,how to measure ring size in cm,100 how to measure ring size,19,ring sizes,21,how to measure ring size in mm,100 how to measure ring size,20,how to measure ring size us,19,how to measure ring size in inches,100 how to measure ring size,21,how to measure your ring size at home,15,how to measure ring size us,100 how to measure ring size,22,how to measure ring size for men,14,ring size in inches,90 how to measure ring size,23,how to measure a ring size at home,14,kay jewelers,90 how to measure ring size,24,pandora,14,how to measure your finger for a ring,80 , cartier love ring,0,cartier love bracelet,100,dhgate,23150 cartier love ring,1,cartier bracelet,91,cartier love ring dupe,350 cartier love ring,2,love ring cartier gold,71,tiffany and co,160 cartier love ring,3,cartier love ring diamond,43,cartier love ring silver,50 cartier love ring,4,cartier rings,32,love ring cartier gold,40 cartier love ring,5,tiffany ring,31,cartier love ring diamond,40 cartier love ring,6,cartier love ring price,30,, cartier love ring,7,tiffany,28,, cartier love ring,8,fake cartier ring,23,, cartier love ring,9,cartier love ring dupe,23,, cartier love ring,10,the love ring cartier,20,, cartier love ring,11,fake cartier love ring,20,, cartier love ring,12,cartier white gold love ring,17,, cartier love ring,13,cartier love ring men,16,, cartier love ring,14,cartier love necklace,14,, cartier love ring,15,cartier love ring silver,14,, cartier love ring,16,louis vuitton,13,, cartier love ring,17,cartier love ring rose gold,13,, cartier love ring,18,mens cartier love ring,13,, cartier love ring,19,tiffany and co,11,, cartier love ring,20,hermes bracelet,6,, cartier love ring,21,cartier love bangle,6,, cartier love ring,22,cartier love ring with diamonds,6,, cartier love ring,23,cartier trinity ring,5,, cartier love ring,24,pandora,4,, , kay jewelers,0,kay jewelers rings,100,kay jewelers center of me,12900 kay jewelers,1,rings,99,kays fine jewelry,3250 kay jewelers,2,kay jewelry,97,kay jewelers hanover pa,2750 kay jewelers,3,jewelry,96,kay jewelers black friday 2020,1650 kay jewelers,4,jewelers near me,90,does kay jewelers sell fake diamonds,1600 kay jewelers,5,kay jewelers near me,88,kayleigh mcenany,1550 kay jewelers,6,zales jewelers,62,kay jewelers cyber monday 2019,1000 kay jewelers,7,zales,59,kay jewelers sioux falls,750 kay jewelers,8,kay jewelers necklace,58,kay jewelers black friday sale,450 kay jewelers,9,necklace,57,kay jewelers grove city,400 kay jewelers,10,kay jewelers card,56,kay jewelers virginia beach,350 kay jewelers,11,kay jewelers credit,55,kay jewelers anklet,350 kay jewelers,12,kay jewelers credit card,50,kay jewelers salem oregon,250 kay jewelers,13,pandora,36,kay jewelers mother rings,200 kay jewelers,14,engagement rings,35,kay jewelers tracking,190 kay jewelers,15,kay jewelers engagement rings,34,kay pee jewelers,180 kay jewelers,16,jared jewelers,34,engagement rings for women,170 kay jewelers,17,jared,33,is kay jewelers good,150 kay jewelers,18,kay jewelers earrings,31,kay jewelers promo code,140 kay jewelers,19,kay jewelers outlet,30,kay jewelers memphis,140 kay jewelers,20,kay outlet,30,zales outlet,130 kay jewelers,21,kays jewelers,28,kay jewelers rochester mn,120 kay jewelers,22,kays,27,kay jewelers discount code,120 kay jewelers,23,kay jewelers sale,26,jewelry stores near me,120 kay jewelers,24,jewelry stores,24,zales near me,120 , swarovski,0,pandora,100,swarovski nl pure,8300 swarovski,1,swarovski crystal,74,swarovski christmas ornament 2020,5150 swarovski,2,crystal,74,swarovski 2019 ornament,1700 swarovski,3,bracelet swarovski,61,swarovski optik dg,1400 swarovski,4,swarovski necklace,60,swarovski schwanger,750 swarovski,5,bracelet,60,harga kalung swarovski,650 swarovski,6,earrings,53,anelli swarovski 2021,650 swarovski,7,swarovski crystals,52,victoria swarovski schwanger,550 swarovski,8,swarovski earrings,51,swarovski adalah,250 swarovski,9,ring swarovski,50,bratara swarovski,250 swarovski,10,swarovski sale,48,swarovski tennis bracelet,110 swarovski,11,swarovski victoria,43,swarovski tennis necklace,100 swarovski,12,outlet swarovski,42,swarovski ireland,90 swarovski,13,swarovski uk,42,swarovski near me,90 swarovski,14,jewelry,27,collana swarovski uomo,90 swarovski,15,swarovski jewelry,27,swarovski store near me,90 swarovski,16,swarovski canada,26,colar swarovski,90 swarovski,17,swarovski rings,25,swarovski black friday,80 swarovski,18,swarovski watch,24,swarovski discount code,80 swarovski,19,swarovski online,24,idee cadeau femme,80 swarovski,20,swarovski kette,23,swarovski voucher code,70 swarovski,21,swarovski jewellery,20,swar,70 swarovski,22,jewellery,20,swarovski crystals for nails,70 swarovski,23,tiffany,19,สร้อย swarovski,70 swarovski,24,swarovski armband,18,swarovski egypt,70 , louis vuitton,0,louis vuitton bag,100,louis vuitton california dream,3150 louis vuitton,1,gucci,89,louis vuitton iphone 11 case,2950 louis vuitton,2,louis vuitton bags,38,louis vuitton face mask,2900 louis vuitton,3,lv,36,masque louis vuitton,2250 louis vuitton,4,chanel,29,louis vuitton filter,1450 louis vuitton,5,louis vuitton purse,27,multi pochette louis vuitton,1300 louis vuitton,6,louis vuitton shoes,27,louis vuitton maske,1250 louis vuitton,7,louis vuitton wallet,27,louis vuitton mask,950 louis vuitton,8,dior,27,louis partridge,900 louis vuitton,9,louis vuitton pochette,26,louis vuitton bts,600 louis vuitton,10,louis vuitton sac,22,on the go louis vuitton,600 louis vuitton,11,louis vuitton belt,19,louis vuitton ombre nomade,500 louis vuitton,12,prada,18,louis vuitton league of legends,450 louis vuitton,13,louis vuitton sale,15,louis vuitton airpod case,400 louis vuitton,14,neverfull louis vuitton,15,christian dior,250 louis vuitton,15,louis vuitton backpack,14,bonnet louis vuitton,200 louis vuitton,16,supreme,13,louis vuitton bucket hat,200 louis vuitton,17,louis vuitton supreme,13,louis vuitton stencil,200 louis vuitton,18,louis vuitton tasche,13,louis vuitton pochette accessoires,200 louis vuitton,19,louis vuitton sneakers,13,louis vuitton felicie pochette,170 louis vuitton,20,hermes,13,louis vuitton beanie,170 louis vuitton,21,burberry,12,dior,160 louis vuitton,22,balenciaga,12,coach outlet,120 louis vuitton,23,fendi,12,louis vuitton lock necklace,120 louis vuitton,24,louis vuitton bracelet,11,louis tomlinson,110 , the bling ring,0,the real bling ring,100,best movies on netflix,36900 the bling ring,1,bling ring movie,48,what happened to the bling ring,29050 the bling ring,2,the bling ring movie,46,the bling ring real story,400 the bling ring,3,the bling ring netflix,43,the bling ring netflix,400 the bling ring,4,emma watson,42,the bling ring rotten tomatoes,300 the bling ring,5,the bling ring emma watson,35,the bling ring real people,250 the bling ring,6,the bling ring cast,31,the bling ring imdb,250 the bling ring,7,the bling ring story,29,the bling ring review,150 the bling ring,8,alexis neiers,24,the real bling ring,130 the bling ring,9,the bling ring real people,22,the bling ring real life,130 the bling ring,10,the bling ring film,19,the bling ring cast,70 the bling ring,11,the bling ring real story,13,, the bling ring,12,the bling ring streaming,13,, the bling ring,13,the bling ring real life,12,, the bling ring,14,the bling ring trailer,11,, the bling ring,15,the bling ring true story,11,, the bling ring,16,the bling ring review,6,, the bling ring,17,best movies on netflix,5,, the bling ring,18,the bling ring full movie,5,, the bling ring,19,the bling ring rotten tomatoes,5,, the bling ring,20,the bling ring imdb,4,, the bling ring,21,the bling ring 2011,4,, the bling ring,22,sofia coppola,4,, the bling ring,23,what happened to the bling ring,4,, the bling ring,24,is the bling ring a true story,3,, , ================================================ FILE: 004-EmailNotify/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 004-EmailNotify/README.md ================================================ # 监听玩客家平台币价变化,使用邮件通知 本小项目主要体现邮箱的作用,如果利用微信绑定收信邮箱,便能实时知道自己想要知道的信息。 注意:发送邮箱的登录密码一般是在设置smtp中生成的授权码。 - 在服务器上启动脚本,与连接无关: ``` nohup python main.py ``` ================================================ FILE: 004-EmailNotify/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 玩客家 币价变化,邮件通知 api @Date :2020/12/17 @Author :xhunmon @Mail :xhunmon@gmail.com """ import json import smtplib import time from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import requests from my_fake_useragent import UserAgent ua = UserAgent() agent = ua.random() class Email(object): def __init__(self, from_user, pwd, to_other): """ :param from_user: 发送者邮箱号 :param pwd: 发送者邮箱密码(非登录密码) :param to_other: 目标邮箱 """ self.__from_user = from_user self.__pwd = pwd self.__to_other = to_other def sendMsg(self, title="", content=""): print("%s向%s发送邮件:标题{%s}\t内容{%s}" % (self.__from_user, self.__to_other, title, content)) retry = 0 while True: print("【%s】开始发送,次数:%3d" % (title, retry)) try: msg = MIMEMultipart() msg.attach(MIMEText(content, 'plain', 'utf-8')) msg['Subject'] = title msg['From'] = self.__from_user s = smtplib.SMTP_SSL("smtp.qq.com", 465) # 通过SSL方式发送,服务器地址和端口 s.login(self.__from_user, self.__pwd) # 登录邮箱 s.sendmail(self.__from_user, self.__to_other, msg.as_string()) # 开始发送 print("邮件发送成功,发送时间【%s】" % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))) break except Exception as e: print( "邮件发送失败,5秒钟后重新发送,发送时间【%s】" % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))) print(e) retry += 1 time.sleep(5) class Coin(object): def __init__(self, coin="", diff=3.0, sleep=3, reload=1 * 60 * 60, email: Email = None): self.__COIN = coin # 币名称 self.__DIFF = diff # 当trend的值幅度差为该设定值,邮箱进行通知 self.__SLEEP_TIME = sleep # 每次获取的睡眠时间 self.__RELOAD_TIME = reload # 每小时重复更新一次页面 self.__email = email self.__RINGE_COUNT = int(self.__RELOAD_TIME / self.__SLEEP_TIME) self.__is_release = False self.__last_price = 0.0 self.__last_time = 0 self.__last_trend = 0.0 self.__is_first = True self.__URL = "https://app.wkj.pub/api/market/getMarketTradeJson" # 请求地址 self.__DATA = 'market=%s_bitcny&sellnum=10&buynum=10&donenum=10' % coin.lower() self.__headers = {"user-agent": agent} def start(self): print("thread【%2s diff is %2s , sleep is %2d ,retry time is %5ld】" % ( self.__COIN, str(self.__DIFF), self.__SLEEP_TIME, self.__RELOAD_TIME)) while True: try: time.sleep(self.__SLEEP_TIME) if self.__is_release: print("%2s 已释放,退出!" % self.__COIN) break req = requests.post(self.__URL, data=self.__DATA, headers=self.__headers) if req.status_code != 200: print("%2s 请求失败-%s" % (self.__COIN, req.status_code)) continue data = json.loads(req.text) # print(req.text) price = float(data.get("data").get("new_price")) # 当前价格 trend = float(data.get("data").get("change")) # 跌涨幅 趋势 num = float(data.get("data").get("doneOrders")[0].get("num")) # 当前成交价 done_time = int(data.get("data").get("doneOrders")[0].get("time")) # 当前成交价 done_type = int(data.get("data").get("doneOrders")[0].get("type")) # 当前状态(1-buy,2-sell) if price <= 0: print("%2s 价格为0,没有获取到数据,跳过" % self.__COIN) continue if done_time == self.__last_time: # 没有最新成交的单 print("【%2s …………】" % self.__COIN) continue status = "buy" if done_type == 1 else "sell" print("【%2s \t| %2s \t| %5s \t| %2s% \t| %2s】" % ( self.__COIN, status, str(price), trend, str(num))) self.__last_price = price self.__last_time = done_time if self.__is_first: # 第一次启动就不必了 self.__is_first = False self.__last_trend = trend print("【%2s __is_first】" % self.__COIN) continue temp_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) if self.__last_trend - trend > self.__DIFF: # 跌了 self.__last_trend = trend self.__email.sendMsg( "【%2s跌:%s | %s%】" % (self.__COIN, str(self.__last_price), str(trend)), str("【价格:%s,跌幅:%s%,时间:%s】" % ( str(self.__last_price), str(trend), temp_time))) elif trend - self.__last_trend > self.__DIFF: # 涨了 self.__last_trend = trend self.__email.sendMsg( "【%2s涨:%s | %s%】" % (self.__COIN, str(self.__last_price), str(trend)), str("【价格:%s,涨幅:%s%,时间:%s】" % ( str(self.__last_price), str(trend), temp_time))) except Exception as e: print("wkj api【%2s】异常,5秒钟后重试!" % self.__COIN) print(e) time.sleep(5) print("【%2s 结束了】" % self.__COIN) if __name__ == "__main__": email = Email(from_user="aaa@qq.com", pwd="bbb", to_other='ccc@qq.com') eth = Coin(coin="ETH", diff=2, sleep=5, reload=2 * 60 * 60, email=email) eth.start() ================================================ FILE: 005-PaidSource/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 005-PaidSource/005-PaidSource.iml ================================================ ================================================ FILE: 005-PaidSource/README.md ================================================ # 这些脚本你肯定会有用到的 ### 操作已打开的chrome浏览器 场景:某些情况我们获取怎么都获取不到cookie,但我们可以使用先在浏览器上登录,然后进行自动化操作。 操作指南: ```shell 需要以该方式启动的浏览器: win: chrome.exe --remote-debugging-port=9222 mac:/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222& ``` 实现脚本:[chrome.py](./chrome.py) ### excel表的常规操作 场景:word文档生活使用就不用多说了,学会定能给生活带来很大的便利。 操作指南:使用 pandas 开源库实现。 实现脚本:① 考勤统计实现 [kaoqin.py](./kaoqin.py) 。②从excel表取数据翻译后重新写入[gtransfer.py](./gtransfer.py) ### 用ffmpeg批量修改视频的md5值 场景:短视频搬运专用。 操作指南:需要安装ffmpeg环境。 实现脚本:[ff_video.py](./ff_video.py) ### 文件相关操作:json读写、文件子目录文件获取、html转word等 场景:文件的一些操作。 操作指南:略。 实现脚本:[file_util.py](./file_util.py) ### 其他站点爬虫与解析 场景:注意学会BeautifulSoup解析,取属性值等。 操作指南:略。 实现脚本:[other_site.py](./other_site.py) ================================================ FILE: 005-PaidSource/__init__.py ================================================ ================================================ FILE: 005-PaidSource/chrome.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 用已经打开的chrome浏览器进行自动化操作。 在某些应用场景我们获取怎么都获取不到cookie,但我们可以使用先在浏览器上登录,然后进行自动化操作。 这里实现book118.com网站自动化操作。 @Date :2022/1/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import asyncio import random import time import aiohttp import requests from bs4 import BeautifulSoup from pyppeteer import launcher import file_util as futls from v2ray_pool import Net loop = asyncio.get_event_loop() async def get_cookie(page): """ 获取cookie :param:page page对象 :return:cookies 处理后的cookie """ cookie_list = await page.cookies() cookies = "" for cookie in cookie_list: coo = "{}={};".format(cookie.get("name"), cookie.get("value")) cookies += coo return cookies async def main(): async with aiohttp.ClientSession() as session: try: async with session.get("http://localhost:9222/json/version") as response: chrome = await response.json() browser = await launcher.connect( defaultViewport=None, loop=loop, browserWSEndpoint=chrome['webSocketDebuggerUrl'] ) except aiohttp.ClientConnectorError: print("start chrome --headless --remote-debugging-port=9222 --disable-gpu") return # pages = await browser.pages() page = await browser.newPage() # "通过 Browser 对象创建页面 Page 对象" await page.goto('https://max.book118.com/user_center_v1/doc/Doclist/trash.html') table = await page.waitForSelector('#table') print(table) content = await page.content() # 获取页面内容 futls.write(content, 'test/src.html') agent = await browser.userAgent() cookies = await get_cookie(page) print('agent:[%s]' % agent) print('cookies:[%s]' % cookies) results = Book118.parse_page(content) print(results) print('需要操作的个数:[%d]' % len(results)) headers = { 'Cookie': cookies, 'User-Agent': agent } for result in results: aids = result.get('aids') title = result.get('title') Book118.recycling(headers, aids, title) time.sleep(random.randint(2, 5)) Book118.recycling_name(headers, aids) time.sleep(random.randint(2, 5)) class Book118(Net): '''https://max.book118.com/user_center_v1/doc/index/index.html#trash''' @staticmethod def recycling(headers, aids, title): data = { 'aids': aids, 'is_optimization': 0, 'title': title, 'keywords': '', 'typeid': 481, 'dirid': 0, 'is_original': 0, 'needmoney': random.randint(3, 35), 'summary': '' } url = 'https://max.book118.com/user_center_v1/doc/Api/updateDocument/docListType/recycling' r = requests.post(url=url, data=data, headers=headers, allow_redirects=False, verify=False, timeout=15, stream=True) if r.status_code == 200: print('修改[%s]成功' % title) else: print(r) raise Exception('[%s]修改失败!' % title) @staticmethod def recycling_name(headers, aids): data = { 'aids': aids, 'reason': '文件名已修复', 'status': 1 } url = 'https://max.book118.com/user_center_v1/doc/Api/recoverDocument/docListType/recycling' r = requests.post(url=url, data=data, headers=headers, allow_redirects=False, verify=False, timeout=15, stream=True) if r.status_code == 200: print('提交[%s]成功' % aids) else: print(r) raise Exception('[%s]操作失败!' % aids) @staticmethod def load_page(url): r = requests.get(url=url, allow_redirects=False, verify=False, timeout=15, stream=True) r.encoding = r.apparent_encoding print('url[%s], code[%d]' % (url, r.status_code)) if r.status_code == 200: return r.text return None @staticmethod def parse_page(content): soup = BeautifulSoup(content, 'html.parser') tbody = soup.find('tbody') results = [] for tr in tbody.find_all('tr'): if '文档名不规范' in tr.find('td', class_='col-delete-reason').text: title: str = tr.get_attribute_list('data-title')[0] if title.endswith('..docx'): title = title.replace('..docx', '') aids = tr.get_attribute_list('data-aid')[0] results.append({'aids': aids, 'title': title}) return results if __name__ == "__main__": ''' 注意:需要以该方式启动的浏览器: win: chrome.exe --remote-debugging-port=9222 mac:/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222& ''' loop.run_until_complete(main()) ================================================ FILE: 005-PaidSource/ff_video.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 使用ffmpeg去掉最后一帧,改变md5。短视频搬运专用 @Date :2022/02/17 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os def cute_video(folder): files = next(os.walk(folder))[2] # 获取文件 for file in files: file_path = os.path.join(folder, file) shotname, extension = os.path.splitext(file) if len(shotname) == 0 or len(extension) == 0: continue out_file = os.path.join(folder, 'out-{}{}'.format(shotname, extension)) # 获取时间。输入自己系统安装的ffmpeg,注意斜杠 time = os.popen( r"/usr/local/ffmpeg/bin/ffmpeg -i {} 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//".format( file_path)).read().replace('\n', '').replace(' ', '') if '.' in time: match_time = time.split('.')[0] else: match_time = time print(match_time) ts = match_time.split(':') sec = int(ts[0]) * 60 * 60 + int(ts[1]) * 60 + int(ts[2]) # 从0分0秒100毫秒开始截切(目的就是去头去尾) os.popen(r"/usr/local/ffmpeg/bin/ffmpeg -ss 0:00.100 -i {} -t {} -c:v copy -c:a copy {}".format(file_path, sec, out_file)) # 主模块执行 if __name__ == "__main__": # path = os.path.dirname('/Users/Qincji/Downloads/ffmpeg/') path = os.path.dirname('需要处理的目录') # 目录下的所有视频 cute_video(path) ================================================ FILE: 005-PaidSource/file_util.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 文件相关处理 @Date :2022/01/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ import datetime import json import os import re import shutil import cairosvg import pandas as pd import pypandoc # 要安装pandoc from docx import Document def file_name(file_dir): results = [] for root, dirs, files in os.walk(file_dir): # print(root) # 当前目录路径 # print(dirs) # 当前路径下所有子目录 # print(files) # 当前路径下所有非目录子文件 results += files return results def deal_one_page(): fs = file_name('100条') for f in fs: try: print('正在检测【%s】' % f) shotname, extension = os.path.splitext('%s' % f) print('正在检测【%s】' % shotname) if '1篇' in shotname: new_name = re.sub(r'1篇', '', f) document = Document(r"html/%s" % f) paragraphs = document.paragraphs p = paragraphs[0] p._element.getparent().remove(p._element) document.save(r"html/%s" % new_name) os.remove('html/%s' % f) except Exception as e: print(e) def copy_doc(): fs = file_name('all') i = 1 k = 1 temp_dir = '01' os.makedirs('100条/%s' % temp_dir) for f in fs: try: # print('正在检测【%s】' % f) shotname, extension = os.path.splitext('%s' % f) shutil.copyfile(r'all/%s' % f, r'100条/%s/%s' % (temp_dir, f)) if i % 100 == 0: temp_dir = '0%d' % k if k < 10 else '%d' % k k += 1 os.makedirs('100条/%s' % temp_dir) i += 1 except Exception as e: print(e) '''########文件处理相关#########''' def html_cover_doc(in_path, out_path): '''将html转化成功doc''' path, file_name = os.path.split(out_path) if path and not os.path.exists(path): os.makedirs(path) pypandoc.convert_file(in_path, 'docx', outputfile=out_path) def svg_cover_jpg(src, dst): '''' drawing = svg2rlg("drawing.svg") renderPDF.drawToFile(drawing, "drawing.pdf") renderPM.drawToFile(drawing, "fdrawing.png", fmt="PNG") renderPM.drawToFile(drawing, "drawing.jpg", fmt="JPG") ''' path, file_name = os.path.split(dst) if path and not os.path.exists(path): os.makedirs(path) # drawing = svg2rlg(src) # renderPM.drawToFile(drawing, dst, fmt="JPG") cairosvg.svg2png(url=src, write_to=dst) def html_cover_excel(content, out_path): '''将html转化成excel''' path, file_name = os.path.split(out_path) if path and not os.path.exists(path): os.makedirs(path) tables = pd.read_html(content, encoding='utf-8') writer = pd.ExcelWriter(out_path) for i in range(len(tables)): tables[i].to_excel(writer, sheet_name='表%d' % (i + 1)) # startrow writer.save() # 写入硬盘 def write_to_html(content, file_path): '''将内容写入本地,自动加上head等信息''' page = ''' ''' page += content page += ''' ''' write(page, file_path) def write_json(content, file_path): '''写入json''' path, file_name = os.path.split(file_path) if path and not os.path.exists(path): os.makedirs(path) with open(file_path, 'w') as f: json.dump(content, f, ensure_ascii=False) f.close() def read_json(file_path): '''读取json''' with open(file_path, 'r') as f: js_get = json.load(f) f.close() return js_get def write(content, file_path): '''写入txt文本内容''' path, file_name = os.path.split(file_path) if path and not os.path.exists(path): os.makedirs(path) with open(file_path, 'w') as f: f.write(content) f.close() def read(file_path) -> str: '''读取txt文本内容''' content = None try: with open(file_path, 'r') as f: content = f.read() f.close() except Exception as e: print(e) return content def get_next_folder(dst, day_diff, folder, max_size): '''遍历目录文件,直到文件夹不存在或者数目达到最大(max_size)时,返回路径''' while True: day_time = (datetime.date.today() + datetime.timedelta(days=day_diff)).strftime('%Y-%m-%d') # 下一天的目录继续遍历 folder_path = os.path.join(dst, day_time, folder) if os.path.exists(folder_path): # 已存在目录 size = len(next(os.walk(folder_path))[2]) if size >= max_size: # 该下一个目录了 day_diff += 1 continue else: os.makedirs(folder_path) return day_diff, folder_path if __name__ == '__main__': pass ================================================ FILE: 005-PaidSource/gsearch.py ================================================ import re import time import os from urllib.parse import quote_plus import chardet import requests_html import pypandoc # 要安装pandoc from v2ray_pool import Net from bs4 import BeautifulSoup import googlesearch as ggs import os import random import sys import time import ssl BLACK_DOMAIN = ['www.google.gf', 'www.google.io', 'www.google.com.lc'] DOMAIN = 'www.google.com' class GSearch(Net): def search_page(self, url, pause=3): """ Google search :param query: Keyword :param language: Language :return: result """ time.sleep(random.randint(1, pause)) try: r = self.request_en(url) print('resp code=%d' % r.status_code) if r.status_code == 200: charset = chardet.detect(r.content) content = r.content.decode(charset['encoding']) return content elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(random.randint(1, pause)) return self.search_page(location) # elif r.status_code == 429 or r.status_code == 443: # time.sleep(3) # return search_page(url) return None except Exception as e: print(e) return None def parse_html(self, html): soup = BeautifulSoup(html, 'html.parser') # 声明BeautifulSoup对象 # find = soup.find('p') # 使用find方法查到第一个p标签 # print('----->>>>%s' % str(find.text)) p_s = soup.find_all('p') results = [] for p in p_s: if p.find('img'): # 不要带有图片的标签 continue if p.find('a'): # 不要带有链接的标签 continue content = str(p) if "文章来源" in content: print('过滤[文章来源]>>>>>%s' % content) continue if "来源:" in content: print('过滤[来源:]>>>>>%s' % content) continue if len(p.text.replace('\n', '').strip()) < 1: # 过滤空内容 # print('过滤[空字符]>>>>>!') continue results.append(content) results.append('


') # 隔一下 return results # return re.findall(r'()', html, re.DOTALL) def get_html(self, url): session = requests_html.HTMLSession() html = session.get(url) html.encoding = html.apparent_encoding return html.text def conver_to_doc(self, in_name, out_name): try: pypandoc.convert_file('%s.html' % in_name, 'docx', outputfile="doc/%s.docx" % out_name) os.remove('%s.html' % in_name) except Exception as e: print(e) def download_and_merge_page(self, urls, name): try: page = [''' '''] k = 0 size = random.randint(3, 5) # 每次合并成功5篇即可 for i in range(len(urls)): if k >= size: break try: temp = self.parse_html(self.get_html(urls[i])) except Exception as e: print(e) continue if len(temp) < 3: # 篇幅太短 continue page.append( '

第%d篇:

' % ( k + 1)) # 加入标题 page += temp page.append('\n') k += 1 page.append('') with open("%s.html" % name, mode="w") as f: # 写入文件 for p in page: f.write(p) return k except Exception as e: print(e) return 0 def get_full_urls(self, html): a_s = re.findall(r'', html, re.DOTALL) results = [] for a in a_s: try: # print(a) # url = re.findall(r'/url\?q=(.*?\.html)', a, re.DOTALL)[0] url: str = re.findall(r'(http[s]{0,1}://.*?\.html)', a, re.DOTALL)[0] # title = re.findall(r'(.*?)', a, re.DOTALL)[0] #会有问题 # print('{"url":"%s","title":"%s"}' % (url, title)) if 'google.com' in url: continue if url in results: continue # 过来同一个网站的 domain = re.findall('http[s]{0,1}://(.*?)/', url, re.DOTALL)[0] # 含有--的 if '-' in domain: continue # www.sz.gov.cn,'.'超过4个时绝对不行的,像:bbs.jrj.ex3.http.80.ipv6.luzhai.gov.cn if domain.count('.') > 4: continue for u in results: if domain in u: continue results.append(url) except Exception as e: # print(e) pass return results def get_full_titles(self, html): results = [] soup = BeautifulSoup(html, "html.parser") results = [] for a in soup.find_all(name='a'): try: h3 = a.find(name='h3') if h3 and h3.has_attr('div'): div = h3.find(name='div') results.append(div.getText()) else: div = a.find(name='span') results.append(div.getText()) except Exception as e: print(e) return results def format_common_url(self, search, domain='www.google.com', start=0): url = 'https://{domain}/search?q={search}&start={start}' url = url.format(domain=domain, search=quote_plus(search), start=start) return url def format_full_url(self, domain, as_q='', as_epq='', as_oq='', as_eq='', as_nlo='', as_nhi='', lr='', cr='', as_qdr='', as_sitesearch='', as_filetype='', tbs='', start=0, num=10): """ https://www.google.com/advanced_search 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= allintext: 衣服 裤子 服装 饰品 珠宝 银饰 服装 OR or OR 配饰 "官网" -鞋子 site:.com 20..1000 :param domain: 域名:google.com :param as_q: 输入重要字词: 砀山鸭梨 :param as_epq: 用引号将需要完全匹配的字词引起: "鸭梨" :param as_oq: 在所需字词之间添加 OR: 批发 OR 特价 :param as_eq: 在不需要的字词前添加一个减号: -山大、-"刺梨" :param as_nlo: 起点,在数字之间加上两个句号并添加度量单位:0..35 斤、300..500 元、2010..2011 年 :param as_nhi: 终点,在数字之间加上两个句号并添加度量单位:0..35 斤、300..500 元、2010..2011 年 :param lr: 查找使用您所选语言的网页。 :param cr: 查找在特定地区发布的网页。 :param as_qdr: 查找在指定时间内更新的网页。 :param as_sitesearch: 搜索某个网站(例如 wikipedia.org ),或将搜索结果限制为特定的域名类型(例如 .edu、.org 或 .gov) :param as_filetype: 查找采用您指定格式的网页。如:filetype:pdf :param tbs: 查找可自己随意使用的网页。 :param start: 第几页,如 90:表示从第9页开始,每一页10条 :param num: 每一页的条数 :return: """ 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}' url = url.format(domain=domain, as_q=quote_plus(as_q), as_epq=quote_plus(as_epq), as_oq=quote_plus(as_oq), as_eq=quote_plus(as_eq), as_nlo=as_nlo, as_nhi=as_nhi, lr=lr, cr=cr, as_qdr=as_qdr, as_sitesearch=as_sitesearch, start=start, num=num, tbs=tbs, as_filetype=as_filetype) return url if __name__ == '__main__': url = 'http://www.sz.gov.cn/cn/zjsz/nj/content/post_1356218.html' domain: str = re.findall('http[s]{0,1}://(.*?)/', url, re.DOTALL)[0] print(domain.count('.')) print(domain) ================================================ FILE: 005-PaidSource/gtransfer.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 实现从excel文件获取关键词进行翻译后写入新文件 @Date :2021/10/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ import json import os import os.path import random import time import chardet import pandas as pd import file_util as futls import v2ray_util as utils from v2ray_pool import Net BLACK_DOMAIN = ['www.google.gf', 'www.google.io', 'www.google.com.lc'] DOMAIN = 'www.google.com' class GTransfer(Net): def search_page(self, url, pause=3): """ Google search :param query: Keyword :param language: Language :return: result """ time.sleep(random.randint(1, pause)) try: r = self.request_en(url) print('resp code=%d' % r.status_code) if r.status_code == 200: charset = chardet.detect(r.content) content = r.content.decode(charset['encoding']) return content elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(random.randint(1, pause)) return self.search_page(location) return None except Exception as e: print(e) return None def transfer(self, content): # url = 'http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=' + content url = 'http://translate.google.cn/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=en&tl=zh-CN&q=' + content try: cache = futls.read_json('data/cache.json') for c in cache: if content in c: print('已存在,跳过:{}'.format(content)) return c.get(content) except Exception as e: pass try: result = self.search_page(url) trans = json.loads(result)['sentences'][0]['trans'] # 解析获取翻译后的数据 # print(result) print(trans) self.local_cache.append({content: trans}) futls.write_json(self.local_cache, 'data/cache.json') # 写入数据吗?下次直接缓存取 except Exception as e: print(e) utils.restart_v2ray() return self.transfer(content) return trans def init_param(self, file_name): utils.restart_v2ray() self.local_cache = [] # 第一次加载本地的(已翻译的就不再翻译了) try: cache = futls.read_json('data/cache.json') for c in cache: self.local_cache.append(c) except Exception as e: pass csv_file = os.path.join('data', file_name) csv_out = os.path.join('data', 'out_' + file_name) df = pd.read_excel(csv_file, sheet_name='CompetitorWords') # 代表取出第一行至最后一行,代表取出第四列至最后一列。 datas = df.values size = len(df) print('总共有{}行数据'.format(size)) titles, titles_zh, keys1, keys2, keys3, pros = [], [], [], [], [], [] for col in range(0, size): t = datas[col][0] titles.append(t) keys1.append(datas[col][1]) keys2.append(datas[col][2]) keys3.append(datas[col][3]) pros.append(datas[col][4]) titles_zh.append(self.transfer(t)) print('总共{},现在到{}'.format(size, col + 1)) df_write = pd.DataFrame( {'标题': titles, '中文标题': titles_zh, '关键词1': keys1, '关键词2': keys2, '关键词3': keys3, '橱窗产品': pros}) df_write.to_excel(csv_out, index=False) utils.kill_all_v2ray() # http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=what if __name__ == '__main__': g = GTransfer() g.init_param('xxx.xls') # utils.search_node() ================================================ FILE: 005-PaidSource/kaoqin.py ================================================ """ @Description: excel表的常规操作,这里实现统计考勤 @Date :2022/02/21 @Author :xhunmon @Mail :xhunmon@gmail.com """ import pandas as pd import calendar from pandas._libs.tslibs.timestamps import Timestamp def get_days(year, month): # 获取输出日期的列明 dates = calendar.monthrange(year, month) week = dates[0] # 1号那天是星期几 days = dates[1] # 总共的天数 print(dates) index_time = [] for day in range(1, days): index_time.append('{}-{}-{} 星期{}'.format(year, month, day, (week + day) % 7)) print(index_time) return index_time def parse_excel(csv_file, out_file, names, dates): df = pd.read_excel(csv_file, sheet_name='Sheet') # 从文件和表格名称读取 datas = df.values size = len(df) print('总共有{}行数据'.format(size)) results = {} for name in names: # 我是根据名字统计 results.update({name: ['' for x in range(len(dates))]}) # 默认生成每个日期的空格 for col in range(0, size): s_name = datas[col][2] # 打印一下就知道去的那是哪里列的值了 t_time: Timestamp = datas[col][6] # 我这里是时间戳,用type(datas[col][6])打印类型可知 if s_name not in names: continue # 获取这天是哪一天的,name_datas是哪个人对应的列表数据 d, h, m, name_datas = t_time.day, t_time.hour, t_time.minute, results.get(s_name) # 早上 9:00前打卡,下午18:00后打卡,取一天最早和最晚的一次即可,门禁可能有很多数据 tt = '2022-1-{} {}:{}'.format(d, h, m) old = name_datas[d - 1] # 下标 if len(old) < 5: # 空的 name_datas[d - 1] = '{} 早 {};'.format(tt, '' if h < 9 else '异常') # 上班打卡 else: # 去除第一个: first = old.split(';')[0] last = '{} 晚 {}'.format(tt, '' if h >= 18 else '异常') name_datas[d - 1] = '{};{}'.format(first, last) print(results) df_write = pd.DataFrame(results, index=dates) df_write.to_excel(out_file, index=True) # 写入输出表格数据 if __name__ == '__main__': names = ['x1', 'x2', 'x3', 'x4', 'x5'] # 要统计那些人 parse_excel('data/一月考勤.xls', 'data/out_kaoqin.xls', names, get_days(2022, 1)) ================================================ FILE: 005-PaidSource/keywords.py ================================================ import json import random import re import time from urllib.parse import quote_plus from v2ray_pool import Net import chardet import requests import urllib3 from bs4 import BeautifulSoup from my_fake_useragent import UserAgent class Keywords(Net): '''url:https://www.5118.com/ciku/index#129''' def get_keys_by_net(self) -> []: try: r = self.request(r'https://www.5118.com/ciku/index#129') if r.status_code != 200: return None r.encoding = r.apparent_encoding # 法律
soup = BeautifulSoup(r.text, "html.parser") results = [] for a in soup.find_all(name='a'): results += re.findall(r'(.*?) []: with open('test/key_tag.json', 'r') as f: js_get = json.load(f) f.close() return js_get def get_titles_by_local(self) -> []: with open('test/key_title.json', 'r') as f: js_get = json.load(f) f.close() return js_get def get_titles_by_net(self, key): '''通过网盘搜索检查出 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 ''' results = [] try: time.sleep(random.randint(1, 4)) r = self.request_en( 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) if r.status_code != 200: print(r.status_code) return None r.encoding = r.apparent_encoding soup = BeautifulSoup(r.text, "html.parser") for a in soup.find_all(name='a'): ts = re.findall(r'(.*?).doc', str(a.get_text()).replace('\n', ''), re.DOTALL) for t in ts: 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: continue if len(t) < 4: continue results.append(t) return results except Exception as e: print(e) return None def test(): js = ['a', 'b', 'c'] with open('test/key_tag.json', 'w') as f: json.dump(js, f) f.close() with open('test/key_tag.json', 'r') as f: js_get = json.load(f) f.close() print(js_get) if __name__ == "__main__": # test() keys = Keywords().get_keys_by_net() print(keys) with open('test/key_tag.json', 'w') as f: json.dump(keys, f, ensure_ascii=False) f.close() ================================================ FILE: 005-PaidSource/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 关键词获取 @Date :2021/09/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ # from amazon import run_api import json import re import yagooglesearch import v2ray_util as utils from gsearch import GSearch from keywords import Keywords def start_task(): kd = Keywords() keywords = kd.get_titles_by_local() name = 'temp' gs = GSearch() i = 0 is_need_start = True while i < len(keywords): if is_need_start: utils.restart_v2ray() gs.update_agent() is_need_start = True key = keywords[i] query = 'site:gov.cn filetype:html "%s"' % key client = yagooglesearch.SearchClient( query, tbs="li:1", max_search_result_urls_to_return=100, http_429_cool_off_time_in_minutes=49, http_429_cool_off_factor=1.5, proxy="socks5h://127.0.0.1:1080", verbosity=5, ) client.assign_random_user_agent() try: page_urls = client.search() except Exception: continue new_urls = [] for u1 in page_urls: domain: str = re.findall('http[s]{0,1}://(.*?)/', u1, re.DOTALL)[0] # 含有--的 if '-' in domain: continue # www.sz.gov.cn,'.'超过4个时绝对不行的,像:bbs.jrj.ex3.http.80.ipv6.luzhai.gov.cn if domain.count('.') > 4: continue for u2 in new_urls: if domain in u2: continue new_urls.append(u2) print('过滤器链接数:%d, 过滤后链接数:%d' % (len(page_urls), len(new_urls))) page_size = len(new_urls) if page_size == 0: print('[%s]获取文章链接失败!' % key) continue if not gs.download_and_merge_page(page_urls, name): # 合并文章 print('下载或者合并失败') continue doc_name = '%d篇%s' % (page_size, key) gs.conver_to_doc(name, doc_name) is_need_start = False i += 1 utils.kill_all_v2ray() def start_proxy_task(): kd = Keywords() keywords: [] = kd.get_titles_by_local() name = 'temp' gs = GSearch() key_s = [] key_s.pop() def start_task2(): kd = Keywords() keywords: [] = kd.get_titles_by_local() name = 'temp' gs = GSearch() i = 0 is_need_start = True key_size = len(keywords) key = keywords.pop() while i < key_size: if is_need_start: utils.restart_v2ray() gs.update_agent() else: key = keywords.pop() is_need_start = True # key_url = gs.format_full_url(domain='google.com', as_sitesearch='.gov.cn', as_filetype='html', as_epq=key, # lr='lang_zh-CN', cr='countryCN') # key_url = gs.format_full_url(domain='search.iwiki.uk', as_sitesearch='gov.cn', as_filetype='html', as_epq=key, # lr='lang_zh-CN', cr='countryCN') # key_url = gs.format_common_url('site:gov.cn filetype:html %s' % key, domain='search.iwiki.uk') key_url = gs.format_common_url('site:gov.cn intitle:%s' % key, domain='www.google.com') print(key_url) content = gs.search_page(key_url) if content is None: print('[%s]搜索失败,进行重试!' % key) continue with open('test/test_search.html', 'w') as f: f.write(content) f.close() page_urls = gs.get_full_urls(content) # 获取文章的url page_size = len(page_urls) if page_size == 0: print('[%s]没有内容,下一个...' % key) else: size = gs.download_and_merge_page(page_urls, name) if size == 0: # 合并文章 print('下载或者合并失败,跳过!') else: doc_name = '%d篇%s' % (size, key) gs.conver_to_doc(name, doc_name) print('生成[%s]文章成功!!!' % doc_name) is_need_start = False i += 1 # 重新覆盖本地关键词 with open('test/key_title.json', 'w') as f: json.dump(keywords, f, ensure_ascii=False) f.close() utils.kill_all_v2ray() def test_titles(): kd = Keywords() keywords = kd.get_titles_by_local() print('总共需要加载%d个关键词' % len(keywords)) def test_task(): kd = Keywords() keywords = kd.get_keys_by_local() print('总共需要加载%d个关键词' % len(keywords)) # keywords = ['股市基金'] i = 0 search_keys = [] utils.restart_v2ray() # 第一次用固定agent is_need_start = False while i < len(keywords): if is_need_start: utils.restart_v2ray() # kd.update_agent() is_need_start = True key = keywords[i] print('开始搜索:%s' % key) titles = kd.get_titles_by_net(key) if titles is None: print('[%s]获取关键词标题失败!' % key) continue print(titles) for t in titles: if t not in search_keys: search_keys.append(t) # 每次都要更新一次 with open('test/key_title.json', 'w') as f: json.dump(search_keys, f, ensure_ascii=False) f.close() is_need_start = False i += 1 utils.kill_all_v2ray() def test_get_title(): with open('search_page.html', 'r') as f: page = f.read() f.close() gs = GSearch() titles = gs.get_full_titles(page) # 获取文章的标题 print(titles) if __name__ == "__main__": # utils.restart_v2ray() utils.search_node() # utils.kill_all_v2ray() ================================================ FILE: 005-PaidSource/other_site.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 获取其他站点信息爬虫 @Date :2022/1/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os import random import re import time import requests from bs4 import BeautifulSoup import file_util as futls import v2ray_util as utils from v2ray_pool import Net class Cncic(Net): '''中华全国商业中心:https://www.cncic.org/''' def start_task(self): utils.restart_v2ray() cncic = Cncic() keys = [{'cat': 92, 'name': '专题分析报告'}, {'cat': 95, 'name': '政策法规'}, {'cat': 8, 'name': '月度分析'}, {'cat': 10, 'name': '黄金周分析'}, {'cat': 16, 'name': '零售百强'}, {'cat': 94, 'name': '市场观察'}, ] success_datas = [] for key in keys: cat = key.get('cat') name = key.get('name') datas = cncic.load_list(cat) while len(datas) == 0: utils.restart_v2ray() datas = cncic.load_list(cat) success_datas.append({'name': name, 'data': datas}) futls.write_json(success_datas, 'data/cncic/keys.json') # 每次保存到本地 success_datas = futls.read_json('data/cncic/keys.json') key_size = len(success_datas) is_need_start = False key = None for i in range(key_size): if is_need_start: utils.restart_v2ray() else: key = success_datas.pop() if key is None: key = success_datas.pop() is_need_start = True folder = key.get('name') datas = key.get('data') for data in datas: try: load_page = cncic.load_page(data.get('url')) except Exception as e: print(e) continue title, content = cncic.parse_page(load_page) html_path = 'data/html/cncic/%s/%s.html' % (folder, title) doc_path = 'data/doc/cncic/%s/%s.docx' % (folder, title) futls.write_to_html(content, html_path) try: futls.html_cover_doc(html_path, doc_path) except Exception as e: print(e) futls.write_json(success_datas, 'data/cncic/keys.json') # 更新本地数据库 is_need_start = False i += 1 utils.kill_all_v2ray() def load_list(self, cat, paged=1) -> []: results = [] while True: url = 'https://www.cncic.org/?cat=%d&paged=%d' % (cat, paged) try: page = self.load_page(url) results += self.parse_list(page) paged += 1 time.sleep(random.randint(3, 6)) except Exception as e: print(e) break return results def load_page(self, url): '''加载页面,如:https://www.cncic.org/?p=3823''' r = self.request_zh(url) r.encoding = r.apparent_encoding print('Cncic[%s] code[%d]' % (url, r.status_code)) if r.status_code == 200: return r.text elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(1) return self.load_page(location) return None def parse_page(self, page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') article = soup.find('article') header = article.find('header') title = header.text.replace('\n', '').replace(' ', '') content = article.find('div', class_='single-content') result = str(header) result += str(content) return title, result def parse_list(self, page): '''解析列表(如:https://www.cncic.org/?cat=92)页面,返回标题、连接、日期''' soup = BeautifulSoup(page, 'html.parser') main = soup.find('main') articles = main.find_all('article') results = [] for article in articles: header = article.find('header') url = header.find('a').get_attribute_list('href')[0] title = header.text.replace('\n', '').replace(' ', '') date = article.find('span', class_='date').text results.append({'url': url, 'title': title, 'date': date}) return results class Ceicdata(Net): '''https://www.ceicdata.com/''' def start_task_1(self): cd = Ceicdata() utils.restart_v2ray() success_datas = futls.read_json('data/keys/ceicdata.json') key_size = len(success_datas) print(key_size) is_need_start = False key = None for i in range(key_size): if is_need_start: utils.restart_v2ray() cd.update_agent() else: key = success_datas.pop() is_need_start = True if key is None: key = success_datas.pop() title = key.get('title') url = key.get('url') try: page = cd.load_page(url) except Exception as e: page = None print(e) if page is None: continue html_path = 'data/html/ceicdata/%s.html' % title content = cd.parse_page_1(page) futls.write_to_html(content, html_path) futls.html_cover_excel(html_path, 'data/doc/ceicdata/%s.xlsx' % title) futls.write_json(success_datas, 'data/keys/ceicdata.json') # 更新本地数据库 is_need_start = False i += 1 utils.kill_all_v2ray() @staticmethod def start_task2(): utils.restart_v2ray() cd = Ceicdata() keys_path = 'data/keys/ceicdata.json' keys = futls.read_json(keys_path) if not keys: url = 'https://www.ceicdata.com/zh-hans/country/china' page = cd.load_page(url) if page is None: raise Exception('获取页面失败') keys = cd.parse_main_2(page) if len(keys) == 0: raise Exception('获取链接失败') futls.write_json(keys, keys_path) key_size = len(keys) print('下载数量[%d]' % key_size) is_need_start = False key = None for i in range(key_size): if is_need_start: utils.restart_v2ray() cd.update_agent() else: key = keys.pop() is_need_start = True if key is None: key = keys.pop() url = key.get('url') try: page = cd.load_page(url) except Exception as e: page = None print(e) if page is None: continue try: title, content = cd.parse_page_2(page) except Exception as e: print(e) continue html_path = 'data/html/ceicdata2/%s.html' % title futls.write_to_html(content, html_path) futls.html_cover_doc(html_path, 'data/doc/ceicdata/%s.docx' % title) futls.write_json(keys, keys_path) # 更新本地数据库 is_need_start = False i += 1 utils.kill_all_v2ray() def load_page(self, url): '''加载页面,如:https://www.cncic.org/?p=3823''' r = self.request_en(url) # r = self.request(url) r.encoding = r.apparent_encoding print('Cncic[%s] code[%d]' % (url, r.status_code)) if r.status_code == 200: return r.text elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(1) return self.load_page(location) return None def parse_main_1(self, page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') main = soup.find('main') lists = main.find('div', class_='indicators-lists') results = [] for a in lists.find_all('a'): # https://www.ceicdata.com/zh-hans/indicator/nominal-gdp title = a.text.replace(' ', '') url = 'https://www.ceicdata.com' + a.get_attribute_list('href')[0].replace(' ', '') results.append({'title': title, 'url': url}) return results def parse_main_2(self, page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') main = soup.find('main') results = [] for tbody in main.find_all('tbody'): for a in tbody.find_all('a'): # https://www.ceicdata.com/zh-hans/indicator/nominal-gdp title = a.text.replace(' ', '') url = 'https://www.ceicdata.com' + a.get_attribute_list('href')[0].replace(' ', '') results.append({'title': title, 'url': url}) return results def parse_page_1(self, page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') main = soup.find('main') clearfix = main.find('div', class_='clearfix') h1 = clearfix.find('h1') h2 = clearfix.find('h2') tables = clearfix.find_all('table') content = str(h1) + str(tables[0]) + str(h2) + str(tables[1]) return content def parse_page_2(self, page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') main = soup.find('main') left = main.find('div', id='left-col-7') title = left.find('span', class_='c-purple').text.replace(' ', '').replace('\n', '') left.find('div', id='breadcrumb').decompose() # 移除节点 for ele in left.find_all('div', class_='hide'): ele.decompose() # 移除节点 for ele in left.find_all('div', class_='div-chart-btns'): ele.decompose() # 移除节点 for ele in left.find_all('div', class_='table-buy'): ele.decompose() # 移除节点 for ele in left.find_all('div', class_='div-bgr-2'): if '查看价格选项' in str(ele): ele.decompose() # 移除节点 for ele in left.find_all('h4'): if '购买' in str(ele): ele.decompose() # 移除节点 for ele in left.find_all('button'): if '加载更多' in str(ele): ele.decompose() # 移除节点 for ele in left.find_all('div', class_='div-bgr-1'): if '详细了解我们' in str(ele): ele.decompose() # 移除节点 i = 1 for img in left.find_all('img'): src = str(img.get('src')) path = '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/data/img/%d.svg' % i dst = '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/data/img/%d.png' % i if 'www.ceicdata.com' in src: print('下载图片url[%s]' % src) r = self.request_zh(src) # r = self.request(src) if r.status_code == 200: with open(path, 'wb') as f: f.write(r.content) futls.svg_cover_jpg(path, dst) # 将svg转换成jpg img['src'] = dst i += 1 else: raise Exception('下载图片失败!') rs = re.sub(r'href=".*?"', '', str(left)) # 移除href return title, rs class Cnnic(Net): '''http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/ , 注意:因为文件过大,使用别人代理下载,固定代理''' @staticmethod def start_task(): cnnic = Cnnic() keys_path = 'data/keys/cnnic.json' all_keys = futls.read_json(keys_path) if not all_keys: all_keys = [] for i in range(7): if i == 0: url = 'http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/index.htm' else: url = 'http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/index_%d.htm' % i page = cnnic.load_page(url) if page: futls.write(page, 'test/src.html') all_keys += cnnic.parse_page(page) futls.write_json(all_keys, keys_path) size = len(all_keys) print('将要下载数量[%d]' % size) for i in range(size): key = all_keys.pop() name = key.get('title') url = key.get('url') path = 'data/doc/cnnic/%s.pdf' % name cnnic.download(url, path) futls.write_json(all_keys, keys_path) print('已下载[%d] | 还剩[%d]' % (i + 1, size - i - 1)) def load_page(self, url): time.sleep(3) # r = self.request_en(url) # r = self.request(url) proxies = {'http': 'http://11.0.222.4:80', 'https': 'http://11.0.222.4:80'} r = requests.get(url=url, headers=self._headers, allow_redirects=False, verify=False, proxies=proxies, timeout=15) r.encoding = r.apparent_encoding print('Cnnic[%s] code[%d]' % (url, r.status_code)) if r.status_code == 200: return r.text elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(1) return self.load_page(location) return None def parse_page(self, page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') content = soup.find('div', class_='content') results = [] for li in content.find_all('li'): a = li.find('a') date = li.find('div', class_='date').text[0:4] # 只要年份 title = a.text.replace('\n', '').replace(' ', '') # http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/hlwtjbg/202109/P020210915523670981527.pdf # ./hlwtjbg/202109/P020210915523670981527.pdf url = 'http://www.cnnic.net.cn/hlwfzyj/hlwxzbg/' + str(a.get('href')).replace('./', '') if not '年' in title: title = '%s年发布%s' % (date, title) results.append({'title': title, 'url': url}) return results def download(self, url, path): if os.path.exists(path): os.remove(path) proxies = {'http': 'http://11.0.222.4:80', 'https': 'http://11.0.222.4:80'} r = requests.get(url=url, headers=self._headers, allow_redirects=False, verify=False, proxies=proxies, timeout=15, stream=True) i = 0 print('name[%s]|code[%d]' % (path, r.status_code)) with open(path, "wb") as pdf: for chunk in r.iter_content(chunk_size=1024): if chunk: i += 1 if i % 30 == 0: print('.', end='') pdf.write(chunk) pdf.close() time.sleep(random.randint(3, 12)) class Othersite(Net): def __init__(self): super(Othersite, self).__init__() self.dir = 'Othersite' @staticmethod def start_task(): reqs = [{'title': 'xxx', 'url': 'https://www.xxx.com/wedding/engagement-rings.html'}] # utils.restart_v2ray() sl = Othersite() datas = futls.read_json(os.path.join('Othersite', 'page_urls.json')) if datas is None: datas = [] for req in reqs: title = req.get('title') url = req.get('url') has_write = False for data in datas: if title in data.get('title'): has_write = True break if has_write: # 已经请求过了 print('页面连接 [%s]已存在,跳过!' % title) continue start = 1 page_urs = [] while True: if start != 1: temp = url + '?p=' + str(start) else: temp = url try: page = sl.load_page(temp) except Exception as e: print(e) print('--------000') utils.restart_v2ray() continue futls.write(page, 'test/src.html') has_next, results = sl.parse_list(page) print('has_next: {} | {}'.format(has_next, results)) page_urs += results if not has_next: break start += 1 # page = futls.read('test/src.html') # print(sl.parse_details(page)) datas.append({'title': title, 'urls': page_urs}) futls.write_json(datas, os.path.join('Othersite', 'page_urls.json')) all_results = [] # 总数据表 size = len(datas) alls_local = futls.read_json(os.path.join('Othersite', 'all.json')) for i in range(size): data = datas.pop() title = data.get('title') page_urs = data.get('urls') has_write_all = False # for local in alls_local: # if title in local.get('title'): # has_write_all = True # break # if has_write_all: # print('[%s]已下载,跳过!' % title) # continue sl.dir = os.path.join('Othersite', title) url_size = len(page_urs) print('下载数量[%d]' % url_size) is_need_start = False url = None results = [] for i in range(url_size): if is_need_start: utils.restart_v2ray() sl.update_agent() else: url = page_urs.pop() is_need_start = True if url is None: url = page_urs.pop() # 本地是否也已经存在 sku1 = url[url.rfind('-') + 1:].replace('.html', '').upper() if os.path.exists(os.path.join(sl.dir, sku1)): is_need_start = False print('ksu [%s]已存在,跳过!' % sku1) continue try: page = sl.load_page(url) sku = sl.parse_details(page) results.append(sku) is_need_start = False except Exception as e: print(e) print('--------333') all_results.append({'title': title, 'skus': results}) futls.write_json(all_results, os.path.join('Othersite', 'all.json')) futls.write_json(datas, os.path.join('Othersite', 'page_urls.json')) utils.kill_all_v2ray() def load_page(self, url): '''加载页面,如:https://www.cncic.org/?p=3823''' time.sleep(random.randint(3, 8)) r = self.request_en(url) # r = self.request(url) r.encoding = r.apparent_encoding print('Othersite code[%d] |url [%s] ' % (r.status_code, url)) if r.status_code == 200: return r.text elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(1) return self.load_page(location) return None def parse_home(self, page): soup = BeautifulSoup(page, 'html.parser') nav = soup.find('nav') results = [] for a in nav.find_all('a'): results.append({'title': a.text, 'url': str(a.get('href'))}) return results def parse_list(self, page): soup = BeautifulSoup(page, 'html.parser') ol = soup.find('ol') # 判断是否还有下一页 next = False pages = soup.find('div', class_='pages') if pages: pages_n = pages.find('li', class_='pages-item-next') if pages_n: next = True results = [] for a in ol.find_all('a'): results.append(str(a.get('href'))) return next, results def parse_details(self, page): soup = BeautifulSoup(page, 'html.parser') # content = soup.find('div', class_='content') # main = content.find('main') right = soup.find('div', class_='product-info-main') title = right.find('h1').text.replace('Othersite ', '') sku = right.find('div', class_='value').text try: price = right.find('span', id='price-saved').find('span').text except Exception: print('没有折扣,继续找..') price = right.find('span', class_='special-price').find('span', class_='price').text # 下载图片 layout = soup.find('amp-layout') carousel = layout.find('amp-carousel') imgs = [] i = 0 content = '{}\n{}'.format(sku, title) for img in carousel.find_all('amp-img'): src = str(img.get('src')) imgs.append(src) path = self.download_img(src, sku, i) content = content + '\n' + path i += 1 futls.write(content, os.path.join(self.dir, sku, '{}.txt'.format(sku))) return {'sku': sku, 'title': title, 'price': price, 'imgs': imgs} def download_img(self, src, sku, i): path = os.path.join(self.dir, sku, '{}-{}.jpg'.format(sku, i)) pre_path, file_name = os.path.split(path) if pre_path and not os.path.exists(pre_path): os.makedirs(pre_path) time.sleep(random.randint(1, 2)) r = self.request_en(src) if r.status_code == 200: with open(path, 'wb') as f: f.write(r.content) else: raise Exception('下载图片失败!') return path if __name__ == "__main__": pass ================================================ FILE: 005-PaidSource/v2ray_pool/__init__.py ================================================ # 运行时路径。并非__init__.py的路径 import os import sys BASE_DIR = "../002-V2rayPool" if os.path.exists(BASE_DIR): sys.path.append(BASE_DIR) from core import utils from core.conf import Config from core.client import Creator from db.db_main import DBManage from base.net_proxy import Net ================================================ FILE: 005-PaidSource/v2ray_pool/_db-checked.txt ================================================ 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,美国 ss://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,美国 ss://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,美国 vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIiA6Iue/u+WimeWFmmZhbnFpYW5nZGFuZy5jb20iLCIiIDogIkBTU1JTVUItVjE1LeS7mOi0ueaOqOiNkDpzdW8ueXQvc3Nyc3ViIiwNCiAgImFkZCI6ICI0Mi4xOTMuNDguNjQiLA0KICAicG9ydCI6ICI1MDAwMiIsDQogICJpZCI6ICI0MTgwNDhhZi1hMjkzLTRiOTktOWIwYy05OGNhMzU4MGRkMjQiLA0KICAiYWlkIjogIjY0IiwNCiAgInNjeSI6ICJhdXRvIiwNCiAgIm5ldCI6ICJ0Y3AiLA0KICAidHlwZSI6ICJub25lIiwNCiAgImhvc3QiOiAiNDIuMTkzLjQ4LjY0IiwNCiAgInBhdGgiOiAiIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiINCn0=,42.193.48.64,中国 上海 上海市 电信 ================================================ FILE: 005-PaidSource/v2ray_pool/_db-uncheck.txt ================================================ ================================================ FILE: 005-PaidSource/v2ray_util.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 管理v2ray_pool的工具 @Date :2022/1/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import time from v2ray_pool import utils, Config, DBManage def search_node(): # 如果有系统全局代理,可不需要开启v2ray_core代理,GoogleTrend(proxies=False) utils.kill_all_v2ray() Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64') # v2ray内核存放路径 Config.set_v2ray_node_path( '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool') # 保存获取到节点的路径 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' dbm = DBManage() dbm.init() # 必须初始化 if dbm.check_url_single(proxy_url): urls = dbm.load_urls_by_net(proxy_url=proxy_url) dbm.check_and_save(urls, append=False) # dbm.load_urls_and_save_auto() # urls = dbm.load_unchecked_urls_by_local() # dbm.check_and_save(urls, append=False) utils.kill_all_v2ray() def restart_v2ray(isSysOn=False): utils.kill_all_v2ray() Config.set_v2ray_core_path('/Users/Qincji/Desktop/develop/soft/intalled/v2ray-macos-64') # v2ray内核存放路径 Config.set_v2ray_node_path( '/Users/Qincji/Desktop/develop/py/project/PythonIsTools/005-PaidSource/v2ray_pool') # 保存获取到节点的路径 dbm = DBManage() dbm.init() # 必须初始化 while 1: if dbm.start_random_v2ray_by_local(isSysOn=isSysOn): break else: print("启动失败,进行重试!") time.sleep(1) def kill_all_v2ray(): utils.kill_all_v2ray() ================================================ FILE: 006-TikTok/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 006-TikTok/006-TikTok.iml ================================================ ================================================ FILE: 006-TikTok/README.md ================================================ # App自动化 ## 效果 抖音和tiktok能自动刷App评论。 ## 实现 1. 使用android手机,能与电脑正常使用adb连接 2. 使用[uiautomator2](https://github.com/openatx/uiautomator2) 开源库。 3. 借助:weditor 来获取元素(xpath等)。 ## seo ``` 1、重量副词词汇 https://gist.github.com/mkulakowski2/4289437 2、查看网站排名 https://www.sdwebseo.com/googlerankcheck/ 3、看看google浏览器中我的排名,如: https://www.google.com/search?q=giant stuffed animal caterpillar&num=10&gl=US https://www.google.com/search?q=4 foot stuffed panda bear&num=100&gl=US 4、获取外链 帮记者一个小忙,获取优质的外链:http://app.helpareporter.com/Pitches https://www.seoactionblog.com/haro-link-building-strategy/ ``` > 其他:注意需要把电脑要把代理关掉 ================================================ FILE: 006-TikTok/__init__.py ================================================ ================================================ FILE: 006-TikTok/dy_review.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 抖音app刷评论 @Date :2021/12/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ import random import time import uiautomator2 as u2 d = u2.connect() d.implicitly_wait(80) def review_douyin(): # 评论 d.press("home") d.app_start('com.ss.android.ugc.aweme', stop=True) time.sleep(1) d(resourceId="com.ss.android.ugc.aweme:id/foj").click() # 点击搜索 time.sleep(1) d(resourceId="com.ss.android.ugc.aweme:id/et_search_kw").click() # 点击输入框,预防键盘弹不起来 keys = ['元宵节创意视频'] # , '情人节', '搞笑视频', '真人动漫特效' comments = ['[比心]', '[强壮]', '[击掌]', '[给力]', '[爱心]', '[派对]', '[不看]', '[炸弹]', '[憨笑]', '[悠闲]', '[嘿哈]', '[西瓜]', '[咖啡]', '[太阳]', '[月亮]', '[发]', '[红包]', '[拳头]', '[勾引]', '[胜利]', '[抱拳]', '[左边]', '[送心]', '[来看我]', '[来看我]', '[来看我]', '[灵机一动]', '[耶]', '[色]', '[震惊]', '[小鼓掌]', '[发呆]', '[偷笑]', '[石化]', '[思考]', '[笑哭]', '[奸笑]', '[坏笑]', '[得意]', '[钱]', '[亲亲]', '[愉快]', '[玫瑰]', '[赞]', '[鼓掌]', '[感谢]', '[666]', '[胡瓜]', '[啤酒]', '[飞吻]', '[紫薇别走]', '[听歌]', '[绝望的凝视]', '[不失礼貌的微笑]', '[吐舌]', '[呆无辜]', '[看]', '[熊吉]', '[黑脸]', '[吃瓜群众]', '[呲牙]', '[绿帽子]', '[摸头]', '[皱眉]', '[OK]', '[碰拳]', '[强壮]', '[比心]', '[吐彩虹]', '[奋斗]', '[敲打]', '[惊喜]', '[如花]', '[强]', '[做鬼脸]', '[尬笑]', '[红脸]', '牛啊', '牛啊牛', 'nb', '666', '赞一个', '赞', '棒', '学到了', '1', '已阅', '板凳', '插一楼:变戏法的亮手帕', '插一楼:狗吃豆腐脑', '插一楼:癞蛤蟆打伞', '插一楼:离了水晶宫的龙', '插一楼:盲人聊天', '插一楼:五百钱分两下', '插一楼:盲公戴眼镜', '插一楼:王八倒立', '插一楼:癞蛤蟆背小手', '插一楼:韩湘子吹笛', '插一楼:剥了皮的蛤蟆', '插一楼:马蜂蜇秃子', '插一楼:冷水烫鸡', '插一楼:老孔雀开屏', '插一楼:大姑娘养的', '插一楼:三角坟地', '插一楼:蜡人玩火', '插一楼:发了霉的葡萄', '插一楼:厥着看天', '插一楼:种地不出苗', '插一楼:老虎头上的苍蝇', '插一楼:雷婆找龙王谈心', '插一楼:菩萨的胸怀', '插一楼:牛屎虫搬家', '插一楼:变戏法的拿块布', '插一楼:老虎上吊', '插一楼:王八', '插一楼:老虎吃田螺', '插一楼:大肚子踩钢丝', '插一楼:耗子腰粗', '插一楼:乌龟的', '插一楼:神仙放屁', '插一楼:麻油煎豆腐', '插一楼:汽车坏了方向盘', '插一楼:病床上摘牡丹', '插一楼:芝麻地里撒黄豆', '插一楼:打开棺材喊捉贼', '插一楼:卤煮寒鸭子', '插一楼:鲤鱼找鲤鱼,鲫鱼找鲫鱼', '插一楼:癞蛤蟆插羽毛', '插一楼:烂伞遮日', '插一楼:老虎头上的苍蝇', '插一楼:三角坟地', '插一楼:卖布兼卖盐', '插一楼:耗子腰粗', '插一楼:老孔雀开屏', '插一楼:筐中捉鳖', '插一楼:拐子进医院', '插一楼:茅房里打灯笼', '插一楼:癞蛤蟆背小手', '插一楼:肉骨头吹喇叭', '插一楼:老鼠进棺材', '插一楼:种地不出苗', '插一楼:病床上摘牡丹', '插一楼:裤裆里摸黄泥巴', '插一楼:狗拿耗子', '插一楼:铁匠铺的料', '插一楼:高梁撒在粟地里', '插一楼:茅厕里题诗', '插一楼:痰盂里放屁', '插一楼:老母猪打喷嚏', '插一楼:厕所里点灯', '插一楼:棺材铺的买卖', '插一楼:老公鸡着火', '插一楼:乌龟翻筋斗', '插一楼:被窝里的跳蚤', '插一楼:赶着牛车拉大粪', '插一楼:老太婆上鸡窝', '插一楼:狗背上贴膏药', '插一楼:狗咬瓦片', '插一楼:哪吒下凡', '插一楼:二十一天不出鸡', '插一楼:鞭炮两头点', '插一楼:抱黄连敲门', '插一楼:猫儿踏破油瓶盖', '插一楼:和尚念经', '插一楼:裁缝不带尺', '插一楼:上山钓鱼', '插一楼:狗长犄角', '插一楼:带着存折进棺材', '插一楼:豁子拜师', '插一楼:宁来看棋', '插一楼:盲公戴眼镜', '插一楼:南来的燕,北来的风', '插一楼:杯水车薪', '插一楼:玉皇大帝放屁', '插一楼:给刺儿头理发', '插一楼:九月的甘蔗', '插一楼:两只公牛打架', '插一楼:百川归海', '插一楼:挨打的乌龟', '插一楼:和尚挖墙洞', '插一楼:八月十五蒸年糕', '插一楼:毒蛇钻进竹筒里', '插一楼:苍蝇叮菩萨', '插一楼:白布进染缸', '插一楼:粪堆上开花', '插一楼:癞蛤蟆上蒸笼', '插楼:沙漠里钓鱼', '插楼:青㭎树雕菩萨', '插楼:看鸭勿上棚', '插楼:下大雨前刮大风', '插楼:在看羊的狗', '插楼:耍大刀里唱小生', '插楼:罗锅上山', '插楼:大车不拉', '插楼:瞎子白瞪眼', '插楼:铁拐的葫芦', '插楼:苣荬菜炖鲇鱼', '插楼:旅馆里的蚊子', '插楼:石刻底下的冰瘤子', '插楼:吃稀饭摆脑壳', '插楼:叫化子背不起', '插楼:火车拉大粪', '插楼:寿星玩琵琶', '插楼:六月的腊肉', '插楼:夜叉骂街', '插楼:孩儿的脊梁', '插楼:长了个钱串子脑袋', '插楼:现场看乒乓球比赛', '插楼:寡妇梦丈夫', '插楼:马背上放屁', '插楼:落雨出太阳', '插楼:猴子捡生姜', '插楼:啄木鸟屙薄屎', '插楼:鸡毛扔火里', '插楼:油火腿子被蛇咬', '插楼:属秦椒的', '插楼:千亩地里一棵草', '插楼:药铺倒了', '插楼:黄连水做饭', '插楼:卸架的黄烟叶儿', '插楼:螺蛳壳里赛跑', '插楼:躲了和尚躲不了庙', '插楼:驴槽子里面伸出一颗头来', '插楼:老妈妈吃火锅', '插楼:阎王的脸', '插楼:吃粮勿管事', '插楼:脚跟拴石头', '插楼:麻秸秆儿打狼', '插楼:阎王7粑子', '插楼:画上的美女', '插楼:团鱼下滚汤', '插楼:孔夫子的脸', '插楼:曹操贪慕小乔', '插楼:蒙住眼睛走路', '插楼:炒菜不放盐', '插楼:三月里的桃花', '插楼:老鼠吃面饽', '插楼:粥锅里煮铁球', '插楼:戴起眼镜喝滚茶', '插楼:吃香油唱曲子', '插楼:过冬的咸菜缸', '插楼:三个小鬼没抓住', '插楼:对着坛子放屁', '插楼:赤骨肋受棒', '插楼:百灵鸟唱歌', '插楼:雨过天晴放干雷', '插楼:拄着拐棍上炭窑', '插楼:搁着料吃草', '插楼:王八碰桥桩', '插楼:水上油', '插楼:偷鸡不得摸了一只鸭子', '插楼:黄瓜熬白瓜', '插楼:海瑞的棺材', '插楼:蛤蟆翻田坎', '插楼:乌龟进砂锅', '插楼:夜壶出烟', '插楼:李逵骂宋江', '插楼:小孩买个花棒槌', '插楼:漏网之虾', '插楼:一口吹灭火焰山', '插楼:冷水调浆'] for key in keys: # 不能搜索search d(resourceId="com.ss.android.ugc.aweme:id/et_search_kw").clear_text() # 清除历史 d(resourceId="com.ss.android.ugc.aweme:id/et_search_kw").set_text(key) # 输入 time.sleep(1) d(text='搜索', className='android.widget.TextView').click() # 点击搜索 # d(resourceId=" com.ss.android.ugc.aweme:id/d0t").click() # 点击搜索 time.sleep(1) d(text='视频', className='android.widget.Button').click() # 点击视频,然后点击第一条 d.xpath( '//*[@resource-id="com.ss.android.ugc.aweme:id/gw1"]/android.widget.FrameLayout[1]').click() stop, index = random.randint(15, 30), 0 while index < stop: # 随机刷几十条 print('总共有:{}条 | 现在到:{}条'.format(stop, index)) try: time.sleep(random.randint(3, 12)) # 随机停顿1~5秒 d(resourceId="com.ss.android.ugc.aweme:id/b2b").click() # 点击评论按钮 except Exception as e: print(e) try: time.sleep(random.randint(1, 2)) # 随机停顿1~2秒 d(resourceId="com.ss.android.ugc.aweme:id/b1y").click() # 点击弹出键盘 except Exception as e: print(e) time.sleep(random.randint(1, 2)) # 随机停顿1~2秒 try: d(resourceId="com.ss.android.ugc.aweme:id/b1y").set_text( comments[random.randint(0, len(comments) - 1)]) # 输入 except Exception as e: print(e) try: time.sleep(random.randint(1, 2)) # 随机停顿1~2秒 d(resourceId="com.ss.android.ugc.aweme:id/b1r").click() # 发送 except Exception as e: print(e) try: time.sleep(random.randint(1, 2)) # 随机停顿1~2秒 d(resourceId="com.ss.android.ugc.aweme:id/back_btn").click() # 关闭 except Exception as e: print(e) try: time.sleep(random.randint(5, 15)) # 随机停顿1~5秒 d.swipe_ext("up") # 上划,下一个视频 except Exception as e: print(e) index += 1 d(resourceId="com.ss.android.ugc.aweme:id/back_btn").click() # 返回搜索 ''' 教程: 1. 使用 weditor 来查看元素 2. 当找不到resourceId时,先用d(text='画动漫人物', className='android.widget.EditText').info找个resourceId,再使用。因为输入框内容会变化,所有不能直接用。 ''' if __name__ == "__main__": review_douyin() ================================================ FILE: 006-TikTok/file_util.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 文件相关处理 @Date :2022/01/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ import datetime import json import os import re import shutil import cairosvg import pandas as pd import pypandoc # 要安装pandoc from docx import Document def file_name(file_dir): results = [] for root, dirs, files in os.walk(file_dir): # print(root) # 当前目录路径 # print(dirs) # 当前路径下所有子目录 # print(files) # 当前路径下所有非目录子文件 results += files return results def deal_one_page(): fs = file_name('100条') for f in fs: try: print('正在检测【%s】' % f) shotname, extension = os.path.splitext('%s' % f) print('正在检测【%s】' % shotname) if '1篇' in shotname: new_name = re.sub(r'1篇', '', f) document = Document(r"html/%s" % f) paragraphs = document.paragraphs p = paragraphs[0] p._element.getparent().remove(p._element) document.save(r"html/%s" % new_name) os.remove('html/%s' % f) except Exception as e: print(e) def copy_doc(): fs = file_name('all') i = 1 k = 1 temp_dir = '01' os.makedirs('100条/%s' % temp_dir) for f in fs: try: # print('正在检测【%s】' % f) shotname, extension = os.path.splitext('%s' % f) shutil.copyfile(r'all/%s' % f, r'100条/%s/%s' % (temp_dir, f)) if i % 100 == 0: temp_dir = '0%d' % k if k < 10 else '%d' % k k += 1 os.makedirs('100条/%s' % temp_dir) i += 1 except Exception as e: print(e) '''########文件处理相关#########''' def html_cover_doc(in_path, out_path): '''将html转化成功doc''' path, file_name = os.path.split(out_path) if path and not os.path.exists(path): os.makedirs(path) pypandoc.convert_file(in_path, 'docx', outputfile=out_path) def svg_cover_jpg(src, dst): '''' drawing = svg2rlg("drawing.svg") renderPDF.drawToFile(drawing, "drawing.pdf") renderPM.drawToFile(drawing, "fdrawing.png", fmt="PNG") renderPM.drawToFile(drawing, "drawing.jpg", fmt="JPG") ''' path, file_name = os.path.split(dst) if path and not os.path.exists(path): os.makedirs(path) # drawing = svg2rlg(src) # renderPM.drawToFile(drawing, dst, fmt="JPG") cairosvg.svg2png(url=src, write_to=dst) def html_cover_excel(content, out_path): '''将html转化成excel''' path, file_name = os.path.split(out_path) if path and not os.path.exists(path): os.makedirs(path) tables = pd.read_html(content, encoding='utf-8') writer = pd.ExcelWriter(out_path) for i in range(len(tables)): tables[i].to_excel(writer, sheet_name='表%d' % (i + 1)) # startrow writer.save() # 写入硬盘 def write_to_html(content, file_path): '''将内容写入本地,自动加上head等信息''' page = ''' ''' page += content page += ''' ''' write(page, file_path) def write_json(content, file_path): '''写入json''' path, file_name = os.path.split(file_path) if path and not os.path.exists(path): os.makedirs(path) with open(file_path, 'w') as f: json.dump(content, f, ensure_ascii=False) f.close() def read_json(file_path): '''读取json''' with open(file_path, 'r') as f: js_get = json.load(f) f.close() return js_get def write(content, file_path): '''写入txt文本内容''' path, file_name = os.path.split(file_path) if path and not os.path.exists(path): os.makedirs(path) with open(file_path, 'w') as f: f.write(content) f.close() def read(file_path) -> str: '''读取txt文本内容''' content = None try: with open(file_path, 'r') as f: content = f.read() f.close() except Exception as e: print(e) return content def get_next_folder(dst, day_diff, folder, max_size): '''遍历目录文件,直到文件夹不存在或者数目达到最大(max_size)时,返回路径''' while True: day_time = (datetime.date.today() + datetime.timedelta(days=day_diff)).strftime('%Y-%m-%d') # 下一天的目录继续遍历 folder_path = os.path.join(dst, day_time, folder) if os.path.exists(folder_path): # 已存在目录 size = len(next(os.walk(folder_path))[2]) if size >= max_size: # 该下一个目录了 day_diff += 1 continue else: os.makedirs(folder_path) return day_diff, folder_path if __name__ == '__main__': pass ================================================ FILE: 006-TikTok/google_transfer_by_excel.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 实现从excel文件获取关键词进行翻译后写入新文件 @Date :2021/10/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ import json import os import os.path import random import time import chardet import pandas as pd import requests import file_util as futls from my_fake_useragent import UserAgent class GTransfer(object): def __init__(self, file): self.file = file self._ua = UserAgent() self._agent = self._ua.random() # 随机生成的agent self.USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0" self._headers = {"user-agent": self.USER_AGENT, 'Connection': 'close'} def request(self, url, allow_redirects=False, verify=False, proxies=None, timeout=8): """最终的请求实现""" requests.packages.urllib3.disable_warnings() if proxies: return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify, proxies=proxies, timeout=timeout) else: return requests.get(url=url, headers=self._headers, allow_redirects=allow_redirects, verify=verify, timeout=timeout) def search_page(self, url, pause=3): """ Google search :param query: Keyword :param language: Language :return: result """ time.sleep(random.randint(1, pause)) try: r = self.request(url) print('resp code=%d' % r.status_code) if r.status_code == 200: charset = chardet.detect(r.content) content = r.content.decode(charset['encoding']) return content elif r.status_code == 301 or r.status_code == 302 or r.status_code == 303: location = r.headers['Location'] time.sleep(random.randint(1, pause)) return self.search_page(location) return None except Exception as e: print(e) return None def transfer(self, content): # url = 'http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=' + content url = 'http://translate.google.cn/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=zh-CN&tl=en&q=' + content try: cache = futls.read_json('lib/cache.json') for c in cache: if content in c: print('已存在,跳过:{}'.format(content)) return c.get(content) except Exception as e: pass try: result = self.search_page(url) trans = json.loads(result)['sentences'][0]['trans'] # 解析获取翻译后的数据 # print(result) print(trans) self.local_cache.append({content: trans}) futls.write_json(self.local_cache, self.file) # 写入数据吗?下次直接缓存取 except Exception as e: print(e) return self.transfer(content) return trans def transfer_list(self, lists): self.local_cache = [] for c in lists: self.transfer(c) # http://translate.google.com/translate_a/single?client=gtx&dt=t&dj=1&ie=UTF-8&sl=auto&tl=zh-CN&q=what if __name__ == '__main__': files = next(os.walk('xxx/folder'))[2] results = [] for f in files: name, ext = os.path.splitext(f) if len(name) > 0 and len(ext): results.append(name) g = GTransfer('xxx/en.json') g.transfer_list(results) ================================================ FILE: 006-TikTok/img_2_webp.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: WordPress 站点 将图片转 webp 格式,实现内容重组 @Date :2022/10/12 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os def get_contain_pics(folder, content): files = next(os.walk(folder))[2] results = [] for f in files: name, ext = os.path.splitext(f) if len(name) < 1 or len(ext) < 1 or '.json' == str(ext) or '.txt' == str(ext): # 非视频文件 continue if content in f: results.append(f) return results def convert_pic_to_webp(folder, sku, pic_files, t, key1, key2, key3, youtube_num): dst_folder = os.path.join('data', sku) desc_links = [] desc_links.append( '
\n'.format( youtube_num)) desc_links.append( '\n') if not os.path.exists(dst_folder): os.makedirs(dst_folder) # TODO - 上传日期需要注意 月份 full_url = '{} \n' tail = '''

key1 Design Philosophy

1st: Ideal Size key1 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. 2nd: Fine Workmanship The 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! 3rd: Wide Range Of Uses key2 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! 4th: Perfect Gift Our 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. 5th: Develop Empathy And Social Skills With 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!

key2 Buyer notice

1st: About hair removal Did 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. 2nd: About size All products are measured in kind, and the key2 itself is soft and will have a certain error (about 3 cm). 3rd: About packaging We 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 the 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!

Our guarantee

Insurance

Each order includes real-time tracking details and insurance.

Order And Safe

We 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.

Other aspects

we’re constantly reinventing and reimagining our key3. If you have feedback you would like to share with us, please contact us! 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! large stuffed animals for adults and kids ''' tail = tail.replace('key1', key1).replace('key2', key2).replace('key3', key3) for i in range(0, len(pic_files)): f = pic_files[i] src = os.path.join(folder, f) # dst_name = '{}-m-{}{}'.format(sku, i + 1, ext) dst_name = '{}-{}-{}.webp'.format(sku, t, i + 1) # 更新至webp dst = os.path.join(dst_folder, dst_name) from PIL import Image im = Image.open(src) # 读入文件 print(im.size) width = 500 if im.size[0] > width: h = int(width * im.size[1] / im.size[0]) print(h) im.thumbnail((width, h), Image.ANTIALIAS) # 重新设置图片大小 im.save(dst) # 保存 print(dst) if i < 1: alt_key = key3 elif i < 3: alt_key = key2 else: alt_key = key1 if t == 'd': size = Image.open(dst).size desc_links.append(full_url.format(dst_name, alt_key, size[0], size[1])) if t == 'd': with open(os.path.join('data/desc-html', '{}.txt'.format(sku)), mode='w', encoding='utf-8') as f: for link in desc_links: f.write(link) f.write('\n') f.write(tail) f.close() def deal_sku(folder, sku, key1, key2, key3, youtube_num): folder = os.path.join(folder, sku) main_pics = sorted(get_contain_pics(folder, '主图')) desc_pics = sorted(get_contain_pics(folder, '详情')) convert_pic_to_webp(folder, sku, main_pics, 'm', key1, key2, key3, youtube_num) convert_pic_to_webp(folder, sku, desc_pics, 'd', key1, key2, key3, youtube_num) def deal_sku_list(): folder = 'xxx/SKU/' deal_sku(folder, 'FYTA032DX', 'elephant plush toy', 'stuffed animal elephant', 'elephant stuffed toy', '4kB0ZJBmMkU') # 奶瓶大象 def convert_2_webp(src, dst): from PIL import Image im = Image.open(src) # 读入文件 print(im.size) # width = 500 # if im.size[0] > width: # h = int(width * im.size[1] / im.size[0]) # print(h) # im.thumbnail((width, h), Image.ANTIALIAS) # 重新设置图片大小 im.save(dst) # 保存 def convert_folder(src_folder, dst_folder): files = next(os.walk(src_folder))[2] for f in files: if str(f).startswith('.'): continue name, ext = os.path.splitext(f) convert_2_webp(os.path.join(src_folder, f), os.path.join(dst_folder, '{}.webp'.format(name))) ''' 操作说明: 一:使用Fatkun工具下载图片后进行刷选 1、主图主要4张 2、详情图看情况删除一些,最好保留产品尺寸相关说明的图,正常不会超过10张 二:使用本脚本进行进图压缩以及格式转换(压缩了10倍左右) 1.本脚本将图片宽带缩小至500,高度等比例缩小 2.本脚本将jpg、png格式转换成webp格式 三:新建产品编辑 1.本脚本生成描述图片连接,直接拷贝过去 2.将变量尺寸属性值数字后面加上cm 3.两种产品,定价范围 A. 长条(如:FYTA004BR,长条兔子),属性名称:Long 70cm 18~21 90cm 35~40 110cm 60~70 130cm 90~100 150cm 120~130 B. 圆形(如:FYTA001SP,奶瓶猪),属性名称:Height 35cm 30~35 45cm 55~59 55cm 80~88 65cm 120~130 75cm 160~180 ''' if __name__ == '__main__': # deal_sku_list() convert_folder('xxx/png', 'xxx/webp') #Best Stuffed Pig Toys With A Hat ================================================ FILE: 006-TikTok/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: tiktok 相关开源库 @Date :2021/12/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ # import TikTokApi from TikTokAPI import TikTokAPI # https://github.com/avilash/TikTokAPI-Python if __name__ == "__main__": pass ================================================ FILE: 006-TikTok/post_autotk.py ================================================ """ @Description: 生成autotk.app(https://github.com/xhunmon/autotk)中post发送内容解析, @Date :2022/3/20 @Author :xhunmon @Mail :xhunmon@gmail.com """ import datetime import os import random import re import time import file_util as futls def get_real_files(folder): ''' 获取真实文件,去掉(.xxx)等文隐藏文件 :param files: :return: ''' files = next(os.walk(folder))[2] results = [] for f in files: name, ext = os.path.splitext(f) if len(name) < 1 or len(ext) < 1 or '.json' == str(ext) or '.txt' == str(ext): # 非视频文件 print('非视频文件:{}'.format(f)) continue results.append(f) return results def format_num(num): if num < 10: return '00{}'.format(num) elif num < 100: return '0{}'.format(num) else: return '{}'.format(num) def split_en_title(spl, content): if spl in content: temps = content.split(spl) coverDesc, title = "", "" for temp in temps: if len(temp) < 35: coverDesc = temp else: title = temp # print('------->过滤《{}》-> {} | {}'.format(spl, coverDesc, title)) return coverDesc, title return None, None def chose_cover_title(transfer: str): # 截取合适的标题和封面内容 # 大于5个字符才开始截取 trans = re.findall(r'(.{5,}?\?|.{5,}?\.|.{5,}?#|.{5,}?!|.{5,}?$)', transfer) title, cover = '', '' for tran in trans: tran = tran.replace('#', '').strip() t_size = len(tran) if len(title) < t_size: # title取最长的 title = tran # cover 优先取10~35个字符的,然后取靠拢的值 if len(cover) == 0: cover = tran elif 35 > t_size > 10: cover = tran elif 10 > t_size > len(cover): cover = tran elif 35 < t_size < len(cover): cover = tran return cover, title def match_info(pre, files, transfers): file_name, cover, title, transfer = None, None, None, None for v in files: # 文件名称 if str(v).startswith(pre): file_name = str(v) break if not file_name: raise RuntimeError('没有找到匹配的文件:{}'.format(pre)) for k in transfers: # 翻译后当前数据 if str(k).startswith(pre): transfer = str(k) break if not transfer: raise RuntimeError('没有找到匹配的文件:{}'.format(pre)) # 处理标题 transfer = transfer[3:].replace('-', '').strip() cover, title = chose_cover_title(transfer) if not cover or not title: raise RuntimeError('获取数据异常:{} | {} | {}'.format(file_name, cover, title)) return file_name, cover, title def format_time_by_str(date): # str转换为时间戳 time_array = time.strptime(date, "%Y-%m-%d %H:%M:%S") return int(time.mktime(time_array)) def format_time_by_stamp(stamp): # 时间戳转换为时间 return str(datetime.datetime.fromtimestamp(stamp)) def random_tag(tags): num = random.randint(1, 2) if len(tags) < num: num = len(tags) results = [] rsp = '' while True: if len(results) == num: break tag = random.choice(tags) if tag in results: continue results.append(tag) rsp += '#{} '.format(tag) return rsp def write_title_one(file_name, folder): datas = get_real_files(folder) with open(file_name, 'a', encoding='utf-8') as txt_f: for f in datas: name, ext = os.path.splitext(f) txt_f.write('{}\n'.format(name)) # txt_f.write('AAAAAAAA\n') def write_title(): file_name = 'lib/title.txt' write_title_one(file_name, 'xxx/bzhsrc/') def read_title_one(file_name): datas = [] with open(file_name, 'r', encoding='utf-8') as txt_f: while True: txt = txt_f.readline().replace('\n', '') if not txt: break if len(txt) > 5: datas.append(txt) return datas def del_content(content, dels): for d in dels: content = content.replace(d, '') return content def is_number(s): try: int(s) return True except ValueError: pass return False def rename_files(src_folder): # 重新生成序列 dels = ['#热࿆门', '#࿆热࿆门', '#热门', '“', '”', '-', '-', '[', ']', '《', '》', '/', ':', ' '] files = get_real_files(src_folder) index = 1 for f in files: src = os.path.join(src_folder, f) new_name: str = del_content(f, dels) num = new_name[0:3] if is_number(num): # 如果有序号 new_name = '{}-{}'.format(num, new_name[3:]) # 有序号时使用 else: new_name = '{}-{}'.format(format_num(index), new_name) dst = os.path.join(src_folder, new_name) print(new_name) os.rename(src, dst) index += 1 print('总共有[{}]个!'.format(len(files))) def start_post(userId, src_folder, date_start, title_tags, transfers, musics, dst='post.txt', index=1, space_start=30, space_end=40, num_start=3, num_end=6, half_time=7): ''' 开始生成post.txt文件 :param userId: 用户id :param src_folder: 视频目录 :param date_start: 第一个启动的时间,格式如:'2022-3-23 19:05:00' :param title_tags: :param transfers: :param musics: :param index: 起点位置 :param space_start: 下一个随机间隔开始发送的视频时间(分钟) :param space_end: 下一个随机间隔结束发送的视频时间(分钟) :param num_start: 每天随机发几个开始区间 :param num_end: 每天随机发几个结束区间 :param half_time(小时): 保证一半在12点,一半在6点后 :return: ''' posts = [] stamp = format_time_by_str(date_start) cover_tags = ["Vector", "Glitch", "Tint", "News", "Retro", "Skew", "Pill", "Pop"] # "Standard" --》有问题 files = get_real_files(src_folder) import copy temp_files: list = copy.deepcopy(files) size = len(files) while index <= size: stamp_1 = stamp num = random.randint(num_start, num_end) half_num = int(num / 2) for j in range(0, num): if index > size: print('已经处理完了:{}'.format(index)) break stamp_1 += random.randint(1, 30) * 60 if j == half_num: # 在一半的时候加上秒 stamp_1 += half_time * 60 * 60 date = format_time_by_stamp(stamp_1) sound = random.choice(musics) # pre = format_num(index) pre = temp_files.pop()[0:3] file, coverDesc, title1 = match_info(pre, files, transfers) # TODO- #foryoutoy 玩具需要 title = '{} {} #foryoutoy'.format(title1, random_tag(title_tags)) coverTag = random.choice(cover_tags) coverPic = random.randint(1, 3) post = {"date": date, "sound": sound, "title": title, "file": file, "userId": userId, "coverDesc": coverDesc, "coverTag": coverTag, "coverPic": coverPic} print(post) posts.append(post) stamp_1 += random.randint(space_start, space_end) * 60 index += 1 # print('----' * 5) stamp += 24 * 60 * 60 # 下一天 futls.write_json(posts, dst) def all_step(): # 1. 翻译文件,整理成txt # 2. 标题标签tag,整理成json # 3. 过滤无效信息,重命名文件 # 4. post # 美国比英国晚5个小时 transfers = futls.read_2_list('lib/furrytoy/fanyi_en.txt') # 英国 19~21,最多8个视频 title_tags = futls.read_json('lib/furrytoy/tags.json') musics = futls.read_json('lib/musics.json') dst = 'lib/furrytoy/post.json' start_post('kif_nee2022', 'xxx/fyt_toy/', '2022-5-8 10:00:00', transfers=transfers, title_tags=title_tags, musics=musics, dst=dst, space_start=70, space_end=83, num_start=2, num_end=2, half_time=8) # 每天1~2个,12小时*60 if __name__ == '__main__': # write_title() # all_step() rename_files('xxx/beizhihui') # 去掉冗余信息 ================================================ FILE: 006-TikTok/tikstar.py ================================================ """ @Description: 解析www.tikstar.com网站相关内容,获取tags @Date :2021/12/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ from bs4 import BeautifulSoup import file_util as futil def parse_tags(page): '''解析页面,返回标题和文章页面内容,如果生成文章则还需要组装''' soup = BeautifulSoup(page, 'html.parser') trs = soup.find('tbody').find_all('tr') result = [] for tr in trs: tds = tr.find_all('td') tag_name = tds[0].find('h3').text.replace('\n', '').replace(' ', '') video_num = tds[1].text.replace('\n', '').replace(' ', '') views = tds[2].text.replace('\n', '').replace(' ', '') result.append('标签:{} 视频数:{} 观看数:{}'.format(tag_name, video_num, views)) return result if __name__ == '__main__': html = futil.read('tags.html') result = parse_tags(html) print(result) futil.write_json(result, 'shoes.json') ================================================ FILE: 006-TikTok/tt_review.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: tiktok app(版本:22.8.2)刷评论脚本 @Date :2021/12/22 @Author :xhunmon @Mail :xhunmon@gmail.com """ import random import time from datetime import datetime import file_util as futil import uiautomator2 as u2 ''' https://github.com/openatx/uiautomator2 运行pip3 install -U uiautomator2 安装uiautomator2 运行python3 -m uiautomator2 init安装包含httprpc服务的apk到安卓手机 uiautomator2操作:https://python.iitter.com/other/35522.html 借助:weditor 来获取元素,双击找控件id (注意电脑要把代理关掉) ''' d = u2.connect() print(d.info) d.implicitly_wait(20) comments = ['good job', 'good', 'look me', 'crazy', 'emm...', 'I wish you a happy new year', 'May you be happy, lucky and happy.', 'I wish you a happy new year and good luck!', 'to put people and life above everything else', 'heroes in harm’s way', 'spirited', ' behind wave', 'mythical creatures', 'dagongren, which refers to people who work for others', 'involution', 'Versailles literature', 'Look at my', 'Too good', 'To learn', 'learned', 'Thank you', 'I got it.', '666', 'nice', 'Well done', 'Look at my', 'Wonderful', 'Mine is not bad either.', 'Kudos', 'like u', 'lean it', 'well...', '😊', 'My god!', 'Me too', 'I see', 'Come on', 'See you', 'Allow me', 'Have fun', 'I\'m home', 'Bless you!', 'Follow me', 'Good luck!', 'Bottoms up!', 'Guess what?', 'Keep it up!', 'Time is up', 'I like it!', 'That\'s neat', 'Let\'s face it.', 'Let\'s get started', 'Is that so', 'That\'s something', 'Do you really mean it', 'Mind you', 'I am behind you', 'That depends', 'What\'s up today?', 'Cut it out', 'What did you say', 'Knock it off', '[angel]', '[astonish]', '[awkward]', '[blink]', '[complacent]', '[cool]', '[cool][cute]', '[cool][cool]', '[cool][cool][cool]', '[cry]', '[cute]', '[cute][cute]', '[cute][cute][cute]', '[disdain]', '[drool]', '[embarrassed]', '[evil]', '[excited]', '[facewithrollingeyes]', '[flushed]', '[funnyface]', '[greedy]', '[happy]', '[hehe]', '[joyful]', '[laugh]', '[laughwithtears]', '[loveface]', '[lovely]', '[nap]', '[pride]', '[proud]', '[rage]', '[scream]', '[shock]', '[shout]', '[slap]', '[smile]', '[smileface]', '[speechless]', '[stun]', '[sulk]', '[surprised]', '[tears]', '[thinking]', '[weep]', '[wicked]', '[wow]', '[wronged]', '[yummy]', ' You kick ass.', ' You did a great job.', " You're a really strong person.", ' You read a lot.', ' That was impressive.', ' Your work on that project was incredible.', ' Keep up the good work!', " We're so proud of you.", ' How smart of you!', ' You have a real talent.', ' Well, a good teacher makes good student.', ' I would like to compliment you on your diligence.', " We're proud of you.", ' He has a long head.', ' You look great today.', " You're beautiful/gorgeous.", ' You look so healthy.', ' You look like a million dollars.', ' You have a good taste.', ' I am impressed.', ' You inspire me.', ' You are an amazing friend.', 'You are such a good friend.', ' You have a good sense of humor.', " You're really talented.", " You're so smart.", " You've got a great personality.", ' You are just perfect!', ' You are one of a kind.', ' You make me want to be a better person.', 'brb', 'g2g', 'AMA', 'dbd', 'this look great', 'we’re so proud of you.', 'nice place.', 'nice going! ', 'emm...amazing!', 'ohh...unbelievable!', 'yeh,impressive.', 'terrific..', 'fantastic!', 'fabulous.', 'attractive..', 'hei...splendid.', 'ooh, remarkable', 'gorgeous', 'h.., glamorous', 'marvelous.', 'brilliant..', 'well...glorious', 'outstanding...', 'stunning!', 'appealing.', 'yeh,impressive[cool]', 'terrific[angel]', 'fantastic[cool]', 'fabulous[angel]', 'attractive[cool]', 'splendid[angel]', 'remarkable[cool]', 'gorgeous[angel]', 'glamorous[angel]', 'marvelous[cool]', 'brilliant[angel]', 'glorious[cool]', 'outstanding[angel]', 'stunning[cool]', 'appealing[angel]', 'Would you like me?[angel]', 'Do you like crafts?[angel]', 'I have a new creative work, welcome![thinking]'] print('总共有{}条随机评论!'.format(len(comments))) def start_vpn(): # 启动代理app d.press("home") # d.app_stop('com.v2ray.ang') d.app_start('com.v2ray.ang') if 'Not' in d(resourceId="com.v2ray.ang:id/tv_test_state").get_text(): print_t('正在启动v2ray...') d(resourceId='com.v2ray.ang:id/fab').click() if 'Connected' in d(resourceId="com.v2ray.ang:id/tv_test_state").get_text(): print_t('启动v2ray完成,正在测试速度...') d(resourceId='com.v2ray.ang:id/layout_test').click() while 'Testing' in d(resourceId="com.v2ray.ang:id/tv_test_state").get_text(): time.sleep(1) print_t(d(resourceId="com.v2ray.ang:id/tv_test_state").get_text()) def review_forYou(): # d.press("home") # d.app_start('com.zhiliaoapp.musically', stop=True) time.sleep(1) stop, index = random.randint(40, 70), 0 while index < stop: # 随机刷几十条 cur_comment = comments[random.randint(0, len(comments) - 1)] print_t('foryou-总共有:{}条 | 现在到:{}条\t评论:{}'.format(stop, index, cur_comment)) comment_foryou(cur_comment) try: time.sleep(random.randint(7, 35)) # 随机停顿1~5秒 d.swipe_ext("up") # 上划,下一个视频 except Exception as e: print_t(e) index += 1 def review_tiktok(): # 评论 keys = ['handmadecraft'] # '#homedecor #flowershower #handmadecraft' d.press("home") d.app_start('com.zhiliaoapp.musically', stop=False) time.sleep(1) try: d(text='Discover', className='android.widget.TextView').click() # 点击发现 time.sleep(1) d(resourceId="com.zhiliaoapp.musically:id/fbt").click() # 点击搜索 except Exception as e: print_t(e) for key in keys: # 不能搜索search try: d(resourceId="com.zhiliaoapp.musically:id/b15").clear_text() # 清除历史 d(resourceId="com.zhiliaoapp.musically:id/b15").set_text(key) # 输入 d(resourceId="com.zhiliaoapp.musically:id/fap").click() # 点击搜索 d(resourceId="android:id/text1", text="Videos").click() # 点击视频,然后点击第一条 d.xpath( '//*[@resource-id="com.zhiliaoapp.musically:id/cfh"]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]').click() except Exception as e: print_t(e) stop, index = random.randint(35, 60), 0 while index < stop: # 随机刷几十条 cur_comment = comments[random.randint(0, len(comments) - 1)] print_t('{}-总共有:{}条 | 现在到:{}条\t评论:{}'.format(key, stop, index, cur_comment)) comment(cur_comment) try: time.sleep(random.randint(5, 20)) # 随机停顿1~5秒 d.swipe_ext("up") # 上划,下一个视频 except Exception as e: print_t(e) index += 1 time.sleep(random.randint(int(0.5 * 60), 5 * 60)) d(resourceId="com.zhiliaoapp.musically:id/t4").click() # 返回搜索 def comment(content): try: time.sleep(random.randint(3, 10)) # 随机停顿1~5秒 d(resourceId="com.zhiliaoapp.musically:id/acm").click() # 点击评论按钮 except Exception as e: print_t(e) try: time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 e_ele = d(text='Add comment...', className='android.widget.EditText') e_ele.click() # 点击弹出键盘 time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 e_ele.clear_text() e_ele.set_text(content) # 输入 except Exception as e: print_t(e) try: time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 d(resourceId="com.zhiliaoapp.musically:id/ad6").click() # 发送 except Exception as e: print_t(e) # 关闭系统键盘 try: time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 d(resourceId="com.zhiliaoapp.musically:id/t4").click() # 关闭评论 except Exception as e: print_t(e) def comment_foryou(content): try: time.sleep(random.randint(3, 10)) # 随机停顿1~5秒 d(resourceId="com.zhiliaoapp.musically:id/acm").click() # 点击评论按钮 except Exception as e: print_t(e) try: time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 e_ele = d(text='Add comment...', className='android.widget.EditText') e_ele.click() # 点击弹出键盘 time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 e_ele.clear_text() e_ele.set_text(content) # 输入 except Exception as e: print_t(e) try: time.sleep(random.randint(1, 3)) # 随机停顿1~2秒 d(resourceId="com.zhiliaoapp.musically:id/ad6").click() # 发送 except Exception as e: print_t(e) # 关闭系统键盘 try: d.press("back") # 返回1,关闭系统键盘 except Exception as e: print_t(e) def print_t(content): dt = datetime.now() print(dt.strftime('%H:%M:%S') + '\t' + str(content)) if __name__ == "__main__": # start_vpn() # review_tiktok() # review_forYou() # comment('Look at my') d(resourceId="com.zhiliaoapp.musically:id/afa").click() ================================================ FILE: 006-TikTok/v2ray_pool/__init__.py ================================================ # 运行时路径。并非__init__.py的路径 import os import sys BASE_DIR = "../002-V2rayPool" if os.path.exists(BASE_DIR): sys.path.append(BASE_DIR) from core import utils from core.conf import Config from db.db_main import DBManage ================================================ FILE: 007-CutVideoAudio/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: 007-CutVideoAudio/007-CutVideoAudio.iml ================================================ ================================================ FILE: 007-CutVideoAudio/README.md ================================================ # FFmpeg批量剪切音视频 [V2版本脚本](ff_util_v2.py)更加完善,但是未接入GUI 本项目主要通过ffmpeg工具进行批量视频剪辑,随机剪辑,从而躲过自媒体平台的检查,从而达到一份视频多个账号运营。 使用前提:**必须要安装ffmpeg程序**,安装过程请自行百度。 下载地址: MacOS:[QincjiCut1.0.0-mac](https://github.com/xhunmon/PythonIsTools/releases/download/1.0.4/QincjiCut1.0.0.app.zip) 下载后解压后使用 Window:QincjiCut1.0.0-win (未打包) 效果如图: ![剪辑器截图](./doc/example.png) #主要知识点 ## python GUI(界面) 本文使用tkinter GUI(界面)框架进行界面显示:[./ui.py](ui.py) ,[学习参考](https://www.cnblogs.com/shwee/p/9427975.html) 。 ## [pyinstaller](https://pyinstaller.readthedocs.io/en/stable/) 打包 使用pyinstaller把python程序打包成window和mac可执行文件,主要命令如下: ```shell #① :生成xxx.spec文件;(去掉命令窗口-w) pyinstaller -F -i res/logo.ico main.py -w #②:修改xxx.spec,参考main.spec #③:再次进行打包,参考installer-mac.sh pyinstaller -F -i res/logo.ico main.spec -w ``` 打包脚本与配置已放在 `doc` 目录下,需要拷贝出根目录进行打包。 注意: pyinstaller打包工具的版本与python版本、python所需第三方库以及操作系统会存在各种问题,所以需要看日志查找问题。例如:打包后运用,发现导入pyppeteer报错,通过降低版本后能正常使用:pip install pyppeteer==0.2.2 ## 项目 本项目跟Downloader下载器基本相同,而ffmpeg命令则可以通过 [](https://qincji.gitee.io/2021/01/18/ffmpeg/18_command/) ================================================ FILE: 007-CutVideoAudio/__init__.py ================================================ ================================================ FILE: 007-CutVideoAudio/config.ini ================================================ # 常用配置模块 [common] #软件使用截止日期 expired_time=2025/12/15 23:59:59 #app的版本名称 version_name=1.0.0 #app的版本号 version_code=1000 ================================================ FILE: 007-CutVideoAudio/doc/mac-sh/main.spec ================================================ # -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis(['main.py','type_enum.py','ui.py','utils.py','editors.py','ff_util.py','ff_cut.py'], pathex=['.'], binaries=[], datas=[('res/logo.ico', 'images'),('config.ini', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='main', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None , icon='res/logo.ico') app = BUNDLE(exe, name='QincjiCut.app', icon='res/logo.ico', bundle_identifier=None) ================================================ FILE: 007-CutVideoAudio/doc/mac-sh/pyinstaller.sh ================================================ #!/bin/bash pyinstaller -F -i res/logo.ico main.spec main.py -w \ -p type_enum.py \ -p ui.py \ -p utils.py \ -p ff_util.py \ -p editors.py \ -p ff_cut.py ================================================ FILE: 007-CutVideoAudio/editors.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:editing.py 所有视频类的基类,负责与UI界面的绑定 @Date :2022/03/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import time from threading import Lock import requests from my_fake_useragent import UserAgent from type_enum import PrintType from utils import Config ua = UserAgent(family='chrome') class Editors(object): func_ui_print = None __mutex_total = Lock() __mutex_success = Lock() __mutex_failed = Lock() __mutex_bmg = Lock() __count_total = 0 __count_success = 0 __count_failed = 0 __count_bmg = 0 __beijing_time = 0 # 在线北京时间 def __init__(self): self._headers = {'user-agent': ua.random()} self.get_beijing_time() @staticmethod def print_hint(): """显示初始提示信息""" Editors.print_ui( """ 程序原理: \t1、随机裁剪掉视频头尾; \t2、随机改变视频帧率和比特率; \t3、随机更换视频bgm; \t4、保证每次输出不重复。 使用说明: \t1、必须安装FFmpeg; \t2、最短bgm长度大于最大视频长度。 """ ) def start(self, ffmpeg, video, music, dst): """业务逻辑由子类实现""" pass @staticmethod def print_ui(txt): """在界面显示内容""" Editors.print_all_ui(txt=txt) # 打印日志 @staticmethod def print_all_ui(txt, print_type: PrintType = PrintType.log): """通知ui中func_ui_print更新内容""" if Editors.func_ui_print is not None: Editors.func_ui_print(txt=txt, print_type=print_type) @staticmethod def get_beijing_time(): """静态方法:获取在线的北京时间""" if Editors.__beijing_time > 0: return Editors.__beijing_time try: response = requests.get(url='http://www.beijing-time.org/t/time.asp', headers={'user-agent': ua.random()}) result = response.text data = result.split("\r\n") year = data[1][len("nyear") + 1: len(data[1]) - 1] month = data[2][len("nmonth") + 1: len(data[2]) - 1] day = data[3][len("nday") + 1: len(data[3]) - 1] # wday = data[4][len("nwday")+1 : len(data[4])-1] hrs = data[5][len("nhrs") + 1: len(data[5]) - 1] minute = data[6][len("nmin") + 1: len(data[6]) - 1] sec = data[7][len("nsec") + 1: len(data[7]) - 1] beijinTimeStr = "%s/%s/%s %s:%s:%s" % (year, month, day, hrs, minute, sec) beijinTime = time.strptime(beijinTimeStr, "%Y/%m/%d %X") Editors.__beijing_time = int(time.mktime(beijinTime)) except: pass return Editors.__beijing_time @staticmethod def is_expired(): """静态方法:判断是否已过期""" if Editors.__beijing_time == 0: # 还没获取到时间 return True expired_time_str = time.strptime(Config.instance().get_expired_time(), "%Y/%m/%d %X") expired_time_int = int(time.mktime(expired_time_str)) return Editors.__beijing_time > expired_time_int @staticmethod def add_total_count(count=1): """静态方法:添加总下载任务数""" Editors.__mutex_total.acquire() Editors.__count_total += count Editors.__mutex_total.release() Editors.print_all_ui(txt="视频总数:%d" % Editors.__count_total, print_type=PrintType.total) @staticmethod def add_bgm_count(count=1): """静态方法:添加总下载任务数""" Editors.__mutex_bmg.acquire() Editors.__count_bmg += count Editors.__mutex_bmg.release() Editors.print_all_ui(txt="bmg数量:%d" % Editors.__count_bmg, print_type=PrintType.bgm) @staticmethod def get_total_count(): """静态方法:获取总下载任务数""" return Editors.__count_total @staticmethod def get_bgm_count(): """静态方法:获取正在下载任务数""" return Editors.__count_bmg @staticmethod def add_success_count(): """静态方法:添加下载成功任务数""" Editors.__mutex_success.acquire() Editors.__count_success += 1 Editors.__mutex_success.release() # 成功一条,减正在下载的一条 Editors.print_all_ui(txt="已完成:%d" % Editors.__count_success, print_type=PrintType.success) @staticmethod def get_success_count(): """静态方法:获取下载成功任务数""" return Editors.__count_success @staticmethod def add_failed_count(): """静态方法:添加下载失败任务数""" Editors.__mutex_failed.acquire() Editors.__count_failed += 1 Editors.__mutex_failed.release() # 失败一条,减正在下载的一条 Editors.print_all_ui(txt="已失败:%d" % Editors.__count_failed, print_type=PrintType.failed) @staticmethod def get_failed_count(): """静态方法:获取下载失败任务数""" return Editors.__count_failed ================================================ FILE: 007-CutVideoAudio/ff_cut.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:editing.py 所有视频类的基类,负责与UI界面的绑定 @Date :2022/03/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os.path import random import shutil import time from editors import Editors from ff_util import * from utils import * class ListCut(Editors): # 初始化 def __init__(self): super().__init__() self.headers = self._headers # 抓获所有视频 self.end = False def start(self, ffmpeg, video, music, dst): Editors.print_ui("开始准备处理本地数据") if not os.path.exists(ffmpeg) or not os.path.exists(video) or not os.path.exists(music) or not os.path.exists( dst): Editors.print_ui("文件或者目录不存在!") return dst_temp = os.path.join(dst, 'temp') if not os.path.exists(dst_temp): os.makedirs(dst_temp) fps, bit = random.randint(25, 30), random.randint(1200, 1800) start_v, end_v = random.randint(100, 500), random.randint(100, 500) v_files, a_files = get_real_files(video), get_real_files(music) index, size_v, size_a, start_time = 1, len(v_files), len(a_files), time.time() Editors.add_total_count(size_v) Editors.add_bgm_count(size_a) Editors.print_ui("改系列帧率:{} | 比特率:{} | 截切开头:{} | 截切结尾:{}".format(fps, bit, start_v, end_v)) time.sleep(3) for i in range(0, size_v): a_file = random.choice(a_files) v_file = random.choice(v_files) v_files.remove(v_file) Editors.print_ui("{} 与 {} 开始合并!".format(v_file, a_file)) try: name_v, ext_v = os.path.splitext(v_file) name_a, ext_a = os.path.splitext(a_file) src_v = os.path.join(video, v_file) dst_file = os.path.join(dst, '{}-{}{}'.format(format_num(index), name_v, ext_v)) dur_src_v = get_duration(ffmpeg, src_v) # 毫秒 duration = dur_src_v - start_v - end_v src_a = os.path.join(music, a_file) dur_src_a = get_duration(ffmpeg, src_a) random_a = (dur_src_a - duration) if dur_src_a > duration else 100 start_a = random.randint(0, random_a) # 随机取音频 temp_file_v = os.path.join(dst_temp, 'temp{}'.format(ext_v)) temp_file_a = os.path.join(dst_temp, 'temp{}'.format(ext_a)) cut_video(ffmpeg, src_v, start_v, duration, fps, bit, temp_file_v) cut_audio(ffmpeg, src_a, start_a, duration, temp_file_a) muxer_va(ffmpeg, temp_file_v, temp_file_a, dst_file) index += 1 Editors.add_success_count() except Exception as e: Editors.print_ui(str(e)) Editors.add_failed_count() time.sleep(1) use_time = time.time() - start_time Editors.print_ui('已运行{}分{}秒...'.format(int(use_time / 60), int(use_time % 60))) time.sleep(2) shutil.rmtree(dst_temp) ================================================ FILE: 007-CutVideoAudio/ff_util.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:ff_util.py ffmpeg截切命令工具 @Date :2022/03/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os def get_duration(ff_file, src): ''' 执行命令获取输出这样的:Duration: 00:00:31.63, start: 0.000000, bitrate: 1376 kb/s :param ff_file: ffmpeg程序路径 :param src: 音视频文件 :return: 返回毫秒 ''' info = os.popen(r'{} -i "{}" 2>&1 | grep "Duration"'.format(ff_file, src)).read() dur = info.split(',')[0].replace(' ', '').split(':') h, m, ss = int(dur[1]) * 60 * 60 * 1000, int(dur[2]) * 60 * 1000, dur[3] if '.' in ss: s1 = ss.split('.') s = int(s1[0]) * 1000 + int(s1[1]) * 10 else: s = int(ss) * 1000 return h + m + s def format_h_m_s(t): if t < 10: return '0{}'.format(t) else: return '{}'.format(t) def format_ms(t): if t < 10: return '00{}'.format(t) elif t < 100: return '0{}'.format(t) else: return '{}'.format(t) def format_duration_by_ms(ms): ''' 通过毫秒转化成 'xx:xx:xx.xxx' 格式 :param ms: 毫秒 :return: ''' h = format_h_m_s(int(ms / 1000 / 60 / 60)) m = format_h_m_s(int(ms / 1000 / 60 % 60)) s = format_h_m_s(int(ms / 1000 % 60)) m_s = format_ms(int(ms % 1000)) return '{}:{}:{}.{}'.format(h, m, s, m_s) def cut_audio(ff_file, src, start, dur, dst): ''' 裁剪一段音频进行输出 :param ff_file: ffmpeg程序路径 :param src: 要裁剪的文件路径,可以是视频文件 :param start: 开始裁剪点,单位毫秒开始 :param dur: 裁剪时长,单位秒 :param dst: 输出路径,包括后缀名 :return: ''' if os.path.exists(dst): os.remove(dst) os.system( r'{} -i "{}" -vn -acodec copy -ss {} -t {} "{}"'.format(ff_file, src, format_duration_by_ms(start), dur, dst)) def cut_video(ff_file, src, start, dur, fps, bit, dst): ''' 裁剪一段视频进行输出, -ss xx:xx:xx.xxx :param ff_file: ffmpeg程序路径 :param src: 要裁剪的文件路径,可以是视频文件 :param start: 开始裁剪点,单位毫秒开始 :param dur: 裁剪时长,单位秒 :param fps: 帧率,通常是25~30 :param bit: 比特率,通常是1600~2000即可 :param dst: 输出路径,包括后缀名 :return: ''' if os.path.exists(dst): os.remove(dst) os.system( r'{} -i "{}" -ss {} -t {} -r {} -b:v {}K -an "{}"'.format(ff_file, src, format_duration_by_ms(start), dur, fps, bit, dst)) def muxer_va(ff_file, src_v, src_a, dst): if os.path.exists(dst): os.remove(dst) os.system(r'{} -i "{}" -i "{}" -c:v copy -c:a aac -strict experimental "{}"'.format(ff_file, src_v, src_a, dst)) ================================================ FILE: 007-CutVideoAudio/ff_util_v2.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:ff_util.py ffmpeg截切命令工具 @Date :2022/03/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os import re import time from datetime import datetime, timedelta def get_va_infos(ff_file, src): """ 获取视频的基本信息 @param ff_file: ffmpeg路径 @param src: 视频路径 @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'} """ cmd = r'{} -i "{}" -hide_banner 2>&1'.format(ff_file, src) output = os.popen(cmd).read() lines = output.splitlines() result = {} for line in lines: if line.strip().startswith('Duration:'): result['duration'] = line.split(',')[0].split(': ')[-1] result['bitrate'] = line.split(',')[-1].strip().split(': ')[-1].split(' ')[0] elif line.strip().startswith('Stream #0'): line = re.sub(r'\[.*?\]', '', re.sub(r'\(.*?\)', '', line)) if 'Video' in line: result['v_codec'] = line.split(',')[0].split(': ')[-1].strip() result['v_size'] = line.split(',')[2].strip().split(' ')[0].strip() result['v_bitrate'] = line.split(',')[3].strip().split(' ')[0].strip() result['v_fps'] = line.split(',')[4].strip().split(' ')[0].strip() elif 'Audio' in line: result['a_codec'] = line.split(',')[0].split(': ')[-1].strip() result['a_bitrate'] = line.split(',')[4].strip().split(' ')[0].strip() print(result) return result def get_duration(ff_file, src): ''' 执行命令获取输出这样的:Duration: 00:00:31.63, start: 0.000000, bitrate: 1376 kb/s :param ff_file: ffmpeg程序路径 :param src: 音视频文件 :return: 返回毫秒 ''' cmd = r'{} -i "{}" 2>&1 | grep "Duration"'.format(ff_file, src) info = os.popen(cmd).read() dur = info.split(',')[0].replace(' ', '').split(':') h, m, ss = int(dur[1]) * 60 * 60 * 1000, int(dur[2]) * 60 * 1000, dur[3] if '.' in ss: s1 = ss.split('.') s = int(s1[0]) * 1000 + int(s1[1]) * 10 else: s = int(ss) * 1000 return h + m + s def format_h_m_s(t): return f'0{t}' if t < 10 else f'{t}' def format_ms(t): if t < 10: return f'00{t}' return f'0{t}' if t < 100 else f'{t % 1000}' def format_to_time(ms): ''' 毫秒 --> 'xx:xx:xx.xxx' :param ms: 毫秒 :return: 'xx:xx:xx.xxx' ''' # t = timedelta(milliseconds=ms) # return str(t) h = format_h_m_s(int(ms / 1000 / 60 / 60)) m = format_h_m_s(int(ms / 1000 / 60 % 60)) s = format_h_m_s(int(ms / 1000 % 60)) m_s = format_ms(int(ms % 1000)) return '{}:{}:{}.{}'.format(h, m, s, m_s) def format_to_ms(duration: str): """ 'xx:xx:xx.xxx' --> 毫秒 @param duration: 时间长度'xx:xx:xx.xxx' @return: 毫秒 """ hms = duration.split(':') s_str = hms[2] ms_str = '0' if '.' in s_str: s_ms = s_str.split('.') s_str = s_ms[0] ms_str = s_ms[1] h = int(hms[0]) * 1000 * 60 * 60 m = int(hms[1]) * 1000 * 60 s = int(s_str) * 1000 ms = int(ms_str) return h + m + s + ms def srt_to_ass(ff_file, src, dst): os.system(f'{ff_file} -i {src} {dst}') def cut_with_subtitle(ff_file, src, dst, srt, width, height, margin_v, font_size=50, dur_full: str = None, start='00:00:00.000', tail='00:00:00.000', fps=None, v_bit=None, a_bit=None): """ 添加硬字幕: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 """ t_start = time.time() dur_ms = format_to_ms(dur_full) - format_to_ms(tail) - format_to_ms(start) dur = format_to_time(dur_ms) filename, ext = os.path.splitext(srt) ass = f'{filename}.ass' if os.path.exists(ass): os.remove(ass) ass_cmd = '{} -i "{}" "{}"'.format(ff_file, srt, ass) print(ass_cmd) os.system(ass_cmd) # ffmpeg -i "input.mp4" -ss "00:02:10.000" -t 12397 -r 15 -b:v 500K -c:v libx264 -c:a aac cmd = '{} -i "{}"'.format(ff_file, src) cmd = '{} -ss "{}" -t {}'.format(cmd, start, int(format_to_ms(dur) / 1000)) if fps: # 添加裁剪的fps cmd = '{} -r {}'.format(cmd, fps) # -c copy 不经过解码,会出现黑屏,因为有可能是P帧和B帧 if v_bit: # 添加视频bitrate,并且指定用libx264进行编码(ffmpeg必须安装) cmd = '{} -b:v {}K -c:v libx264'.format(cmd, v_bit) if a_bit: # 添加音频bitrate,并且指定用aac进行编码 cmd = '{} -b:a {}K -c:a aac'.format(cmd, a_bit) # -vf "subtitles=input.ass:force_style='PlayResX=1280,PlayResY=720,MarginV=70,Fontsize=50'" style = "PlayResX={},PlayResY={},MarginV={},Fontsize={}".format(width, height, margin_v, font_size) sub_file = "{}".format(ass) cmd = '''{} -vf "subtitles={}:force_style='{}'"'''.format(cmd, sub_file, style) # cmd = f'{cmd} -vf "subtitles={ass}:force_style="""PlayResX={width},PlayResY={height},MarginV={margin_v},Fontsize={font_size}""""' cmd = '{} {}'.format(cmd, dst) print(cmd) if os.path.exists(dst): os.remove(dst) os.system(cmd) os.remove(ass) print('一共花了 {} 秒 进行裁剪并添加字幕 {}'.format(int(time.time() - t_start), src)) def cut_va_full(ff_file, src, dst, dur: str = None, start='00:00:00.000', fps=None, v_bit=None, a_bit=None, copy_a=False): """ 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" 其他所有的视频裁剪命令都需要通过这个实现 @param ff_file: ffmpeg路径 @param src: 输入路径 @param dst: 输出路径 @param dur: 裁剪长度,格式为'00:00:00.000' @param start: 裁剪的起点,如果dur=None,表示需要对时间进行裁剪,只是转换格式罢了 @param fps: 帧率,通常视频15~18帧即可,动漫一般24帧 @param v_bit: 单独控制视频的比特率 @param a_bit: 单独控制音频的比特率 @param copy_a: 直接复制音频通道数据,当 a_bit=None方有效 """ cmd = '{} -i "{}"'.format(ff_file, src) # 输入文件 if dur is not None: # 添加裁剪时间 cmd = '{} -ss "{}" -t {}'.format(cmd, start, int(format_to_ms(dur) / 1000)) if fps: # 添加裁剪的fps cmd = '{} -r {}'.format(cmd, fps) # -c copy 不经过解码,会出现黑屏,因为有可能是P帧和B帧 if v_bit: # 添加视频bitrate,并且指定用libx264进行编码(ffmpeg必须安装) cmd = '{} -b:v {}K -c:v libx264'.format(cmd, v_bit) if a_bit: # 添加音频bitrate,并且指定用aac进行编码 cmd = '{} -b:a {}K -c:a aac'.format(cmd, a_bit) elif copy_a: # 是否完全复制音频 cmd = '{} -c:a copy'.format(cmd) cmd = '{} "{}"'.format(cmd, dst) # 添加输出 if os.path.exists(dst): os.remove(dst) t_start = time.time() os.system(cmd) print('一共花了 {} 秒 进行裁剪 {}'.format(int(time.time() - t_start), src)) def cut_va_tail(ff_file, src, dst, dur_full: str = None, start='00:00:00.000', tail='00:00:00.000', fps=None, v_bit=None, a_bit=None, copy_a=False): """ 裁剪头尾 """ dur_ms = format_to_ms(dur_full) - format_to_ms(tail) - format_to_ms(start) dur = format_to_time(dur_ms) cut_va_full(ff_file, src, dst, dur, start, fps, v_bit, a_bit, copy_a) def cut_audio(ff_file, src, start, dur, dst): ''' 裁剪一段音频进行输出 :param ff_file: ffmpeg程序路径 :param src: 要裁剪的文件路径,可以是视频文件 :param start: 开始裁剪点,单位毫秒开始 :param dur: 裁剪时长,单位秒 :param dst: 输出路径,包括后缀名 :return: ''' if os.path.exists(dst): os.remove(dst) os.system( r'{} -i "{}" -vn -acodec copy -ss {} -t {} "{}"'.format(ff_file, src, format_to_time(start), dur, dst)) def cut_video(ff_file, src, start, dur, fps, bit, dst): ''' 裁剪一段视频进行输出, -ss xx:xx:xx.xxx :param ff_file: ffmpeg程序路径 :param src: 要裁剪的文件路径,可以是视频文件 :param start: 开始裁剪点,单位毫秒开始 :param dur: 裁剪时长,单位秒 :param fps: 帧率,通常是25~30 :param bit: 比特率,通常是1600~2000即可 :param dst: 输出路径,包括后缀名 :return: ''' if os.path.exists(dst): os.remove(dst) os.system( r'{} -i "{}" -ss {} -t {} -r {} -b:v {}K -an "{}"'.format(ff_file, src, format_to_time(start), dur, fps, bit, dst)) def cut_va_dur(ff_file, src, dst, start=0, dur=0, fps=None, bit=None): """ 根据头尾裁剪视频 :param ff_file: ffmpeg工具 :param src: 输入资源 :param dst: 输出文件 :param start: 起点 :param end: 终点 :param fps: 帧率 :param bit: 比特率 :return: """ length = get_duration(ff_file, src) if start + dur > length: print('裁剪比视频长') return if os.path.exists(dst): os.remove(dst) cmd = r'{} -i "{}"'.format(ff_file, src) cmd = cmd + ' -ss {}'.format(format_to_time(start)) cmd = cmd + ' -t {}'.format(format_to_time(dur)) if fps: cmd = cmd + ' -r {}'.format(fps) if bit: cmd = cmd + ' -b:v {}K'.format(bit) cmd = cmd + ' -c:v libx264 "{}"'.format(dst) # 使用x264解码后重新封装 # cmd = cmd+' -c copy {}'.format(dst) #不经过解码,会出现黑屏 os.system(cmd) def cut_va_start_end(ff_file, src, dst, start='00:00:00', end='00:00:00', dur='00:00:00', fps=None, bit=None): dur = format_to_ms(dur) - format_to_ms(end) cmd = f'{ff_file} -i "{src}" -ss {start} -t {dur} -c copy "{dst}"' if os.path.exists(dst): os.remove(dst) os.system(cmd) def cut_va_end(ff_file, src, dst, start=0, end=0, fps=None, bit=None): """ 根据头尾裁剪视频 :param ff_file: ffmpeg工具 :param src: 输入资源 :param dst: 输出文件 :param start: 起点 :param end: 终点 :param fps: 帧率 :param bit: 比特率 :return: """ length = get_duration(ff_file, src) dur = length - (start + end) if dur <= 0: print('裁剪比视频长') return cut_va_dur(ff_file, src, dst, start, dur, fps, bit) def muxer_va(ff_file, src_v, src_a, dst): if os.path.exists(dst): os.remove(dst) os.system(r'{} -i "{}" -i "{}" -c:v copy -c:a aac -strict experimental "{}"'.format(ff_file, src_v, src_a, dst)) ================================================ FILE: 007-CutVideoAudio/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: 程序主入口 @Date :2022/03/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ import os import sys from ui import Ui # 主模块执行 if __name__ == "__main__": path = os.path.dirname(os.path.realpath(sys.argv[0])) # ffmpeg = os.path.dirname('/usr/local/ffmpeg/bin/ffmpeg/') # video = os.path.dirname('/Users/Qincji/Documents/zmt/handmade/') # music = os.path.dirname('/Users/Qincji/Documents/zmt/music/') # dst = os.path.dirname('/Users/Qincji/Downloads/ffmpeg/') app = Ui() # app.set_dir(ffmpeg, video, music, dst) app.set_dir(path, path, path, path) # to do app.mainloop() ================================================ FILE: 007-CutVideoAudio/type_enum.py ================================================ #!/usr/bin/env python # -*- coding:utf-8 -*- """ @Description:dy_download.py @Date :2022/03/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ from enum import Enum class PrintType(Enum): log = 1 total = 2 bgm = 3 success = 4 failed = 5 ================================================ FILE: 007-CutVideoAudio/ui.py ================================================ #!/usr/bin/env python # -*- coding:utf-8 -*- """ @Description: 用于GUI界面显示 @Date :2021/08/14 @Author :xhunmon @Mail :xhunmon@gmail.com """ from tkinter import * from tkinter.filedialog import (askdirectory, askopenfilename) from editors import Editors from ff_cut import ListCut from type_enum import PrintType from utils import * # from PIL import Image, ImageTk class Ui(Frame): def __init__(self, master=None): global bg_color bg_color = '#373434' Frame.__init__(self, master, bg=bg_color) self.ui_width = 0 self.pack(expand=YES, fill=BOTH) self.window_init() self.createWidgets() def window_init(self): self.master.title( '欢迎使用-音视频批量截切工具' + Config.instance().get_version_name() + ' 如有疑问请联系:xhunmon@gmail.com') self.master.bg = bg_color width, height = self.master.maxsize() # self.master.geometry("{}x{}".format(width, height)) self.master.geometry("%dx%d+%d+%d" % (width / 2, height / 2, width / 4, height / 4)) self.ui_width = width / 2 def createWidgets(self): # fm1 self.fm1 = Frame(self, bg=bg_color) self.fm1.pack(fill='y', pady=10) # window没有原生PIL 64位支持 # load = Image.open('res/logo.png') # load.thumbnail((38, 38), Image.ANTIALIAS) # initIamge = ImageTk.PhotoImage(load) # self.panel = Label(self.fm1, image=initIamge, bg=bg_color) # self.panel.image = initIamge # self.panel.pack(side=LEFT, fill='y', padx=5) self.titleLabel = Label(self.fm1, text="视频批量剪辑,适合一个视频多账号发送", font=('微软雅黑', 28), fg="white", bg=bg_color) self.titleLabel.pack(side=LEFT, fill='y') # fm2 self.fm2 = Frame(self, bg=bg_color) self.fm2.pack(side=TOP, fill="y") self.fm2_right = Frame(self.fm2, bg=bg_color) self.fm2_right.pack(side=RIGHT, padx=0, pady=10, expand=YES, fill='y') self.fm2_left = Frame(self.fm2, bg=bg_color) self.fm2_left.pack(side=LEFT, padx=15, pady=10, expand=YES, fill='x') self.fm2_left_ffmpeg = Frame(self.fm2_left, bg=bg_color) self.fm2_left_souce_video = Frame(self.fm2_left, bg=bg_color) self.fm2_left_souce_music = Frame(self.fm2_left, bg=bg_color) self.fm2_left_dst = Frame(self.fm2_left, bg=bg_color) self.downloadBtn = Button(self.fm2_right, text='开始', fg="#aaaaaa", bg=bg_color, font=('微软雅黑', 18), command=self.start_download) self.downloadBtn.pack(side=RIGHT) self.ffmFileEntry = Entry(self.fm2_left_ffmpeg, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1) self.ffmFileEntry.config(insertbackground='#ffffff') self.ffmFileBtn = Button(self.fm2_left_ffmpeg, text='选择FFmpeg程序:', bg=bg_color, fg='#aaaaaa', font=('微软雅黑', 12), width='12', command=self.save_file) self.ffmFileBtn.pack(side=LEFT) self.ffmFileEntry.pack(side=LEFT, fill='y') self.fm2_left_ffmpeg.pack(side=TOP, fill='x') self.videoDirEntry = Entry(self.fm2_left_souce_video, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1) self.videoDirEntry.config(insertbackground='#ffffff') self.videoDirBtn = Button(self.fm2_left_souce_video, text='选择视频源目录:', bg=bg_color, fg='#aaaaaa', font=('微软雅黑', 12), width='12', command=self.save_dir) self.videoDirBtn.pack(side=LEFT) self.videoDirEntry.pack(side=LEFT, fill='y') self.fm2_left_souce_video.pack(side=TOP, fill='x') self.musicDirEntry = Entry(self.fm2_left_souce_music, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1) self.musicDirEntry.config(insertbackground='#ffffff') self.musicDirBtn = Button(self.fm2_left_souce_music, text='选择bmg源目录:', bg=bg_color, fg='#aaaaaa', font=('微软雅黑', 12), width='12', command=self.save_dir) self.musicDirBtn.pack(side=LEFT) self.musicDirEntry.pack(side=LEFT, fill='y') self.fm2_left_souce_music.pack(side=TOP, fill='x') self.dstEntry = Entry(self.fm2_left_dst, font=('微软雅黑', 14), width='72', fg='#ffffff', bg=bg_color, bd=1) self.dstEntry.config(insertbackground='#ffffff') self.dstBtn = Button(self.fm2_left_dst, text='选择输出目录:', bg=bg_color, fg='#aaaaaa', font=('微软雅黑', 12), width='12', command=self.download_url) self.dstBtn.pack(side=LEFT) self.dstEntry.pack(side=LEFT, fill='y') self.fm2_left_dst.pack(side=TOP, pady=10, fill='x') # fm3 任务数状态 self.fm3 = Frame(self, bg=bg_color, height=6) self.fm3.pack(side=TOP, fill="x") self.totalLabel = Label(self.fm3, width=10, text="视频总数:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.totalLabel.pack(side=LEFT, fill='y', padx=20) self.totalBgmLabel = Label(self.fm3, width=10, text="bgm总数:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.totalBgmLabel.pack(side=LEFT, fill='y', padx=20) self.successLabel = Label(self.fm3, width=10, text="已完成:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.successLabel.pack(side=LEFT, fill='y', padx=20) self.failLabel = Label(self.fm3, width=10, text="已失败:0", font=('微软雅黑', 12), fg="white", bg=bg_color) self.failLabel.pack(side=LEFT, fill='y', padx=20) # fm4 self.fm4 = Frame(self, bg=bg_color) self.fm4.pack(side=TOP, expand=YES, fill="both") self.logLabel = Label(self.fm4, anchor='w', wraplength=self.ui_width - 40, text="", font=('微软雅黑', 12), fg="white", bg=bg_color) self.logLabel.pack(side=TOP, fill='both', padx=20) # 注册回调 Editors.func_ui_print = self.func_ui_print # 判断是否有网络 if Editors.get_beijing_time() == 0: self.output("获取数据异常,请检查您的网络!") else: Editors.print_hint() def save_dir(self): path = askdirectory() self.set_dir(path) def save_file(self): path = askopenfilename() self.set_dir(path) def set_dir(self, ffmpeg=None, video=None, music=None, dst=None): if ffmpeg: self.ffmFileEntry.delete(0, END) self.ffmFileEntry.insert(0, ffmpeg) if video: self.videoDirEntry.delete(0, END) self.videoDirEntry.insert(0, video) if music: self.musicDirEntry.delete(0, END) self.musicDirEntry.insert(0, music) if dst: self.dstEntry.delete(0, END) self.dstEntry.insert(0, dst) def download_url(self): ground_truth = '' self.dstEntry.delete(0, END) self.dstEntry.insert(0, ground_truth) def output(self, txt): self.logLabel.config(text=txt) def func_ui_print(self, txt, print_type: PrintType = None): if print_type == PrintType.log: self.logLabel.config(text=txt) elif print_type == PrintType.total: self.totalLabel.config(text=txt) elif print_type == PrintType.bgm: self.totalBgmLabel.config(text=txt) elif print_type == PrintType.success: self.successLabel.config(text=txt) elif print_type == PrintType.failed: self.failLabel.config(text=txt) def start_download(self): # 判断是否有网络,去掉 # if Editors.get_beijing_time() == 0: # self.output("获取数据异常,请检查您的网络!") # return if Editors.is_expired(): self.output("授权证书已到期,请联系客服!") return ffmpeg = self.ffmFileEntry.get() video = self.videoDirEntry.get() music = self.musicDirEntry.get() dst = self.dstEntry.get() editors: Editors = ListCut() editor = threading.Thread(target=editors.start, args=(ffmpeg, video, music, dst)) editor.setDaemon(True) # 设置守护进程,避免界面卡死 editor.start() ================================================ FILE: 007-CutVideoAudio/utils.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:工具类 @Date :2021/08/16 @Author :xhunmon @Mail :xhunmon@gmail.com """ import configparser import os import re import threading def get_domain(url: str = None): """ 获取链接地址的域名 :param url: :return: """ # http://youtube.com/watch return re.match(r"(http://|https://).*?\/", url, re.DOTALL).group(0) def get_real_files(folder): ''' 获取真实文件,去掉(.xxx)等文隐藏文件 :param files: :return: ''' files = next(os.walk(folder))[2] results = [] for f in files: name, ext = os.path.splitext(f) if len(name) > 0 and len(ext): results.append(f) return results def format_num(num): if num < 10: return '00{}'.format(num) elif num < 100: return '0{}'.format(num) else: return '{}'.format(num) class Config(object): """ 配置文件的单例类 """ _instance_lock = threading.Lock() def __init__(self): parent_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(parent_dir, 'config.ini') self.conf = configparser.ConfigParser() self.conf.read(conf_path, encoding="utf-8") @classmethod def instance(cls, *args, **kwargs): with Config._instance_lock: if not hasattr(Config, "_instance"): Config._instance = Config(*args, **kwargs) return Config._instance def get_expired_time(self): return self.conf.get("common", "expired_time") def get_version_name(self): return self.conf.get("common", "version_name") def get_version_code(self): return self.conf.get("common", "version_code") ================================================ FILE: 008-ChatGPT-UI/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .idea/ ================================================ FILE: 008-ChatGPT-UI/README.md ================================================ # GPT-UI 2.0 移动☞: [iMedia](https://github.com/xhunmon/iMedia) - UI大优化 - 支持图片生成 演示: https://user-images.githubusercontent.com/26396755/234748470-6203534e-9845-4a3b-a512-09c315670175.mp4 ================================================ FILE: 008-ChatGPT-UI/config.ini ================================================ [common] expired_time=2025/12/15 23:59:59 title=GPT-UI version_name=v1.0.1--github/xhunmon version_code=1010 email=xhunmon@126.com ================================================ FILE: 008-ChatGPT-UI/config.json ================================================ { "key": "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", "api_base": "", "model": "gpt-3.5-turbo", "stream": true, "response": true, "folder": "", "repeat": true, "proxy": "socks5://127.0.0.1:7890" } ================================================ FILE: 008-ChatGPT-UI/doc/config.json ================================================ { "api_base": "", "key": "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", "model": "gpt-3.5-turbo", "stream": true, "response": true, "folder": "/Users/Qincji/Desktop/develop/py/opengpt/gptcli/doc/", "repeat": true, "proxy": "socks5://127.0.0.1:7890" } ================================================ FILE: 008-ChatGPT-UI/doc/pyinstaller.sh ================================================ #!/bin/bash pyinstaller --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py #if use --onefile, the build file is small, but star very slow. #pyinstaller --onefile --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py ================================================ FILE: 008-ChatGPT-UI/gpt.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description:gpt.py @Date :2023/03/31 @Author :xhunmon @Mail :xhunmon@126.com """ import time from datetime import datetime from utils import * class Gpt(object): func_ui_print = None def __init__(self, config: Config): self.session = [] self.api_prompt = [] self.update_config(config) self.content = "" self.is_change = False self.is_finish = True gpt_t = threading.Thread(target=self.start) gpt_t.setDaemon(True) gpt_t.start() def update_config(self, config: Config): self.cfg = config self.api_key = self.cfg.api_key self.api_base = self.cfg.api_base self.api_model = self.cfg.model self.api_stream = self.cfg.stream self.api_response = self.cfg.response self.proxy = self.cfg.proxy openai.api_key = self.api_key if self.api_base: openai.api_base = self.api_base openai.proxy = self.proxy def start(self): while True: if self.is_finish: while not self.is_change: time.sleep(0.3) self.print("\nMY:\n{}".format(self.content)) self.print("\nGPT:\n") self.is_change = False self.is_finish = False self.handle_input(self.content) time.sleep(1) def print(self, content): Gpt.func_ui_print(content) def query_openai_stream(self, data: dict) -> str: messages = [] messages.extend(self.api_prompt) messages.extend(data) answer = "" try: response = openai.ChatCompletion.create( model=self.api_model, messages=messages, stream=True) for part in response: finish_reason = part["choices"][0]["finish_reason"] if "content" in part["choices"][0]["delta"]: content = part["choices"][0]["delta"]["content"] answer += content self.print(content) elif finish_reason: pass except KeyboardInterrupt: self.print("Canceled") except openai.error.OpenAIError as e: self.print("OpenAIError:{}".format(e)) answer = "" return answer def content_change(self, content: str): if not content: return if self.content != content: self.content = content self.is_change = True def handle_input(self, content: str): if not content: return self.is_finish = False self.session.append({"role": "user", "content": content}) if self.api_stream: answer = self.query_openai_stream(self.session) else: answer = self.query_openai(self.session) if not answer: self.session.pop() elif self.api_response: self.session.append({"role": "assistant", "content": answer}) if answer: try: if self.cfg.folder and not os.path.exists(self.cfg.folder): os.makedirs(self.cfg.folder) wfile = os.path.join(self.cfg.folder, "gpt.md" if self.cfg.repeat else "gpt_{}.md".format( datetime.now().strftime("%Y%m%d%H%M:%S"))) if self.cfg.repeat: with open(wfile, mode='a', encoding="utf-8") as f: f.write("MY:\n{}\n".format(content)) f.write("\nGPT:\n{}\n\n".format(answer)) f.close() else: with open(wfile, mode='w', encoding="utf-8") as f: f.write("MY:\n{}\n".format(content)) f.write("\nGPT:{}".format(answer)) f.close() except Exception as e: self.print("Write error: {} ".format(e)) self.is_finish = True def query_openai(self, data: dict) -> str: messages = [] messages.extend(self.api_prompt) messages.extend(data) try: response = openai.ChatCompletion.create( model=self.api_model, messages=messages ) content = response["choices"][0]["message"]["content"] self.print(content) return content except openai.error.OpenAIError as e: self.print("OpenAI error: {} ".format(e)) return "" ================================================ FILE: 008-ChatGPT-UI/main.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: main @Date :2023/03/31 @Author :xhunmon @Mail :xhunmon@126.com """ import sys import tkinter as tk from tkinter.filedialog import * from gpt import * from utils import * class EntryWithPlaceholder(tk.Entry): def __init__(self, master=None, placeholder='', **kwargs): super().__init__(master, **kwargs) self.placeholder = placeholder self.placeholder_color = 'grey' self.default_fg_color = self['fg'] self.bind('', self.on_focus_in) self.bind('', self.on_focus_out) self.put_placeholder() def put_placeholder(self): self.insert(0, self.placeholder) self['fg'] = self.placeholder_color def remove_placeholder(self): cur_value = self.get() if cur_value == self.placeholder: self.delete(0, tk.END) self['fg'] = self.default_fg_color def on_focus_in(self, event): self.remove_placeholder() def on_focus_out(self, event): if not self.get(): self.put_placeholder() class Application(tk.Frame): def __init__(self, config: Config, master=None): super().__init__(master) self.cfg = config self.gpt = None self.repeat = False self.master = master self.master.title(ConfigIni.instance().get_title()) self.pack() self.create_widgets() def create_config(self): row = tk.Frame(self) row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) button = tk.Button(row, text="Config", width='7', command=self.click_config) button.pack(side=tk.LEFT, padx=5, pady=5) self.configEntry = EntryWithPlaceholder(row, placeholder=self.cfg.config_path, width=45) self.configEntry.pack(side=tk.LEFT, padx=5, pady=5) button = tk.Button(row, text="Create", width='7', command=self.click_create) button.pack(side=tk.LEFT, padx=5, pady=5) def create_folder(self): row = tk.Frame(self) row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) button = tk.Button(row, text="Folder", width='7', command=self.click_folder) button.pack(side=tk.LEFT, padx=5, pady=5) self.folderEntry = EntryWithPlaceholder(row, placeholder=self.cfg.folder if self.cfg.folder else f'{Config.pre_tips} chat output directory, default current', width=50) self.folderEntry.pack(side=tk.LEFT, padx=5, pady=5) def create_key(self): row = tk.Frame(self) row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) label = tk.Label(row, text=f"Key: ", width='7') label.pack(side=tk.LEFT) self.keyEntry = EntryWithPlaceholder(row, placeholder=self.cfg.api_key if self.cfg.api_key else f'{Config.pre_tips} input key id', width=50) self.keyEntry.pack(side=tk.LEFT, padx=5, pady=5) def create_model(self): row = tk.Frame(self) row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) label = tk.Label(row, text=f"Model: ", width='7') label.pack(side=tk.LEFT) self.modelEntry = EntryWithPlaceholder(row, placeholder=self.cfg.model if self.cfg.model else f'{Config.pre_tips} default gpt-3.5-turbo, or: gpt-4/gpt-4-32k', width=50) self.modelEntry.pack(side=tk.LEFT, padx=5, pady=5) def create_proxy(self): row = tk.Frame(self) row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) label = tk.Label(row, text=f"Proxy: ", width='7') label.pack(side=tk.LEFT) self.proxyEntry = EntryWithPlaceholder(row, placeholder=self.cfg.proxy if self.cfg.proxy else f'{Config.pre_tips} default empty, or http/https/socks4a/socks5', width=50) self.proxyEntry.pack(side=tk.LEFT, padx=5, pady=5) def create_send(self): row = tk.Frame(self) row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) self.sendEntry = EntryWithPlaceholder(row, placeholder=f'{Config.pre_tips} say something, then click send.', width=55) self.sendEntry.pack(side=tk.LEFT, padx=5, pady=5) self.sendEntry.bind("", self.on_return_key) button = tk.Button(row, text="Send", width='7', command=self.click_send) button.pack(side=tk.LEFT, padx=5, pady=5) def create_widgets(self): self.create_config() self.create_folder() self.create_key() self.create_model() self.create_proxy() self.create_send() # bottom text text_frame = tk.Frame(self) text_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) self.text = tk.Text(text_frame, wrap=tk.WORD, undo=True, font=("Helvetica", 12)) self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll_bar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.text.yview) scroll_bar.pack(side=tk.RIGHT, fill=tk.Y) self.text.config(yscrollcommand=scroll_bar.set) # email text email_button = tk.Button(self, text=ConfigIni.instance().get_email()) email_button.pack(side=tk.LEFT, padx=5, pady=5) # version text version_button = tk.Button(self, text=ConfigIni.instance().get_version_name()) version_button.pack(side=tk.RIGHT, padx=5, pady=5) # clear text clear_button = tk.Button(self, text="clear", width=10, command=self.clear) clear_button.pack(side=tk.RIGHT, padx=5, pady=5) # copy text copy_button = tk.Button(self, text="copy", width=10, command=self.copy) copy_button.pack(side=tk.RIGHT, padx=5, pady=5) Gpt.func_ui_print = self.func_ui_print def refresh(self): # self.set_entry(self.configEntry, self.cfg.default) self.set_entry(self.folderEntry, self.cfg.folder) self.set_entry(self.keyEntry, self.cfg.api_key) self.set_entry(self.modelEntry, self.cfg.model) self.set_entry(self.proxyEntry, self.cfg.proxy) def func_ui_print(self, txt): self.show_text(txt) def click_config(self): path = askopenfilename() self.set_entry(self.configEntry, path) if self.cfg.update(path): self.refresh() else: self.show_text("update fail !") def click_create(self): self.cfg.click_create() self.show_text("create file :{} ".format(self.cfg.config_path)) def click_folder(self): path = askdirectory() self.set_entry(self.folderEntry, path) def set_entry(self, entry: tk.Entry, content): entry.delete(0, tk.END) entry.insert(0, content) def on_return_key(self, event): self.click_send() def click_send(self): # config = self.configEntry.get() self.cfg.update_by_content(self.keyEntry.get(), self.modelEntry.get(), self.folderEntry.get(), self.proxyEntry.get()) content: str = self.sendEntry.get() # self.show_text("me: {}\n".format(content)) if not self.gpt: self.gpt: Gpt = Gpt(self.cfg) else: self.gpt.update_config(self.cfg) self.gpt.content_change(content) def show_text(self, content): self.text.insert(tk.END, "{}".format(content)) self.text.yview_moveto(1.0) # auto scroll to new def clear(self): self.text.delete("1.0", "end") def copy(self): self.master.clipboard_clear() self.master.clipboard_append(self.text.get("1.0", tk.END)) if __name__ == "__main__": root = tk.Tk() folder = os.path.dirname(os.path.realpath(sys.argv[0])) app = Application(Config(folder), master=root) app.mainloop() ================================================ FILE: 008-ChatGPT-UI/requirements.txt ================================================ openai requests[socks] tkinter ================================================ FILE: 008-ChatGPT-UI/utils.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ @Description: tool @Date :2023/03/31 @Author :xhunmon @Mail :xhunmon@126.com """ import configparser import json import os import platform import re import threading import openai def get_domain(url: str = None): # http://youtube.com/watch return re.match(r"(http://|https://).*?\/", url, re.DOTALL).group(0) class ConfigIni(object): _instance_lock = threading.Lock() def __init__(self): parent_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(parent_dir, 'config.ini') self.conf = configparser.ConfigParser() self.conf.read(conf_path, encoding="utf-8") @classmethod def instance(cls, *args, **kwargs): with ConfigIni._instance_lock: if not hasattr(ConfigIni, "_instance"): ConfigIni._instance = ConfigIni(*args, **kwargs) return ConfigIni._instance def get_expired_time(self): return self.conf.get("common", "expired_time") def get_version_name(self): return self.conf.get("common", "version_name") def get_version_code(self): return self.conf.get("common", "version_code") def get_title(self): return self.conf.get("common", "title") def get_email(self): return self.conf.get("common", "email") class Config: sep = "" pre_tips = "Tips:" # baseDir = os.path.dirname(os.path.realpath(sys.argv[0])) base_dir = '' md_sep = '\n\n' + '-' * 10 + '\n' encodings = ["utf8", "gbk"] api_key = "" api_base = "" model = "" prompt = [] stream = True response = False proxy = "" folder = "" config_path = "" repeat = True def __init__(self, dir: str) -> None: self.base_dir = dir if platform.system() == 'Darwin': # MacOS:use pyinstaller pack issue. if '/Contents/MacOS' in dir: # ./GPT-UI.app/Contents/MacOS/ --> ./ app_path = dir.rsplit('/Contents/MacOS')[0] self.base_dir = app_path[:app_path.rindex('/')] self.config_path = os.path.join(self.base_dir, "config.json") self.cfg = {} self.load(self.config_path) def load(self, file): if not os.path.exists(file): return with open(file, "r") as f: self.cfg = json.load(f) c = self.cfg self.api_key = c.get("api_key", c.get("key", openai.api_key)) # compatible with history key self.api_base = c.get("api_base", openai.api_base) self.model = c.get("model", "gpt-3.5-turbo") self.stream = c.get("stream", True) self.response = c.get("response", False) self.proxy = c.get("proxy", "") self.folder = c.get("folder", self.base_dir) self.repeat = c.get("repeat", True) def get(self, key, default=None): return self.cfg.get(key, default) def click_create(self): results = { "key": "", "api_base": "", "model": "gpt-3.5-turbo", "stream": True, "response": True, "folder": "", "repeat": False, "proxy": "", "prompt": [] } self.write_json(results, self.config_path) def write_json(self, content, file_path): path, file_name = os.path.split(file_path) if path and not os.path.exists(path): os.makedirs(path) with open(file_path, 'w') as f: json.dump(content, f, ensure_ascii=False) f.close() def update(self, path: str): if not path.endswith(".json"): return False if path and not os.path.exists(path): return False self.load(path) return True def update_by_content(self, key: str = None, model: str = None, folder: str = None, proxy: str = None): if key and len(key.strip()) > 0 and not key.startswith(Config.pre_tips): self.api_key = key else: self.api_key = '' if model and len(model.strip()) > 0 and not model.startswith(Config.pre_tips): self.model = model else: self.model = 'gpt-3.5-turbo' if folder and len(folder.strip()) > 0 and not folder.startswith(Config.pre_tips): self.folder = folder else: self.folder = self.base_dir if proxy.startswith(Config.pre_tips): self.proxy = None else: self.proxy = proxy if len(proxy.strip()) > 0 else None ================================================ FILE: 009-Translate/README.md ================================================ # 多平台免费翻译神器 ## 1. 输入框翻译 直接在输入框输入内容,选择目标语言,翻译平台即可。 ## 2.文件翻译 - 1.支持txt翻译,但是文本内容不宜过大 - 2.支持html翻译 - 3.定制版srt字幕文件翻译,自定义修改[load_srt.py](load_srt.py) - 4.其他文本文件也是可以的,但是肯定有bug ## 3.打包应用程序 可以自行打包 exe和Mac平台的app。打包脚本参考[doc/pyinstall.sh](doc/pyinstaller.sh),注意window平台的路径反斜杠更改。 > 主要引入:translators ================================================ FILE: 009-Translate/asset/ch.ini ================================================ [Main] Title = Super86翻译 Description = 免费多平台多语言翻译... EnableProxy = 使用代理 Run = 翻译 Clear = 清空 Copy = 复制 File = 选择文件 Settings = 设置 Exit = 退出 Version = 版本 Business = 联系我 Email = 邮箱: RedBook = 小红书: [Settings] Title = 设置 Proxy = 代理 ProxyEnable = 启用 ProxyDesc = 支持http/https/socks5 Theme = 主题 ThemeDesc = 留空使用默认主题 FullTranslate = 开启全翻译(未验证) Advanced = 开启高级功能 Restart = 立刻重启,使所有修改生效 Ok = 确认修改 Cancel = 取消 Reset = 恢复默认 [Loading] Content = 加载中... Cancel = 关闭 [Language] LanguageCH = 简体中文 LanguageCHINESE_CHT = 繁体中文 LanguageEN = 英文 LanguageJAPAN = 日文 LanguageKOREAN = 韩文 LanguageAR = 阿拉伯文 LanguageFRENCH = 法文 LanguageGERMAN = 德文 LanguageRU = 俄罗斯文 LanguageES = 西班牙文 LanguagePT = 葡萄牙文 LanguageIT = 意大利文 LanguageAF = 南非荷兰文 LanguageAZ = 阿塞拜疆文 LanguageBS = 波斯尼亚文 LanguageCS = 捷克文 LanguageCY = 威尔士文 LanguageDA = 丹麦文 LanguageDE = 德文 LanguageET = 爱沙尼亚文 LanguageFR = 法文 LanguageGA = 爱尔兰文 LanguageHR = 克罗地亚文 LanguageHU = 匈牙利文 LanguageID = 印尼文 LanguageIS = 冰岛文 LanguageKU = 库尔德文 LanguageLA = 拉丁文 LanguageLT = 立陶宛文 LanguageLV = 拉脱维亚文 LanguageMI = 毛利文 LanguageMS = 马来文 LanguageMT = 马耳他文 LanguageNL = 荷兰文 LanguageNO = 挪威文 LanguageOC = 欧西坦文 LanguagePI = 巴利文 LanguagePL = 波兰文 LanguageRO = 罗马尼亚文 LanguageRS_LATIN = 塞尔维亚文(latin) LanguageSK = 斯洛伐克文 LanguageSL = 斯洛文尼亚文 LanguageSQ = 阿尔巴尼亚文 LanguageSV = 瑞典文 LanguageSW = 西瓦希里文 LanguageTL = 塔加洛文 LanguageTR = 土耳其文 LanguageUZ = 乌兹别克文 LanguageVI = 越南文 LanguageLATIN = 拉丁文 LanguageFA = 波斯文 LanguageUG = 维吾尔文 LanguageUR = 乌尔都文 LanguageRS_CYRILLIC = 塞尔维亚文(cyrillic) LanguageBE = 白俄罗斯文 LanguageBG = 保加利亚文 LanguageUK = 乌克兰文 LanguageMN = 蒙古文 LanguageABQ = 阿巴扎文 LanguageADY = 阿迪赫文 LanguageKBD = 卡巴尔达文 LanguageAVA = 阿瓦尔文 LanguageDAR = 达尔瓦文 LanguageINH = 因古什文 LanguageCHE = 车臣文 LanguageLBE = 拉克文 LanguageLEZ = 莱兹甘文 LanguageTAB = 塔巴萨兰文 LanguageCYRILLIC = 西里尔文 LanguageHI = 印地文 LanguageMR = 马拉地文 LanguageNE = 尼泊尔文 LanguageBH = 比尔哈文 LanguageMAI = 迈蒂利文 LanguageANG = 昂加文 LanguageBHO = 孟加拉文 LanguageMAH = 摩揭陀文 LanguageSCK = 那格浦尔文 LanguageNEW = 尼瓦尔文 LanguageGOM = 保加利亚文 LanguageSA = 沙特阿拉伯文 LanguageBGC = 哈里亚纳文 LanguageDEVANAGARI = 德瓦那加里文 LanguageTA = 泰米尔文 LanguageKN = 卡纳达文 LanguageTE = 泰卢固文 LanguageKA = 卡纳达文 ================================================ FILE: 009-Translate/asset/config.ini ================================================ [Config] Loading = R0lGODlhoAAYAKEAALy+vOTm5P7+/gAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCQACACwAAAAAoAAYAAAC55SPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvHMgzU9u3cOpDvdu/jNYI1oM+4Q+pygaazKWQAns/oYkqFMrMBqwKb9SbAVDGCXN2G1WV2esjtup3mA5o+18K5dcNdLxXXJ/Ant7d22Jb4FsiXZ9iIGKk4yXgl+DhYqIm5iOcJeOkICikqaUqJavnVWfnpGso6Clsqe2qbirs61qr66hvLOwtcK3xrnIu8e9ar++sczDwMXSx9bJ2MvWzXrPzsHW1HpIQzNG4eRP6DfsSe5L40Iz9PX29/j5+vv8/f7/8PMKDAgf4KAAAh+QQJCQAHACwAAAAAoAAYAIKsqqzU1tTk4uS8urzc3tzk5uS8vrz+/v4D/ni63P4wykmrvTjrzbv/YCiOZGliQKqurHq+cEwBRG3fOAHIfB/TAkJwSBQGd76kEgSsDZ1QIXJJrVpowoF2y7VNF4aweCwZmw3lszitRkfaYbZafnY0B4G8Pj8Q6hwGBYKDgm4QgYSDhg+IiQWLgI6FZZKPlJKQDY2JmVgEeHt6AENfCpuEmQynipeOqWCVr6axrZy1qHZ+oKEBfUeRmLesb7TEwcauwpPItg1YArsGe301pQery4fF2sfcycy44MPezQx3vHmjv5rbjO3A3+Th8uPu3fbxC567odQC1tgsicuGr1zBeQfrwTO4EKGCc+j8AXzH7l5DhRXzXSS4c1EgPY4HIOqR1stLR1nXKKpSCctiRoYvHcbE+GwAAC03u1QDFCaAtJ4D0vj0+RPlT6JEjQ7tuebN0qJKiyYt83SqsyBR/GD1Y82K168htfoZ++QP2LNfn9nAytZJV7RwebSYyyKu3bt48+rdy7ev378NEgAAIfkECQkABQAsAAAAAKAAGACCVFZUtLK05ObkvL68xMLE/v7+AAAAAAAAA/5Yutz+MMpJq7046827/2AojmRpYkCqrqx6vnBMAcRA1LeN74Ds/zGabYgjDnvApBIkLDqNyKV0amkGrtjswBZdDL+1gSRM3hIk5vQQXf6O1WQ0OM2Gbx3CQUC/3ev3NV0KBAKFhoVnEQOHh4kQi4yIaJGSipQCjg+QkZkOm4ydBVZbpKSAA4IFn42TlKEMhK5jl69etLOyEbGceGF+pX1HDruguLyWuY+3usvKyZrNC6PAwYHD0dfP2ccQxKzM2g3ehrWD2KK+v6YBOKmr5MbF4NwP45Xd57D5C/aYvTbqSp1K1a9cgYLxvuELp48hv33mwuUJaEqHO4gHMSKcJ2BvIb1tHeudG8UO2ECQCkU6jPhRnMaXKzNKTJdFC5dhN3LqZKNzp6KePh8BzclzaFGgR3v+C0ONlDUqUKMu1cG0yE2pWKM2AfPkadavS1qIZQG2rNmzaNOqXcu2rdsGCQAAIfkECQkACgAsAAAAAKAAGACDVFZUpKKk1NbUvLq85OLkxMLErKqs3N7cvL685Obk/v7+AAAAAAAAAAAAAAAAAAAABP5QyUmrvTjrzbv/YCiOZGmeaKqubOuCQCzPtCwZeK7v+ev/KkABURgWicYk4HZoOp/QgwFIrYaEgax2ux0sFYYDQUweE8zkqXXNvgAQgYF8TpcHEN/wuEzmE9RtgWxYdYUDd3lNBIZzToATRAiRkxpDk5YFGpKYmwianJQZoJial50Wb3GMc4hMYwMCsbKxA2kWCAm5urmZGbi7ur0Yv8AJwhfEwMe3xbyazcaoBaqrh3iuB7CzsrVijxLJu8sV4cGV0OMUBejPzekT6+6ocNV212BOsAWy+wLdUhbiFXsnQaCydgMRHhTFzldDCoTqtcL3ahs3AWO+KSjnjKE8j9sJQS7EYFDcuY8Q6clBMIClS3uJxGiz2O1PwIcXSpoTaZLnTpI4b6KcgMWAJEMsJ+rJZpGWI2ZDhYYEGrWCzo5Up+YMqiDV0ZZgWcJk0mRmv301NV6N5hPr1qrquMaFC49rREZJ7y2due2fWrl16RYEPFiwgrUED9tV+fLlWHxlBxgwZMtqkcuYP2HO7Gsz52GeL2sOPdqzNGpIrSXa0ydKE42CYr9IxaV2Fr2KWvvxJrv3DyGSggsfjhsNnz4ZfStvUaM5jRs5AvDYIX259evYs2vfzr279+8iIgAAIfkECQkACgAsAAAAAKAAGACDVFZUrKqszMrMvL683N7c5ObklJaUtLK0xMLE5OLk/v7+AAAAAAAAAAAAAAAAAAAABP5QyUmrvTjrzbv/YCiOZGmeaKqubOuCQSzPtCwBeK7v+ev/qgBhSCwaCYEbYoBYNpnOKABIrYaEhqx2u00kFQCm2DkWD6bWtPqCFbjfcLcBqSyT7wj0eq8OJAxxgQIGXjdiBwGIiokBTnoTZktmGpKVA0wal5ZimZuSlJqhmBmilhZtgnBzXwBOAZewsAdijxIIBbi5uAiZurq8pL65wBgDwru9x8QXxsqnBICpb6t1CLOxsrQWzcLL28cF3hW3zhnk3cno5uDiqNKDdGBir9iXs0u1Cue+4hT7v+n4BQS4rlwxds+iCUDghuFCOfFaMblW794ZC/+GUUJYUB2GjMrIOgoUSZCCH4XSqMlbQhFbIyb5uI38yJGmwQsgw228ibHmBHcpI7qqZ89RT57jfB71iFNpUqT+nAJNpTIMS6IDXub5BnVCzn5enUbtaktsWKSoHAqq6kqSyyf5vu5kunRmU7L6zJZFC+0dRFaHGDFSZHRck8MLm3Q6zPDwYsSOSTFurFgy48RgJUCBXNlkX79V7Ry2c5GP6SpYuKjOEpH0nTH5TsteISTBkdtCXZOOPbu3iRrAadzgQVyH7+PIkytfzry58+fQRUQAACH5BAkJAAwALAAAAACgABgAg1RWVKSipMzOzNze3Ly6vNTW1OTm5MTCxKyqrOTi5Ly+vNza3P7+/gAAAAAAAAAAAAT+kMlJq7046827/2AojmRpnmiqrmzrvhUgz3Q9S0iu77wO/8AT4KA4EI3FoxKAGzif0OgAEaz+eljqZBjoer9fApOBGCTM6LM6rbW6V2VptM0AKAKEvH6fDyjGZWdpg2t0b4clZQKLjI0JdFx8kgR+gE4Jk3pPhgxFCp6gGkSgowcan6WoCqepoRmtpRiKC7S1tAJTFHZ4mXqVTWcEAgUFw8YEaJwKBszNzKYZy87N0BjS0wbVF9fT2hbczt4TCAkCtrYCj7p3vb5/TU4ExPPzyGbK2M+n+dmi/OIUDvzblw8gmQHmFhQYoJAhLkjs2lF6dzAYsWH0kCVYwElgQX/+H6MNFBkSg0dsBmfVWngr15YDvNr9qjhA2DyMAuypqwCOGkiUP7sFDTfU54VZLGkVWPBwHS8FBKBKjTrRkhl59OoJ6jjSZNcLJ4W++mohLNGjCFcyvLVTwi6JVeHVLJa1AIEFZ/CVBEu2glmjXveW7YujnFKGC4u5dBtxquO4NLFepHs372DBfglP+KtvLOaAmlUebgkJJtyZcTBhJMZ0QeXFE3p2DgzUc23aYnGftaCoke+2dRpTfYwaTTu8sCUYWc7coIQkzY2wii49GvXq1q6nREMomdPTFOM82Xhu4z1E6BNl4aELJpj3XcITwrsxQX0nnNLrb2Hnk///AMoplwZe9CGnRn77JYiCDQzWgMMOAegQIQ8RKmjhhRhmqOGGHHbo4YcZRAAAIfkECQkADQAsAAAAAKAAGACDVFZUrKqs1NbUvL685ObkxMbE3N7clJaUtLK0xMLE7O7szMrM5OLk/v7+AAAAAAAABP6wyUmrvTjrzbv/YCiOZGmeaKqubOu+VSDPdD1LQK7vvA7/wFPAQCwaj4YALjFIMJ3NpxQQrP4E2KxWSxkevuBwmKFsAJroZxo9oFrfLIFiTq/PBV3DYcHv+/kHSUtraoUJbnCJJ3J8CY2PCngTAQx7f5cHZDhoCAGdn54BT4gTbExsGqeqA00arKtorrCnqa+2rRdyCQy8vbwFkXmWBQvExsULgWUATwGsz88IaKQSCQTX2NcJrtnZ2xkD3djfGOHiBOQX5uLpFIy9BrzxC8GTepeYgmZP0tDR0xbMKbg2EB23ggUNZrCGcFwqghAVliPQUBuGd/HkEWAATJIESv57iOEDpO8ME2f+WEljQq2BtXPtKrzMNjAmhXXYanKD+bCbzlwKdmns1VHYSD/KBiXol3JlGwsvBypgMNVmKYhTLS7EykArhqgUqTKwKkFgWK8VMG5kkLGovWFHk+5r4uwUNFFNWq6bmpWsS4Jd++4MKxgc4LN+owbuavXdULb0PDYAeekYMbkmBzD1h2AUVMCL/ZoTy1d0WNJje4oVa3ojX6qNFSzISMDARgJuP94TORJzs5Ss8B4KeA21xAuKXadeuFi56deFvx5mfVE2W1/z6umGi0zk5ZKcgA8QxfLza+qGCXc9Tlw9Wqjrxb6vIFA++wlyChjTv1/75EpHFXQgQAG+0YVAJ6F84plM0EDBRCqrSCGLLQ7KAkUUDy4UYRTV2eGhZF4g04d3JC1DiBOFAKTIiiRs4WIWwogh4xclpagGIS2xqGMLQ1xnRG1AFmGijVGskeOOSKJgw5I14NDDkzskKeWUVFZp5ZVYZqnllhlEAAAh+QQJCQAMACwAAAAAoAAYAINUVlSkoqTMzszc3ty8urzU1tTk5uTEwsSsqqzk4uS8vrzc2tz+/v4AAAAAAAAAAAAE/pDJSau9OOvNu/9gKI5kaZ5oqq5s674pIM90PUtIru+8Dv/AE+CgOBCNxaMSgBs4n9DoABGs/npY6mQY6Hq/XwKTgRgkzOdEem3WWt+rsjTqZgAUAYJ+z9cHFGNlZ2ZOg4ZOdXCKE0UKjY8YZQKTlJUJdVx9mgR/gYWbe4WJDI9EkBmmqY4HGquuja2qpxgKBra3tqwXkgu9vr0CUxR3eaB7nU1nBAIFzc4FBISjtbi3urTV1q3Zudvc1xcH3AbgFLy/vgKXw3jGx4BNTgTNzPXQT6Pi397Z5RX6/TQArOaPArWAuxII6FVgQIEFD4NhaueOEzwyhOY9cxbtzLRx/gUnDMQVUsJBgvxQogIZacDCXwOACdtyoJg7ZBiV2StQr+NMCiO1rdw3FCGGoN0ynCTZcmHDhhBdrttCkYACq1ivWvRkRuNGaAkWTDXIsqjKo2XRElVrtAICheigSmRnc9NVnHIGzGO2kcACRBaQkhOYNlzhwIcrLBVq4RzUdD/t1NxztTIfvBmf2fPr0cLipGzPGl47ui1i0uZc9nIYledYO1X7WMbclW+zBQs5R5YguCSD3oRR/0sM1Ijx400rKY9MjDLWPpiVGRO7m9Tx67GuG8+u3XeS7izeEkqDps2wybKzbo1XCJ2vNKMWyf+QJUcAH1TB6PdyUdB4NWKpNBFWZ/MVCMQdjiSo4IL9FfJEgGJRB5iBFLpgw4U14IDFfTpwmEOFIIYo4ogklmjiiShSGAEAIfkECQkADQAsAAAAAKAAGACDVFZUrKqs1NbUvL685ObkxMbE3N7clJaUtLK0xMLE7O7szMrM5OLk/v7+AAAAAAAABP6wyUmrvTjrzbv/YCiOZGmeaKqubOu+aSDPdD1LQK7vvA7/wFPAQCwaj4YALjFIMJ3NpxQQrP4E2KxWSxkevuBwmKFsAJroZxo9oFrfLIFiTq/PBV3DYcHv+/kHSUtraoUJbnCJFWxMbBhyfAmRkwp4EwEMe3+bB2Q4aAgBoaOiAU+IE4wDjhmNrqsJGrCzaLKvrBgDBLu8u7EXcgkMw8TDBZV5mgULy83MC4FlAE8Bq9bWCGioEgm9vb+53rzgF7riBOQW5uLpFd0Ku/C+jwoLxAbD+AvIl3qbnILMPMl2DZs2dfESopNFQJ68ha0aKoSIoZvEi+0orOMFL2MDSP4M8OUjwOCYJQmY9iz7ByjgGSbVCq7KxmRbA4vsNODkSLGcuI4Mz3nkllABg3nAFAgbScxkMpZ+og1KQFAmzTYWLMIzanRoA3Nbj/bMWlSsV60NGXQNmtbo2AkgDZAMaYwfSn/PWEoV2KRao2ummthcx/Xo2XhH3XolrNZwULeKdSJurBTDPntMQ+472SDlH2cr974cULUgglNk0yZmsHgXZbWtjb4+TFL22gxgG5P0CElkSJIEnPZTyXKZaGoyVwU+hLC2btpuG59d7Tz267cULF7nXY/uXH12O+Nd+Yy8aFDJB5iqSbaw9Me6sadC7FY+N7HxFzv5C4WepAIAAnjIjHAoZQLVMwcQIM1ApZCCwFU2/RVFLa28IoUts0ChHxRRMBGHHSCG50Ve5QlQgInnubKfKk7YpMiLH2whYxbJiGHjFy5JYY2OargI448sDEGXEQQg4RIjOhLiI5BMCmHDkzTg0MOUOzRp5ZVYZqnlllx26SWTEQAAIfkECQkADAAsAAAAAKAAGACDVFZUpKKkzM7M3N7cvLq81NbU5ObkxMLErKqs5OLkvL683Nrc/v7+AAAAAAAAAAAABP6QyUmrvTjrzbv/YCiOZGmeaKqubOu+cAfMdG3TEqLvfL/HwCAJcFAcikcjcgnIDZ7QqHSAEFpfvmx1Qgx4v2AwoclADBLnNHqt3l7fKfNU6mYAFAGCfs/XBxRkZmhqhGx1cCZGCoqMGkWMjwcYZgKVlpcJdV19nAR/gU8JnXtQhwyQi4+OqaxGGq2RCq8GtLW0khkKtra4FpQLwMHAAlQUd3mje59OaAQCBQXP0gRpprq7t7PYBr0X19jdFgfb3NrgkwMCwsICmcZ4ycqATk8E0Pf31GfW5OEV37v8URi3TeAEgLwc9ZuUQN2CAgMeRiSmCV48T/PKpLEnDdozav4JFpgieC4DyYDmUJpcuLIgOocRIT5sp+kAsnjLNDbDh4/AAjT8XLYsieFkwlwsiyat8KsAsIjDinGxqIBA1atWMYI644xnNAIhpQ5cKo5sBaO1DEpAm22oSl8NgUF0CpHiu5vJcsoZYO/eM2g+gVpAmFahUKWHvZkdm5jCr3XD3E1FhrWyVmZ8o+H7+FPsBLbl3B5FTPQCaLUMTr+UOHdANM+bLuoN1dXjAnWBPUsg3Jb0W9OLPx8ZTvwV8eMvLymXLOGYHstYZ4eM13nk8eK5rg83rh31FQRswoetiHfU7Cgh1yUYZAqR+w9adAT4MTmMfS8ZBan5uX79gmrvBS4YBBGLFGjggfmFckZnITUIoIAQunDDhDbkwMN88mkR4YYcdujhhyCGKOKIKkQAACH5BAkJAA0ALAAAAACgABgAg1RWVKyqrNTW1Ly+vOTm5MTGxNze3JSWlLSytMTCxOzu7MzKzOTi5P7+/gAAAAAAAAT+sMlJq7046827/2AojmRpnmiqrmzrvnAXzHRt0xKg73y/x8AgKWAoGo9IQyCXGCSaTyd0ChBaX4KsdrulEA/gsFjMWDYAzjRUnR5Ur3CVQEGv2+kCr+Gw6Pv/fQdKTGxrhglvcShtTW0ajZADThhzfQmWmAp5EwEMfICgB2U5aQgBpqinAVCJE4ySjY+ws5MZtJEaAwS7vLsJub29vxdzCQzHyMcFmnqfCwV90NELgmYAUAGS2toIaa0SCcG8wxi64gTkF+bi6RbhCrvwvsDy8uiUCgvHBvvHC8yc9kwDFWjUmVLbtnVr8q2BuXrzbBGAGBHDu3jjgAWD165CuI3+94gpMIbMAAEGBv5tktDJGcFAg85ga6PQm7tzIS2K46ixF88MH+EpYFBRXTwGQ4tSqIQymTKALAVKI1igGqEE3RJKWujm5sSJSBl0pPAQrFKPGJPmNHo06dgJxsy6xUfSpF0Gy1Y2+DLwmV+Y1tJk0zpglZOG64bOBXrU7FsJicOu9To07MieipG+/aePqNO8Xjy9/GtVppOsWhGwonwM7GOHuyxrpncs8+uHksU+OhpWt0h9/OyeBB2Qz9S/fkpfczJY6yqG7jxnnozWbNjXcZNe331y+u3YSYe+Zdp6HwGVzfpOg6YcIWHDiCzoyrxdIli13+8TpU72SSMpAzx9EgUj4ylQwIEIQnMgVHuJ9sdxgF11SiqpRNHQGgA2IeAsU+QSSRSvXTHHHSTqxReECgpQVUxoHKKGf4cpImMJXNSoRTNj5AgGi4a8wmFDMwbZQifBHUGAXUUcGViPIBoCpJBQonDDlDbk4MOVPESp5ZZcdunll2CGKaYKEQAAIfkECQkADAAsAAAAAKAAGACDVFZUpKKkzM7M3N7cvLq81NbU5ObkxMLErKqs5OLkvL683Nrc/v7+AAAAAAAAAAAABP6QyUmrvTjrzbv/YCiOZGmeaKqubOu+cAzMdG3TEqLvfL/HwCAJcFAcikcjcgnIDZ7QqHSAEFpfvmx1Qgx4v2AwoclADBLnNHqt3l7fKfNU6mYAFAGCfs/XBxRkZmxsaml1cBJGCoqMGkWMjwcai5GUChhmApqbmwVUFF19ogR/gU8Jo3tQhwyQlpcZlZCTBrW2tZIZCre3uRi7vLiYAwILxsfGAgl1d3mpe6VOaAQCBQXV1wUEhhbAwb4X3rzgFgfBwrrnBuQV5ufsTsXIxwKfXHjP0IBOTwTW//+2nWElrhetdwe/OVIHb0JBWw0RJJC3wFPFBfWYHXCWL1qZNP7+sInclmABK3cKYzFciFBlSwwoxw0rZrHiAIzLQOHLR2rfx2kArRUTaI/CQ3QwV6Z7eSGmQZcpLWQ6VhNjUTs7CSjQynVrT1NnqGX7J4DAmpNKkzItl7ZpW7ZrJ0ikedOmVY0cR231KGeAv6DWCCxAQ/BtO8NGEU9wCpFl1ApTjdW8lvMex62Y+fAFOXaswMqJ41JgjNSt6MWKJZBeN3OexYw68/LJvDkstqCCCcN9vFtmrCPAg08KTnw4ceAzOSkHbWfjnsx9NpfMN/hqouPIdWE/gmiFxDMLCpW82kxU5r0++4IvOa8k8+7wP2jxETuMfS/pxQ92n8C99fgAsipAxCIEFmhgfmmAd4Z71f0X4IMn3CChDTloEYAWEGao4YYcdujhhyB2GAEAIfkECQkADQAsAAAAAKAAGACDVFZUrKqs1NbUvL685ObkxMbE3N7clJaUtLK0xMLE7O7szMrM5OLk/v7+AAAAAAAABP6wyUmrvTjrzbv/YCiOZGmeaKqubOu+cBzMdG3TEqDvfL/HwCApYCgaj0hDIJcYJJpPJ3QKEFpfgqx2u6UQD+CwWMxYNgDONFSdHlSvcJVAQa/b6QKv4bDo+/99B0pMbGuGCW9xFG1NbRqNkANOGpKRaRhzfQmanAp5EwEMfICkB2U5aQgBqqyrAVCJE4yVko+0jJQEuru6Cbm8u74ZA8DBmAoJDMrLygWeeqMFC9LT1QuCZgBQAZLd3QhpsRIJxb2/xcIY5Aq67ObDBO7uBOkX6+3GF5nLBsr9C89A7SEFqICpbKm8eQPXRFwDYvHw0cslLx8GiLzY1bNADpjGc/67PupTsIBBP38EGDj7JCEUH2oErw06s63NwnAcy03M0DHjTnX4FDB4d7EdA6FE7QUd+rPCnGQol62EFvMPNkIJwCmUxNBNzohChW6sAJEd0qYWMIYdOpZCsnhDkbaVFfIo22MlDaQ02Sxgy4HW+sCUibAJt60DXjlxqNYu2godkcp9ZNQusnNrL8MTapnB3Kf89hoAyLKBy4J+qF2l6UTrVgSwvnKGO1cCxM6ai8JF6pkyXLu9ecYdavczyah6Vfo1PXCwNWmrtTk5vPVVQ47E1z52azSlWN+dt9P1Prz2Q6NnjUNdtneqwGipBcA8QKDwANcKFSNKu1vZd3j9JYOV1hONSDHAI1EwYl6CU0xyAUDTFCDhhNIsdxpq08gX3TYItNJKFA6tYWATCNIyhSIrzHHHiqV9EZhg8kE3ExqHqEHgYijmOAIXPGoBzRhAgjGjIbOY6JCOSK5ABF9IEFCEk0XYV2MUsSVpJQs3ZGlDDj50ycOVYIYp5phklmnmmWRGAAAh+QQJCQAMACwAAAAAoAAYAINUVlSkoqTMzszc3ty8urzU1tTk5uTEwsSsqqzk4uS8vrzc2tz+/v4AAAAAAAAAAAAE/pDJSau9OOvNu/9gKI5kaZ5oqq5s675wTAJ0bd+1hOx87/OyoDAEOCgORuQxyQToBtCodDpADK+tn9Y6KQa+4HCY4GQgBgl0OrFuo7nY+OlMncIZAEWAwO/7+QEKZWdpaFCFiFB3JkcKjY8aRo+SBxqOlJcKlpiQF2cCoKGiCXdef6cEgYOHqH2HiwyTmZoZCga3uLeVtbm5uxi2vbqWwsOeAwILysvKAlUUeXutfao6hQQF2drZBIawwcK/FwfFBuIW4L3nFeTF6xTt4RifzMwCpNB609SCT2nYAgoEHNhNkYV46oi5i1Tu3YR0vhTK85QgmbICAxZgdFbqgLR9/tXMRMG2TVu3NN8aMlyYAWHEliphsrRAD+PFjPdK6duXqp/IfwKDZhNAIMECfBUg4nIoQakxDC6XrpwINSZNZMtsNnvWZacCAl/Dgu25Cg3JkgUIHOUKz+o4twfhspPbdmYFBBVvasTJFo9HnmT9DSAQUFthtSjR0X24WELUp2/txpU8gd6CjFlz5pMmtnNgkVDOBlwQEHFfx40ZPDY3NaFMqpFhU6i51ybHzYBDEhosVCDpokdTUoaHpLjxTcaP10quHBjz4vOQiZqOVIKpsZ6/6mY1bS2s59DliJ+9xhAbNJd1fpy2Pc1lo/XYpB9PP4SWAD82i9n/xScdQ2qwMiGfN/UV+EIRjiSo4IL+AVjIURCWB4uBFJaAw4U36LDFDvj5UOGHIIYo4ogklmgiChEAACH5BAkJAA0ALAAAAACgABgAg1RWVKyqrNTW1Ly+vOTm5MTGxNze3JSWlLSytMTCxOzu7MzKzOTi5P7+/gAAAAAAAAT+sMlJq7046827/2AojmRpnmiqrmzrvnBMBnRt37UE7Hzv87KgMBQwGI/IpCGgSwwSTugzSgUMry2BdsvlUoqHsHg8ZjAbgKc6ulYPrNg4SqCo2+91wddwWPj/gH4HS01tbIcJcChuTm4ajZADTxqSkWqUlo0YdH4JnZ8KehMBDH2BpwdmOmoIAa2vrgFRihOMlZKUBLq7ugm5vLu+GQPAwb/FwhZ0CQzNzs0FoXumBQvV13+DZwBRAZLf3whqtBIJxb2PBAq66+jD6uzGGebt7QTJF+bw+/gUnM4GmgVcIG0Un1OBCqTaxgocOHFOyDUgtq9dvwoUea27SEGfxnv+x3ZtDMmLY4N/AQUSYBBNlARSfaohFEQITTc3D8dZ8AjMZLl4Chi4w0AxaNCh+YAKBTlPaVCTywCuhFbw5cGZ2WpyeyLOoSSIb3Y6ZeBzokgGR8syUyc07TGjQssWbRt3k4IFDAxMTdlymh+ZgGRqW+XEm9cBsp5IzAiXKQZ9QdGilXvWKOXIcNXqkiwZqgJmKgUSdNkA5inANLdF6eoVwSyxbOlSZnuUbLrYkdXSXfk0F1y3F/7lXamXZdXSB1FbW75gsM0nhr3KirhTqGTgjzc3ni2Z7ezGjvMt7R7e3+dn1o2TBvO3/Z9qztM4Ye0wcSILxOB2xiSlkpNH/UF7olYkUsgFhYD/BXdXAQw2yOBoX5SCUAECUKiQVt0gAAssUkjExhSXyCGieXiUuF5ygS0Hn1aGIFKgRCPGuEEXNG4xDRk4hoGhIbfccp+MQLpQRF55HUGAXkgawdAhIBaoWJBQroDDlDfo8MOVPUSp5ZZcdunll2CGiUIEACH5BAkJAAwALAAAAACgABgAg1RWVKSipMzOzNze3Ly6vNTW1OTm5MTCxKyqrOTi5Ly+vNza3P7+/gAAAAAAAAAAAAT+kMlJq7046827/2AojmRpnmiqrmzrvnAsW0Bt37gtIXzv/72ZcOgBHBSHYxKpbAJ2g6h0Sh0giNgVcHudGAPgsFhMeDIQg0R6nVC30+pudl5CV6lyBkARIPj/gH4BCmZoamxRh4p5EkgKjpAaR5CTBxqPlZgKl5mRGZ2VGGgCpKWmCXlfgasEg4WJrH9SjAwKBre4t5YZtrm4uxi9vgbAF8K+xRbHuckTowvQ0dACVhR7fbF/rlBqBAUCBd/hAgRrtAfDupfpxJLszRTo6fATy7+iAwLS0gKo1nzZtBGCEsVbuIPhysVR9s7dvHUPeTX8NNHCM2gFBiwosIBaKoD+AVsNPLPGGzhx4MqlOVfxgrxh9CS8ROYQZk2aFxAk0JcRo0aP1g5gC7iNZLeDPBOmWUDLnjqKETHMZHaTKlSbOfNF6znNnxeQBBSEHStW5Ks0BE6K+6bSa7yWFqbeu4pTKtwKcp9a1LpRY0+gX4eyElvUzgCTCBMmWFCtgtN2dK3ajery7lvKFHTq27cRsARVfsSKBlS4ZOKDBBYsxGt5Ql7Ik7HGrlsZszOtPbn2+ygY0OjSaNWCS6m6cbwkyJNzSq6cF/PmwZ4jXy4dn6nrnvWAHR2o9OKAxWnRGd/BUHE3iYzrEbpqNOGRhqPsW3xePPn7orj8+Demfxj4bLQwIeBibYSH34Et7PHIggw2COAaUxBYXBT2IWhhCDlkiMMO+nFx4YcghijiiCSWGGIEACH5BAkJAA0ALAAAAACgABgAg1RWVKyqrNTW1Ly+vOTm5MTGxNze3JSWlLSytMTCxOzu7MzKzOTi5P7+/gAAAAAAAAT+sMlJq7046827/2AojmRpnmiqrmzrvnAsW0Ft37gtAXzv/72ZcOgJGI7IpNIQ2CUGiWcUKq0CiNiVYMvtdinGg3hMJjOaDQB0LWWvB9es3CRQ2O94uwBsOCz+gIF/B0xObm2ICXEUb09vGo6RA1Aak5JrlZeOkJadlBd1fwmipAp7EwEMfoKsB2c7awgBsrSzAVKLEwMEvL28CZW+vsAZu8K/wccExBjGx8wVdQkM1NXUBaZ8qwsFf93cg4VpUgGT5uYIa7kSCQQKvO/Ixe7wvdAW7fHxy5D19Pzz9NnDEIqaAYPUFmRD1ccbK0CE0ACQku4cOnUWnPV6d69CO2H+HJP5CjlPWUcKH0cCtCDNmgECDAwoPCUh1baH4SSuKWdxUron6xp8fKeAgbxm8BgUPXphqDujK5vWK1r0pK6pUK0qXBDT2rWFNRt+wxnRUIKKPX/CybhRqVGr7IwuXQq3gTOqb5PNzZthqFy+LBVwjUng5UFsNBuEcQio27ey46CUc3TuFpSgft0qqHtXM+enmhnU/ejW7WeYeDcTFPzSKwPEYFThDARZzRO0FhHgYvt0qeh+oIv+7vsX9XCkqQFLfWrcakHChgnM1AbOoeOcZnn2tKwIH6/QUXm7fXoaL1N8UMeHr2DM/HoJLV3LBKu44exutWP1nHQLaMYolE1+AckUjYwmyRScAWiJgH0dSAUGWxUg4YSO0WdTdeCMtUBt5CAgiy207DbHiCLUkceJiS2GUwECFHAAATolgqAbQZFoYwZe5MiFNmX0KIY4Ex3SCBs13mikCUbEpERhhiERo5Az+nfklCjkYCUOOwChpQ9Udunll2CGKeaYX0YAACH5BAkJAAsALAAAAACgABgAg1RWVKSipMzOzLy6vNze3MTCxOTm5KyqrNza3Ly+vOTi5P7+/gAAAAAAAAAAAAAAAAT+cMlJq7046827/2AojmRpnmiqrmzrvnAsq0Bt37g977wMFIkCUBgcGgG9pPJyaDqfT8ovQK1arQPkcqs8EL7g8PcgTQQG6LQaHUhoKcFEfK4Bzu0FjRy/T+j5dBmAeHp3fRheAoqLjApkE1NrkgNtbxMJBpmamXkZmJuanRifoAaiF6Sgpxapm6sVraGIBAIItre2AgSPEgBmk2uVFgWlnHrFpnXIrxTExcyXy8rPs7W4twKOZWfAacKw0oLho+Oo5cPn4NRMCtbXCLq8C5HdbG7o6xjOpdAS+6rT+AUEKC5fhUTvcu3aVs+eJQmxjBUUOJGgvnTNME7456paQninCyH9GpCApMmSJb9lNIiP4kWWFTjKqtiR5kwLB9p9jCelALd6KqPBXOnygkyJL4u2tGhUI8KEPEVyQ3nSZFB/GrEO3Zh1wdFkNpE23fr0XdReI4Heiymkrds/bt96iit3FN22cO/mpVuNkd+QaKdWpXqVi2EYXhSIESOPntqHhyOzgELZybYrmKmslcz5sC85oEOL3ty5tJIcqHGYXs26tevXsGMfjgAAIfkECQkACgAsAAAAAKAAGACDlJaUxMbE3N7c7O7svL681NbU5ObkrKqszMrM5OLk/v7+AAAAAAAAAAAAAAAAAAAABP5QyUmrvTjrzbv/YCiOZGmeaKqubOu+cCyrR23fuD3vvHwIwKBwKDj0jshLYclsNik/gHRKpSaMySyyMOh6v90CVABAmM9oM6BoIbjfcA18TpDT3/Z7PaN35+8YXGYBg4UDYhMHCWVpjQBXFgEGBgOTlQZ7GJKUlpOZF5uXl5+RnZyYGqGmpBWqp6wSXAEJtLW0AYdjjAiEvbxqbBUEk8SWsBPDxcZyyst8zZTHEsnKA9IK1MXWgQMItQK04Ai5iWS/jWdrWBTDlQMJ76h87vCUCdcE9PT4+vb89vvk9Ht3TJatBOAS4EIkQdEudMDWTZhlKYE/gRbfxeOXEZ5Fjv4AP2IMKQ9Dvo4buXlDeHChrkIQ1bWx55Egs3ceo92kFW/bM5w98dEMujOnTwsGw7FUSK6hOYi/ZAqrSHSeUZEZZl0tCYpnR66RvNoD20psSiXdDhoQYGAcQwUOz/0ilC4Yu7E58dX0ylGjx757AfsV/JebVnBsbzWF+5TuGV9SKVD0azOrxb1HL5wcem8k0M5WOYP8XDCtrYQuyz2EWVfiNDcB4MSWEzs2bD98CNjejU/3bd92eAPPLXw22gC9kPMitDiu48cFCEXWQl0GFzDY30aBSRey3ergXTgZz0RXlfNSvodfr+UHSyFr47NVz75+jxz4cdjfz7+///8ABgNYXQQAIfkECQkABQAsAAAAAKAAGACCfH58vL685ObkzM7M1NLU/v7+AAAAAAAAA/5Yutz+MMpJq7046827/2AojmRpnmiqrmzrvnAsw0Bt3/es7xZA/MDgDwAJGI9ICXIZUDKPzmczIjVGn1cmxDfoer8E4iMgKJvL0+L5nB6vzW0H+S2IN+ZvOwO/1i/4bFsEA4M/hIUDYnJ0dRIDjH4Kj3SRBZN5jpCZlJuYD1yDX4RdineaVKdqnKirqp6ufUqpDT6hiF2DpXuMA7J0vaxvwLBnw26/vsLJa8YMXLjQuLp/s4utx6/YscHbxHDLgZ+3tl7TCoBmzabI3MXg6e9l6rvs3vJboqOjYfaN7d//0MTz168SOoEBCdJCFMpLrn7zqNXT5i5hxHO8Bl4scE5QQEQADvfZMsdxQACTXU4aVInS5EqUJ106gZnyJUuZVFjGtJKTJk4HoKLpI8mj6I5nDPcRNcqUBo6nNZpKnUq1qtWrWLNq3cq1q1cKCQAAO2ZvZlpFYkliUkxFdG9ZdlpHWWpMU3d6N0VKTDNnVk01aWxQaXBDSXJ2SDMxK3lHMGxMVHJVY0lUU0xvTGdvemw= Logo = iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ4IDc5LjE2NDAzNiwgMjAxOS8wOC8xMy0wMTowNjo1NyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIxLjAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDA1M0ExN0REQzE1MTFFRDkzOTVGQzQ4OTU1NERGRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDA1M0ExN0VEQzE1MTFFRDkzOTVGQzQ4OTU1NERGRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MDUzQTE3QkRDMTUxMUVEOTM5NUZDNDg5NTU0REZFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0MDUzQTE3Q0RDMTUxMUVEOTM5NUZDNDg5NTU0REZFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PojFojMAACEJSURBVHja7F0H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 Version = 1.2.0 Date = 2023.4.23 Email = xhunmon@126.com RedBook = Super86 ================================================ FILE: 009-Translate/asset/en.ini ================================================ [Main] Title = Super86 Translator Description = Free multi-platform and multi-language translation software... EnableProxy = enable proxy Run = Run Clear = Clear Copy = Copy File = File Settings = Settings Exit = Exit Version = Version Business = Contact Me Email = Email: RedBook = RedBook: [Settings] Title = Settings Proxy = Proxy ProxyEnable = Enable ProxyDesc = support http/https/socks5 Theme = Theme ThemeDesc = Leave blank to use global default FullTranslate = full translation (Not verified) Advanced = Use Advanced Interface Restart = Restart app now Ok = Ok Cancel = Cancel Reset = Reset [Loading] Content = Loading... Cancel = Close [Language] LanguageCH = Simplified Chinese LanguageCHINESE_CHT = Traditional Chinese LanguageEN = English LanguageJAPAN = Japanese LanguageKOREAN = Korean LanguageAR = Arabic LanguageFRENCH = French LanguageGERMAN = German LanguageRU = Russian LanguageES = Spanish LanguagePT = Portuguese LanguageIT = Italian LanguageAF = Afrikaans LanguageAZ = Azerbaijani LanguageBS = Bosnian LanguageCS = Czech LanguageCY = Welsh LanguageDA = Danish LanguageDE = German LanguageET = Estonian LanguageFR = French LanguageGA = Irish LanguageHR = Croatian LanguageHU = Hungarian LanguageID = Indonesian LanguageIS = Icelandic LanguageKU = Kurdish LanguageLA = Latin LanguageLT = Lithuanian LanguageLV = Latvian LanguageMI = Maori LanguageMS = Malay LanguageMT = Maltese LanguageNL = Dutch LanguageNO = Norwegian LanguageOC = Occitan LanguagePI = Pali LanguagePL = Polish LanguageRO = Romanian LanguageRS_LATIN = Serbian(latin) LanguageSK = Slovak LanguageSL = Slovenian LanguageSQ = Albanian LanguageSV = Swedish LanguageSW = Swahili LanguageTL = Tagalog LanguageTR = Turkish LanguageUZ = Uzbek LanguageVI = Vietnamese LanguageLATIN = Latin LanguageFA = Persian LanguageUR = Urdu LanguageRS_CYRILLIC = Serbian(cyrillic) LanguageBE = Belarusian LanguageBG = Bulgarian LanguageUK = Ukranian LanguageMN = Mongolian LanguageABQ = Abaza LanguageADY = Adyghe LanguageKBD = Kabardian LanguageAVA = Avar LanguageDAR = Dargwa LanguageINH = Ingush LanguageCHE = Chechen LanguageLBE = Lak LanguageLEZ = Lezghian LanguageTAB = Tabassaran LanguageCYRILLIC = Cyrillic LanguageHI = Hindi LanguageMR = Marathi LanguageNE = Nepali LanguageBH = Bihari LanguageMAI = Maithili LanguageANG = Angika LanguageBHO = Bhojpuri LanguageMAH = Magahi LanguageSCK = Nagpur LanguageNEW = Newari LanguageGOM = Goan Konkani LanguageSA = Saudi Arabia LanguageBGC = Haryanvi LanguageDEVANAGARI = Devanagari LanguageTA = Tamil LanguageKN = Kannada LanguageUG = Uyghur LanguageTE = Telugu LanguageKA = Kannada ================================================ FILE: 009-Translate/asset/language.json ================================================ [ { "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" }, { "en_name": "Chinese(繁体)", "id_name": "zh-CHT", "zh_name": "繁体", "google": "zh-TW", "yandex": "", "bing": "zh-Hant", "baidu": "cht", "alibaba": "zh-TW", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "cnt", "caiyun": "", "argos": "" }, { "en_name": "Chinese(文言文)", "id_name": "wyw", "zh_name": "文言文", "google": "", "yandex": "", "bing": "", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "Chinese(粤语)", "id_name": "yue", "zh_name": "粤语", "google": "", "yandex": "", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "Y", "argos": "" }, { "en_name": "Chinese(内蒙语)", "id_name": "mn", "zh_name": "内蒙语", "google": "", "yandex": "", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "Y", "argos": "" }, { "en_name": "Chinese(维吾尔语)", "id_name": "uy", "zh_name": "维吾尔语", "google": "", "yandex": "", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "Chinese(藏语)", "id_name": "ti", "zh_name": "藏语", "google": "", "yandex": "", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "Chinese(白苗文)", "id_name": "mww", "zh_name": "白苗文", "google": "", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "Chinese(彝语)", "id_name": "ii", "zh_name": "彝语", "google": "", "yandex": "", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "Y", "argos": "" }, { "en_name": "english", "id_name": "en", "zh_name": "英语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "Y", "argos": "Y" }, { "en_name": "arabic", "id_name": "ar", "zh_name": "阿拉伯语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "ara", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "Y" }, { "en_name": "russian", "id_name": "ru", "zh_name": "俄语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "Y", "argos": "Y" }, { "en_name": "french", "id_name": "fr", "zh_name": "法语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "fra", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "Y", "argos": "Y" }, { "en_name": "german", "id_name": "de", "zh_name": "德语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "Y" }, { "en_name": "spanish", "id_name": "es", "zh_name": "西班牙语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "spa", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "Y", "argos": "Y" }, { "en_name": "portuguese", "id_name": "pt", "zh_name": "葡萄牙语", "google": "Y", "yandex": "Y", "bing": "pt", "baidu": "Y", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "Y" }, { "en_name": "italian", "id_name": "it", "zh_name": "意大利语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "Y" }, { "en_name": "japanese", "id_name": "ja", "zh_name": "日语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "jp", "alibaba": "", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "Y", "argos": "Y" }, { "en_name": "korean", "id_name": "ko", "zh_name": "韩语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "kor", "alibaba": "", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "Y" }, { "en_name": "greek", "id_name": "el", "zh_name": "希腊语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "dutch", "id_name": "nl", "zh_name": "荷兰语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "Y", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "hindi", "id_name": "hi", "zh_name": "北印度语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "Y", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "Y" }, { "en_name": "turkish", "id_name": "tr", "zh_name": "土耳其语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "Y", "tencent": "Y", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "Y" }, { "en_name": "malay", "id_name": "ms", "zh_name": "马来西亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "Y", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "thai", "id_name": "th", "zh_name": "泰国语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "Y", "tencent": "Y", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "vietnamese", "id_name": "vi", "zh_name": "越南语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "vie", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "Y" }, { "en_name": "indonesian", "id_name": "id", "zh_name": "印度尼西亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "Y", "tencent": "Y", "youdao": "Y", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "Y" }, { "en_name": "hebrew", "id_name": "he", "zh_name": "希伯来语", "google": "iw", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "polish", "id_name": "pl", "zh_name": "波兰语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "Y" }, { "en_name": "mongolian", "id_name": "mn", "zh_name": "蒙古语", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "czech", "id_name": "cs", "zh_name": "捷克语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "hungarian", "id_name": "hu", "zh_name": "匈牙利语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "Y", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "estonian", "id_name": "et", "zh_name": "爱沙尼亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "est", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "bulgarian", "id_name": "bg", "zh_name": "保加利亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "bul", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "danish", "id_name": "da", "zh_name": "丹麦语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "dan", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "finnish", "id_name": "fi", "zh_name": "芬兰语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "fin", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "romanian", "id_name": "ro", "zh_name": "罗马尼亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "rom", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "swedish", "id_name": "sv", "zh_name": "瑞典语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "swe", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "slovenian", "id_name": "sl", "zh_name": "斯洛文尼亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "slo", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "persian", "id_name": "fa", "zh_name": "波斯语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "bosnian", "id_name": "bs", "zh_name": "波斯尼亚语", "google": "Y", "yandex": "Y", "bing": "bs-Latn", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "bs-Latn", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "serbian", "id_name": "sr", "zh_name": "塞尔维亚语", "google": "Y", "yandex": "Y", "bing": "sr-Latn", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "sr-Latn", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "fijian", "id_name": "fj", "zh_name": "斐济语", "google": "", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "filipino", "id_name": "tl", "zh_name": "菲律宾语", "google": "Y", "yandex": "Y", "bing": "fil", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "fil", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "haitiancreole", "id_name": "ht", "zh_name": "海地克里奥尔语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "catalan", "id_name": "ca", "zh_name": "加泰罗尼亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "croatian", "id_name": "hr", "zh_name": "克罗地亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "latvian", "id_name": "lv", "zh_name": "拉脱维亚语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "lithuanian", "id_name": "lt", "zh_name": "立陶宛语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "urdu", "id_name": "ur", "zh_name": "乌尔都语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "ukrainian", "id_name": "uk", "zh_name": "乌克兰语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "welsh", "id_name": "cy", "zh_name": "威尔士语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "tahiti", "id_name": "ty", "zh_name": "塔希提岛语", "google": "", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "tongan", "id_name": "to", "zh_name": "汤加语", "google": "", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "swahili", "id_name": "sw", "zh_name": "斯瓦希里语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "samoan", "id_name": "sm", "zh_name": "萨摩亚语", "google": "Y", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "slovak", "id_name": "sk", "zh_name": "斯洛伐克", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "Y", "caiyun": "", "argos": "" }, { "en_name": "afrikaans", "id_name": "af", "zh_name": "南非荷兰语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "norwegian", "id_name": "no", "zh_name": "挪威语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "bengali", "id_name": "bn", "zh_name": "孟加拉语", "google": "Y", "yandex": "Y", "bing": "bn-BD", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "malagasy", "id_name": "mg", "zh_name": "马达加斯加语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "maltese", "id_name": "mt", "zh_name": "马耳他语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "queretarootomi", "id_name": "otq", "zh_name": "克雷塔罗托米语", "google": "", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "klingon", "id_name": "tlh", "zh_name": "克林贡语", "google": "", "yandex": "", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "Y", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "gujarati", "id_name": "gu", "zh_name": "古吉拉特语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "tamil", "id_name": "ta", "zh_name": "泰米尔语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "telugu", "id_name": "te", "zh_name": "泰卢固语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "punjabi", "id_name": "pa", "zh_name": "旁遮普语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "amharic", "id_name": "am", "zh_name": "阿姆哈拉语", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "azerbaijani", "id_name": "az", "zh_name": "阿塞拜疆语", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "bashkir", "id_name": "ba", "zh_name": "巴什基尔语", "google": "", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "belarusian", "id_name": "be", "zh_name": "白俄罗斯语", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "cebuano", "id_name": "ceb", "zh_name": "切布亚诺", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "chuvash", "id_name": "cv", "zh_name": "楚瓦什语", "google": "", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "esperanto", "id_name": "eo", "zh_name": "世界语", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "basque", "id_name": "eu", "zh_name": "巴斯克语", "google": "Y", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "irish", "id_name": "ga", "zh_name": "爱尔兰语", "google": "Y", "yandex": "Y", "bing": "Y", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" }, { "en_name": "emoji", "id_name": "emj", "zh_name": "表情", "google": "", "yandex": "Y", "bing": "", "baidu": "", "alibaba": "", "tencent": "", "youdao": "", "sogou": "", "deepl": "", "caiyun": "", "argos": "" } ] ================================================ FILE: 009-Translate/config.py ================================================ # -*- coding: utf-8 -*- """ @Author : Xhunmon @Time : 2023/4/23 08:51 @FileName: config.py @desc: """ import configparser import json import locale import os from utils import * __version__ = '1.0.0' __email__ = 'xhunmon@126.com' __wechet__ = 'VABlog' language, encoding = locale.getdefaultlocale() def is_zh_language(): cache = get_cache(Key.LANGUAGE, None) # if cache == 'zh_CN' or language == 'zh_CN': # return True # else: # return False return True class IniConfig(object): def __init__(self): asset_path = os.path.join(os.path.dirname(__file__), 'asset') language_path = os.path.join(asset_path, 'language.json') config_path = os.path.join(asset_path, 'config.ini') ch_ini = os.path.join(asset_path, 'ch.ini') en_ini = os.path.join(asset_path, 'en.ini') ini = ch_ini if is_zh_language() else en_ini self.language = configparser.ConfigParser() self.language.read(ini, encoding='utf-8') self.cfg = configparser.ConfigParser() self.cfg.read(config_path, encoding='utf-8') with open(language_path, 'r') as f: self.trl = json.load(f) f.close() def full(self, tag, name): return self.language[tag][name] def main(self, name): return self.full('Main', name) def settings(self, name): return self.full('Settings', name) def loading(self, name): return self.full('Loading', name) def language(self, name): return self.full('Language', name) def config(self, name): return self.cfg['Config'][name] def translate(self): return self.trl conf = IniConfig() ================================================ FILE: 009-Translate/core.py ================================================ # -*- coding: utf-8 -*- """ @Author : Xhunmon @Time : 2023/4/22 22:29 @FileName: core.py @desc: """ import translators as ts from config import * from ui import LoadingWin class Language(object): def __init__(self, id_name, full_name, zh_name=None, google=True, yandex=True, bing=True, baidu=True, alibaba=True, tencent=True, youdao=True, sogou=True, deepl=True, caiyun=True, argos=True): self.id_name = id_name self.zh_name = zh_name if zh_name else full_name self.full_name = full_name self.google = google self.yandex = yandex self.bing = bing self.baidu = baidu self.alibaba = alibaba self.tencent = tencent self.youdao = youdao self.sogou = sogou self.deepl = deepl self.caiyun = caiyun self.argos = argos def is_enable(self, translator: str): return eval('self.' + translator) class Translation(object): def __init__(self): self.is_full = get_cache(Key.FULL_TRANSLATE, False) # 显示所有翻译平台和语言,但是未校验 self.is_zh = is_zh_language() # 是否是中文 self.select_translator = 'baidu' self.select_from_lang = '自动' if self.is_zh else 'auto' self.select_to_lang = '英语' if self.is_zh else 'english' self.languages = conf.translate() # self.languages.append(Language('en', 'english', '英语')) # self.languages.append(Language('zh', 'chinese', '中文')) # self.languages.append(Language('ar', 'arabic', '阿拉伯语', deepl=False, caiyun=False)) # self.languages.append(Language('ru', 'russian', '俄语')) # self.languages.append(Language('fr', 'french', '法语')) # self.languages.append(Language('de', 'german', '德语', alibaba=False, caiyun=False)) # self.languages.append(Language('es', 'spanish', '西班牙语')) # self.languages.append(Language('pt', 'portuguese', '葡萄牙语', caiyun=False)) # self.languages.append(Language('it', 'italian', '意大利语', caiyun=False)) # self.languages.append(Language('ja', 'japanese', '日本语', alibaba=False)) # self.languages.append(Language('ko', 'korean', '朝鲜语', alibaba=False, deepl=False, caiyun=False)) # self.languages.append( # Language('el', 'greek', '希腊语', alibaba=False, tencent=False, youdao=False, caiyun=False, argos=False)) def set_to_lang(self, lang): if lang: self.select_to_lang = lang def set_from_lang(self, lang): if lang: self.select_from_lang = lang def check_select_language(self): languages = self.get_languages() has_from = False has_to = False for lg in languages: # lg为full_name if lg == self.select_from_lang: has_from = True if lg == self.select_to_lang: has_to = True if not has_from: self.select_from_lang = '自动' if self.is_zh else 'auto' if not has_to: self.select_to_lang = '英语' if self.is_zh else 'english' def get_translators(self): # 'google', 'yandex', 'bing', 'baidu', 'alibaba', 'tencent', 'youdao', 'sogou', 'deepl', 'caiyun', 'argos', # 'apertium', 'cloudYi', 'elia', 'iciba', 'iflytek', 'iflyrec', 'itranslate', 'judic', 'languageWire', # 'lingvanex', 'niutrans', 'mglip', 'modernMt', 'myMemory', 'papago', 'qqFanyi', 'qqTranSmart', 'reverso', # 'sysTran', 'tilde', 'translateCom', 'translateMe', 'utibet', 'volcEngine', 'yeekit' # {"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"} tors = list(self.languages[0].keys()) tors.remove('en_name') tors.remove('id_name') tors.remove('zh_name') return tors def get_languages(self): support = [] for lg in self.languages: # 取出字典 if lg[self.select_translator] == '': # 不支持 continue if self.is_zh: support.append(lg['zh_name']) else: support.append(lg['en_name']) return support def choose_translator(self, tl): self.select_translator = tl def _search_id_name(self, is_from=True): key = self.select_from_lang if is_from else self.select_to_lang for lg in self.languages: # 取出字典 if self.is_zh: # 查找出 zh_name 对应的 if lg['zh_name'] == key: if lg[self.select_translator] == '': continue elif lg[self.select_translator] == 'Y': return lg['id_name'] else: return lg[self.select_translator] else: if lg['en_name'] == key: if lg[self.select_translator] == '': continue elif lg[self.select_translator] == 'Y': return lg['id_name'] else: return lg[self.select_translator] return None def get_id_name(self, is_from=True): if is_from: from_key = self.select_from_lang if from_key == '自动' or from_key == 'auto': return 'auto' search = self._search_id_name(True) return search if search else 'auto' else: # to_ 目标 search = self._search_id_name(False) return search if search else 'en' # 最后还是没有,默认英语 def translate(self, window, content, is_html=False, file_path: str = None): try: if file_path and file_path.endswith('.srt'): from load_srt import Translator import utils, os filename, ext = os.path.splitext(file_path) out_path = f'{filename}_output{ext}' tl = Translator() proxy = get_cache(Key.PROXY_INPUT, None) if get_cache(Key.PROXY_ENABLE, False) else None if proxy: proxy_real = proxy.replace(' ', '').replace('\n', '') proxy_spit = proxy_real.split('://') proxy_user = {proxy_spit[0]: proxy_real} tl.proxy_user = proxy_user else: tl.proxy_user = None tl.translators = {self.select_translator: 3} tl.translate_file(file_path, out_path, self.get_id_name(is_from=True), self.get_id_name(is_from=False)) window['OUT_TEXT'].update(f'输出文件:{out_path}') else: # ts.preaccelerate() # 是否使用了代理 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 proxy = get_cache(Key.PROXY_INPUT, None) if get_cache(Key.PROXY_ENABLE, False) else None proxy_user = None if proxy: proxy_real = proxy.replace(' ', '').replace('\n', '') proxy_spit = proxy_real.split('://') proxy_user = {proxy_spit[0]: proxy_real} if is_html: # result = ts.translate_html(content, translator=self.select_translator, # from_language=self.get_id_name(is_from=True), # to_language=self.get_id_name(is_from=False), proxies=proxy_user) result = ts.translate_html(content, translator=self.select_translator, from_language=self.get_id_name(is_from=True), to_language=self.get_id_name(is_from=False), proxies=proxy_user, if_ignore_empty_query=True, if_show_time_stat=True) else: result = ts.translate_text(content, translator=self.select_translator, from_language=self.get_id_name(is_from=True), to_language=self.get_id_name(is_from=False), proxies=proxy_user) if file_path is not None: import utils, os filename, ext = os.path.splitext(file_path) out_path = f'{filename}_output{ext}' utils.write(result, out_path) window['OUT_TEXT'].update(f'输出文件:{out_path}') else: window['OUT_TEXT'].update(result) except Exception as e: result = str(e) LoadingWin.is_loading = False ================================================ FILE: 009-Translate/doc/pyinstaller.sh ================================================ #!/bin/bash #pyinstaller --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py pyinstaller --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 #https://blog.csdn.net/COCO56/article/details/117452383 #if use --onefile, the build file is small, but star very slow. #pyinstaller --onefile --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py.py pyinstaller --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 ================================================ FILE: 009-Translate/load_srt.py ================================================ # -*- coding: utf-8 -*- """ @Author : Xhunmon @Time : 2023/4/18 14:47 @FileName: load_srt.py @desc: 从本地加载srt字幕文件 """ import os import threading import time class Tag: def __init__(self): self.num = None self.duration = None self.msg = '' class Translator(object): STATUS_SUCCESS = 1 STATUS_FAIL = -1 def __init__(self): # TODO --> 一定要对应自己的代理,可以为None self.proxy_user = {"socks5": "socks5://127.0.0.1:7890"} # 翻译平台,deepl尝试3次,失败后,用google尝试2次,再失败后,用百度尝试3次,当所有尝试都失败了,表示该次翻译失败 self.translators = {"deepl": 3, "google": 2, "baidu": 3} # 是否支持翻译 self.__supports = ['.srt', '.txt'] # 需翻译的语音,可以为自动:auto self.__from_language = 'zh' # 目标语言 self.__to_language = 'en' # 一次性翻译的字符串数组长度 self.__one_length = 1024 # 某些场景需要休眠时间,再进行尝试 self.__user_sleep = 2 # 监听的事件 self.__listener = None # 需要被翻译的文件路径 self.__list = [] def __callback(self, status, src, dst, msg=None): """ 翻译结果回调 :param status: 1成功,2失败 :param src: 返回需要翻译的路径 :param dst: 返回翻译后的文件路径 :param msg: 返回提示信息 """ if self.__listener: self.__listener(status, src=src, dst=dst, msg=msg) def __support_file(self, src: str): """ 判断源文件是否支持被翻译 :param src: 文件路径 :return: """ for item in self.__supports: if src.endswith(item): return True return False def __deal_file(self): """ 从队列取出文件,进行相关判断和操作 :return: (是否成功,源文件路径,目标文件路径,操作信息,临时文件路径) """ item = self.__list.pop(0) src, dst = item[0], item[1] temp = None if not os.path.exists(src): return False, src, dst, "源文件不存在", temp if not self.__support_file(src): return False, src, dst, "源文件格式不支持", temp # try: # folder = os.path.dirname(src) # filename, ext = os.path.splitext(src) # if dst is None: # dst = os.path.join(folder, '{}_out{}'.format(filename, ext)) # else: # folder = os.path.dirname(dst) # if not os.path.exists(folder): # os.mkdir(folder) # temp = os.path.join(folder, '{}_temp{}'.format(filename, ext)) # except Exception as e: # return False, src, dst, str(e), temp if dst is None or dst == '': folder = os.path.dirname(src) filename, ext = os.path.splitext(src) dst = os.path.join(folder, '{}_out{}'.format(filename, ext)) if os.path.exists(dst): os.remove(dst) return True, src, dst, '成功', temp def __parse_srt(self, src: str): """ 获取srt文件内容,一行一行的 :param src: 路径 :return: [Line,Line...] """ i = 0 # 遇到空一行方为一组 tags = [] tag = Tag() with open(src, 'r', encoding="utf-8") as f: for line in f: line = line.strip().replace('\n', '') if line == '': # 结束了 tags.append(tag) i = 0 tag = Tag() continue if i == 0: tag.num = line elif i == 1: tag.duration = line else: tag.msg += line i += 1 return tags def __parse_file(self, src: str): """ 一行一行获取文件内容 :param src: 文件路径 :return: 返回[Line, Line...] """ if src.endswith('.srt'): return self.__parse_srt(src) return [] def __merge_content(self, tags): """ 合并需要翻译的内容,为了避免请求其次太多,将整个文件的内容进行分组翻译 :param tags: :return: """ result = [] item = '' for tag in tags: if item == '': item = tag.msg else: item = item + '\n' + tag.msg if len(item) > self.__one_length: # 开启下一组 result.append(item) item = '' if item != '': # 最后一组数据 result.append(item) return result def __request_item(self, content): """ 真正的请求网络进行翻译 :param content: 翻译内容 :return: 是否成功,翻译后的内容 """ for key, value in self.translators.items(): for i in range(1, value + 1): # 每个失败后尝试的次数 try: print('使用 {} 翻译,进行次数{}'.format(key, i)) import translators as ts item = ts.translate_text(content, translator=key, from_language=self.__from_language, to_language=self.__to_language, proxies=self.proxy_user) return True, item except Exception as e: print(e) time.sleep(self.__user_sleep) return False, '翻译失败' def __request_list(self, contents): """ 拆分列表中的数据 :param contents: :return: """ results = [] for content in contents: success, item = self.__request_item(content) if not success: return False, results for x in item.split('\n'): results.append(x) return True, results def __merge_file(self, tags, items, dst): with open(dst, 'w', encoding="utf-8") as f: for tag in tags: f.write(f'{tag.num}\n') f.write(f'{tag.duration}\n') f.write(f'{items.pop(0)}\n') f.write('\n') def __translate(self): """ 子线程不断监听翻译文件,进行翻译 """ success, src, dst, msg, temp = self.__deal_file() if not success: self.__callback(Translator.STATUS_FAIL, src=src, dst=dst, msg=msg) return # 解析得到一行行数据 tags = self.__parse_file(src) if len(tags) <= 0: self.__callback(Translator.STATUS_FAIL, src=src, dst=dst, msg="解析文件内容失败") return # 将一行行待翻译的文件进行合并,减少翻译次数 contents = self.__merge_content(tags) success, results = self.__request_list(contents) if not success: # # 缓存已翻译的数据 # if len(result) > 0: # self.__merge_file(tags, result, temp) self.__callback(Translator.STATUS_FAIL, src=src, dst=dst, msg='翻译失败') return self.__merge_file(tags, results, dst) self.__callback(Translator.STATUS_SUCCESS, src=src, dst=dst, msg="成功") def add_callback(self, listener): """ 添加监听器, :param listener: 监听器设计模式如:method_listener(status,**kwargs) :return: """ self.__listener = listener def translate_file(self, src, dst=None, from_lang='zh', to_lang='en'): """ 对外只需要知道传入的文件即可,其余全部在本翻译器处理,较少参数传递 @param dst: 必传,需要进行翻译的文件。 @param src: 如果不传,自动根据src所在的目录生成同后缀名的文件 @param from_lang: 从什么语言翻译 @param to_lang: 翻译目标语言 """ self.__from_language = from_lang self.__to_language = to_lang item = (src, dst) self.__list.append(item) # self.__event.set() # 唤醒线程 self.__translate() ================================================ FILE: 009-Translate/main.py ================================================ # -*- coding: utf-8 -*- """ @Author : Xhunmon @Time : 2023/4/21 23:03 @FileName: guiv2.py @desc: """ import PySimpleGUI as sg from core import * from ui import * from ui import settings_show from utils import * class MainWin(object): def __init__(self): self.tl = Translation() self.lw = LoadingWin() def advanced_ui(self, window): # 高级才显示的UI advanced_mode = get_cache(Key.ADVANCED_MODE, False) window[Key.PROXY_ENABLE].update(visible=advanced_mode) def make_window(self): """ Creates the main window :return: The main window object :rtype: (sg.Window) """ sg.theme(get_theme()) left_col = sg.Column([ [sg.Multiline(size=(50, 25), write_only=True, expand_x=True, expand_y=True, key='IN_TEXT', reroute_stdout=True, echo_stdout_stderr=True, reroute_cprint=True)], [sg.B(conf.main(Key.M_RUN)), sg.B(conf.main(Key.M_CLEAR)), sg.B(conf.main(Key.M_COPY)), sg.Input('', key="_FILEBROWSE_", enable_events=True, visible=False), sg.FileBrowse(conf.main(Key.M_FILE), file_types=(('ALL files', '.*')), target='_FILEBROWSE_')], [sg.CB(conf.main('EnableProxy'), default=get_cache(Key.PROXY_ENABLE, False), font='_ 12', enable_events=True, k=Key.PROXY_ENABLE)]], element_justification='l', expand_x=True, expand_y=True) right_col = [ [sg.Multiline(size=(70, 25), write_only=True, expand_x=True, expand_y=True, key='OUT_TEXT', reroute_stdout=True, echo_stdout_stderr=True, reroute_cprint=True)], [sg.B(conf.main(Key.M_SETTINGS)), sg.Button(conf.main(Key.M_EXIT))], [sg.T(conf.main('Version') + conf.config('Version'))] ] operation_at_bottom = sg.pin(sg.Column([[sg.T(conf.main('Business'), font='Default 12', pad=(0, 0)), sg.T(conf.main('Email') + conf.config('Email') + ' ', font='Default 12', pad=(0, 0)), sg.T(conf.main('RedBook') + conf.config('RedBook') + ' ', font='Default 12', pad=(0, 0))]], pad=(0, 0), k='-OPTIONS BOTTOM-', expand_x=True, expand_y=False), expand_x=True, expand_y=False) self.tl.select_translator = get_cache('PLATFORM_TYPES', 'baidu') self.tl.set_from_lang(get_cache('INPUT_TYPES')) self.tl.set_to_lang(get_cache('OUTPUT_TYPES')) self.tl.check_select_language() lgs1 = self.tl.get_languages() lgs1.insert(0, '自动') if is_zh_language() else lgs1.insert(0, 'auto') choose_type_at_top = sg.pin( # sg.user_settings_get_entry('INPUT_TYPES', lgs1) sg.Column([[sg.Combo(values=lgs1, default_value=self.tl.select_from_lang, size=(50, 30), key='IN_TYPE', enable_events=True, readonly=True), # sg.Combo(sg.user_settings_get_entry('OUTPUT_TYPES', lgs2), sg.Combo(values=self.tl.get_languages(), default_value=self.tl.select_to_lang, size=(50, 30), key='OUT_TYPE', enable_events=True, readonly=True), # sg.Combo(sg.user_settings_get_entry('PLATFORM_TYPES', tls), sg.Combo(values=self.tl.get_translators(), default_value=self.tl.select_translator, size=(20, 30), key='PLATFORM_TYPE', enable_events=True, readonly=True) ]], pad=(0, 0), k='-FOLDER CHOOSE-')) # ----- Full layout ----- layout = [ [sg.Text(conf.main('Description'), font='Any 15', pad=(0, 5))], [choose_type_at_top], [sg.Pane( [sg.Column([[left_col]], element_justification='l', expand_x=True, expand_y=True), sg.Column(right_col, element_justification='c', expand_x=True, expand_y=True)], orientation='h', relief=sg.RELIEF_SUNKEN, expand_x=True, expand_y=True, k='-PANE-')], [operation_at_bottom, sg.Sizegrip()]] # --------------------------------- Create Window --------------------------------- window = sg.Window(conf.main('Title'), layout, finalize=True, resizable=True, use_default_focus=False) window.set_min_size(window.size) window.bind('', 'Exit') # window.bind("", 'Enter') window['IN_TEXT'].bind('', ' Return') self.advanced_ui(window) window.bring_to_front() return window def show(self): """ The main program that contains the event loop. It will call the make_window function to create the window. """ global python_only # icon = sg.EMOJI_BASE64_HAPPY_WINK icon = conf.config('Logo').encode('utf-8') # icon = os.path.join(os.path.dirname(__file__), 'doc', 'logo.ico') # sg.user_settings_filename('psgdemos.json') sg.set_options(icon=icon) window = self.make_window() window.force_focus() counter = 0 while True: event, values = window.read() # print(event, values) counter += 1 if event in (sg.WINDOW_CLOSED, conf.main(Key.M_EXIT)): break elif event == conf.main(Key.M_SETTINGS): change, restart = settings_show() if change: # settings 可能更改的内容:主题,代理,高级 if restart: window.close() window = self.make_window() else: self.advanced_ui(window) elif event == conf.main(Key.M_RUN) or event == 'IN_TEXT Return': # IN_TEXT 的回车键监听, 需要从input获取到数据,然后进行翻译 sg.threading.Thread(target=self.tl.translate, args=(window, window['IN_TEXT'].get(), False), daemon=True).start() loading_show() # result = ts.translate_text(text, translator='deepl', from_language='zh', to_language='en') # window['OUT_TEXT'].update(result) # pw.close() elif event == conf.main(Key.M_CLEAR): window['IN_TEXT'].update('') window['OUT_TEXT'].update('') elif event == conf.main(Key.M_COPY): sg.clipboard_set(window['OUT_TEXT'].get()) elif event == '_FILEBROWSE_': file_path: str = values['_FILEBROWSE_'] # 不判断了,能解析就用 # if file_path.endswith('.txt') or file_path.endswith('.html'): content = read(file_path) window['IN_TEXT'].update(file_path) sg.threading.Thread(target=self.tl.translate, args=(window, content, file_path.endswith('.html'), file_path), daemon=True).start() loading_show() elif event == 'IN_TYPE': # 输入 type1 = values['IN_TYPE'] self.tl.select_from_lang = type1 get_cache('INPUT_TYPES', type1) elif event == 'OUT_TYPE': # 输出 type1 = values['OUT_TYPE'] self.tl.select_to_lang = type1 save_cache('OUTPUT_TYPES', type1) elif event == 'PLATFORM_TYPE': # 选择平台后,更新输入输出的可选 type1 = values['PLATFORM_TYPE'] self.tl.select_translator = type1 save_cache('PLATFORM_TYPES', type1) window.close() window = self.make_window() elif event == 'Version': sg.popup_scrolled(sg.get_versions(), keep_on_top=True, non_blocking=True) elif event == Key.PROXY_ENABLE: # 高级时在本窗口开启或关闭代理,但是需要在设置中设置代理地址 proxy_enable = values[Key.PROXY_ENABLE] save_cache(Key.PROXY_ENABLE, proxy_enable) window[Key.PROXY_ENABLE].update(value=proxy_enable) window.close() if __name__ == '__main__': MainWin().show() ================================================ FILE: 009-Translate/tran_test.py ================================================ #!/usr/bin/python3.9 # -*- coding: utf-8 -*- """ @Time : 2023/5/18 14:38 @Author : xhunmon @Email : xhunmon@126.com @File : tran_test.py @Desc : """ import re from load_srt import Translator import time from utils import * def t_test(srt_source): sub_start = '00:02:00' v_start = '00:00:17' def translate_callback(status, **kwargs): print(status) print(kwargs["src"]) print(kwargs["dst"]) print(kwargs["msg"]) if __name__ == '__main__': tl = Translator() tl.add_callback(translate_callback) try: tl.translate_file("output/test.srt", "output/test_out.srt") except Exception as e: print(e) ================================================ FILE: 009-Translate/ui.py ================================================ # -*- coding: utf-8 -*- """ @Author : Xhunmon @Time : 2023/4/22 22:31 @FileName: ui.py @desc: """ from config import * def loading_show(): LoadingWin.is_loading = True layout = [[sg.Text(conf.loading('Content'), font='ANY 15')], [sg.Image(data=conf.config('Loading').encode('utf-8'), key='_IMAGE_')], [sg.Button(conf.loading('Cancel'))] ] window = sg.Window('').Layout(layout) while LoadingWin.is_loading: # Event Loop event, values = window.Read(timeout=25) if event in (None, 'Exit', conf.loading('Cancel')): break window.Element('_IMAGE_').UpdateAnimation(conf.config('Loading').encode('utf-8'), time_between_frames=50) window.close() class LoadingWin(object): """ loading dialog """ is_loading = True def settings_show(): """ Show the settings window. This is where the folder paths and program paths are set. Returns True if settings were changed :return: True if settings were changed :rtype: (bool) """ global_theme = get_theme() proxy_enable = get_cache(Key.PROXY_ENABLE, False) translate_enable = get_cache(Key.FULL_TRANSLATE, False) restart_enable = get_cache(Key.RESTART_WINDOW, True) layout = [ [sg.T(' ', font='_ 16', size=(40, 1))], [sg.T(conf.settings('Proxy'), font='_ 16')], [sg.CB(conf.settings('ProxyEnable'), default=proxy_enable, enable_events=True, k=Key.PROXY_ENABLE, font='_ 12')], [sg.Column([[sg.Input(size=(38, 1), default_text=get_cache(Key.PROXY_INPUT, ''), key=Key.PROXY_INPUT), sg.T(conf.settings('ProxyDesc'), font='_ 12')]], expand_x=True, expand_y=True, k=Key.PROXY_LAYOUT, visible=proxy_enable)], [sg.T(conf.settings('Theme'), font='_ 16')], [sg.T(conf.settings('ThemeDesc'), font='_ 11'), sg.T(global_theme, font='_ 13')], [sg.Combo([''] + sg.theme_list(), get_cache(Key.THEME, ''), readonly=True, k=Key.THEME)], [sg.CB(conf.settings('Advanced'), default=get_cache(Key.ADVANCED_MODE, True), font='_ 12', k=Key.ADVANCED_MODE)], [sg.CB(conf.settings('FullTranslate'), visible=False, default=translate_enable, font='_ 12', k=Key.ADVANCED_MODE)], [sg.CB(conf.settings('Restart'), default=restart_enable, enable_events=True, k=Key.RESTART_WINDOW, font='_ 12')], [sg.B(conf.settings('Ok'), bind_return_key=True), sg.B(conf.settings('Cancel')), sg.B(conf.settings('Reset'))], ] window = sg.Window(conf.settings('Title'), layout, finalize=True) settings_changed = False while True: event, values = window.read() if event in (conf.settings('Cancel'), sg.WIN_CLOSED): break if event == conf.settings('Ok'): save_cache(Key.THEME, values[Key.THEME]) save_cache(Key.ADVANCED_MODE, values[Key.ADVANCED_MODE]) save_cache(Key.PROXY_ENABLE, proxy_enable) save_cache(Key.FULL_TRANSLATE, translate_enable) save_cache(Key.PROXY_INPUT, window[Key.PROXY_INPUT].get()) save_cache(Key.RESTART_WINDOW, window[Key.RESTART_WINDOW].get()) settings_changed = True break elif event == conf.settings('Reset'): # 恢复所有默认设置 save_cache(Key.THEME, '') save_cache(Key.ADVANCED_MODE, False) save_cache(Key.PROXY_ENABLE, False) save_cache(Key.FULL_TRANSLATE, False) save_cache(Key.RESTART_WINDOW, True) save_cache(Key.PROXY_INPUT, '') settings_changed = True break elif event == Key.PROXY_ENABLE: proxy_enable = values[Key.PROXY_ENABLE] window[Key.PROXY_ENABLE].update(value=proxy_enable) window[Key.PROXY_LAYOUT].update(visible=proxy_enable) elif event == Key.FULL_TRANSLATE: translate_enable = values[Key.FULL_TRANSLATE] window[Key.FULL_TRANSLATE].update(value=translate_enable) elif event == Key.RESTART_WINDOW: restart_enable = values[Key.RESTART_WINDOW] window[Key.RESTART_WINDOW].update(value=restart_enable) window.close() return settings_changed, restart_enable ================================================ FILE: 009-Translate/utils.py ================================================ # -*- coding: utf-8 -*- """ @Author : Xhunmon @Time : 2023/4/22 21:54 @FileName: utils.py @desc: """ import PySimpleGUI as sg def get_cache(key: str, default=None): """ 获取本地的数据 :param key: :param default: :return: """ cache = sg.user_settings_get_entry(key, default) if cache is None or cache == '': return default return cache def save_cache(key: str, value): """ 将数据保存到本地 :param key: :param value: :return: """ sg.user_settings_set_entry(key, value) def read(file_path) -> str: '''读取txt文本内容''' content = None try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() f.close() except Exception as e: print(e) return content def write(content, file_path): '''写入txt文本内容''' try: with open(file_path, mode='w', encoding='utf-8') as f: f.write(content) except Exception as e: print(e) def get_theme(): """ Get the theme to use for the program Value is in this program's user settings. If none set, then use PySimpleGUI's global default theme :return: The theme :rtype: str """ theme = get_cache(Key.THEME, '') if theme == '': theme = sg.OFFICIAL_PYSIMPLEGUI_THEME # 默认主题 return theme class Key: """ 统一管理本地字符串 """ PROXY_ENABLE = 'proxy_enable' # settings PROXY_LAYOUT = 'proxy_layout' # settings PROXY_INPUT = 'proxy_input' # settings THEME = 'theme' # settings RESTART_WINDOW = 'restart_window' # settings ADVANCED_MODE = 'advanced_mode' # settings FULL_TRANSLATE = 'full_translate' # settings LANGUAGE = 'language' # config # main M_CLEAR = 'Clear' M_RUN = 'Run' M_COPY = 'Copy' M_FILE = 'File' M_SETTINGS = 'Settings' M_EXIT = 'Exit' ================================================ FILE: 010-YouTubeUpload/README.md ================================================ # YouTube uploader 自动上传 因为使用API上传是有限制的,每天只能上传6条。因此得使用浏览器操作 ## 前提 1. Chome浏览器,同时需要配置内核,可参考以下文章 > selenium复用已打开浏览器:https://developer.aliyun.com/article/1121356 > 下载chromedriver:https://chromedriver.storage.googleapis.com/index.html?path=112.0.5615.49/ > 下载安装:https://blog.51cto.com/u_15295315/3042408 2. 通过配置启动debug模式浏览器,然后登录 ## 备注 脚本目前还不完善,因此有bug,因比较忙,不保证能更新。 ================================================ FILE: 010-YouTubeUpload/main.py ================================================ #!/usr/bin/python3.9 # -*- coding: utf-8 -*- """ @Time : 2023/5/22 11:44 @Author : xhunmon @Email : xhunmon@126.com @File : main.py @Desc : 使用案例 """ import sys from selenium import webdriver from selenium.webdriver.chrome.options import Options from youtube import * if __name__ == '__main__': meta = { "title": '标题', "description": "描述内容", "tags": ['标签1', '标签2', '标签3'], "edit": False, "playlist_title": '播放列表1', "schedule": "" } executable_path = "chromedriver" options = Options() # TODO - 以下配置是为了打开现有的浏览器,公用cookie,安全有效,需要提起配置。 if sys.platform == 'linux': print("Current OS is Linux.") elif sys.platform == 'darwin': print("Current OS is mac OS.") executable_path = '/usr/local/bin/chromedriver' options.debugger_address = 'localhost:9222' elif sys.platform == 'win32': print("Current OS is Windows.") __g_browser = webdriver.Chrome(executable_path=executable_path, chrome_options=options) __g_browser.implicitly_wait(10) # 设置查找运算智能等待超时时间 yt = YtbUploader() yt.upload(__g_browser, 'xxx/xxx.mp4', options, None) # See PyCharm help at https://www.jetbrains.com/help/pycharm/ ================================================ FILE: 010-YouTubeUpload/tst.py ================================================ #!/usr/bin/python3.9 # -*- coding: utf-8 -*- """ @Time : 2023/5/22 11:44 @Author : xhunmon @Email : xhunmon@126.com @File : tst.py @Desc : """ ================================================ FILE: 010-YouTubeUpload/youtube.py ================================================ #!/usr/bin/python3.9 # -*- coding: utf-8 -*- """ @Time : 2023/5/9 09:10 @Author : xhunmon @Email : xhunmon@126.com @File : youtube.py @Desc : 1. 关闭所有浏览器: 2. 命令格式:浏览器名称 --remote-debugging-port=端口号 例: windows:chrome --remote-debugging-port=9222 mac:/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 """ import logging import platform import time from datetime import datetime from typing import Optional, Tuple from selenium.webdriver import Chrome from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys logging.basicConfig() class YtbConst: """A class for storing constants for YoutubeUploader class""" YOUTUBE_URL = 'https://www.youtube.com' YOUTUBE_STUDIO_URL = 'https://studio.youtube.com' YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload' USER_WAITING_TIME = 1 VIDEO_TITLE = 'title' VIDEO_DESCRIPTION = 'description' VIDEO_EDIT = 'edit' VIDEO_TAGS = 'tags' TEXTBOX_ID = 'textbox' TEXT_INPUT = 'text-input' RADIO_LABEL = 'radioLabel' UPLOADING_STATUS_CONTAINER = '//*[@id="dialog"]/div[2]/div/ytcp-video-upload-progress/span' NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK' UPLOAD_DIALOG = '//ytcp-uploads-dialog' ADVANCED_BUTTON_ID = 'toggle-button' TAGS_CONTAINER_ID = 'tags-container' 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' NEXT_BUTTON = '//*[@id="next-button"]/div' PUBLIC_BUTTON = '//*[@id="done-button"]/div' VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']" VIDEO_URL_ELEMENT = '//*[@id="share-url"]' HREF = 'href' ERROR_CONTAINER = '//*[@id="error-message"]' VIDEO_NOT_FOUND_ERROR = 'Could not find video_id' DONE_BUTTON = 'done-button' INPUT_FILE_VIDEO = "//input[@type='file']" INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']" # Playlist VIDEO_PLAYLIST = 'playlist_title' PL_DROPDOWN_CLASS = 'ytcp-video-metadata-playlists' PL_SEARCH_INPUT_ID = 'search-input' PL_ITEMS_CONTAINER_ID = 'items' PL_ITEM_CONTAINER = '//span[text()="{}"]' PL_NEW_BUTTON_CLASS = 'new-playlist-button' PL_CREATE_PLAYLIST_CONTAINER_XPATH = '//*[@id="text-item-0"]/ytcp-ve' PL_CREATE_BUTTON_XPATH = '//*[@id="create-button"]/div' PL_DONE_BUTTON_CLASS = 'done-button' # Schedule 发布时间 VIDEO_SCHEDULE = 'schedule' SCHEDULE_CONTAINER_ID = 'schedule-radio-button' SCHEDULE_DATE_ID = 'datepicker-trigger' 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' 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" class YtbUploader: """ YouTube上传 """ def __init__(self) -> None: """ @param video_path: 视频路径 @param metadata_json: 字典数据 meta = { "title": "标题", "description": "描述", "tags": ["标签1","标签2"], "edit": False, "playlist_title": "分栏标题", "schedule": "" } @param new_window: 在现有的的浏览器中打开新的页面,没做登录。需要配置先启动浏览器。 @param thumbnail_path: """ self.video_path = None self.thumbnail_path = None self.metadata_dict = None self.browser: Chrome = None self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) self.is_mac = False if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]): self.is_mac = True def __login(self): self.browser.get(YtbConst.YOUTUBE_URL) time.sleep(YtbConst.USER_WAITING_TIME) def __clear_field(self, field): field.click() time.sleep(YtbConst.USER_WAITING_TIME) if self.is_mac: field.send_keys(Keys.COMMAND + 'a') else: field.send_keys(Keys.CONTROL + 'a') time.sleep(YtbConst.USER_WAITING_TIME) field.send_keys(Keys.BACKSPACE) def __write_in_field(self, field, string, select_all=False): if select_all: self.__clear_field(field) else: field.click() time.sleep(YtbConst.USER_WAITING_TIME) field.send_keys(string) def __get_video_id(self) -> Optional[str]: video_id = None try: video_url_element = self.browser.find_element(By.XPATH, YtbConst.VIDEO_URL_ELEMENT) video_id = video_url_element.get_attribute( YtbConst.HREF).split('/')[-1] except: self.logger.warning(YtbConst.VIDEO_NOT_FOUND_ERROR) pass return video_id def upload(self, browser, path, meta, thumbnail=None): self.browser = browser self.video_path = path self.thumbnail_path = thumbnail self.metadata_dict = meta # self.__login() return self.__upload() def exit(self): self.browser.close() def __wait_loading(self): count = 1 while count <= 10: time.sleep(YtbConst.USER_WAITING_TIME) try: uploading_status_container = self.browser.find_element(By.XPATH, YtbConst.UPLOADING_STATUS_CONTAINER) uploading_progress = uploading_status_container.text self.logger.debug('Upload video progress: {}'.format(uploading_progress)) if uploading_progress is None or len(uploading_progress) < 5: break except: self.logger.debug('Upload loading trying {}'.format(count)) count += 1 def __upload(self) -> Tuple[bool, Optional[str]]: edit_mode = self.metadata_dict[YtbConst.VIDEO_EDIT] if edit_mode: self.browser.get(edit_mode) time.sleep(YtbConst.USER_WAITING_TIME) else: # 切换到最新的开的页面,然后在该页面直接加载地址 self.browser.switch_to.window(self.browser.window_handles[0]) self.browser.get(YtbConst.YOUTUBE_URL) time.sleep(YtbConst.USER_WAITING_TIME) self.browser.get(YtbConst.YOUTUBE_UPLOAD_URL) time.sleep(YtbConst.USER_WAITING_TIME) absolute_video_path = self.video_path self.browser.find_element(By.XPATH, YtbConst.INPUT_FILE_VIDEO).send_keys( absolute_video_path) self.logger.debug('Attached video {}'.format(self.video_path)) # Find status container # self.__wait_loading() time.sleep(YtbConst.USER_WAITING_TIME * 2) if self.thumbnail_path is not None: absolute_thumbnail_path = self.thumbnail_path self.browser.find_element(By.XPATH, YtbConst.INPUT_FILE_THUMBNAIL).send_keys( absolute_thumbnail_path) change_display = "document.getElementById('file-loader').style = 'display: block! important'" self.browser.execute_script(change_display) self.logger.debug( 'Attached thumbnail {}'.format(self.thumbnail_path)) textboxs = self.browser.find_elements(By.ID, YtbConst.TEXTBOX_ID) title_field, description_field = textboxs[0], textboxs[1] # //*[@id="textbox"] self.__write_in_field( title_field, self.metadata_dict[YtbConst.VIDEO_TITLE], select_all=True) self.logger.debug('The video title was set to \"{}\"'.format( self.metadata_dict[YtbConst.VIDEO_TITLE])) video_description = self.metadata_dict[YtbConst.VIDEO_DESCRIPTION] video_description = video_description.replace("\n", Keys.ENTER) if video_description: self.__write_in_field(description_field, video_description, select_all=True) self.logger.debug('Description filled.') kids_section = self.browser.find_element(By.NAME, YtbConst.NOT_MADE_FOR_KIDS_LABEL) kids_section.location_once_scrolled_into_view time.sleep(YtbConst.USER_WAITING_TIME) self.browser.find_element(By.ID, YtbConst.RADIO_LABEL).click() self.logger.debug('Selected \"{}\"'.format(YtbConst.NOT_MADE_FOR_KIDS_LABEL)) # Playlist playlist = self.metadata_dict[YtbConst.VIDEO_PLAYLIST] if playlist: self.browser.find_element(By.CLASS_NAME, YtbConst.PL_DROPDOWN_CLASS).click() # 点击播放列表 time.sleep(YtbConst.USER_WAITING_TIME) self.logger.debug('Playlist xpath: "{}".'.format(YtbConst.PL_ITEM_CONTAINER.format(playlist))) try: playlist_item = self.browser.find_element(By.XPATH, YtbConst.PL_ITEM_CONTAINER.format(playlist)) except: playlist_item = None if playlist_item: self.logger.debug('Playlist found.') playlist_item.click() time.sleep(YtbConst.USER_WAITING_TIME) else: self.logger.debug('Playlist not found. Creating') # self.__clear_field(search_field) time.sleep(YtbConst.USER_WAITING_TIME) new_playlist_button = self.browser.find_element(By.CLASS_NAME, YtbConst.PL_NEW_BUTTON_CLASS) new_playlist_button.click() self.browser.find_element(By.XPATH, YtbConst.PL_CREATE_PLAYLIST_CONTAINER_XPATH).click() playlist_title_textbox = self.browser.find_element(By.XPATH, '/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') self.__write_in_field(playlist_title_textbox, playlist) # //*[@id="textbox"] time.sleep(YtbConst.USER_WAITING_TIME) # //*[@id="create-button"]/div //*[@id="dialog"]/div[3]/div/ytcp-button[1]/div create_playlist_button = self.browser.find_element(By.XPATH, YtbConst.PL_CREATE_BUTTON_XPATH) create_playlist_button.click() time.sleep(YtbConst.USER_WAITING_TIME) time.sleep(YtbConst.USER_WAITING_TIME * 2) done_button = self.browser.find_element(By.CLASS_NAME, YtbConst.PL_DONE_BUTTON_CLASS) self.browser.execute_script("arguments[0].click();", done_button) # js注入实现点击 # done_button.click() # Advanced options time.sleep(YtbConst.USER_WAITING_TIME) self.browser.find_element(By.ID, YtbConst.ADVANCED_BUTTON_ID).click() self.logger.debug('Clicked MORE OPTIONS') time.sleep(YtbConst.USER_WAITING_TIME) # Tags tags = self.metadata_dict[YtbConst.VIDEO_TAGS] if tags: # tags_container = self.browser.find_element(By.ID, Constant.TAGS_CONTAINER_ID) self.browser.find_element(By.XPATH, '//*[@id="toggle-button"]/div') # 展开 tags_field = self.browser.find_element(By.XPATH, YtbConst.TAGS_INPUT) self.__write_in_field(tags_field, ','.join(tags)) self.logger.debug('The tags were set to \"{}\"'.format(tags)) self.browser.find_element(By.XPATH, YtbConst.NEXT_BUTTON).click() self.logger.debug('Clicked {} one'.format(YtbConst.NEXT_BUTTON)) time.sleep(YtbConst.USER_WAITING_TIME) self.browser.find_element(By.XPATH, YtbConst.NEXT_BUTTON).click() self.logger.debug('Clicked {} two'.format(YtbConst.NEXT_BUTTON)) time.sleep(YtbConst.USER_WAITING_TIME) self.browser.find_element(By.XPATH, YtbConst.NEXT_BUTTON).click() self.logger.debug('Clicked {} three'.format(YtbConst.NEXT_BUTTON)) time.sleep(YtbConst.USER_WAITING_TIME) schedule = self.metadata_dict[YtbConst.VIDEO_SCHEDULE] if schedule: # 发布时间,暂不设置 upload_time_object = datetime.strptime(schedule, "%m/%d/%Y, %H:%M") self.browser.find_element(By.ID, YtbConst.SCHEDULE_CONTAINER_ID).click() self.browser.find_element(By.ID, YtbConst.SCHEDULE_DATE_ID).click() self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_DATE_TEXTBOX).clear() self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_DATE_TEXTBOX).send_keys( datetime.strftime(upload_time_object, "%b %e, %Y")) self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_DATE_TEXTBOX).send_keys(Keys.ENTER) self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).click() self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).clear() self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).send_keys( datetime.strftime(upload_time_object, "%H:%M")) self.browser.find_element(By.XPATH, YtbConst.SCHEDULE_TIME).send_keys(Keys.ENTER) self.logger.debug(f"Scheduled the video for {schedule}") else: self.browser.find_element(By.XPATH, YtbConst.PUBLIC_BUTTON).click() self.logger.debug('Made the video {}'.format(YtbConst.PUBLIC_BUTTON)) # Check status container and upload progress self.__wait_loading() self.logger.debug('Upload container gone.') video_id = self.__get_video_id() # done_button = self.browser.find_element(By.ID, Constant.DONE_BUTTON) # # # Catch such error as # # "File is a duplicate of a video you have already uploaded" # if done_button.get_attribute('aria-disabled') == 'true': # error_message = self.browser.find_element(By.XPATH, Constant.ERROR_CONTAINER).text # self.logger.error(error_message) # return False, None # # done_button.click() self.logger.debug( "Published the video with video_id = {}".format(video_id)) time.sleep(YtbConst.USER_WAITING_TIME) # self.browser.get(Constant.YOUTUBE_URL) return True, video_id ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: README.md ================================================ 学习python最好的方式就是通过不断的实践! 项目地址:[https://github.com/xhunmon/PythonIsTools](https://github.com/xhunmon/PythonIsTools) # 实践项目 - [001-Downloader:抖音、快手音视频下载器](./001-Downloader) ——已完成 - [002-v2ray代理池:爬取vmess、ss、trojan协议节点,进行校验,自更新](./002-V2rayPool) ——已完成 - [003-Keywords:获取相关关键词以及其google趋势](./003-Keywords) ——已完成 - [004-EmailNotify:监听虚拟币变化,使用邮箱通知](./004-EmailNotify) ——已完成 - [005-PaidSource:这些脚本你肯定会有用到的](./005-PaidSource) ——已完成 - [006-TikTok:App自动化](./006-TikTok) ——已完成 - [007-CutVideoAudio:自媒体运营之视频剪辑,新增V2版本命令](./007-CutVideoAudio) ——已完成 - [008-ChatGPT-UI:About A very useful ChatGPT 3.5/5 Tools with OpenAI API. 一个非常实用的聊天工具](https://github.com/xhunmon/iMedia) ——已完成 - [009-多平台,多语言,支持文本、文件、srt字幕文件翻译](./009-Translate) ——已完成 - [010-YouTube视频上传脚本](./010-YouTubeUpload) ——已完成 ---------- ##### 声明:本项目仅用于学习交流,禁止任何商业用途,违者自承担相应法律责任!