作者: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!

从登录到 Markdown:用 60 行 Python 批量抓取 Hydro OJ 题目的更多相关文章

  1. 哪吒票房超复联4,100行python代码抓取豆瓣短评,看看网友怎么说

    <哪吒之魔童降世>这部国产动画巅峰之作,上映快一个月时间,票房口碑双丰收. 迄今已有超一亿人次观看,票房达到42.39亿元,超过复联4,跻身中国票房纪录第三名,仅次于<战狼2> ...

  2. python 爬虫抓取心得

    quanwei9958 转自 python 爬虫抓取心得分享 urllib.quote('要编码的字符串') 如果你要在url请求里面放入中文,对相应的中文进行编码的话,可以用: urllib.quo ...

  3. python requests抓取NBA球员数据,pandas进行数据分析,echarts进行可视化 (前言)

    python requests抓取NBA球员数据,pandas进行数据分析,echarts进行可视化 (前言) 感觉要总结总结了,希望这次能写个系列文章分享分享心得,和大神们交流交流,提升提升. 因为 ...

  4. python 处理抓取网页乱码

    python 处理抓取网页乱码问题一招鲜   相信用python的人一定在抓取网页时,被编码问题弄晕过一阵 前几天写了一个测试网页的小脚本,并查找是否包含指定的信息. 在html = urllib2. ...

  5. Python爬虫----抓取豆瓣电影Top250

    有了上次利用python爬虫抓取糗事百科的经验,这次自己动手写了个爬虫抓取豆瓣电影Top250的简要信息. 1.观察url 首先观察一下网址的结构 http://movie.douban.com/to ...

  6. Python爬虫抓取东方财富网股票数据并实现MySQL数据库存储

    Python爬虫可以说是好玩又好用了.现想利用Python爬取网页股票数据保存到本地csv数据文件中,同时想把股票数据保存到MySQL数据库中.需求有了,剩下的就是实现了. 在开始之前,保证已经安装好 ...

  7. python Web抓取(一)[没写完]

    需要的模块: python web抓取通过: webbrowser:是python自带的,打开浏览器获取指定页面 requests:从因特网上下载文件和网页 Beautiful Soup:解析HTML ...

  8. python数据抓取分析(python + mongodb)

    分享点干货!!! Python数据抓取分析 编程模块:requests,lxml,pymongo,time,BeautifulSoup 首先获取所有产品的分类网址: def step(): try: ...

  9. python实现列表页数据的批量抓取练手练手的

    python实现列表页数据的批量抓取,练手的,下回带分页的 #!/usr/bin/env python # coding=utf-8 import requests from bs4 import B ...

  10. python 抓取网上OJ试题

    学校工作需要,需架设一台内网OJ服务器,采用了开源的hustoj.试题下载了hustoj的freeprblem的xml文件.导入时出现很多错误,不知什么原因.另外要将历年noip复赛试题加上去,但苦于 ...

随机推荐

  1. DOM基础操作小结

    最近一个多月都在看看前端的内容. 因为这半年都在做BI嘛, 感觉有些东西呀, 还是用前端来做会更加能满足客户的需求, 于是呢, 就网上找了一些资料, 学习了一波前端基础. 这里也是做个简单的笔记, 关 ...

  2. SQL 日常练习 (十六)

    最近接触了一波 RPA, 可以用来做一些数据采集的事情, 或者任意控制电脑上的软件, 感觉上是挺厉害的, 但我就是不想用, 尽管我尝试了一波, 最后还是放弃 了, 我还是喜欢纯代码的工作方式, 最为讨 ...

  3. React并发机制揭秘

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...

  4. RPC实战与核心原理之负载均衡

    负载均衡:节点负载差距这么大,为什么收到的流量还一样? 回顾 "多场景的路由选择",其核心就是"如何根据不同的场景控制选择合适的目标机器" 问题 RPC 框架有 ...

  5. WPF在Visual studio中打包,发布注意事项

    右键项目,发布的时候,需要选择独立,否则依赖库的话,有可能客户端没有.net core或.net framework,那么就会报错,提醒对方下载. 今天发现,VS 2022,直接Debug运行后,Re ...

  6. java常用包的介绍

    java.* java.lang    包含Java程序所需要的基本类(默认导入) java.util         包含丰富的常用工具类,如集合框架.事件模式.日期时间等 java.math   ...

  7. 必看!手把手教你玩转Dify的3大核心工具!

    Dify 中的工具是指其平台内置或支持集成的功能插件,用于扩展 AI 应用的能力. 1.工具作用 扩展 LLM 的能力:工具可以赋予 LLM 连接外部世界的能力,例如联网搜索.科学计算.绘制图片等.例 ...

  8. centos 7 安装 netcoresdk 和Nginx 并发布netcore

    微软官网的yum安装: 打开linux终端程序 netcore sdk 地址https://dotnet.microsoft.com/download/linux-package-manager/ce ...

  9. 【2020.11.20提高组模拟】祖先(ancestor) 题解

    [2020.11.20提高组模拟]祖先(ancestor) 题解 题目描述 对于每个\(i\),它都要往前面拜访它的祖先.对于\(i\)之前的编号为\(j\)的节点,如果要拜访的话需要满足对于\(\f ...

  10. Agent-Memory 概述

    1. 结构化记忆生成 Chunks(块状记忆): 将文档分割成固定长度的连续文本段 适合处理长上下文任务(如阅读理解) 优点:简单易用,适合存储大量信息 缺点:可能缺乏语义关联性 Knowledge ...