commit 5bed29566c2009b6246047bc30fce404f3fb51a8 Author: gene-2012 Date: Thu May 1 14:33:06 2025 +0800 first edition diff --git a/.env b/.env new file mode 100644 index 0000000..d59f8eb --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +CLIENT_ID= +UID= +CF_CLEARANCE= +C3VK= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81bb05f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +build/ +dist/ +env/ \ No newline at end of file diff --git a/contests.py b/contests.py new file mode 100644 index 0000000..83cf44b --- /dev/null +++ b/contests.py @@ -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}") \ No newline at end of file diff --git a/luogu.ico b/luogu.ico new file mode 100644 index 0000000..80c5505 Binary files /dev/null and b/luogu.ico differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..37fbe21 --- /dev/null +++ b/main.py @@ -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]") \ No newline at end of file diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..e505279 --- /dev/null +++ b/main.spec @@ -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'], +) diff --git a/models.py b/models.py new file mode 100644 index 0000000..c5fd67b --- /dev/null +++ b/models.py @@ -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}") \ No newline at end of file