init
This commit is contained in:
1
XboxAutoRegister-main/README.md
Normal file
1
XboxAutoRegister-main/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# XboxAutoRegister
|
||||||
209
XboxAutoRegister-main/calc_us_price.js
Normal file
209
XboxAutoRegister-main/calc_us_price.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
// 终极破甲器:手动追踪跳转,继承 Cookie,半路截取游戏 ID
|
||||||
|
async function resolveGameId(startUrl) {
|
||||||
|
let currentUrl = startUrl;
|
||||||
|
let cookies = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) { // 最多追踪 7 层跳转
|
||||||
|
try {
|
||||||
|
// 拼接继承的 Cookie
|
||||||
|
const cookieHeader = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||||
|
|
||||||
|
const response = await fetch(currentUrl, {
|
||||||
|
redirect: 'manual', // 关键:关闭自动跳转,改为我们手动一步步跟
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Cookie': cookieHeader
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 继承服务器发下来的 Cookie,伪装得更像真人
|
||||||
|
const setCookieHeader = response.headers.get('set-cookie');
|
||||||
|
if (setCookieHeader) {
|
||||||
|
const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_-]+\s*=)/);
|
||||||
|
for (const part of parts) {
|
||||||
|
const cookiePair = part.split(';')[0];
|
||||||
|
const [key, ...val] = cookiePair.split('=');
|
||||||
|
if (key && val) cookies[key.trim()] = val.join('=').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 300 && response.status < 400) {
|
||||||
|
const location = response.headers.get('location');
|
||||||
|
if (!location) break;
|
||||||
|
|
||||||
|
const nextUrl = location.startsWith('http') ? location : new URL(location, currentUrl).href;
|
||||||
|
|
||||||
|
// 解密 URL,剥开追踪网的层层包装
|
||||||
|
let decodedUrl = nextUrl;
|
||||||
|
try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){}
|
||||||
|
try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){}
|
||||||
|
|
||||||
|
// ⭐️ 半路截胡:只要在跳转链接里发现了 9 开头的 12 位代码,直接带走,不再往下跳!
|
||||||
|
const idMatch = decodedUrl.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i);
|
||||||
|
if (idMatch) return idMatch[1].toUpperCase();
|
||||||
|
|
||||||
|
currentUrl = nextUrl;
|
||||||
|
} else if (response.status === 200) {
|
||||||
|
const htmlText = await response.text();
|
||||||
|
|
||||||
|
// 搜刮网页源码,防备 JS 动态跳转
|
||||||
|
const htmlMatch = htmlText.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i);
|
||||||
|
if (htmlMatch) return htmlMatch[1].toUpperCase();
|
||||||
|
|
||||||
|
// 检查 Meta Refresh 自动跳转
|
||||||
|
const metaRefresh = htmlText.match(/<meta[^>]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']/i);
|
||||||
|
if (metaRefresh) {
|
||||||
|
currentUrl = metaRefresh[1].replace(/&/g, '&');
|
||||||
|
if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 JS 自动跳转
|
||||||
|
const jsRedirect = htmlText.match(/(?:window\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i);
|
||||||
|
if (jsRedirect) {
|
||||||
|
currentUrl = jsRedirect[1];
|
||||||
|
if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心查询逻辑
|
||||||
|
async function getUSGameData(startUrl) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(startUrl);
|
||||||
|
urlObj.searchParams.set('r', 'en-us');
|
||||||
|
|
||||||
|
let bigId = await resolveGameId(urlObj.toString());
|
||||||
|
|
||||||
|
if (!bigId) {
|
||||||
|
return { success: false, reason: "防爬虫拦截,未能从底层剥离出 12 位游戏代码" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${bigId}&market=US&languages=en-us&MS-CV=DUMMY.1`;
|
||||||
|
|
||||||
|
const apiResponse = await fetch(apiUrl);
|
||||||
|
const data = await apiResponse.json();
|
||||||
|
|
||||||
|
if (!data.Products || data.Products.length === 0) {
|
||||||
|
return { success: false, reason: `成功获取 ID (${bigId}),但美区查无此游戏数据` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = data.Products[0];
|
||||||
|
const gameName = product.LocalizedProperties?.[0]?.ProductTitle || "未知游戏";
|
||||||
|
|
||||||
|
let finalPrice = null;
|
||||||
|
|
||||||
|
if (!product.DisplaySkuAvailabilities || product.DisplaySkuAvailabilities.length === 0) {
|
||||||
|
return { success: false, name: gameName, reason: "该游戏没有销售规格 (无法购买)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能找买断价
|
||||||
|
for (const skuObj of product.DisplaySkuAvailabilities) {
|
||||||
|
if (skuObj.Sku && (skuObj.Sku.SkuType === 'full' || skuObj.Sku.SkuType === 'dlc' || skuObj.Sku.SkuType === 'consumable')) {
|
||||||
|
for (const avail of skuObj.Availabilities || []) {
|
||||||
|
if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) {
|
||||||
|
finalPrice = avail.OrderManagementData.Price.ListPrice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalPrice !== null) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalPrice === null) {
|
||||||
|
for (const skuObj of product.DisplaySkuAvailabilities) {
|
||||||
|
for (const avail of skuObj.Availabilities || []) {
|
||||||
|
if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) {
|
||||||
|
finalPrice = avail.OrderManagementData.Price.ListPrice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalPrice !== null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalPrice === null) {
|
||||||
|
return { success: false, name: gameName, reason: "只有 XGP 订阅试玩或捆绑包专属,无单买价格" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, name: gameName, price: finalPrice };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, reason: `发生异常: ${e.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputUrls = [];
|
||||||
|
|
||||||
|
console.log('🎮 请粘贴你的 Xbox 链接串 (支持包含回车的多行文本)。');
|
||||||
|
console.log('💡 提示:粘贴完成后,请在【新的一行】按一次回车开始计算:\n');
|
||||||
|
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine === '') {
|
||||||
|
if (inputUrls.length > 0) {
|
||||||
|
rl.close();
|
||||||
|
processUrls(inputUrls);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const splitUrls = trimmedLine.split(/(?=https?:\/\/)/).filter(u => u.startsWith('http'));
|
||||||
|
inputUrls.push(...splitUrls);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function processUrls(urls) {
|
||||||
|
console.log(`\n✅ 成功读取到 ${urls.length} 个链接,开始逐个查询美区价格...\n`);
|
||||||
|
|
||||||
|
let totalPrice = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
const failedDetails = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < urls.length; i++) {
|
||||||
|
const url = urls[i];
|
||||||
|
process.stdout.write(`[${i + 1}/${urls.length}] 正在查询... `);
|
||||||
|
|
||||||
|
const result = await getUSGameData(url);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ 成功 | ${result.name} | 现价: $${result.price}`);
|
||||||
|
totalPrice += result.price;
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
const namePart = result.name ? `[${result.name}] ` : "";
|
||||||
|
console.log(`❌ 失败 | ${namePart}原因: ${result.reason}`);
|
||||||
|
failedDetails.push({ url, reason: result.reason, name: result.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n================ 结算单 ================");
|
||||||
|
console.log(`总计识别: ${urls.length} 个游戏`);
|
||||||
|
console.log(`成功查询: ${successCount} 个游戏`);
|
||||||
|
console.log(`美元总价: $${totalPrice.toFixed(2)}`);
|
||||||
|
console.log("========================================\n");
|
||||||
|
|
||||||
|
if (failedDetails.length > 0) {
|
||||||
|
console.log("⚠️ 以下链接需要手动核查:");
|
||||||
|
failedDetails.forEach((f, idx) => {
|
||||||
|
const nameStr = f.name ? `游戏: ${f.name}\n ` : "";
|
||||||
|
console.log(`${idx + 1}. ${nameStr}原因: ${f.reason}\n 链接: ${f.url}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
XboxAutoRegister-main/geckodriver.exe
Normal file
BIN
XboxAutoRegister-main/geckodriver.exe
Normal file
Binary file not shown.
60
XboxAutoRegister-main/rotate.ps1
Normal file
60
XboxAutoRegister-main/rotate.ps1
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# ================= CONFIG =================
|
||||||
|
$API_URL = "http://127.0.0.1:9090"
|
||||||
|
$SECRET = "8130899"
|
||||||
|
$GROUP = "Rotate"
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. Force UTF-8 Output
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
Write-Host "1. Connecting to Clash..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Use WebClient (Stable & Fixes Encoding)
|
||||||
|
$wc = New-Object System.Net.WebClient
|
||||||
|
$wc.Encoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
try {
|
||||||
|
# URL
|
||||||
|
$ListUrl = "$API_URL/proxies/$GROUP"
|
||||||
|
|
||||||
|
# === Step A: Get List ===
|
||||||
|
# Add Auth Header
|
||||||
|
if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") }
|
||||||
|
|
||||||
|
$JsonContent = $wc.DownloadString($ListUrl)
|
||||||
|
|
||||||
|
$JsonObj = $JsonContent | ConvertFrom-Json
|
||||||
|
$NodeList = $JsonObj.all
|
||||||
|
|
||||||
|
if (!$NodeList -or $NodeList.Count -eq 0) {
|
||||||
|
throw "Error: Group [$GROUP] is empty!"
|
||||||
|
}
|
||||||
|
Write-Host " > Found $($NodeList.Count) nodes." -ForegroundColor Green
|
||||||
|
|
||||||
|
# === Step B: Pick Random ===
|
||||||
|
$Target = $NodeList | Get-Random
|
||||||
|
Write-Host "2. Switching to: $Target" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# === Step C: Send Switch Command ===
|
||||||
|
$Payload = @{ name = $Target } | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
# 【Critical Fix】Clear headers to remove old Auth, preventing duplicate headers
|
||||||
|
$wc.Headers.Clear()
|
||||||
|
|
||||||
|
# Re-add Headers correctly
|
||||||
|
$wc.Headers.Add("Content-Type", "application/json")
|
||||||
|
if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") }
|
||||||
|
|
||||||
|
# Send PUT
|
||||||
|
$Response = $wc.UploadString($ListUrl, "PUT", $Payload)
|
||||||
|
|
||||||
|
Write-Host "3. [SUCCESS] Switched to: $Target" -ForegroundColor Green
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-Host "==== ERROR ====" -ForegroundColor Red
|
||||||
|
Write-Host $_.Exception.Message -ForegroundColor Yellow
|
||||||
|
if ($_.Exception.InnerException) {
|
||||||
|
Write-Host "Detail: $($_.Exception.InnerException.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
382
XboxAutoRegister-main/run_bot.py
Normal file
382
XboxAutoRegister-main/run_bot.py
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.firefox.service import Service
|
||||||
|
from selenium.webdriver.firefox.options import Options
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# === 配置区域 ===
|
||||||
|
INPUT_CSV = r'input\outlook账号_part_2.csv' # 原始输入文件
|
||||||
|
TEMP_RETRY_CSV = r'output\temp_retry.csv' # 第一轮失败存放处(复活赛的输入)
|
||||||
|
FINAL_FAILED_CSV = r'output\failed.csv' # 最终失败文件
|
||||||
|
SUCCESS_CSV = r'output\success.csv' # 成功文件
|
||||||
|
|
||||||
|
POWERSHELL_SCRIPT = r"E:\ClashScript\rotate.ps1"
|
||||||
|
GECKODRIVER_PATH = "geckodriver.exe"
|
||||||
|
FIREFOX_BINARY_PATH = r"C:\Program Files\Mozilla Firefox\firefox.exe"
|
||||||
|
|
||||||
|
|
||||||
|
# ================= 工具函数 =================
|
||||||
|
|
||||||
|
def rotate_ip():
|
||||||
|
"""切换IP"""
|
||||||
|
print(">>> [系统] 正在切换 IP (后台运行中)...")
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", POWERSHELL_SCRIPT],
|
||||||
|
check=True,
|
||||||
|
shell=True
|
||||||
|
)
|
||||||
|
print(">>> [系统] IP 切换完成,等待网络恢复...")
|
||||||
|
time.sleep(2)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"!!! IP 切换失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def append_to_csv(file_path, email, password):
|
||||||
|
"""追加写入一行 CSV"""
|
||||||
|
file_exists = os.path.exists(file_path)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'a', encoding='utf-8') as f:
|
||||||
|
if not file_exists:
|
||||||
|
f.write("卡号\n")
|
||||||
|
f.write(f"{email}----{password}\n")
|
||||||
|
f.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"写入文件 {file_path} 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_file_lines(file_path):
|
||||||
|
"""读取文件所有行"""
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.readlines()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='gb18030') as f:
|
||||||
|
return f.readlines()
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_source_file(file_path, lines):
|
||||||
|
"""重写源文件(用于删除行)"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! 更新源文件失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_account(line):
|
||||||
|
"""解析账号密码"""
|
||||||
|
line = line.strip()
|
||||||
|
if not line or "卡号" in line:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
email = ""
|
||||||
|
pwd = ""
|
||||||
|
# 支持 ---- 分割
|
||||||
|
if "----" in line:
|
||||||
|
parts = line.split("----")
|
||||||
|
email = parts[0].strip()
|
||||||
|
if len(parts) > 1:
|
||||||
|
pwd = parts[1].strip()
|
||||||
|
# 支持 , 分割
|
||||||
|
elif "," in line:
|
||||||
|
parts = line.split(",")
|
||||||
|
email = parts[0].strip()
|
||||||
|
if len(parts) > 1:
|
||||||
|
pwd = parts[1].strip()
|
||||||
|
|
||||||
|
if email and pwd:
|
||||||
|
return email, pwd
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def count_valid_accounts(file_path):
|
||||||
|
"""统计有效账号数"""
|
||||||
|
lines = read_file_lines(file_path)
|
||||||
|
count = 0
|
||||||
|
for line in lines:
|
||||||
|
e, _ = parse_account(line)
|
||||||
|
if e:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def login_process(driver, email, password):
|
||||||
|
"""
|
||||||
|
业务逻辑:登录 Xbox
|
||||||
|
返回: True(成功) / False(失败)
|
||||||
|
"""
|
||||||
|
print(f"=== 开始处理: {email} ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
driver.get("https://www.xbox.com/en-us/auth/msa?action=logIn")
|
||||||
|
|
||||||
|
# 1. 输入账号
|
||||||
|
try:
|
||||||
|
WebDriverWait(driver, 30).until(
|
||||||
|
EC.visibility_of_element_located((By.ID, "usernameEntry"))
|
||||||
|
).send_keys(email)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
try:
|
||||||
|
driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. 输入密码
|
||||||
|
WebDriverWait(driver, 30).until(
|
||||||
|
EC.visibility_of_element_located((By.NAME, "passwd"))
|
||||||
|
).send_keys(password.strip())
|
||||||
|
|
||||||
|
time.sleep(1.5)
|
||||||
|
driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click()
|
||||||
|
|
||||||
|
# === 3. URL检测循环 ===
|
||||||
|
print(">>> 进入 URL 监控模式...")
|
||||||
|
loop_start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if time.time() - loop_start_time > 60:
|
||||||
|
print(">>> URL 检测超时 (60s),强制下一步")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_url = driver.current_url
|
||||||
|
|
||||||
|
if "xbox.com" in current_url:
|
||||||
|
print(f"√√√ 直接跳转到了 Xbox 首页,成功!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if "account.live.com" in current_url or "login.live.com" in current_url:
|
||||||
|
try:
|
||||||
|
# 处理跳过按钮
|
||||||
|
skip_btns = driver.find_elements(By.ID, "iShowSkip")
|
||||||
|
if skip_btns and skip_btns[0].is_displayed():
|
||||||
|
print(">>> 点击 '跳过'...")
|
||||||
|
skip_btns[0].click()
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理常规确认按钮
|
||||||
|
primary_btns = driver.find_elements(By.XPATH, "//button[@data-testid='primaryButton']")
|
||||||
|
if primary_btns and primary_btns[0].is_displayed():
|
||||||
|
print(f">>> 检测到主按钮,点击确认...")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
# === 4. 后续确认流程 ===
|
||||||
|
clicked_yes = False
|
||||||
|
try:
|
||||||
|
yes_btn = WebDriverWait(driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[@data-testid='primaryButton']"))
|
||||||
|
)
|
||||||
|
yes_btn.click()
|
||||||
|
clicked_yes = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if clicked_yes:
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# 点击 "保存并继续"
|
||||||
|
print(" [关键] 等待 '保存并继续' (60s)...")
|
||||||
|
try:
|
||||||
|
save_btn = WebDriverWait(driver, 60).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(., '保存并继续')]"))
|
||||||
|
)
|
||||||
|
save_btn.click()
|
||||||
|
time.sleep(3)
|
||||||
|
except:
|
||||||
|
print(f" [失败] 未找到 '保存并继续'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检测成功标志
|
||||||
|
print(" [关键] 等待 '可选诊断数据' (60s)...")
|
||||||
|
try:
|
||||||
|
WebDriverWait(driver, 60).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH, "//h1[contains(., '可选诊断数据')]"))
|
||||||
|
)
|
||||||
|
print(f"√√√√√√ 成功!账号 {email} 处理完毕!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except:
|
||||||
|
print(f" [失败] 未检测到成功标志")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! 发生未知异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_process_loop(source_file, success_output, fail_output, round_name):
|
||||||
|
"""
|
||||||
|
通用处理循环:
|
||||||
|
1. 读取 source_file
|
||||||
|
2. 处理一个 -> 删一个
|
||||||
|
3. 成功 -> success_output
|
||||||
|
4. 失败 -> fail_output
|
||||||
|
"""
|
||||||
|
print(f"\n========== 启动 {round_name} ==========")
|
||||||
|
print(f"输入: {source_file}")
|
||||||
|
print(f"失败将存入: {fail_output}")
|
||||||
|
|
||||||
|
# 1. 统计总数
|
||||||
|
total_count = count_valid_accounts(source_file)
|
||||||
|
if total_count == 0:
|
||||||
|
print(f"✨ {round_name} 无待处理账号,跳过。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"📊 {round_name} 待处理任务数: {total_count}")
|
||||||
|
|
||||||
|
processed_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 2. 读取文件寻找下一个
|
||||||
|
all_lines = read_file_lines(source_file)
|
||||||
|
|
||||||
|
target_line_index = -1
|
||||||
|
email = None
|
||||||
|
password = None
|
||||||
|
|
||||||
|
for i, line in enumerate(all_lines):
|
||||||
|
e, p = parse_account(line)
|
||||||
|
if e and p:
|
||||||
|
target_line_index = i
|
||||||
|
email = e
|
||||||
|
password = p
|
||||||
|
break
|
||||||
|
|
||||||
|
# 3. 如果找不到,说明本轮结束
|
||||||
|
if target_line_index == -1:
|
||||||
|
print(f"\n🎉 {round_name} 结束!(进度: {processed_count}/{total_count})")
|
||||||
|
break
|
||||||
|
|
||||||
|
processed_count += 1
|
||||||
|
print(f"\n--------------------------------------------------")
|
||||||
|
print(f"🚀 [{round_name}] 进度: {processed_count}/{total_count} | 账号: {email}")
|
||||||
|
print(f"--------------------------------------------------")
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
try:
|
||||||
|
rotate_ip() # 换IP
|
||||||
|
|
||||||
|
# 启动浏览器
|
||||||
|
options = Options()
|
||||||
|
options.binary_location = FIREFOX_BINARY_PATH
|
||||||
|
options.add_argument("-headless") # 无头模式
|
||||||
|
options.add_argument("--width=1920")
|
||||||
|
options.add_argument("--height=1080")
|
||||||
|
options.set_preference("general.useragent.override",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0")
|
||||||
|
options.add_argument("-private")
|
||||||
|
|
||||||
|
# 性能优化参数
|
||||||
|
options.set_preference("security.webauth.webauthn", False)
|
||||||
|
options.set_preference("security.webauth.u2f", False)
|
||||||
|
options.set_preference("signon.rememberSignons", False)
|
||||||
|
|
||||||
|
service = Service(GECKODRIVER_PATH)
|
||||||
|
driver = webdriver.Firefox(service=service, options=options)
|
||||||
|
|
||||||
|
# 执行
|
||||||
|
is_success = login_process(driver, email, password)
|
||||||
|
|
||||||
|
# 结果分流
|
||||||
|
if is_success:
|
||||||
|
print(f"OOO 成功 -> 写入 {success_output}")
|
||||||
|
append_to_csv(success_output, email, password)
|
||||||
|
else:
|
||||||
|
print(f"XXX 失败 -> 写入 {fail_output}")
|
||||||
|
append_to_csv(fail_output, email, password)
|
||||||
|
fail_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! 运行异常: {e}")
|
||||||
|
append_to_csv(fail_output, email, password)
|
||||||
|
fail_count += 1
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if driver:
|
||||||
|
try:
|
||||||
|
driver.quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. 【核心】从源文件移除该行(即时保存进度)
|
||||||
|
if target_line_index != -1 and target_line_index < len(all_lines):
|
||||||
|
check_e, _ = parse_account(all_lines[target_line_index])
|
||||||
|
if check_e == email:
|
||||||
|
del all_lines[target_line_index]
|
||||||
|
rewrite_source_file(source_file, all_lines)
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 循环结束,尝试删除源文件(如果已空)
|
||||||
|
final_lines = read_file_lines(source_file)
|
||||||
|
has_valid = any(parse_account(x)[0] for x in final_lines)
|
||||||
|
if not has_valid:
|
||||||
|
print(f"🗑️ {source_file} 已处理完毕,删除文件。")
|
||||||
|
try:
|
||||||
|
os.remove(source_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return fail_count
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(FIREFOX_BINARY_PATH):
|
||||||
|
print(f"❌ 错误: 找不到 Firefox,请检查路径: {FIREFOX_BINARY_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# === 第一轮:初赛 ===
|
||||||
|
# 输入: outlook账号.csv
|
||||||
|
# 失败去向: temp_retry.csv (临时复活池)
|
||||||
|
fail_round_1 = run_process_loop(INPUT_CSV, SUCCESS_CSV, TEMP_RETRY_CSV, "第一轮(初赛)")
|
||||||
|
|
||||||
|
if fail_round_1 == 0:
|
||||||
|
print("\n🎉🎉🎉 第一轮全胜!无需复活赛。")
|
||||||
|
if os.path.exists(TEMP_RETRY_CSV): os.remove(TEMP_RETRY_CSV)
|
||||||
|
return
|
||||||
|
|
||||||
|
# === 第二轮:复活赛 ===
|
||||||
|
print(f"\n⚠️ 第一轮产生了 {fail_round_1} 个失败账号,准备进入复活赛...")
|
||||||
|
print("⏳ 等待 5 秒...")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 输入: temp_retry.csv (第一轮的失败者)
|
||||||
|
# 失败去向: failed.csv (最终失败记录)
|
||||||
|
fail_round_2 = run_process_loop(TEMP_RETRY_CSV, SUCCESS_CSV, FINAL_FAILED_CSV, "第二轮(复活赛)")
|
||||||
|
|
||||||
|
print(f"\n================ 最终统计 ================")
|
||||||
|
print(f"第一轮失败: {fail_round_1}")
|
||||||
|
print(f"第二轮救回: {fail_round_1 - fail_round_2}")
|
||||||
|
print(f"最终失败数: {fail_round_2}")
|
||||||
|
|
||||||
|
if fail_round_2 == 0:
|
||||||
|
print("🎉 复活赛全部成功!")
|
||||||
|
if os.path.exists(FINAL_FAILED_CSV): os.remove(FINAL_FAILED_CSV)
|
||||||
|
else:
|
||||||
|
print(f"😭 仍有账号失败,请查看: {FINAL_FAILED_CSV}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
78
XboxAutoRegister-main/split_csv.py
Normal file
78
XboxAutoRegister-main/split_csv.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def split_to_input_folder(input_file, rows_per_file=15):
|
||||||
|
"""
|
||||||
|
将原始 CSV 切分并存入 ./input/ 文件夹,完成后删除原文件
|
||||||
|
"""
|
||||||
|
target_dir = "input"
|
||||||
|
|
||||||
|
# 1. 检查源文件
|
||||||
|
if not os.path.exists(input_file):
|
||||||
|
print(f"❌ 错误:找不到源文件 '{input_file}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 创建 input 文件夹(如果不存在)
|
||||||
|
if not os.path.exists(target_dir):
|
||||||
|
os.makedirs(target_dir)
|
||||||
|
print(f"📁 已创建目录: {target_dir}")
|
||||||
|
|
||||||
|
# 3. 读取内容
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
with open(input_file, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
with open(input_file, 'r', encoding='gb18030') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 读取文件失败: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(lines) < 2:
|
||||||
|
print("⚠️ 文件中没有足够的账号数据。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 提取表头和数据行
|
||||||
|
header = lines[0]
|
||||||
|
data_lines = [l for l in lines[1:] if l.strip()]
|
||||||
|
total_accounts = len(data_lines)
|
||||||
|
|
||||||
|
print(f"📂 发现 {total_accounts} 个账号,准备切分...")
|
||||||
|
|
||||||
|
# 4. 开始切分并写入 input 文件夹
|
||||||
|
file_count = 0
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
|
for i in range(0, total_accounts, rows_per_file):
|
||||||
|
file_count += 1
|
||||||
|
chunk = data_lines[i: i + rows_per_file]
|
||||||
|
|
||||||
|
# 生成输出路径,例如: input/outlook账号_part_1.csv
|
||||||
|
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
||||||
|
output_filename = f"{base_name}_part_{file_count}.csv"
|
||||||
|
output_path = os.path.join(target_dir, output_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(header)
|
||||||
|
f.writelines(chunk)
|
||||||
|
success_count += 1
|
||||||
|
print(f"✅ 已存入: {output_path} ({len(chunk)} 个账号)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 写入 {output_path} 失败: {e}")
|
||||||
|
return # 安全起见,失败则不删除原文件
|
||||||
|
|
||||||
|
# 5. 彻底删除原大文件
|
||||||
|
if success_count > 0:
|
||||||
|
try:
|
||||||
|
os.remove(input_file)
|
||||||
|
print(f"\n🚀 分割完成!共生成 {success_count} 个文件并存入 '{target_dir}' 文件夹。")
|
||||||
|
print(f"🗑️ 原始文件 '{input_file}' 已删除。")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n⚠️ 子文件已生成,但删除原文件失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
split_to_input_folder('outlook账号.csv', rows_per_file=15)
|
||||||
209
calc_us_price.js
Normal file
209
calc_us_price.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
// 终极破甲器:手动追踪跳转,继承 Cookie,半路截取游戏 ID
|
||||||
|
async function resolveGameId(startUrl) {
|
||||||
|
let currentUrl = startUrl;
|
||||||
|
let cookies = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) { // 最多追踪 7 层跳转
|
||||||
|
try {
|
||||||
|
// 拼接继承的 Cookie
|
||||||
|
const cookieHeader = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||||
|
|
||||||
|
const response = await fetch(currentUrl, {
|
||||||
|
redirect: 'manual', // 关键:关闭自动跳转,改为我们手动一步步跟
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Cookie': cookieHeader
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 继承服务器发下来的 Cookie,伪装得更像真人
|
||||||
|
const setCookieHeader = response.headers.get('set-cookie');
|
||||||
|
if (setCookieHeader) {
|
||||||
|
const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_-]+\s*=)/);
|
||||||
|
for (const part of parts) {
|
||||||
|
const cookiePair = part.split(';')[0];
|
||||||
|
const [key, ...val] = cookiePair.split('=');
|
||||||
|
if (key && val) cookies[key.trim()] = val.join('=').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 300 && response.status < 400) {
|
||||||
|
const location = response.headers.get('location');
|
||||||
|
if (!location) break;
|
||||||
|
|
||||||
|
const nextUrl = location.startsWith('http') ? location : new URL(location, currentUrl).href;
|
||||||
|
|
||||||
|
// 解密 URL,剥开追踪网的层层包装
|
||||||
|
let decodedUrl = nextUrl;
|
||||||
|
try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){}
|
||||||
|
try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){}
|
||||||
|
|
||||||
|
// ⭐️ 半路截胡:只要在跳转链接里发现了 9 开头的 12 位代码,直接带走,不再往下跳!
|
||||||
|
const idMatch = decodedUrl.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i);
|
||||||
|
if (idMatch) return idMatch[1].toUpperCase();
|
||||||
|
|
||||||
|
currentUrl = nextUrl;
|
||||||
|
} else if (response.status === 200) {
|
||||||
|
const htmlText = await response.text();
|
||||||
|
|
||||||
|
// 搜刮网页源码,防备 JS 动态跳转
|
||||||
|
const htmlMatch = htmlText.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i);
|
||||||
|
if (htmlMatch) return htmlMatch[1].toUpperCase();
|
||||||
|
|
||||||
|
// 检查 Meta Refresh 自动跳转
|
||||||
|
const metaRefresh = htmlText.match(/<meta[^>]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']/i);
|
||||||
|
if (metaRefresh) {
|
||||||
|
currentUrl = metaRefresh[1].replace(/&/g, '&');
|
||||||
|
if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 JS 自动跳转
|
||||||
|
const jsRedirect = htmlText.match(/(?:window\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i);
|
||||||
|
if (jsRedirect) {
|
||||||
|
currentUrl = jsRedirect[1];
|
||||||
|
if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心查询逻辑
|
||||||
|
async function getUSGameData(startUrl) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(startUrl);
|
||||||
|
urlObj.searchParams.set('r', 'en-us');
|
||||||
|
|
||||||
|
let bigId = await resolveGameId(urlObj.toString());
|
||||||
|
|
||||||
|
if (!bigId) {
|
||||||
|
return { success: false, reason: "防爬虫拦截,未能从底层剥离出 12 位游戏代码" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${bigId}&market=US&languages=en-us&MS-CV=DUMMY.1`;
|
||||||
|
|
||||||
|
const apiResponse = await fetch(apiUrl);
|
||||||
|
const data = await apiResponse.json();
|
||||||
|
|
||||||
|
if (!data.Products || data.Products.length === 0) {
|
||||||
|
return { success: false, reason: `成功获取 ID (${bigId}),但美区查无此游戏数据` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = data.Products[0];
|
||||||
|
const gameName = product.LocalizedProperties?.[0]?.ProductTitle || "未知游戏";
|
||||||
|
|
||||||
|
let finalPrice = null;
|
||||||
|
|
||||||
|
if (!product.DisplaySkuAvailabilities || product.DisplaySkuAvailabilities.length === 0) {
|
||||||
|
return { success: false, name: gameName, reason: "该游戏没有销售规格 (无法购买)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能找买断价
|
||||||
|
for (const skuObj of product.DisplaySkuAvailabilities) {
|
||||||
|
if (skuObj.Sku && (skuObj.Sku.SkuType === 'full' || skuObj.Sku.SkuType === 'dlc' || skuObj.Sku.SkuType === 'consumable')) {
|
||||||
|
for (const avail of skuObj.Availabilities || []) {
|
||||||
|
if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) {
|
||||||
|
finalPrice = avail.OrderManagementData.Price.ListPrice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalPrice !== null) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalPrice === null) {
|
||||||
|
for (const skuObj of product.DisplaySkuAvailabilities) {
|
||||||
|
for (const avail of skuObj.Availabilities || []) {
|
||||||
|
if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) {
|
||||||
|
finalPrice = avail.OrderManagementData.Price.ListPrice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalPrice !== null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalPrice === null) {
|
||||||
|
return { success: false, name: gameName, reason: "只有 XGP 订阅试玩或捆绑包专属,无单买价格" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, name: gameName, price: finalPrice };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, reason: `发生异常: ${e.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputUrls = [];
|
||||||
|
|
||||||
|
console.log('🎮 请粘贴你的 Xbox 链接串 (支持包含回车的多行文本)。');
|
||||||
|
console.log('💡 提示:粘贴完成后,请在【新的一行】按一次回车开始计算:\n');
|
||||||
|
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine === '') {
|
||||||
|
if (inputUrls.length > 0) {
|
||||||
|
rl.close();
|
||||||
|
processUrls(inputUrls);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const splitUrls = trimmedLine.split(/(?=https?:\/\/)/).filter(u => u.startsWith('http'));
|
||||||
|
inputUrls.push(...splitUrls);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function processUrls(urls) {
|
||||||
|
console.log(`\n✅ 成功读取到 ${urls.length} 个链接,开始逐个查询美区价格...\n`);
|
||||||
|
|
||||||
|
let totalPrice = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
const failedDetails = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < urls.length; i++) {
|
||||||
|
const url = urls[i];
|
||||||
|
process.stdout.write(`[${i + 1}/${urls.length}] 正在查询... `);
|
||||||
|
|
||||||
|
const result = await getUSGameData(url);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ 成功 | ${result.name} | 现价: $${result.price}`);
|
||||||
|
totalPrice += result.price;
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
const namePart = result.name ? `[${result.name}] ` : "";
|
||||||
|
console.log(`❌ 失败 | ${namePart}原因: ${result.reason}`);
|
||||||
|
failedDetails.push({ url, reason: result.reason, name: result.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n================ 结算单 ================");
|
||||||
|
console.log(`总计识别: ${urls.length} 个游戏`);
|
||||||
|
console.log(`成功查询: ${successCount} 个游戏`);
|
||||||
|
console.log(`美元总价: $${totalPrice.toFixed(2)}`);
|
||||||
|
console.log("========================================\n");
|
||||||
|
|
||||||
|
if (failedDetails.length > 0) {
|
||||||
|
console.log("⚠️ 以下链接需要手动核查:");
|
||||||
|
failedDetails.forEach((f, idx) => {
|
||||||
|
const nameStr = f.name ? `游戏: ${f.name}\n ` : "";
|
||||||
|
console.log(`${idx + 1}. ${nameStr}原因: ${f.reason}\n 链接: ${f.url}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
geckodriver.exe
Normal file
BIN
geckodriver.exe
Normal file
Binary file not shown.
60
rotate.ps1
Normal file
60
rotate.ps1
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# ================= CONFIG =================
|
||||||
|
$API_URL = "http://127.0.0.1:9090"
|
||||||
|
$SECRET = "8130899"
|
||||||
|
$GROUP = "Rotate"
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. Force UTF-8 Output
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
Write-Host "1. Connecting to Clash..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Use WebClient (Stable & Fixes Encoding)
|
||||||
|
$wc = New-Object System.Net.WebClient
|
||||||
|
$wc.Encoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
try {
|
||||||
|
# URL
|
||||||
|
$ListUrl = "$API_URL/proxies/$GROUP"
|
||||||
|
|
||||||
|
# === Step A: Get List ===
|
||||||
|
# Add Auth Header
|
||||||
|
if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") }
|
||||||
|
|
||||||
|
$JsonContent = $wc.DownloadString($ListUrl)
|
||||||
|
|
||||||
|
$JsonObj = $JsonContent | ConvertFrom-Json
|
||||||
|
$NodeList = $JsonObj.all
|
||||||
|
|
||||||
|
if (!$NodeList -or $NodeList.Count -eq 0) {
|
||||||
|
throw "Error: Group [$GROUP] is empty!"
|
||||||
|
}
|
||||||
|
Write-Host " > Found $($NodeList.Count) nodes." -ForegroundColor Green
|
||||||
|
|
||||||
|
# === Step B: Pick Random ===
|
||||||
|
$Target = $NodeList | Get-Random
|
||||||
|
Write-Host "2. Switching to: $Target" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# === Step C: Send Switch Command ===
|
||||||
|
$Payload = @{ name = $Target } | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
# 【Critical Fix】Clear headers to remove old Auth, preventing duplicate headers
|
||||||
|
$wc.Headers.Clear()
|
||||||
|
|
||||||
|
# Re-add Headers correctly
|
||||||
|
$wc.Headers.Add("Content-Type", "application/json")
|
||||||
|
if ($SECRET -ne "") { $wc.Headers.Add("Authorization", "Bearer $SECRET") }
|
||||||
|
|
||||||
|
# Send PUT
|
||||||
|
$Response = $wc.UploadString($ListUrl, "PUT", $Payload)
|
||||||
|
|
||||||
|
Write-Host "3. [SUCCESS] Switched to: $Target" -ForegroundColor Green
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-Host "==== ERROR ====" -ForegroundColor Red
|
||||||
|
Write-Host $_.Exception.Message -ForegroundColor Yellow
|
||||||
|
if ($_.Exception.InnerException) {
|
||||||
|
Write-Host "Detail: $($_.Exception.InnerException.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
382
run_bot.py
Normal file
382
run_bot.py
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.firefox.service import Service
|
||||||
|
from selenium.webdriver.firefox.options import Options
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# === 配置区域 ===
|
||||||
|
INPUT_CSV = r'input\outlook账号_part_2.csv' # 原始输入文件
|
||||||
|
TEMP_RETRY_CSV = r'output\temp_retry.csv' # 第一轮失败存放处(复活赛的输入)
|
||||||
|
FINAL_FAILED_CSV = r'output\failed.csv' # 最终失败文件
|
||||||
|
SUCCESS_CSV = r'output\success.csv' # 成功文件
|
||||||
|
|
||||||
|
POWERSHELL_SCRIPT = r"E:\ClashScript\rotate.ps1"
|
||||||
|
GECKODRIVER_PATH = "geckodriver.exe"
|
||||||
|
FIREFOX_BINARY_PATH = r"C:\Program Files\Mozilla Firefox\firefox.exe"
|
||||||
|
|
||||||
|
|
||||||
|
# ================= 工具函数 =================
|
||||||
|
|
||||||
|
def rotate_ip():
|
||||||
|
"""切换IP"""
|
||||||
|
print(">>> [系统] 正在切换 IP (后台运行中)...")
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", POWERSHELL_SCRIPT],
|
||||||
|
check=True,
|
||||||
|
shell=True
|
||||||
|
)
|
||||||
|
print(">>> [系统] IP 切换完成,等待网络恢复...")
|
||||||
|
time.sleep(2)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"!!! IP 切换失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def append_to_csv(file_path, email, password):
|
||||||
|
"""追加写入一行 CSV"""
|
||||||
|
file_exists = os.path.exists(file_path)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'a', encoding='utf-8') as f:
|
||||||
|
if not file_exists:
|
||||||
|
f.write("卡号\n")
|
||||||
|
f.write(f"{email}----{password}\n")
|
||||||
|
f.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"写入文件 {file_path} 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_file_lines(file_path):
|
||||||
|
"""读取文件所有行"""
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.readlines()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='gb18030') as f:
|
||||||
|
return f.readlines()
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_source_file(file_path, lines):
|
||||||
|
"""重写源文件(用于删除行)"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! 更新源文件失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_account(line):
|
||||||
|
"""解析账号密码"""
|
||||||
|
line = line.strip()
|
||||||
|
if not line or "卡号" in line:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
email = ""
|
||||||
|
pwd = ""
|
||||||
|
# 支持 ---- 分割
|
||||||
|
if "----" in line:
|
||||||
|
parts = line.split("----")
|
||||||
|
email = parts[0].strip()
|
||||||
|
if len(parts) > 1:
|
||||||
|
pwd = parts[1].strip()
|
||||||
|
# 支持 , 分割
|
||||||
|
elif "," in line:
|
||||||
|
parts = line.split(",")
|
||||||
|
email = parts[0].strip()
|
||||||
|
if len(parts) > 1:
|
||||||
|
pwd = parts[1].strip()
|
||||||
|
|
||||||
|
if email and pwd:
|
||||||
|
return email, pwd
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def count_valid_accounts(file_path):
|
||||||
|
"""统计有效账号数"""
|
||||||
|
lines = read_file_lines(file_path)
|
||||||
|
count = 0
|
||||||
|
for line in lines:
|
||||||
|
e, _ = parse_account(line)
|
||||||
|
if e:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def login_process(driver, email, password):
|
||||||
|
"""
|
||||||
|
业务逻辑:登录 Xbox
|
||||||
|
返回: True(成功) / False(失败)
|
||||||
|
"""
|
||||||
|
print(f"=== 开始处理: {email} ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
driver.get("https://www.xbox.com/en-us/auth/msa?action=logIn")
|
||||||
|
|
||||||
|
# 1. 输入账号
|
||||||
|
try:
|
||||||
|
WebDriverWait(driver, 30).until(
|
||||||
|
EC.visibility_of_element_located((By.ID, "usernameEntry"))
|
||||||
|
).send_keys(email)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
try:
|
||||||
|
driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. 输入密码
|
||||||
|
WebDriverWait(driver, 30).until(
|
||||||
|
EC.visibility_of_element_located((By.NAME, "passwd"))
|
||||||
|
).send_keys(password.strip())
|
||||||
|
|
||||||
|
time.sleep(1.5)
|
||||||
|
driver.find_element(By.XPATH, "//button[@data-testid='primaryButton']").click()
|
||||||
|
|
||||||
|
# === 3. URL检测循环 ===
|
||||||
|
print(">>> 进入 URL 监控模式...")
|
||||||
|
loop_start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if time.time() - loop_start_time > 60:
|
||||||
|
print(">>> URL 检测超时 (60s),强制下一步")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_url = driver.current_url
|
||||||
|
|
||||||
|
if "xbox.com" in current_url:
|
||||||
|
print(f"√√√ 直接跳转到了 Xbox 首页,成功!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if "account.live.com" in current_url or "login.live.com" in current_url:
|
||||||
|
try:
|
||||||
|
# 处理跳过按钮
|
||||||
|
skip_btns = driver.find_elements(By.ID, "iShowSkip")
|
||||||
|
if skip_btns and skip_btns[0].is_displayed():
|
||||||
|
print(">>> 点击 '跳过'...")
|
||||||
|
skip_btns[0].click()
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理常规确认按钮
|
||||||
|
primary_btns = driver.find_elements(By.XPATH, "//button[@data-testid='primaryButton']")
|
||||||
|
if primary_btns and primary_btns[0].is_displayed():
|
||||||
|
print(f">>> 检测到主按钮,点击确认...")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
# === 4. 后续确认流程 ===
|
||||||
|
clicked_yes = False
|
||||||
|
try:
|
||||||
|
yes_btn = WebDriverWait(driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[@data-testid='primaryButton']"))
|
||||||
|
)
|
||||||
|
yes_btn.click()
|
||||||
|
clicked_yes = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if clicked_yes:
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# 点击 "保存并继续"
|
||||||
|
print(" [关键] 等待 '保存并继续' (60s)...")
|
||||||
|
try:
|
||||||
|
save_btn = WebDriverWait(driver, 60).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(., '保存并继续')]"))
|
||||||
|
)
|
||||||
|
save_btn.click()
|
||||||
|
time.sleep(3)
|
||||||
|
except:
|
||||||
|
print(f" [失败] 未找到 '保存并继续'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检测成功标志
|
||||||
|
print(" [关键] 等待 '可选诊断数据' (60s)...")
|
||||||
|
try:
|
||||||
|
WebDriverWait(driver, 60).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH, "//h1[contains(., '可选诊断数据')]"))
|
||||||
|
)
|
||||||
|
print(f"√√√√√√ 成功!账号 {email} 处理完毕!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except:
|
||||||
|
print(f" [失败] 未检测到成功标志")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! 发生未知异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_process_loop(source_file, success_output, fail_output, round_name):
|
||||||
|
"""
|
||||||
|
通用处理循环:
|
||||||
|
1. 读取 source_file
|
||||||
|
2. 处理一个 -> 删一个
|
||||||
|
3. 成功 -> success_output
|
||||||
|
4. 失败 -> fail_output
|
||||||
|
"""
|
||||||
|
print(f"\n========== 启动 {round_name} ==========")
|
||||||
|
print(f"输入: {source_file}")
|
||||||
|
print(f"失败将存入: {fail_output}")
|
||||||
|
|
||||||
|
# 1. 统计总数
|
||||||
|
total_count = count_valid_accounts(source_file)
|
||||||
|
if total_count == 0:
|
||||||
|
print(f"✨ {round_name} 无待处理账号,跳过。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"📊 {round_name} 待处理任务数: {total_count}")
|
||||||
|
|
||||||
|
processed_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 2. 读取文件寻找下一个
|
||||||
|
all_lines = read_file_lines(source_file)
|
||||||
|
|
||||||
|
target_line_index = -1
|
||||||
|
email = None
|
||||||
|
password = None
|
||||||
|
|
||||||
|
for i, line in enumerate(all_lines):
|
||||||
|
e, p = parse_account(line)
|
||||||
|
if e and p:
|
||||||
|
target_line_index = i
|
||||||
|
email = e
|
||||||
|
password = p
|
||||||
|
break
|
||||||
|
|
||||||
|
# 3. 如果找不到,说明本轮结束
|
||||||
|
if target_line_index == -1:
|
||||||
|
print(f"\n🎉 {round_name} 结束!(进度: {processed_count}/{total_count})")
|
||||||
|
break
|
||||||
|
|
||||||
|
processed_count += 1
|
||||||
|
print(f"\n--------------------------------------------------")
|
||||||
|
print(f"🚀 [{round_name}] 进度: {processed_count}/{total_count} | 账号: {email}")
|
||||||
|
print(f"--------------------------------------------------")
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
try:
|
||||||
|
rotate_ip() # 换IP
|
||||||
|
|
||||||
|
# 启动浏览器
|
||||||
|
options = Options()
|
||||||
|
options.binary_location = FIREFOX_BINARY_PATH
|
||||||
|
options.add_argument("-headless") # 无头模式
|
||||||
|
options.add_argument("--width=1920")
|
||||||
|
options.add_argument("--height=1080")
|
||||||
|
options.set_preference("general.useragent.override",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0")
|
||||||
|
options.add_argument("-private")
|
||||||
|
|
||||||
|
# 性能优化参数
|
||||||
|
options.set_preference("security.webauth.webauthn", False)
|
||||||
|
options.set_preference("security.webauth.u2f", False)
|
||||||
|
options.set_preference("signon.rememberSignons", False)
|
||||||
|
|
||||||
|
service = Service(GECKODRIVER_PATH)
|
||||||
|
driver = webdriver.Firefox(service=service, options=options)
|
||||||
|
|
||||||
|
# 执行
|
||||||
|
is_success = login_process(driver, email, password)
|
||||||
|
|
||||||
|
# 结果分流
|
||||||
|
if is_success:
|
||||||
|
print(f"OOO 成功 -> 写入 {success_output}")
|
||||||
|
append_to_csv(success_output, email, password)
|
||||||
|
else:
|
||||||
|
print(f"XXX 失败 -> 写入 {fail_output}")
|
||||||
|
append_to_csv(fail_output, email, password)
|
||||||
|
fail_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"!!! 运行异常: {e}")
|
||||||
|
append_to_csv(fail_output, email, password)
|
||||||
|
fail_count += 1
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if driver:
|
||||||
|
try:
|
||||||
|
driver.quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. 【核心】从源文件移除该行(即时保存进度)
|
||||||
|
if target_line_index != -1 and target_line_index < len(all_lines):
|
||||||
|
check_e, _ = parse_account(all_lines[target_line_index])
|
||||||
|
if check_e == email:
|
||||||
|
del all_lines[target_line_index]
|
||||||
|
rewrite_source_file(source_file, all_lines)
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 循环结束,尝试删除源文件(如果已空)
|
||||||
|
final_lines = read_file_lines(source_file)
|
||||||
|
has_valid = any(parse_account(x)[0] for x in final_lines)
|
||||||
|
if not has_valid:
|
||||||
|
print(f"🗑️ {source_file} 已处理完毕,删除文件。")
|
||||||
|
try:
|
||||||
|
os.remove(source_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return fail_count
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(FIREFOX_BINARY_PATH):
|
||||||
|
print(f"❌ 错误: 找不到 Firefox,请检查路径: {FIREFOX_BINARY_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# === 第一轮:初赛 ===
|
||||||
|
# 输入: outlook账号.csv
|
||||||
|
# 失败去向: temp_retry.csv (临时复活池)
|
||||||
|
fail_round_1 = run_process_loop(INPUT_CSV, SUCCESS_CSV, TEMP_RETRY_CSV, "第一轮(初赛)")
|
||||||
|
|
||||||
|
if fail_round_1 == 0:
|
||||||
|
print("\n🎉🎉🎉 第一轮全胜!无需复活赛。")
|
||||||
|
if os.path.exists(TEMP_RETRY_CSV): os.remove(TEMP_RETRY_CSV)
|
||||||
|
return
|
||||||
|
|
||||||
|
# === 第二轮:复活赛 ===
|
||||||
|
print(f"\n⚠️ 第一轮产生了 {fail_round_1} 个失败账号,准备进入复活赛...")
|
||||||
|
print("⏳ 等待 5 秒...")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 输入: temp_retry.csv (第一轮的失败者)
|
||||||
|
# 失败去向: failed.csv (最终失败记录)
|
||||||
|
fail_round_2 = run_process_loop(TEMP_RETRY_CSV, SUCCESS_CSV, FINAL_FAILED_CSV, "第二轮(复活赛)")
|
||||||
|
|
||||||
|
print(f"\n================ 最终统计 ================")
|
||||||
|
print(f"第一轮失败: {fail_round_1}")
|
||||||
|
print(f"第二轮救回: {fail_round_1 - fail_round_2}")
|
||||||
|
print(f"最终失败数: {fail_round_2}")
|
||||||
|
|
||||||
|
if fail_round_2 == 0:
|
||||||
|
print("🎉 复活赛全部成功!")
|
||||||
|
if os.path.exists(FINAL_FAILED_CSV): os.remove(FINAL_FAILED_CSV)
|
||||||
|
else:
|
||||||
|
print(f"😭 仍有账号失败,请查看: {FINAL_FAILED_CSV}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
78
split_csv.py
Normal file
78
split_csv.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def split_to_input_folder(input_file, rows_per_file=15):
|
||||||
|
"""
|
||||||
|
将原始 CSV 切分并存入 ./input/ 文件夹,完成后删除原文件
|
||||||
|
"""
|
||||||
|
target_dir = "input"
|
||||||
|
|
||||||
|
# 1. 检查源文件
|
||||||
|
if not os.path.exists(input_file):
|
||||||
|
print(f"❌ 错误:找不到源文件 '{input_file}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 创建 input 文件夹(如果不存在)
|
||||||
|
if not os.path.exists(target_dir):
|
||||||
|
os.makedirs(target_dir)
|
||||||
|
print(f"📁 已创建目录: {target_dir}")
|
||||||
|
|
||||||
|
# 3. 读取内容
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
with open(input_file, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
with open(input_file, 'r', encoding='gb18030') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 读取文件失败: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(lines) < 2:
|
||||||
|
print("⚠️ 文件中没有足够的账号数据。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 提取表头和数据行
|
||||||
|
header = lines[0]
|
||||||
|
data_lines = [l for l in lines[1:] if l.strip()]
|
||||||
|
total_accounts = len(data_lines)
|
||||||
|
|
||||||
|
print(f"📂 发现 {total_accounts} 个账号,准备切分...")
|
||||||
|
|
||||||
|
# 4. 开始切分并写入 input 文件夹
|
||||||
|
file_count = 0
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
|
for i in range(0, total_accounts, rows_per_file):
|
||||||
|
file_count += 1
|
||||||
|
chunk = data_lines[i: i + rows_per_file]
|
||||||
|
|
||||||
|
# 生成输出路径,例如: input/outlook账号_part_1.csv
|
||||||
|
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
||||||
|
output_filename = f"{base_name}_part_{file_count}.csv"
|
||||||
|
output_path = os.path.join(target_dir, output_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(header)
|
||||||
|
f.writelines(chunk)
|
||||||
|
success_count += 1
|
||||||
|
print(f"✅ 已存入: {output_path} ({len(chunk)} 个账号)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 写入 {output_path} 失败: {e}")
|
||||||
|
return # 安全起见,失败则不删除原文件
|
||||||
|
|
||||||
|
# 5. 彻底删除原大文件
|
||||||
|
if success_count > 0:
|
||||||
|
try:
|
||||||
|
os.remove(input_file)
|
||||||
|
print(f"\n🚀 分割完成!共生成 {success_count} 个文件并存入 '{target_dir}' 文件夹。")
|
||||||
|
print(f"🗑️ 原始文件 '{input_file}' 已删除。")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n⚠️ 子文件已生成,但删除原文件失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
split_to_input_folder('outlook账号.csv', rows_per_file=15)
|
||||||
Reference in New Issue
Block a user