0.1: Downloader Implemented.
This commit is contained in:
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
.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
|
||||||
|
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
*.log
|
||||||
|
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
instance/
|
||||||
|
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
output/
|
||||||
|
output_arena/
|
||||||
|
arena/
|
||||||
85
downloader/README.md
Normal file
85
downloader/README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Downloader 使用说明
|
||||||
|
|
||||||
|
## 作用
|
||||||
|
用于从 5E Arena 比赛页面抓取 iframe 内的 JSON 结果,并按需下载 demo 文件到本地目录。
|
||||||
|
|
||||||
|
## 运行环境
|
||||||
|
- Python 3.9+
|
||||||
|
- Playwright
|
||||||
|
|
||||||
|
安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install playwright
|
||||||
|
python -m playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
单场下载(默认 URL):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python downloader.py
|
||||||
|
```
|
||||||
|
|
||||||
|
指定比赛 URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python downloader.py --url https://arena.5eplay.com/data/match/g161-20260118222715609322516
|
||||||
|
```
|
||||||
|
|
||||||
|
批量下载(从文件读取 URL):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python downloader.py --url-list gamelist/match_list_2026.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
指定输出目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python downloader.py --out output_arena
|
||||||
|
```
|
||||||
|
|
||||||
|
只抓 iframe 数据或只下载 demo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python downloader.py --fetch-type iframe
|
||||||
|
python downloader.py --fetch-type demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要参数
|
||||||
|
- --url:单场比赛 URL,未传时使用默认值
|
||||||
|
- --url-list:包含多个比赛 URL 的文本文件,一行一个 URL
|
||||||
|
- --out:输出目录,默认 output_arena
|
||||||
|
- --match-name:输出目录前缀名,默认从 URL 提取
|
||||||
|
- --headless:是否无头模式,true/false,默认 false
|
||||||
|
- --timeout-ms:页面加载超时毫秒,默认 30000
|
||||||
|
- --capture-ms:主页面 JSON 监听时长毫秒,默认 5000
|
||||||
|
- --iframe-capture-ms:iframe 页面 JSON 监听时长毫秒,默认 8000
|
||||||
|
- --concurrency:并发数量,默认 3
|
||||||
|
- --goto-retries:页面打开重试次数,默认 1
|
||||||
|
- --fetch-type:抓取类型,iframe/demo/both,默认 both
|
||||||
|
|
||||||
|
## 输出结构
|
||||||
|
下载目录会以比赛编号或自定义名称创建子目录:
|
||||||
|
|
||||||
|
```
|
||||||
|
output_arena/
|
||||||
|
g161-20260118222715609322516/
|
||||||
|
iframe_network.json
|
||||||
|
g161-20260118222715609322516_de_ancient.zip
|
||||||
|
g161-20260118222715609322516_de_ancient.dem
|
||||||
|
```
|
||||||
|
|
||||||
|
## URL 列表格式
|
||||||
|
文本文件一行一个 URL,空行和以 # 开头的行会被忽略:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://arena.5eplay.com/data/match/g161-20260118222715609322516
|
||||||
|
# 注释
|
||||||
|
https://arena.5eplay.com/data/match/g161-20260118212021710292006
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
- 如果提示 Playwright 未安装,请先执行安装命令再运行脚本
|
||||||
|
- 如果下载目录已有文件,会跳过重复下载
|
||||||
416
downloader/downloader.py
Normal file
416
downloader/downloader.py
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def build_args():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--url",
|
||||||
|
default="https://arena.5eplay.com/data/match/g161-20260118222715609322516",
|
||||||
|
)
|
||||||
|
parser.add_argument("--url-list", default="")
|
||||||
|
parser.add_argument("--out", default="output_arena")
|
||||||
|
parser.add_argument("--match-name", default="")
|
||||||
|
parser.add_argument("--headless", default="false")
|
||||||
|
parser.add_argument("--timeout-ms", type=int, default=30000)
|
||||||
|
parser.add_argument("--capture-ms", type=int, default=5000)
|
||||||
|
parser.add_argument("--iframe-capture-ms", type=int, default=8000)
|
||||||
|
parser.add_argument("--concurrency", type=int, default=3)
|
||||||
|
parser.add_argument("--goto-retries", type=int, default=1)
|
||||||
|
parser.add_argument("--fetch-type", default="both", choices=["iframe", "demo", "both"])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dir(path):
|
||||||
|
Path(path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def truthy(value):
|
||||||
|
return str(value).lower() in {"1", "true", "yes", "y", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def log(message):
|
||||||
|
stamp = time.strftime("%H:%M:%S")
|
||||||
|
print(f"[{stamp}] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_folder(value):
|
||||||
|
keep = []
|
||||||
|
for ch in value:
|
||||||
|
if ch.isalnum() or ch in {"-", "_"}:
|
||||||
|
keep.append(ch)
|
||||||
|
return "".join(keep) or "match"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_match_code(url):
|
||||||
|
for part in url.split("/"):
|
||||||
|
if part.startswith("g") and "-" in part:
|
||||||
|
return part
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def read_url_list(path):
|
||||||
|
if not path:
|
||||||
|
return []
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return []
|
||||||
|
urls = []
|
||||||
|
with open(path, "r", encoding="utf-8-sig") as f:
|
||||||
|
for line in f:
|
||||||
|
value = line.strip()
|
||||||
|
if not value or value.startswith("#"):
|
||||||
|
continue
|
||||||
|
urls.append(value)
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def collect_demo_urls(value, results):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, item in value.items():
|
||||||
|
if key == "demo_url" and isinstance(item, str):
|
||||||
|
results.add(item)
|
||||||
|
collect_demo_urls(item, results)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
collect_demo_urls(item, results)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_demo_urls_from_payloads(payloads):
|
||||||
|
results = set()
|
||||||
|
for payload in payloads:
|
||||||
|
collect_demo_urls(payload, results)
|
||||||
|
return list(results)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_demo_urls_from_network(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
payload = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
return extract_demo_urls_from_payloads([payload])
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url, dest_dir):
|
||||||
|
if not url:
|
||||||
|
return ""
|
||||||
|
ensure_dir(dest_dir)
|
||||||
|
filename = os.path.basename(urlparse(url).path) or "demo.zip"
|
||||||
|
dest_path = os.path.join(dest_dir, filename)
|
||||||
|
if os.path.exists(dest_path):
|
||||||
|
return dest_path
|
||||||
|
temp_path = dest_path + ".part"
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url) as response, open(temp_path, "wb") as f:
|
||||||
|
while True:
|
||||||
|
chunk = response.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
f.write(chunk)
|
||||||
|
os.replace(temp_path, dest_path)
|
||||||
|
return dest_path
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def download_demo_from_iframe(out_dir, iframe_payloads=None):
|
||||||
|
if iframe_payloads is None:
|
||||||
|
network_path = os.path.join(out_dir, "iframe_network.json")
|
||||||
|
demo_urls = extract_demo_urls_from_network(network_path)
|
||||||
|
else:
|
||||||
|
demo_urls = extract_demo_urls_from_payloads(iframe_payloads)
|
||||||
|
downloaded = []
|
||||||
|
for url in demo_urls:
|
||||||
|
path = download_file(url, out_dir)
|
||||||
|
if path:
|
||||||
|
downloaded.append(path)
|
||||||
|
return downloaded
|
||||||
|
|
||||||
|
|
||||||
|
async def safe_goto(page, url, timeout_ms, retries):
|
||||||
|
attempt = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await page.goto(url, wait_until="domcontentloaded", timeout=timeout_ms)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
attempt += 1
|
||||||
|
if attempt > retries:
|
||||||
|
log(f"打开失败 {url} {exc}")
|
||||||
|
return False
|
||||||
|
await page.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
|
||||||
|
async def intercept_json_responses(page, sink, capture_ms):
|
||||||
|
active = True
|
||||||
|
|
||||||
|
async def handle_response(response):
|
||||||
|
try:
|
||||||
|
if not active:
|
||||||
|
return
|
||||||
|
headers = response.headers
|
||||||
|
content_type = headers.get("content-type", "")
|
||||||
|
if "application/json" in content_type or "json" in content_type:
|
||||||
|
body = await response.json()
|
||||||
|
sink.append(
|
||||||
|
{
|
||||||
|
"url": response.url,
|
||||||
|
"status": response.status,
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
page.on("response", handle_response)
|
||||||
|
await page.wait_for_timeout(capture_ms)
|
||||||
|
active = False
|
||||||
|
|
||||||
|
|
||||||
|
async def open_iframe_page(
|
||||||
|
context, iframe_url, out_dir, timeout_ms, capture_ms, goto_retries, write_iframe_network
|
||||||
|
):
|
||||||
|
iframe_page = await context.new_page()
|
||||||
|
json_sink = []
|
||||||
|
response_task = asyncio.create_task(intercept_json_responses(iframe_page, json_sink, capture_ms))
|
||||||
|
ok = await safe_goto(iframe_page, iframe_url, timeout_ms, goto_retries)
|
||||||
|
if not ok:
|
||||||
|
await response_task
|
||||||
|
await iframe_page.close()
|
||||||
|
return json_sink
|
||||||
|
try:
|
||||||
|
await iframe_page.wait_for_load_state("domcontentloaded", timeout=timeout_ms)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
clicked = False
|
||||||
|
try:
|
||||||
|
await iframe_page.wait_for_timeout(1000)
|
||||||
|
try:
|
||||||
|
await iframe_page.wait_for_selector(".ya-tab", timeout=timeout_ms)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
tab_names = ["5E Swing Score", "5E 摆动分", "摆动分", "Swing Score", "Swing", "SS"]
|
||||||
|
for name in tab_names:
|
||||||
|
locator = iframe_page.locator(".ya-tab", has_text=name)
|
||||||
|
if await locator.count() > 0:
|
||||||
|
await locator.first.scroll_into_view_if_needed()
|
||||||
|
await locator.first.click(timeout=timeout_ms, force=True)
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
locator = iframe_page.get_by_role("tab", name=name)
|
||||||
|
if await locator.count() > 0:
|
||||||
|
await locator.first.scroll_into_view_if_needed()
|
||||||
|
await locator.first.click(timeout=timeout_ms, force=True)
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
locator = iframe_page.get_by_role("button", name=name)
|
||||||
|
if await locator.count() > 0:
|
||||||
|
await locator.first.scroll_into_view_if_needed()
|
||||||
|
await locator.first.click(timeout=timeout_ms, force=True)
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
locator = iframe_page.get_by_text(name, exact=True)
|
||||||
|
if await locator.count() > 0:
|
||||||
|
await locator.first.scroll_into_view_if_needed()
|
||||||
|
await locator.first.click(timeout=timeout_ms, force=True)
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
locator = iframe_page.get_by_text(name, exact=False)
|
||||||
|
if await locator.count() > 0:
|
||||||
|
await locator.first.scroll_into_view_if_needed()
|
||||||
|
await locator.first.click(timeout=timeout_ms, force=True)
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
if not clicked:
|
||||||
|
clicked = await iframe_page.evaluate(
|
||||||
|
"""() => {
|
||||||
|
const labels = ["5E Swing Score", "5E 摆动分", "摆动分", "Swing Score", "Swing", "SS"];
|
||||||
|
const roots = [document];
|
||||||
|
const elements = [];
|
||||||
|
while (roots.length) {
|
||||||
|
const root = roots.pop();
|
||||||
|
const tree = root.querySelectorAll ? Array.from(root.querySelectorAll("*")) : [];
|
||||||
|
for (const el of tree) {
|
||||||
|
elements.push(el);
|
||||||
|
if (el.shadowRoot) roots.push(el.shadowRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const target = elements.find(el => {
|
||||||
|
const text = (el.textContent || "").trim();
|
||||||
|
if (!text) return false;
|
||||||
|
if (!labels.some(l => text.includes(l))) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
});
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({block: "center", inline: "center"});
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const x = rect.left + rect.width / 2;
|
||||||
|
const y = rect.top + rect.height / 2;
|
||||||
|
const events = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"];
|
||||||
|
for (const type of events) {
|
||||||
|
target.dispatchEvent(new MouseEvent(type, {bubbles: true, cancelable: true, clientX: x, clientY: y}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
if not clicked:
|
||||||
|
clicked = await iframe_page.evaluate(
|
||||||
|
"""() => {
|
||||||
|
const tabs = Array.from(document.querySelectorAll(".ya-tab"));
|
||||||
|
if (tabs.length === 0) return false;
|
||||||
|
const target = tabs.find(tab => {
|
||||||
|
const text = (tab.textContent || "").replace(/\\s+/g, " ").trim();
|
||||||
|
return text.includes("5E Swing Score") || text.includes("5E 摆动分") || text.includes("摆动分");
|
||||||
|
}) || tabs[tabs.length - 1];
|
||||||
|
if (!target) return false;
|
||||||
|
target.scrollIntoView({block: "center", inline: "center"});
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const x = rect.left + rect.width / 2;
|
||||||
|
const y = rect.top + rect.height / 2;
|
||||||
|
const events = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"];
|
||||||
|
for (const type of events) {
|
||||||
|
target.dispatchEvent(new MouseEvent(type, {bubbles: true, cancelable: true, clientX: x, clientY: y}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
if not clicked:
|
||||||
|
tab_locator = iframe_page.locator(".ya-tab")
|
||||||
|
if await tab_locator.count() > 0:
|
||||||
|
target = tab_locator.nth(await tab_locator.count() - 1)
|
||||||
|
box = await target.bounding_box()
|
||||||
|
if box:
|
||||||
|
await iframe_page.mouse.click(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2)
|
||||||
|
clicked = True
|
||||||
|
except Exception:
|
||||||
|
clicked = False
|
||||||
|
if clicked:
|
||||||
|
await iframe_page.wait_for_timeout(1500)
|
||||||
|
await intercept_json_responses(iframe_page, json_sink, capture_ms)
|
||||||
|
try:
|
||||||
|
await iframe_page.wait_for_load_state("networkidle", timeout=timeout_ms)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await response_task
|
||||||
|
if write_iframe_network:
|
||||||
|
with open(os.path.join(out_dir, "iframe_network.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(json_sink, f, ensure_ascii=False, indent=2)
|
||||||
|
await iframe_page.close()
|
||||||
|
return json_sink
|
||||||
|
|
||||||
|
|
||||||
|
async def run_match(pw, args, url, index, total):
|
||||||
|
base_out = os.path.abspath(args.out)
|
||||||
|
ensure_dir(base_out)
|
||||||
|
match_code = extract_match_code(url)
|
||||||
|
base_name = args.match_name.strip() or match_code or "match"
|
||||||
|
if total > 1:
|
||||||
|
suffix = match_code or str(index + 1)
|
||||||
|
if base_name != suffix:
|
||||||
|
name = f"{base_name}-{suffix}"
|
||||||
|
else:
|
||||||
|
name = base_name
|
||||||
|
else:
|
||||||
|
name = base_name
|
||||||
|
out_dir = os.path.join(base_out, safe_folder(name))
|
||||||
|
ensure_dir(out_dir)
|
||||||
|
headless = truthy(args.headless)
|
||||||
|
timeout_ms = args.timeout_ms
|
||||||
|
capture_ms = args.capture_ms
|
||||||
|
iframe_capture_ms = args.iframe_capture_ms
|
||||||
|
goto_retries = args.goto_retries
|
||||||
|
fetch_type = str(args.fetch_type or "both").lower()
|
||||||
|
want_iframe = fetch_type in {"iframe", "both"}
|
||||||
|
want_demo = fetch_type in {"demo", "both"}
|
||||||
|
|
||||||
|
browser = await pw.chromium.launch(headless=headless, slow_mo=50)
|
||||||
|
context = await browser.new_context(accept_downloads=True)
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
log(f"打开比赛页 {index + 1}/{total}")
|
||||||
|
ok = await safe_goto(page, url, timeout_ms, goto_retries)
|
||||||
|
if not ok:
|
||||||
|
await browser.close()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await page.wait_for_load_state("networkidle", timeout=timeout_ms)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
iframe_url = await page.evaluate(
|
||||||
|
"""() => {
|
||||||
|
const iframe = document.querySelector('iframe')
|
||||||
|
return iframe ? iframe.getAttribute('src') : null
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
iframe_sink = []
|
||||||
|
if iframe_url and (want_iframe or want_demo):
|
||||||
|
log(f"进入内嵌页面 {iframe_url}")
|
||||||
|
iframe_sink = await open_iframe_page(
|
||||||
|
context, iframe_url, out_dir, timeout_ms, iframe_capture_ms, goto_retries, want_iframe
|
||||||
|
)
|
||||||
|
|
||||||
|
if want_demo:
|
||||||
|
downloaded = download_demo_from_iframe(out_dir, iframe_sink if iframe_sink else None)
|
||||||
|
if downloaded:
|
||||||
|
log(f"已下载 demo: {len(downloaded)}")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_match_with_semaphore(semaphore, pw, args, url, index, total):
|
||||||
|
async with semaphore:
|
||||||
|
try:
|
||||||
|
await run_match(pw, args, url, index, total)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"任务失败 {url} {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
args = build_args().parse_args()
|
||||||
|
try:
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
except Exception:
|
||||||
|
print("Playwright 未安装,请先安装: python -m pip install playwright && python -m playwright install")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
urls = read_url_list(args.url_list)
|
||||||
|
if not urls:
|
||||||
|
urls = [args.url]
|
||||||
|
|
||||||
|
async with async_playwright() as pw:
|
||||||
|
concurrency = max(1, int(args.concurrency or 1))
|
||||||
|
semaphore = asyncio.Semaphore(concurrency)
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(run_match_with_semaphore(semaphore, pw, args, url, index, len(urls)))
|
||||||
|
for index, url in enumerate(urls)
|
||||||
|
]
|
||||||
|
if tasks:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
log("完成")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
47
downloader/gamelist/match_list_2026.txt
Normal file
47
downloader/gamelist/match_list_2026.txt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
https://arena.5eplay.com/data/match/g161-20260118222715609322516
|
||||||
|
https://arena.5eplay.com/data/match/g161-20260118215640650728700
|
||||||
|
https://arena.5eplay.com/data/match/g161-20260118212021710292006
|
||||||
|
https://arena.5eplay.com/data/match/g161-20260118202243599083093
|
||||||
|
https://arena.5eplay.com/data/match/g161-20260118195105311656229
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251227204147532432472
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251224212749300709409
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251224204010707719140
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251130213145958206941
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251130210025158075163
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251130202604606424766
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251121221256211567778
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251121213002842778327
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251121204534531429599
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251120225541418811147
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251120215752770546182
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251120212307767251203
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251120204855361553501
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251119224637611106951
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251119220301211708132
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251119212237018904830
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251113221747008211552
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251113213926308316564
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251113205020504700482
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251222211554225486531
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251222204652101389654
|
||||||
|
https://arena.5eplay.com/data/match/g161-20251213224016824985377
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251031232529838133039
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251031222014957918049
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251031214157458692406
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251031210748072610729
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251030222146222677830
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251030213304728467793
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251030205820720066790
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251029215222528748730
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251029223307353807510
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251027231404235379274
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251028213320660376574
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251028221342615577217
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251027223836601395494
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251027215238222152932
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251027210631831497570
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251025230600131718164
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251025213429016677232
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251025210415433542948
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251025203218851223471
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251025195106739608572
|
||||||
48
downloader/gamelist/match_list_before_0913.txt
Normal file
48
downloader/gamelist/match_list_before_0913.txt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
https://arena.5eplay.com/data/match/g161-n-20250913220512141946989
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250913213107816808164
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250913205742414202329
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250827221331843083555
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250817225217269787769
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250817221445650638471
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250817213333244382504
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250817204703953154600
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250816230720637945240
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250816223209989476278
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250816215000584183999
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250810000507840654837
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250809232857469499842
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250809224113646082440
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250805224735339106659
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250805221246768259380
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250805213044671459165
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250729224539870249509
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250729221017411617812
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250726230753271236792
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250726222011747090952
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250726213213252258654
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250726210250462966112
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250726202108438713376
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250708223526502973398
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250629224717702923977
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250629221632707741592
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250629214005898851985
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625233517097081378
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625233517097081378
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625233517097081378
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625225637201689118
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625220051296084673
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625212340196552999
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250625204055608218332
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250624224559896152236
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250624221215091912088
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250624213649835216392
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250329215431484950790
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250404102704857102834
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250404110639758722580
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250404113912053638456
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250404124315256663822
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250418212920157087385
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250423212911381760420
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250423221015836808051
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250505212901236776044
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250505210156662230606
|
||||||
23
downloader/gamelist/match_list_before_1025.txt
Normal file
23
downloader/gamelist/match_list_before_1025.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
https://arena.5eplay.com/data/match/g161-n-20251012225545036903374
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251012220151962958852
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251012220151962958852
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251012211416764734636
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003170554517340798
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251006130250489051437
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251006122000914844735
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251005185512726501951
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251005182335443677587
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003192720361556278
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003185649812523095
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003182922419032199
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003175831422195120
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003170554517340798
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20251003161937522875514
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250913220512141946989
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250913205742414202329
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250913213107816808164
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250729221017411617812
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250816215000584183999
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250816223209989476278
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250810000507840654837
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250809224113646082440
|
||||||
73
downloader/gamelist/match_list_early_2025.txt
Normal file
73
downloader/gamelist/match_list_early_2025.txt
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
https://arena.5eplay.com/data/match/g161-n-20250103201445137702215
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250103203331443454143
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250103211644789725355
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250105000114157444753
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250105004102938304243
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250109205825766219524
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250109214524585140725
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250109222317807381679
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250109225725438125765
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250110000800438550163
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250115210950870494621
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250115214227730237642
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250115222151238089028
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250115224837069753503
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250119201843917352000
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250119205646572572033
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250119214057134288558
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250119221209668234775
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250212194801048099163
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250212204500213129957
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250212211417251548261
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250212224659856768179
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250212232524442488205
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250214164955786323546
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250214172202090993964
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250214174757585798948
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250215204022294779045
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250215211846894242128
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250217202409685923399
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250217205402386409635
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250217212436510051874
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250217220552927034811
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250218160114138124831
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250218162428685487349
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250218165542404622024
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250218211240395943608
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250218214056585823614
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250218221355585818088
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250221200134537532083
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250221202611846934043
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250221205801951388015
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250221212924852778522
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250221220520358691141
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250224190530943492421
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250224192756599598828
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250224211003642995175
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250224214246751262216
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250224221018957359594
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250227201006443002972
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250227204400163237739
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250227211802698292906
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250301200647442341789
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250301204325972686590
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250301211319138257939
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250301214842394094370
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250301221920464983026
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250301225228585801638
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250302154200385322147
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250302161030995093939
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250302165056088320401
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250306212929308811302
|
||||||
|
https://arena.5eplay.com/data/match/g161-20250306220339391113038
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250307202729007357677
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250307205954649678046
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250307214542342522277
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250307220959454626136
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250311202342544577031
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250311220347557866712
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250311212924644001588
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250311205101348741496
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250313200635729548487
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250313204903360834136
|
||||||
|
https://arena.5eplay.com/data/match/g161-n-20250313211821260060301
|
||||||
Reference in New Issue
Block a user