first edition
This commit is contained in:
commit
5bed29566c
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
env/
|
42
contests.py
Normal file
42
contests.py
Normal file
@ -0,0 +1,42 @@
|
||||
class Contest:
|
||||
def __init__(self, contest_id, unrated=False):
|
||||
self.contest_id = contest_id
|
||||
self.unrated = unrated
|
||||
self.contest_url = f"https://luogu.com.cn/contest/{contest_id}"
|
||||
self.join_url = f"https://www.luogu.com.cn/fe/api/contest/join/{contest_id}"
|
||||
|
||||
# 动态设置 Referer
|
||||
self.headers = BASE_HEADERS.copy()
|
||||
self.headers['Referer'] = self.contest_url
|
||||
|
||||
self.csrf_token = self._get_csrf_token()
|
||||
self._join_contest()
|
||||
|
||||
def _get_csrf_token(self):
|
||||
resp = requests.get(
|
||||
self.contest_url,
|
||||
headers=self.headers,
|
||||
cookies=cookies
|
||||
)
|
||||
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||
meta_tag = soup.find('meta', {'name': 'csrf-token'})
|
||||
if meta_tag:
|
||||
return meta_tag.get('content')
|
||||
else:
|
||||
raise ValueError("无法找到 csrf-token")
|
||||
|
||||
def _join_contest(self):
|
||||
headers = self.headers.copy()
|
||||
headers['X-CSRF-Token'] = self.csrf_token
|
||||
|
||||
data = {"unrated": str(self.unrated).lower()}
|
||||
|
||||
resp = requests.post(
|
||||
self.join_url,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
json=data
|
||||
)
|
||||
|
||||
print(f"Status Code: {resp.status_code}")
|
||||
print(f"Response Text: {resp.text}")
|
114
main.py
Normal file
114
main.py
Normal file
@ -0,0 +1,114 @@
|
||||
import argparse
|
||||
import os
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from models import Problem
|
||||
|
||||
console = Console()
|
||||
|
||||
def parse_args():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Luogu CLI 工具")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# lgcli parse PID
|
||||
parse_parser = subparsers.add_parser("parse", help="下载指定题目的题面、样例和题解")
|
||||
parse_parser.add_argument("pid", help="题目编号,例如 P1145")
|
||||
|
||||
# lgcli view PID
|
||||
view_parser = subparsers.add_parser("view", help="查看题目描述")
|
||||
view_parser.add_argument("pid", help="题目编号,例如 P1145")
|
||||
|
||||
# lgcli solution PID
|
||||
solution_parser = subparsers.add_parser("solution", help="查看题解列表或具体题解内容")
|
||||
solution_parser.add_argument("pid", help="题目编号,例如 P1145")
|
||||
solution_parser.add_argument("--id", help="指定题解文件名(不带.md)", default=None)
|
||||
|
||||
return parser.parse_args()
|
||||
except argparse.ArgumentError as e:
|
||||
console.print(f"[red]参数解析错误:{e}[/red]")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
console.print(f"[red]未知错误:{e}[/red]")
|
||||
exit(1)
|
||||
|
||||
|
||||
def cmd_view(pid):
|
||||
filename = f"{pid}/{pid}.md"
|
||||
if not os.path.exists(filename):
|
||||
console.print(f"[red]错误:{pid} 尚未解析,请先运行 lgcli parse {pid}[/red]")
|
||||
return
|
||||
|
||||
try:
|
||||
console.clear()
|
||||
with open(filename, "r", encoding="utf8") as f:
|
||||
content = f.read()
|
||||
console.print(Markdown(content))
|
||||
except IOError as e:
|
||||
console.print(f"[red]读取文件失败:{e}[/red]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]渲染Markdown失败:{e}[/red]")
|
||||
|
||||
|
||||
def cmd_solution(pid):
|
||||
folder = f"{pid}/solutions"
|
||||
if not os.path.exists(folder):
|
||||
console.print(f"[red]错误:{pid} 题解尚未下载,请先运行 lgcli parse {pid}[/red]")
|
||||
return
|
||||
|
||||
try:
|
||||
files = [f for f in os.listdir(folder) if f.endswith(".md")]
|
||||
if not files:
|
||||
console.print("[yellow]暂无题解。[/yellow]")
|
||||
return
|
||||
|
||||
if args.id is None:
|
||||
console.clear()
|
||||
console.print("[bold blue]可用题解:[/bold blue]")
|
||||
for i, fname in enumerate(files):
|
||||
console.print(f"{i+1}. {fname[:-3]}")
|
||||
return
|
||||
|
||||
selected_file = f"{args.id}.md"
|
||||
full_path = os.path.join(folder, selected_file)
|
||||
if not os.path.exists(full_path):
|
||||
console.print(f"[red]错误:题解 {selected_file} 不存在。请检查名称。[/red]")
|
||||
return
|
||||
|
||||
console.clear()
|
||||
with open(full_path, "r", encoding="utf8") as f:
|
||||
content = f.read()
|
||||
console.print(Markdown(content))
|
||||
except IOError as e:
|
||||
console.print(f"[red]读取题解文件失败:{e}[/red]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]渲染Markdown失败:{e}[/red]")
|
||||
|
||||
|
||||
def cmd_parse(pid):
|
||||
try:
|
||||
p = Problem(pid)
|
||||
p.gen_all()
|
||||
console.print(f"[green]✅ 题目 {pid} 已成功下载到目录 '{pid}'[/green]")
|
||||
except ConnectionError as e:
|
||||
console.print(f"[red]网络连接错误:{e}[/red]")
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]数据解析错误:{e}[/red]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]未知错误:{e}[/red]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
args = parse_args()
|
||||
|
||||
if args.command == "parse":
|
||||
cmd_parse(args.pid)
|
||||
elif args.command == "view":
|
||||
cmd_view(args.pid)
|
||||
elif args.command == "solution":
|
||||
cmd_solution(args.pid)
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[yellow]用户中断操作。[/yellow]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]发生致命错误:{e}[/red]")
|
39
main.spec
Normal file
39
main.spec
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='main',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['luogu.ico'],
|
||||
)
|
168
models.py
Normal file
168
models.py
Normal file
@ -0,0 +1,168 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from dotenv import load_dotenv
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# 加载 .env 文件
|
||||
load_dotenv()
|
||||
|
||||
# 请求头与 Cookie
|
||||
headers = {
|
||||
'User-Agent': (
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) '
|
||||
'Gecko/20100101 Firefox/115.0'
|
||||
)
|
||||
}
|
||||
|
||||
cookies = {
|
||||
"__client_id": os.getenv("CLIENT_ID"),
|
||||
"_uid": os.getenv("UID"),
|
||||
"cf_clearance": os.getenv("CF_CLEARANCE"),
|
||||
"C3VK": os.getenv("C3VK")
|
||||
}
|
||||
|
||||
BASE_HEADERS = {
|
||||
'User-Agent': (
|
||||
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36'
|
||||
),
|
||||
'Origin': 'https://www.luogu.com.cn',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
|
||||
|
||||
class Problem:
|
||||
def __init__(self, problem_id):
|
||||
self.problem_id = problem_id
|
||||
self.problem_url = f"https://luogu.com.cn/problem/{problem_id}"
|
||||
self.html = None
|
||||
self.json = None
|
||||
self.title = ""
|
||||
self.background = ""
|
||||
self.description = ""
|
||||
self.formatI = ""
|
||||
self.formatO = ""
|
||||
self.hint = ""
|
||||
self.samples = []
|
||||
|
||||
try:
|
||||
resp = requests.get(self.problem_url, headers=headers, cookies=cookies, timeout=10)
|
||||
resp.raise_for_status()
|
||||
self.html = resp.text
|
||||
except RequestException as e:
|
||||
raise ConnectionError(f"无法访问题目页面:{self.problem_url},错误:{e}")
|
||||
|
||||
try:
|
||||
self.json = self._extract_json_from_html(self.html)['data']['problem']
|
||||
except (KeyError, ValueError) as e:
|
||||
raise RuntimeError(f"解析题目数据失败:{e}")
|
||||
|
||||
self.title = self.json.get('title', '')
|
||||
self.background = self.json['contenu'].get('background', '')
|
||||
self.description = self.json['contenu'].get('description', '')
|
||||
self.formatI = self.json['contenu'].get('formatI', '')
|
||||
self.formatO = self.json['contenu'].get('formatO', '')
|
||||
self.hint = self.json['contenu'].get('hint', '')
|
||||
self.samples = self.json.get('samples', [])
|
||||
|
||||
def _extract_json_from_html(self, html_content):
|
||||
try:
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
script_tag = soup.find('script', {'id': 'lentille-context', 'type': 'application/json'})
|
||||
if not script_tag:
|
||||
raise ValueError("无法找到指定的JSON脚本标签")
|
||||
return json.loads(script_tag.string)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"JSON 解析失败:{e}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"HTML 解析失败:{e}")
|
||||
|
||||
def gen_problem_md(self):
|
||||
try:
|
||||
md = f"# {self.title}"
|
||||
if self.background:
|
||||
md += f"\n\n## 【题目背景】\n{self.background}"
|
||||
if self.description:
|
||||
md += f"\n\n## 【题目描述】\n{self.description}"
|
||||
if self.formatI:
|
||||
md += f"\n\n## 【输入格式】\n{self.formatI}"
|
||||
if self.formatO:
|
||||
md += f"\n\n## 【输出格式】\n{self.formatO}"
|
||||
if self.hint:
|
||||
md += f"\n\n## 【提示】\n{self.hint}"
|
||||
|
||||
with open(f"{self.problem_id}.md", "w", encoding="utf8") as f:
|
||||
f.write(md)
|
||||
except IOError as e:
|
||||
raise RuntimeError(f"写入题面Markdown文件失败:{e}")
|
||||
|
||||
def gen_samples(self):
|
||||
try:
|
||||
os.makedirs("samples", exist_ok=True)
|
||||
cnt = 1
|
||||
for sample in self.samples:
|
||||
with open(f"samples/sample_{cnt}.in", "w", encoding="utf8") as f:
|
||||
f.write(sample[0])
|
||||
with open(f"samples/sample_{cnt}.out", "w", encoding="utf8") as f:
|
||||
f.write(sample[1])
|
||||
cnt += 1
|
||||
except (IOError, IndexError) as e:
|
||||
raise RuntimeError(f"写入样例文件失败:{e}")
|
||||
|
||||
def gen_solutions(self):
|
||||
solution_url = f"https://luogu.com.cn/problem/solution/{self.problem_id}"
|
||||
try:
|
||||
resp = requests.get(solution_url, headers=headers, cookies=cookies, timeout=10)
|
||||
resp.raise_for_status()
|
||||
except RequestException as e:
|
||||
raise ConnectionError(f"无法访问题解页面:{solution_url},错误:{e}")
|
||||
|
||||
try:
|
||||
solutions_data = self._extract_json_from_html(resp.text).get("data", {})
|
||||
solutions = solutions_data.get("solutions", {}).get("result", [])
|
||||
except (KeyError, ValueError) as e:
|
||||
raise RuntimeError(f"解析题解数据失败:{e}")
|
||||
|
||||
try:
|
||||
os.makedirs("solutions", exist_ok=True)
|
||||
for solution in solutions:
|
||||
author = solution.get('author', {})
|
||||
title = solution.get('title', '未命名题解')
|
||||
lid = solution.get('lid', 'unknown')
|
||||
content = solution.get('content', '')
|
||||
|
||||
markdown_content = (
|
||||
f"# {title}\n"
|
||||
f"> By }?"
|
||||
"x-oss-process=image/resize,m_fixed,h_30,w_30,image/circle,r_100/format,png)"
|
||||
f"[{author.get('name', '')}](https://luogu.com.cn/user/{author.get('uid', '')})"
|
||||
"\n\n"
|
||||
f"{content}"
|
||||
)
|
||||
|
||||
filename = f"solutions/{title}_{lid}.md"
|
||||
with open(filename, "w", encoding="utf8") as f:
|
||||
f.write(markdown_content)
|
||||
except IOError as e:
|
||||
raise RuntimeError(f"写入题解文件失败:{e}")
|
||||
|
||||
def gen_all(self):
|
||||
try:
|
||||
os.makedirs(self.problem_id, exist_ok=True)
|
||||
os.chdir(self.problem_id)
|
||||
self.gen_problem_md()
|
||||
self.gen_samples()
|
||||
self.gen_solutions()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"生成全部内容失败:{e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
p = Problem("P1145")
|
||||
p.gen_all()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 发生错误:{e}")
|
Loading…
x
Reference in New Issue
Block a user