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