找回密码
 立即注册
首页 业界区 安全 一行命令自动提交:用 Python 把本地代码秒交评测机 ...

一行命令自动提交:用 Python 把本地代码秒交评测机

静轾 前天 16:07
作者:jason
日期:2025-07-27
1. 为什么需要“命令行交题”?

刷算法题时,最常见的动作是:

  • 本地写完代码
  • 打开浏览器 → 登录 → 选语言 → 选文件 → 点击 Submit
  • 等待结果 →回到 IDE 继续 Debug
整个链路里有大量机械点击。
如果能把第 2 步压缩成 一条命令,效率立刻翻倍,而且还能:

  • 在 Vim/VS Code 里一键绑定快捷键
  • CI 里做回归测试,定时把模板题跑一遍
  • 比赛时批量交暴力,对拍数据
2. 脚本功能一览

脚本 submit.py 只有 100 行不到,却能:
功能实现方式登录 OJrequests.Session + Cookie自动提取 CSRF-tokenBeautifulSoup 双策略(meta/input)语言缩写自动映射cpp → cc.cc14o2 …提交后返回记录号解析 302 Location3. 运行姿势
  1. # 把 1000 题的 C++ 代码交上去
  2. python3 submit.py \
  3.   https://oj.example.com \
  4.   1000 \
  5.   alice \
  6.   secret \
  7.   ~/a.cpp \
  8.   cpp
复制代码
输出示例:
  1. 提交成功!记录号:123456
  2. 查看结果:https://oj.example.com/record/123456
复制代码
4. 代码精讲

4.1 参数检查
  1. if len(sys.argv) != 7:
  2.     print("用法:python submit.py <BASE> <PID> <USER> <PASS> <FILE> <LANG>")
  3.     sys.exit(1)
复制代码

  • 位置参数比交互式 input() 更适合脚本化,易与编辑器/CI 集成。
  • 7 个参数正好是 BASE PID USER PASS FILE LANG + 脚本本身 sys.argv[0]。
4.2 会话初始化
  1. s = requests.Session()
  2. s.headers.update({
  3.     'User-Agent': 'Mozilla/5.0 ...',
  4.     'Referer': SUBMIT_URL
  5. })
复制代码

  • Session 自动管理 Cookie,后续请求无需手动带 sid。
  • Referer 很多 OJ 会检查,防止 CSRF。
4.3 CSRF-token 双保险
  1. # 1) meta 标签
  2. meta = soup.find('meta', attrs={'name': 'csrf-token'})
  3. ...
  4. # 2) 隐藏域
  5. inp = soup.find('input', attrs={'name': re.compile(r'csrf|_csrf')})
  6. # 3) 兜底 Cookie
  7. csrf_submit = s.cookies.get('sid.sig', '')
复制代码

  • 不同 OJ 的 CSRF 位置不一样,脚本做了 三级回退
  • 如果全部失败,打印页面源码方便 Debug。
4.4 语言映射
  1. if LANG == 'cpp':
  2.     LANG = 'cc.cc14o2'
  3. elif LANG in ('python', 'py'):
  4.     LANG = 'py.py3'
复制代码

  • 命令行里敲 cpp、py 即可,无需记住评测机内部代号。
  • 想支持更多语言,直接加 elif 即可。
4.5 提交与结果
  1. resp = s.post(SUBMIT_URL, data=payload, allow_redirects=False)
  2. if resp.status_code == 302:
  3.     rid = resp.headers['Location'].split('/')[-1]
复制代码

  • 评测机成功接收后通常 302 到 /record/{rid}。
  • 解析 Location 即可拿到记录号,再拼成可点击的 URL。
4.6 完整代码
  1. 请在写一篇文章
  2. #!/usr/bin/env python3
  3. import sys, requests, re
  4. from bs4 import BeautifulSoup
  5. def main():
  6.     if len(sys.argv) != 7:
  7.         print("用法:python submit.py <BASE> <PID> <USER> <PASS> <FILE> <LANG>")
  8.         sys.exit(1)
  9.     BASE, PID, USER, PASS, FILE, LANG = sys.argv[1:]
  10.     LOGIN_URL  = f"{BASE.rstrip('/')}/login"
  11.     SUBMIT_URL = f"{BASE.rstrip('/')}/p/{PID}/submit"
  12.     # 1. 建立会话
  13.     s = requests.Session()
  14.     s.headers.update({
  15.         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
  16.                       'AppleWebKit/537.36 (KHTML, like Gecko) '
  17.                       'Chrome/120.0.0.0 Safari/537.36',
  18.         'Referer': SUBMIT_URL
  19.     })
  20.     # 2. 首次 GET 登录页 → 取 Cookie 中的 CSRF
  21.     s.get(LOGIN_URL)
  22.     csrf_login = s.cookies.get('sid.sig', '')
  23.     # 3. 登录
  24.     resp = s.post(LOGIN_URL, data={
  25.         'uname': USER,
  26.         'password': PASS,
  27.         '_csrf': csrf_login
  28.     }, allow_redirects=False)
  29.     if resp.status_code != 302:
  30.         raise RuntimeError('登录失败')
  31.     # 4. GET 提交页 → 取新的 CSRF
  32.     submit_html = s.get(SUBMIT_URL).text
  33.     soup = BeautifulSoup(submit_html, 'lxml')
  34.     csrf_submit = None
  35. # 1) meta 标签
  36.     meta = soup.find('meta', attrs={'name': 'csrf-token'})
  37.     if meta:
  38.         csrf_submit = meta['content']
  39. # 2) 隐藏域
  40.     else:
  41.         inp = soup.find('input', attrs={'name': re.compile(r'csrf|_csrf')})
  42.         csrf_submit = inp['value'] if inp else None
  43. # 3) 兜底:如果都没有,就直接用 Cookie 里的 sid.sig
  44.     if not csrf_submit:
  45.         csrf_submit = s.cookies.get('sid.sig', '')
  46.     if not csrf_submit:
  47.         print('⚠️ 无法提取 CSRF,页面预览:')
  48.         print(submit_html)
  49.         sys.exit(1)
  50.     # 5. 读源码
  51.     with open(FILE, encoding='utf-8') as f:
  52.         code = f.read()
  53.     # 6. POST 提交
  54.     # 6.1 转换 LANG
  55.     if LANG =='cpp':
  56.         LANG = 'cc.cc14o2'
  57.     elif LANG == 'python' or LANG == 'py':
  58.         LANG = 'py.py3'
  59.     payload = {'_csrf': csrf_submit, 'lang': LANG, 'code': code}
  60.     resp = s.post(SUBMIT_URL, data=payload, allow_redirects=False)
  61.     if resp.status_code == 302:
  62.         rid = resp.headers['Location'].split('/')[-1]
  63.         print(f'提交成功!记录号:{rid}')
  64.         print(f'查看结果:{BASE}/record/{rid}')
  65.     else:
  66.         print('提交失败,状态码', resp.status_code)
  67. if __name__ == '__main__':
  68.     main()
复制代码
5. 进阶玩法

5.1 VS Code 一键绑定

在 .vscode/tasks.json 里加:
  1. {
  2.   "label": "submit",
  3.   "type": "shell",
  4.   "command": "python3",
  5.   "args": [
  6.     "${workspaceFolder}/submit.py",
  7.     "https://oj.example.com",
  8.     "${fileBasenameNoExtension}",
  9.     "alice",
  10.     "secret",
  11.     "${file}",
  12.     "cpp"
  13.   ],
  14.   "group": "build"
  15. }
复制代码
写完后 Ctrl+Shift+B 直接交题。
5.2 CI 自动回归

GitHub Actions 每晚跑模板题:
  1. - name: 提交模板题
  2.   run: |
  3.     python3 submit.py https://oj.example.com 1000 bot bot template.cpp cpp
  4.     python3 submit.py https://oj.example.com 1001 bot bot template.py py
复制代码
5.3 多账号/多 OJ 配置

把账号密码写到 .env,脚本里用 python-dotenv 读取,避免明文泄露。
6. 常见坑 & 解决办法

现象原因解决登录 200 不 302密码错误或字段名不对浏览器抓包对比 uname/password/_csrf提交返回 403CSRF 失效确认 Referer,重新 GET 页面语言无效评测机代号变化打开下拉框查看 option value7. 一句话总结

把机械化的“点鼠标”变成可编程的“HTTP 请求”,
你就能在 任何地方、任何工具链 里完成“写完即交”。
脚本虽小,却能把刷题体验提升一个数量级。
Happy hacking,愿每一次 ./submit.py 都是一次愉快的 AC!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册