从登录到 Markdown:用 60 行 Python 批量抓取 Hydro OJ 题目

作者:jason
日期:2025-07-26
版权:wanwusangzhi 2024-2025
项目地址:https://github.com/wanwusangzhigit/hydro


1. 背景故事

很多人第一次刷题时,都会把题目复制到本地笔记软件里做草稿。
动手复制粘贴几次后,你会立刻意识到:
“公式全丢了,图片全是外链,表格惨不忍睹。”
“主要无法用AI解题”

于是就有了今天这 60 行小脚本:
自动登录 OJ → 拉取指定题目 → 把 KaTeX 公式还原成 LaTeX → 输出 Markdown 文件。
以后写题解、做笔记,再也不用对着网页敲公式了。


2. 整体思路

一句话概括:
“用 requests 做登录,用 BeautifulSoup 找内容,用 html2text 转 Markdown。”

流程图:

┌--------------┐
│ 用户输入      │
│ BASE_URL      │
│ Problem_ID    │
│ USERNAME/PWD  │
└------┬--------┘
       │
┌------┴--------┐
│ 1. 拉登录页   │  ← GET /login
│ 取 csrf-token │
└------┬--------┘
       │
┌------┴--------┐
│ 2. 提交登录   │  ← POST /login
│ 302 跳转即成功│
└------┬--------┘
       │
┌------┴--------┐
│ 3. 拉题目页   │  ← GET /p/{Problem_ID}
│ 替换 KaTeX    │
└------┬--------┘
       │
┌------┴--------┐
│ 4. 转 Markdown│  ← html2text
│ 写入 p.md     │
└--------------┘

3. 关键代码拆解

3.1 会话与伪装 UA

s = requests.Session()
s.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/120.0.0.0 Safari/537.36'
})
  • 使用 Session 可自动携带后续 Cookie,不用手动维护。
  • UA 伪装成桌面 Chrome,避免服务器直接拒绝“非浏览器”请求。

3.2 动态获取 CSRF-token

login_html = s.get(LOGIN_URL).text
soup = BeautifulSoup(login_html, 'lxml')
csrf = (soup.find('meta', attrs={'name': 'csrf-token'}) or
        soup.find('input', attrs={'name': re.compile(r'csrf|_csrf')}))
if csrf:
    csrf = csrf.get('content') or csrf['value']
else:
    csrf = ''
  • 兼容两种常见写法:
    • <meta name="csrf-token" content="xxx">
    • <input type="hidden" name="_csrf" value="xxx">
  • 如果站点没开 CSRF,就留空字符串,不报错。

3.3 登录并检查重定向

resp = s.post(LOGIN_URL, data={
    'uname': USERNAME,
    'password': PASSWORD,
    '_csrf': csrf
}, allow_redirects=False)

if resp.status_code != 302:
    raise RuntimeError('登录失败,请检查账号密码或抓包核对字段名')
  • allow_redirects=False
    大多数 OJ 登录成功后 302 跳转到首页 / 个人页,用这一特征即可判断是否成功。
  • 若返回 200,多半是密码错误或字段名不对。

3.4 把 KaTeX 还原成 LaTeX

for katex_span in soup.find_all('span', class_='katex'):
    annotation = katex_span.find('annotation')
    if annotation:
        katex_span.replace_with(f"${annotation.text}$")
  • KaTeX 渲染后的 HTML 会把公式藏在 <annotation encoding="application/x-tex"> 里。
  • 取出文本,再用 $...$ 包裹,Markdown 就能被本地渲染器正确识别为行内公式。

3.5 提取题目主体

problem_content = soup.find('div', class_='problem-content')
  • 不同 OJ 的类名可能不一样,按需修改。
  • 找到后直接传给 html2text

3.6 HTML → Markdown

h = html2text.HTML2Text()
h.ignore_links = False
h.bypass_tables = False
h.ignore_images = False
h.body_width = 0
markdown = h.handle(str(problem_content))
  • 关闭自动换行,防止长公式被截断。
  • 保留链接、图片、表格,保证题目完整性。

4.完整代码

#!/usr/bin/env python3
"""
by jason
2025-07-26
copyright wanwusangzhi 2024-2025
"""
import requests, re, sys
from bs4 import BeautifulSoup
import html2text

# ========== 按需修改 ==========
BASE_URL = input("")       # 你的站点根域名
LOGIN_URL = f'{BASE_URL}/login' # login page
Problem_ID = input("")
HOME_URL = f'{BASE_URL}/p/{Problem_ID}'
USERNAME  = input("")
PASSWORD  = input("")
# ===============================

def main():
    s = requests.Session()
    s.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/120.0.0.0 Safari/537.36'
    })

    # 1. 拉登录页,取 csrf
    login_html = s.get(LOGIN_URL).text
    soup = BeautifulSoup(login_html, 'lxml')
    csrf = (soup.find('meta', attrs={'name': 'csrf-token'}) or
            soup.find('input', attrs={'name': re.compile(r'csrf|_csrf')}))
    if csrf:
        csrf = csrf.get('content') or csrf['value']
    else:
        csrf = ''          # 站点没开 csrf 验证

    # 2. 提交账号密码
    resp = s.post(LOGIN_URL, data={
        'uname': USERNAME,
        'password': PASSWORD,
        '_csrf': csrf
    }, allow_redirects=False)

    if resp.status_code != 302:
        raise RuntimeError('登录失败,请检查账号密码或抓包核对字段名')

    # 3. 登录成功后拿首页
    home_html = s.get(HOME_URL).text
    soup = BeautifulSoup(home_html, 'html.parser')
    for katex_span in soup.find_all('span', class_='katex'):
        annotation = katex_span.find('annotation')
        if annotation:
            katex_span.replace_with(f"${annotation.text}$")  # 可选:加 $ 变成 LaTeX 公式
        # 查找class="problem-content"的div
    problem_content = soup.find('div', class_='problem-content')
    html=problem_content
    print(html)
    # 创建 html2text 处理器
    h = html2text.HTML2Text()
    h.ignore_links = False  # 不忽略链接
    h.bypass_tables = False  # 不忽略表格
    h.ignore_images = False  # 不忽略图片
    h.body_width = 0  # 不自动换行
    		# 转换 HTML 为 Markdown
    markdown = h.handle(str(html))
    print(markdown)		
    with open("p.md","w",encoding='utf-8') as f:
        f.write(markdown)

if __name__ == '__main__':
    main()

5. 运行示例

$ python3 grab.py
https://hydro.ac
H1001
username
password

程序会在当前目录生成 p.md,内容示例:

### 1000. A + B Problem

#### Description
Calculate $a+b$.

#### Input
Two integers $a, b$ ($0 \le a, b \le 10^9$).

#### Output
Output $a+b$.

#### Sample Input

1 2


#### Sample Output

3


6. 小结

60 行代码,解决了“复制题目丢格式”的痛点。
核心思路只有三步:登录 → 解析 → 转换
把它跑通后,你就有了一套完全属于自己的题库快照,离线刷题、写题解、做 LaTeX 笔记都方便很多。

Happy hacking & happy coding!

posted @ 2025-07-27 17:16  爱玩游戏的jason  阅读(232)  评论(0)    收藏  举报