first edition

This commit is contained in:
gene-2012 2025-05-01 14:33:06 +08:00
commit 5bed29566c
7 changed files with 371 additions and 0 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
CLIENT_ID=
UID=
CF_CLEARANCE=
C3VK=

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__/
build/
dist/
env/

42
contests.py Normal file
View 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}")

BIN
luogu.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

114
main.py Normal file
View 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
View 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
View 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 ![{author.get('name', '')}]({author.get('avatar', '')}?"
"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}")