├── .github └── workflows │ ├── Sync.yml │ └── action.yml ├── main.py ├── readme.md ├── requirements.txt └── test_back.py /.github/workflows/Sync.yml: -------------------------------------------------------------------------------- 1 | name: 自动同步脚本更新 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * *' # 每月的第一天凌晨 12 点自动同步 6 | workflow_dispatch: # 支持手动触发 7 | 8 | jobs: 9 | sync: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # 检出 fork 仓库 13 | - name: Checkout the fork repository 14 | uses: actions/checkout@v3 15 | with: 16 | ref: main # 确保同步的是主分支 17 | 18 | # 设置 Git 配置 19 | - name: Set Git global config 20 | run: | 21 | git config user.name "GitHub Actions" 22 | git config user.email "actions@github.com" 23 | 24 | # 添加主项目为上游仓库 25 | - name: Add upstream repository 26 | run: | 27 | git remote add upstream https://github.com/ttm533/v2nodes.git 28 | git fetch upstream 29 | 30 | # 合并主项目的最新提交到 fork 仓库 31 | - name: Merge upstream changes 32 | run: | 33 | git checkout main 34 | git merge upstream/main --no-edit 35 | 36 | # 推送合并后的更改到 fork 仓库 37 | - name: Push changes to fork 38 | run: | 39 | git push origin main 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: 定时任务 自动获取节点信息 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 触发主分支推送时运行 7 | workflow_dispatch: # 允许手动触发 8 | schedule: 9 | - cron: '0 */2 * * *' # 每两个小时运行一次 10 | 11 | jobs: 12 | scraper: 13 | runs-on: ubuntu-latest # 使用 GitHub 提供的 Ubuntu 环境 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 # 检出代码仓库 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 # 设置 Python 环境 21 | with: 22 | python-version: '3.9' # 使用 Python 3.9 23 | 24 | - name: Install dependencies 25 | run: | 26 | pip install requests beautifulsoup4 # 安装必要的依赖库 27 | 28 | - name: Run scraper script 29 | env: 30 | MY_GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} # 从 GitHub secrets 获取 GitHub Token 31 | MY_GIST_ID: ${{ secrets.MY_GIST_ID }} # 从 GitHub secrets 获取 Gist ID 32 | run: | 33 | python main.py # 运行你的 Python 脚本 34 | 35 | - name: Check Git Configuration 36 | run: | 37 | git config --list # 输出 Git 配置信息,确保 safe.directory 被正确配置 38 | git config --get safe.directory # 查看 safe.directory 配置 39 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from bs4 import BeautifulSoup 4 | import time 5 | import json 6 | import base64 7 | 8 | # -------------------------- 配置抓取部分 -------------------------- 9 | BASE_URL = "https://zh.v2nodes.com" 10 | PAGE_START = 1 11 | PAGE_END = 5 12 | PAGES = [f"{BASE_URL}/?page={i}" for i in range(PAGE_START, PAGE_END + 1)] 13 | GITHUB_TOKEN = os.getenv('MY_GITHUB_TOKEN') # 从环境变量中读取 14 | GIST_ID = os.getenv('MY_GIST_ID') # 从环境变量中读取 15 | 16 | def extract_server_info(server_url): 17 | response = requests.get(server_url) 18 | soup = BeautifulSoup(response.text, "html.parser") 19 | config_div = soup.find("textarea", {"id": "config"}) 20 | if config_div: 21 | return config_div.get("data-config") 22 | return None 23 | 24 | def extract_server_links(page_url): 25 | response = requests.get(page_url) 26 | soup = BeautifulSoup(response.text, "html.parser") 27 | servers = soup.find_all("div", class_="col-md-12 servers") 28 | server_links = [] 29 | for server in servers: 30 | server_id = server.get("data-id") 31 | if server_id: 32 | server_url = f"{BASE_URL}/servers/{server_id}/" 33 | server_links.append(server_url) 34 | return server_links 35 | 36 | def upload_to_gist(content, gist_id=None): 37 | url = "https://api.github.com/gists" 38 | headers = { 39 | "Authorization": f"token {GITHUB_TOKEN}", 40 | "Accept": "application/vnd.github.v3+json" 41 | } 42 | 43 | if gist_id: 44 | # 读取现有的 Gist 数据 45 | url = f"https://api.github.com/gists/{gist_id}" 46 | response = requests.get(url, headers=headers) 47 | if response.status_code == 200: 48 | gist_data = response.json() 49 | # 如果 Gist 中没有 V2Nodes_config.txt 文件,则添加该文件 50 | if 'V2Nodes_config.txt' not in gist_data['files']: 51 | gist_data['files']['V2Nodes_config.txt'] = {'content': content} 52 | else: 53 | gist_data['files']['V2Nodes_config.txt']['content'] = content 54 | response = requests.patch(url, headers=headers, data=json.dumps(gist_data)) 55 | else: 56 | print(f"读取 Gist 时出错,状态码: {response.status_code}") 57 | else: 58 | # 创建新的 Gist 59 | gist_data = { 60 | "description": "V2Nodes Server Configurations", 61 | "public": True, 62 | "files": { 63 | "V2Nodes_config.txt": { 64 | "content": content 65 | } 66 | } 67 | } 68 | response = requests.post(url, headers=headers, data=json.dumps(gist_data)) 69 | 70 | if response.status_code != 200 and response.status_code != 201: 71 | print(f"上传 Gist 失败,响应代码: {response.status_code}") 72 | print(f"响应内容: {response.text}") 73 | 74 | return response.json() 75 | 76 | def fetch_country_data(country_abbr): 77 | base_url = "https://www.v2nodes.com/subscriptions/country/" 78 | url = base_url + country_abbr.lower() + "/" 79 | try: 80 | response = requests.get(url) 81 | if response.status_code == 200: 82 | return response.text # 返回页面内容 83 | else: 84 | return f"Error: {response.status_code}" 85 | except requests.exceptions.RequestException as e: 86 | return f"Request failed: {e}" 87 | 88 | def decode_base64_data(data): 89 | try: 90 | decoded_data = base64.urlsafe_b64decode(data + '==').decode('utf-8') # 补齐 Base64 缺失的字符 91 | return decoded_data 92 | except Exception as e: 93 | return f"Decoding failed: {e}" 94 | 95 | # -------------------------- 抓取和解码功能 -------------------------- 96 | def main(): 97 | all_server_configs = [] 98 | 99 | # 第一部分:抓取 V2Nodes 服务器配置 100 | for page in PAGES: 101 | print(f"正在抓取页面: {page}") 102 | server_links = extract_server_links(page) 103 | for server_url in server_links: 104 | print(f"正在抓取服务器: {server_url}") 105 | config = extract_server_info(server_url) 106 | if config: 107 | all_server_configs.append(config) 108 | print(config) 109 | else: 110 | print(f"未能提取配置:{server_url}") 111 | time.sleep(1) 112 | 113 | # 第二部分:抓取国家对应的链接并进行解码 114 | countries = [ 115 | "AQ", "AR", "AU", "AT", "BH", "BY", "BE", "BO", "BR", "BG", 116 | "CA", "CN", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HK" 117 | ] 118 | 119 | for country in countries: 120 | data = fetch_country_data(country) 121 | if "vless://" in data: 122 | # 提取 Base64 数据 123 | base64_data = data.split("vless://")[1].split("#")[0] 124 | decoded_data = decode_base64_data(base64_data) 125 | if decoded_data: 126 | all_server_configs.append(decoded_data) 127 | print(f"解码后的配置:\n{decoded_data}") 128 | else: 129 | print(f"没有找到 Base64 数据: {country}") 130 | 131 | # 合并所有配置并上传到 Gist 132 | content = "\n".join(all_server_configs) 133 | gist_response = upload_to_gist(content, GIST_ID) 134 | 135 | if 'html_url' in gist_response: 136 | print(f"配置信息已上传到 GitHub Gist: {gist_response['html_url']}") 137 | else: 138 | print("上传失败", gist_response) 139 | 140 | if __name__ == "__main__": 141 | main() 142 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # V2Nodes 配置抓取并上传到 GitHub Gist 2 | 3 | ## 项目介绍 4 | 5 | 该项目提供一个 Python 脚本,用于从 **V2Nodes** 网站抓取服务器配置信息,并将这些配置信息上传到 **GitHub Gist**。该脚本支持自动抓取多个页面上的服务器配置,并将抓取到的数据上传到 GitHub Gist,方便后续查看或分享。 6 | 7 | ## 如何在 GitHub Actions 中使用 8 | 9 | 本指南将介绍如何在 GitHub Actions 中使用该项目,包括以下几个步骤: 10 | 11 | 1. **Fork 本项目** 12 | 2. **生成 GitHub Token** 13 | 3. **在 Gist 中设置配置并获取 Gist ID** 14 | 4. **设置 GitHub Token 和其他环境变量** 15 | 5. **触发 Actions 运行** 16 | 17 | --- 18 | 19 | ### 1. Fork 本项目 20 | 21 | 1. 打开项目的 GitHub 仓库页面。 22 | 2. 点击右上角的 **Fork** 按钮,将该项目复制到你自己的 GitHub 账户下。 23 | 24 | --- 25 | 26 | ### 2. 生成 GitHub Token 27 | 28 | 在开始之前,你需要生成一个 GitHub 个人访问令牌(**Personal Access Token**,简称 **PAT**)。这个令牌将用于在 GitHub API 中进行身份验证。以下是生成 Token 的步骤: 29 | 30 | 1. 登录到 GitHub。 31 | 2. 进入 [GitHub Personal Access Tokens 页面](https://github.com/settings/tokens)。 32 | 3. 点击 **Generate new token**。 33 | 4. 在 **Note** 中填写 Token 名称,例如 "V2Nodes Token"。 34 | 5. 在 **Select scopes**(选择权限)部分,选择以下权限: 35 | - **repo**:允许访问和管理私有仓库(如果你有私有仓库或私有 Gist)。 36 | - **gist**:Gist必须给予读写权限(必须选择此权限来上传配置到 Gist)。 37 | 6. 生成 Token 后,复制该 Token(请妥善保管,因为页面关闭后无法再次查看)。 38 | 39 | --- 40 | 41 | ### 3. 在 Gist 中设置配置并获取 Gist ID 42 | 43 | 如果你希望将抓取的配置上传到一个已有的 Gist,你需要先创建一个公开的 Gist。以下是步骤: 44 | 45 | 1. 访问 [GitHub Gist 页面](https://gist.github.com/)。 46 | 2. 点击 **New gist** 创建一个新的 Gist。名称必须为V2Nodes_config.txt,否则就要更改main.py里的文件名。 47 | 3. 在 **Visibility**(可见性)部分,确保选择 **Public**(公开)。如果设置为私有,脚本将无法上传配置。 48 | 4. 创建完 Gist 后,获取该 Gist 的 ID。Gist 的 URL 格式为 `https://gist.github.com/username/gist_id`,其中 `gist_id` 就是你需要的 Gist ID。 49 | - 例如,Gist URL 为 `https://gist.github.com/username/abcdef1234567890`,则 `abcdef1234567890` 就是你的 Gist ID。 50 | 5. 将 Gist ID 记录下来,后续将在环境变量中使用。 51 | 52 | --- 53 | 54 | ### 4. 设置 GitHub Token 和其他环境变量 55 | 56 | 在 GitHub Actions 中,你需要设置一些环境变量来传递配置。以下是需要设置的环境变量: 57 | 58 | - `MY_GITHUB_TOKEN`:你刚刚生成的 GitHub 个人访问令牌,用于认证 API 请求。 59 | - `MY_GIST_ID`:你要上传配置的 GitHub Gist ID。 60 | 61 | #### 如何添加环境变量: 62 | 63 | 1. 打开你的 GitHub 仓库。 64 | 2. 进入 **Settings**(设置) > **Secrets and variables** > **Actions**。 65 | 3. 点击 **New repository secret**,分别添加如下两个 Secret: 66 | - `MY_GITHUB_TOKEN`:粘贴你生成的 GitHub Token。 67 | - `MY_GIST_ID`(可选):如果你希望上传到现有的 Gist,可以添加该变量。如果没有该 ID,脚本会自动创建新的 Gist。 68 | 69 | --- 70 | 71 | ### 5. 触发 Actions 运行 72 | 73 | 1. 每当你推送代码到 `main` 分支时,GitHub Actions 会自动运行该工作流。 74 | 2. 如果你希望手动触发工作流,也可以在 GitHub 仓库页面的 **Actions** 标签下,点击相应的工作流,然后点击 **Run workflow** 按钮手动触发。 75 | 76 | --- 77 | ## 脚本运行输出 78 | 79 | - 如果配置抓取和上传成功,GitHub Actions 会输出类似以下的信息: 80 | 81 | ```bash 82 | 正在抓取页面: https://zh.v2nodes.com/?page=1 83 | 正在抓取服务器: https://zh.v2nodes.com/servers/12345/ 84 | config内容... 85 | 未能提取配置: https://zh.v2nodes.com/servers/12345/ 86 | 正在抓取页面: https://zh.v2nodes.com/?page=2 87 | ... 88 | 配置信息已上传到 GitHub Gist: https://gist.github.com/your_gist_id 89 | ``` 90 | 91 | --- 92 | 93 | ## 使用方法 94 | 95 | 访问 [GitHub Gist 页面](https://gist.github.com/),并找到配置信息上传的文件。 96 | 97 | ![image1](https://github.com/user-attachments/assets/bd375e76-b1d9-4963-87a1-c622a8d37f28) 98 | ![image2](https://github.com/user-attachments/assets/9e927fd0-0e12-4358-a837-6598b424830e) 99 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | beautifulsoup4 3 | -------------------------------------------------------------------------------- /test_back.py: -------------------------------------------------------------------------------- 1 | # 无关文件,仅供测试时文件备份 2 | import os 3 | import requests 4 | from bs4 import BeautifulSoup 5 | import time 6 | import json 7 | import base64 8 | 9 | # -------------------------- 配置抓取部分 -------------------------- 10 | BASE_URL = "https://zh.v2nodes.com" 11 | PAGE_START = 1 12 | PAGE_END = 10 13 | PAGES = [f"{BASE_URL}/?page={i}" for i in range(PAGE_START, PAGE_END + 1)] 14 | GITHUB_TOKEN = os.getenv('MY_GITHUB_TOKEN') # 从环境变量中读取 15 | GIST_ID = os.getenv('MY_GIST_ID') # 从环境变量中读取 16 | 17 | def extract_server_info(server_url): 18 | response = requests.get(server_url) 19 | soup = BeautifulSoup(response.text, "html.parser") 20 | config_div = soup.find("textarea", {"id": "config"}) 21 | if config_div: 22 | return config_div.get("data-config") 23 | return None 24 | 25 | def extract_server_links(page_url): 26 | response = requests.get(page_url) 27 | soup = BeautifulSoup(response.text, "html.parser") 28 | servers = soup.find_all("div", class_="col-md-12 servers") 29 | server_links = [] 30 | for server in servers: 31 | server_id = server.get("data-id") 32 | if server_id: 33 | server_url = f"{BASE_URL}/servers/{server_id}/" 34 | server_links.append(server_url) 35 | return server_links 36 | 37 | def upload_to_gist(content, gist_id=None): 38 | url = "https://api.github.com/gists" 39 | headers = { 40 | "Authorization": f"token {GITHUB_TOKEN}", 41 | "Accept": "application/vnd.github.v3+json" 42 | } 43 | 44 | if gist_id: 45 | # 读取现有的 Gist 数据 46 | url = f"https://api.github.com/gists/{gist_id}" 47 | response = requests.get(url, headers=headers) 48 | if response.status_code == 200: 49 | gist_data = response.json() 50 | # 如果 Gist 中没有 V2Nodes_config.txt 文件,则添加该文件 51 | if 'V2Nodes_config.txt' not in gist_data['files']: 52 | gist_data['files']['V2Nodes_config.txt'] = {'content': content} 53 | else: 54 | gist_data['files']['V2Nodes_config.txt']['content'] = content 55 | response = requests.patch(url, headers=headers, data=json.dumps(gist_data)) 56 | else: 57 | print(f"读取 Gist 时出错,状态码: {response.status_code}") 58 | else: 59 | # 创建新的 Gist 60 | gist_data = { 61 | "description": "V2Nodes Server Configurations", 62 | "public": True, 63 | "files": { 64 | "V2Nodes_config.txt": { 65 | "content": content 66 | } 67 | } 68 | } 69 | response = requests.post(url, headers=headers, data=json.dumps(gist_data)) 70 | 71 | if response.status_code != 200 and response.status_code != 201: 72 | print(f"上传 Gist 失败,响应代码: {response.status_code}") 73 | print(f"响应内容: {response.text}") 74 | 75 | return response.json() 76 | 77 | def fetch_country_data(country_abbr): 78 | base_url = "https://www.v2nodes.com/subscriptions/country/" 79 | url = base_url + country_abbr.lower() + "/" 80 | try: 81 | response = requests.get(url) 82 | if response.status_code == 200: 83 | return response.text # 返回页面内容 84 | else: 85 | return f"Error: {response.status_code}" 86 | except requests.exceptions.RequestException as e: 87 | return f"Request failed: {e}" 88 | 89 | def decode_base64_data(data): 90 | try: 91 | decoded_data = base64.urlsafe_b64decode(data + '==').decode('utf-8') # 补齐 Base64 缺失的字符 92 | return decoded_data 93 | except Exception as e: 94 | return f"Decoding failed: {e}" 95 | 96 | # -------------------------- 抓取和解码功能 -------------------------- 97 | def main(): 98 | all_server_configs = [] 99 | 100 | # 第一部分:抓取 V2Nodes 服务器配置 101 | for page in PAGES: 102 | print(f"正在抓取页面: {page}") 103 | server_links = extract_server_links(page) 104 | for server_url in server_links: 105 | print(f"正在抓取服务器: {server_url}") 106 | config = extract_server_info(server_url) 107 | if config: 108 | all_server_configs.append(config) 109 | print(config) 110 | else: 111 | print(f"未能提取配置:{server_url}") 112 | time.sleep(1) 113 | 114 | # 第二部分:抓取国家对应的链接并进行解码 115 | countries = [ 116 | "AQ", "AR", "AU", "AT", "BH", "BY", "BE", "BO", "BR", "BG", 117 | "CA", "CN", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HK" 118 | ] 119 | 120 | for country in countries: 121 | data = fetch_country_data(country) 122 | if "vless://" in data: 123 | # 提取 Base64 数据 124 | base64_data = data.split("vless://")[1].split("#")[0] 125 | decoded_data = decode_base64_data(base64_data) 126 | if decoded_data: 127 | all_server_configs.append(decoded_data) 128 | print(f"解码后的配置:\n{decoded_data}") 129 | else: 130 | print(f"没有找到 Base64 数据: {country}") 131 | 132 | # 合并所有配置并上传到 Gist 133 | content = "\n".join(all_server_configs) 134 | gist_response = upload_to_gist(content, GIST_ID) 135 | 136 | if 'html_url' in gist_response: 137 | print(f"配置信息已上传到 GitHub Gist: {gist_response['html_url']}") 138 | else: 139 | print("上传失败", gist_response) 140 | 141 | if __name__ == "__main__": 142 | main() 143 | --------------------------------------------------------------------------------