PTA题目爬取转接AI的一次尝试

PTA题目爬取转接AI的尝试

依旧造轮椅

上个Python选修课但是要刷的题目好多啊。鬼脑一动看看能不能抓掉包转接给AI让它自动帮我提交答案,只保存不提交,防止出现某些意外

简单喵两句

抓取流程

首先挺惊喜的,登录没有加密逻辑,就是明文登录

POST /api/users/sessions HTTP/1.1
Host: passport.pintia.cn
Connection: keep-alive
Content-Length: 74
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
sec-ch-ua: "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
X-Lollipop: fe247aa898e49a308f8f302df0145f8c
X-Marshmallow: 
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Accept: application/json;charset=UTF-8
Content-Type: application/json;charset=UTF-8
Origin: https://pintia.cn
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://pintia.cn/
Accept-Encoding: gzip, deflate, br, zstd
Cookie: PTASession=583f97d2-f986-415b-ab90-4425c3b5d504; _ga=GA1.1.841321695.1791381611; _ga_ZHCNP8KECW=GS2.1.s1791381611$o1$g1$t1791381623$j48$l0$h0

{"email":"admin@example.com","password":"admin@114514","rememberMe":true}

image-20261007223618133

下一个就是抓取题单

GET /api/problem-sets?filter=&limit=7&order_by=END_AT&asc=true HTTP/1.1
Host: pintia.cn
Connection: keep-alive
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
sec-ch-ua: "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
X-Lollipop: fe247aa898e49a308f8f302df0145f8c
X-Marshmallow: 
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Accept: application/json;charset=UTF-8
Content-Type: application/json;charset=UTF-8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://pintia.cn/problem-sets/dashboard
Accept-Encoding: gzip, deflate, br, zstd
Cookie: PTASession=583f97d2-f986-415b-ab90-4425c3b5d504; _bl_uid=v4mXUuO9yO76Cmcztiz4apnlddm0; _ga=GA1.1.841321695.1791381611; JSESSIONID=FE077B46A5ED7D1658902616F73CE309; _ga_ZHCNP8KECW=GS2.1.s1791381611$o1$g1$t1791382355$j53$l0$h0

image-20261007223819386

这里select到id就可以,给下一步爬题目奠基来的

接着发个这样的包就行

GET /api/problem-sets/[id]/exams

image-20261007223958143

探测一下是不是存在其实没啥实质性作用,并且取exam的id

虽然后来证明没什么用(,它那个包好像不用examid的参数也能正常取到

image-20261007224915263

但是问题出现了,它的题型是不确定的,手动去返回包里遍历收集又太慢了,我们直接预定义一个set存放就行

接着

/api/problem-sets/[id]/exam-problems?problem_type=[type]

遍历测试一下就好了

提交也很简单

POST /api/exams/2100398912004239360/exam-submissions HTTP/1.1
Host: pintia.cn
Connection: keep-alive
Content-Length: 375
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
sec-ch-ua: "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
X-Lollipop: fe247aa898e49a308f8f302df0145f8c
X-Marshmallow: 
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Accept: application/json;charset=UTF-8
Content-Type: application/json;charset=UTF-8
Origin: https://pintia.cn
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://pintia.cn/problem-sets/2099334496336080896/exam/problems/type/2
Accept-Encoding: gzip, deflate, br, zstd
Cookie: PTASession=583f97d2-f986-415b-ab90-4425c3b5d504; _bl_uid=v4mXUuO9yO76Cmcztiz4apnlddm0; _ga=GA1.1.841321695.1791381611; _ga_ZHCNP8KECW=GS2.1.s1791381611$o1$g1$t1791383074$j60$l0$h0; JSESSIONID=5AC75FF31F105E254638B99CB267BC2B

{"problemType":"MULTIPLE_CHOICE","details":[{"problemId":"0","problemSetProblemId":"2099334496411578418","multipleChoiceSubmissionDetail":{"answer":"C"}},{"problemId":"0","problemSetProblemId":"2099334496411578419","multipleChoiceSubmissionDetail":{"answer":"B"}},{"problemId":"0","problemSetProblemId":"2099334496411578422","multipleChoiceSubmissionDetail":{"answer":"C"}}]}

本质就是题型+一个题目答案的列表

至此包就已经扒完了,那么要怎么喂给ai呢?直接写成skill给AI?好像也有点慢啊

写成MCP server怎么样?好像是一个很好的想法

既不通过AI去请求浪费不必要浪费的token,又能很好的把整套逻辑包进去

大概就是

AI client ===================MCP Server ===========================Platform
		request the tool
        -------------------->			login and get the content
        							  ------------------------->  
        <--------------------
        response the problems 
           with their id
           
         request the tool and			
           send answers					   submit the answers
        -------------------->           --------------------------->
访问平台API从响应里面提取题目信息啊,一般的作业平台都是一次能直接看到所有这个类型的题目,这样mcp server处理后就能整合成一个大的list,response给client端之后AI就可以一次直接解答所有类型的所有题目,接着再把答案发送给mcp server让其提交所有的答案

本来想着应该会很顺利,但是不出意外的触发检测了

image-20261008105950286

又要auto绕图形验证码了有感觉吗

image-20261008112000397

不知道哪家的,收集下信息看看有没有自动过验证码的脚本

image-20261008112033080

确认产品,找了一下发现相关的自动破解还是很多的

但是想想还是自己调一下这个过程

TX滑块Captcha绕过

抓了个纯净点的包

image-20261008153125413

要关注的就是中间这几个

先不看加载的那几个静态

GET /cap_union_prehandle?aid=194593025&protocol=https&accver=1&showtype=popup&ua=TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzE1NC4wLjAuMCBTYWZhcmkvNTM3LjM2&noheader=1&fb=1&aged=0&enableAged=0&enableDarkMode=0&grayscale=1&clientype=2&cap_cd=&uid=&lang=zh-cn&entry_url=https%3A%2F%2Fpintia.cn%2Fauth%2Flogin&elder_captcha=0&js=%2Ftcaptcha-frame.ba387dd1.js&login_appid=&wb=2&tkid=581051966&subsid=1&callback=_aq_793505&sess= HTTP/1.1
Host: turing.captcha.qcloud.com
Connection: keep-alive
sec-ch-ua-platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
sec-ch-ua: "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
Accept: */*
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: script
Sec-Fetch-Storage-Access: active
Referer: https://pintia.cn/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9

返回的报文是

HTTP/1.1 200 OK
Date: Thu, 08 Oct 2026 07:29:19 GMT
Content-Type: text/javascript;charset=utf-8
Connection: keep-alive
P3P: CP=CAO PSA OUR
Pragma: No-cache
Server: Trpc httpd
Server: tencent http server
Content-Length: 3123

_aq_793505({
  state: 1,
  ticket: '',
  capclass: '1',
  subcapclass: '2401',
  src_1: 'cap_union_new_show',
  src_2: 'template/new_placeholder.html',
  src_3: 'template/new_slide_placeholder.html',
  sess: 's0aVNnVEjY5oUozi4rpJVPR7-RFxNY6v0BOq00Y767YDbKsExq37-Zzl6EADCEvHN2GwMBxCK7b5YSn5JKtrM4DYDXycFK4jhU5I8S8IR2JcG02MsiidQzMbth0eW3No1o5S0mq_tI0aMfYfZj62dn2aESs13k7jLlP7skhYa3-jrWxiFIqGpV3BXXYksCojrw-PDHN1UB4-RhIk7Fm97oznDGUn0p0h7LWUr0a4h8RyBDPtcB5nVWBm90EKK6yJDH2217-2hZOcEJgadt9aGbgDxcVHCcUpO5ktuulTOxj7QAfRcgyjAw7ywloUyY8zyRTvw55cg6gTAsj8BYI43DJ2Rhr_7sT46ySsVqPROMSaAMwnFVBU9-7LAD_E5O2PSSYvoVwQu5tpweW9OF6BoXtOIMwiRWhTf8',
  randstr: '',
  sid: '1788719716427149312',
  log_js: '',
  data: {
    comm_captcha_cfg: {
      tdc_path: '/tdc.js?app_data=1788719716427149312&t=619891543',
      feedback_url: 'https://support.qq.com/products/304457',
      pow_cfg: {
        prefix: 'a594cb9f0137effe#',
        md5: 'db933e29a68df4fca17a564763c649ba',
      },
    },
    dyn_show_info: {
      lang: 'zh-cn',
      instruction: '拖动下方滑块完成拼图',
      bg_elem_cfg: {
        sprite_pos: [0, 0],
        size_2d: [672, 480],
        img_url:
          '/cap_union_new_getcapbysig?img_index=1&image=026109000000003500000015a28ce71427b0&sess=s0CjkHrSgjdWiWT5RoW4sPl5Kne4TgDGGsPr58cFYe3u1gyiEPr4uEra81fxCn5t4lXeK6hhIJ1YsLzkCkD_ET-U36qkSn6rNo2TxLXPxeLNPyfquU5rv2fQtrLYjiZGDaU18yk7SSFBXhG52yNhucWjQodPpqgoEwguSFG6op4AQgmy12QeYqdR4HAzrLNZtvzbsr_eoLxNQMhdH3rS4bUAsE81CWFVxyqIwefM5ElKYhZ2Y9xghA-mLRf_cdALnjZKA2Tdz2yNKeSHBC49NVIZjeEHr3bFJpJZd2gj-WkQuJA18Kj4orhsz7kl2CsU1kNKu-enh6CY7CPER057xuZqQHjaW4g0-v5uOeLlSU6fRBZW5JLEH-i0oYOu6tg7Iv_QChVBww-JDVbZVtEkzUtZcMiJGvjavP',
      },
      fg_elem_list: [
        { id: 3, sprite_pos: [0, 422], size_2d: [672, 32], init_pos: [0, 422] },
        {
          id: 1,
          sprite_pos: [140, 490],
          size_2d: [120, 120],
          init_pos: [50, 63],
          move_cfg: {
            track_limit: 'x>=50&&x<=552',
            move_factor: [1, 0],
            data_type: ['DynAnswerType_POS'],
          },
        },
        {
          id: 2,
          sprite_pos: [0, 490],
          size_2d: [130, 70],
          init_pos: [45, 402],
          move_cfg: { track_limit: 'x>=50&&x<=552', move_factor: [1, 0] },
          type: 'slider',
        },
      ],
      fg_binding_list: [
        { master: 1, slave: 2, bind_type: 'DynBindType_DX_DX', bind_factor: 1 },
        { master: 2, slave: 1, bind_type: 'DynBindType_DX_DX', bind_factor: 1 },
      ],
      sprite_url:
        '/cap_union_new_getcapbysig?img_index=0&image=026109000000003500000015a28ce71427b0&sess=s0CjkHrSgjdWiWT5RoW4sPl5Kne4TgDGGsPr58cFYe3u1gyiEPr4uEra81fxCn5t4lXeK6hhIJ1YsLzkCkD_ET-U36qkSn6rNo2TxLXPxeLNPyfquU5rv2fQtrLYjiZGDaU18yk7SSFBXhG52yNhucWjQodPpqgoEwguSFG6op4AQgmy12QeYqdR4HAzrLNZtvzbsr_eoLxNQMhdH3rS4bUAsE81CWFVxyqIwefM5ElKYhZ2Y9xghA-mLRf_cdALnjZKA2Tdz2yNKeSHBC49NVIZjeEHr3bFJpJZd2gj-WkQuJA18Kj4orhsz7kl2CsU1kNKu-enh6CY7CPER057xuZqQHjaW4g0-v5uOeLlSU6fRBZW5JLEH-i0oYOu6tg7Iv_QChVBww-JDVbZVtEkzUtZcMiJGvjavP',
      color_scheme: '#0057d4',
    },
  },
  uip: '36.25.1.141',
});

接着就是拿sess

POST /cap_union_new_verify HTTP/1.1
Host: turing.captcha.qcloud.com
Connection: keep-alive
Content-Length: 2727
sec-ch-ua-platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
sec-ch-ua: "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
sec-ch-ua-mobile: ?0
Origin: https://turing.captcha.gtimg.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://turing.captcha.gtimg.com/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9

collect=386NxiV8EKm8YUv94gxyd7xhS%2F3iDHJ3L5kjgBfmtdxMGD%2BVL4dMAwETVWTQMpU1KknUbSXBCWPJwD4gjwaSEFE3kUzyilz3XN7kH7EEnWQ4KDOtRekbwljQ7ca....2FZwghrwWZ720RcPZw7A%3D&sess=s0aVNnVEjY5oUozi4rpJVPR7-RFxNY6v0BOq00Y767YDbKsExq37-Zzl6EADCEvHN2GwMBxCK7b5YSn5JKtrM4DYDXycFK4jhU5I8S8IR2JcG02MsiidQzM...Asj8BYI43DJ2Rhr_7sT46ySsVqPROMSaAMwnFVBU9-7LAD_E5O2PSSYvoVwQu5tpweW9OF6BoXtOIMwiRWhTf8&ans=%5B%7B%22elem_id%22%3A1%2C%22type%22%3A%22DynAnswerType_POS%22%2C%22data%22%3A%22393%2C63%22%7D%5D&tkid=581051966&pow_answer=a594cb9f0137effe%23861&pow_calc_time=19

response

HTTP/1.1 200 OK
Date: Thu, 08 Oct 2026 07:29:25 GMT
Content-Type: application/json
Connection: keep-alive
P3P: CP=CAO PSA OUR
Pragma: No-cache
Server: Trpc httpd
Server: tencent http server
Accept-Ranges: bytes
access-control-allow-origin: *
Content-Length: 243

{
  "errorCode": "0",
  "randstr": "@eAi",
  "ticket": "tr03kNgzAV....L6UilK2-",
  "traceId": "",
  "errMessage": "",
  "sess": ""
}

拿到了ticket和randstr就可以美美登录成功了

问题就是这里怎么拿了,有两种思路

一种就是前面登录的过程全部替换成selenium模拟登录,拿到seesion后一键继承,这种应该比较简单了,但是会慢很多

另一种就是纯走HTTP,通过load到的包来直接用opencv过验证

这里我们用后面一种,直接让AI搓一下就行(

成功进入首页

突然发现自己少看一个包(本来我还纳闷为什么看来看去没有给总的题目类型的)

image-20261009163037940

这样也不用猜题目类型了(

然后我们需要return给AI的是probelmType,problemSetProblemId,content

那么就查询题目类型接着提取json["problemSetProblems"]

存下来的是一个列表,接着提取每个元素的id和content就大功造成了

AI需要返回给我们的是probelmType,problemSetProblemId,answer

但是这里太频繁的测试导致我的ip被腾子的大手限流了,那么就需要把Cookie持久化到本地进行复用了,那么怎么办呢?

巧了,这里我刚好在抓包测试能不能看到测试的完成数据,发现三天的session居然还能使用,我要绷不住了,那么就说明它这个session的签发时间足够长,大可以保存到本地,这对我们后面把不同模块封装成tools也有好处,防止频繁访问导致MCP的交互流出现问题

然后我们看看提交的包,所有题目提交的接口都是一样的

image-20261011204313879

判断题:

{"problemType":"TRUE_OR_FALSE","details":[{"problemId":"0","problemSetProblemId":"2104474233634361344","trueOrFalseSubmissionDetail":{"answer":"TRUE"}},{"problemId":"0","problemSetProblemId":"2104474233634361345","trueOrFalseSubmissionDetail":{"answer":"FALSE"}},{"problemId":"0","problemSetProblemId":"2104474233634361346","trueOrFalseSubmissionDetail":{"answer":"FALSE"}}]}

单选题:

{"problemType":"MULTIPLE_CHOICE","details":[{"problemId":"0","problemSetProblemId":"2104474233634361382","multipleChoiceSubmissionDetail":{"answer":"B"}},{"problemId":"0","problemSetProblemId":"2104474233634361383","multipleChoiceSubmissionDetail":{"answer":"C"}}]}

编程题

{"problemType":"PROGRAMMING","details":[{"problemId":"0","problemSetProblemId":"2104474233634361469","programmingSubmissionDetail":{"program":"nums = eval(input())\nrepeat = sorted({x for x in nums if nums.count(x) > 1})\nprint(*repeat, end=' ')","compiler":"PYTHON3"}}]}

但是这里我们要测试一下编程题是不是支持批量提交的

这里我又抓了另一道题目的包

{"problemType":"PROGRAMMING","details":[{"problemId":"0","problemSetProblemId":"2104474233634361456","programmingSubmissionDetail":{"program":"s = input().strip()\nprint(\"YES\" if int(s[-1]) % 2 == 1 else \"NO\")","compiler":"PYTHON3"}}]}

接着合并一下

{"problemType":"PROGRAMMING","details":[{"problemId":"0","problemSetProblemId":"2104474233634361469","programmingSubmissionDetail":{"program":"nums = eval(input())\nrepeat = sorted({x for x in nums if nums.count(x) > 1})\nprint(*repeat, end=' ')","compiler":"PYTHON3"}},{"problemId":"0","problemSetProblemId":"2104474233634361456","programmingSubmissionDetail":{"program":"s = input().strip()\nprint(\"YES\" if int(s[-1]) % 2 == 1 else \"NO\")","compiler":"PYTHON3"}}]}

image-20261011210039561

失败了

那么submit就只能拆成两个来做了

封装成tool

初步想的是设计成两个tool

一个负责爬取,一个负责提交

但是后来发现两个都需要获取session才能继续操作,那么就把申请session单独分离出来成一个tool然后持久到本地

那么大致就是

  1. pta_login:登录平台获取session实现对话持久化
  2. pta_list_exams:通过指定接口拉取当前正在进行的练习,并根据用户的需求确定要做哪份练习
  3. pull_exam_set:批量拉取指定练习的所有题目,并剥离只剩下需要的字段
  4. submit_answers:批量提交客观题,主观题(比如编程题目)接口上只支持单个提交
  5. pta_submission_detail:查看提交的结果(这里是针对编程题的提交结果做的特化)

比较大的问题就是:如何处理大量的题面以及编程题的提交?

初步想法是

最好是把题面再做一下处理,筛掉无意义的字符,比如大量的连续\n,如果直接传输过去,会很占上下文

再和ai博弈了一下,发现可以调用sampling功能直接和LLM交互

但是实际也差不了多少,题面的那些工作量上减不下来的,最多写一个filter给文本做一些基础的清洗

接着AI的答案也要save到本地去,从而减少token的消耗(只要就是要指定格式这一块)

本来想用AI在这个基础上直接写server的,写完发现还不如自己来,代码质量实在是差到令人发指。无意义的调用和函数编写一堆

手搓tool

中间也发现了很多问题,记录一下

爬取时的filter

就是最开始获取exam时候的filter,filter为空的时候他会打印所有考试,而页面一般加载时会自动载入某个时间节点做筛选

比如这样

GET /api/problem-sets?filter=%7B%22endAtAfter%22%3A%222026-10-12T16%3A00%3A00.000Z%22%7D&limit=7&order_by=END_AT&asc=true

这里解码完是{"endAtAfter":"2026-10-12T16:00:00.000Z"}

那么我们仿造一下在开局就做好筛选就能拿到纯净的alive exam_id

查了下这个是utc的时间标准,也就是说我们查询的时候不是用北京时间的(,那么参数就要控制一下了,最后再做编码

time_point=datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z')
template='{"endAtAfter":"'+time_point+'"}'
print(quote(template))

怎么处理答案

最好就是存储到本地,接着调用submit_answer的时候直接从本地读取,然后自己纠正提交的json格式

从而来减少token的消耗,拿dsf举例吧,如果让它把格式修正好,一般它就要构思脚本去规范格式,这样就会占用一些context,那还不如规范出最简的格式

然后在tool内部自己调整格式

belike:

{"problemType":"MULTIPLE_CHOICE","details":[{"problemSetProblemId":"2099334496411578418","answer":"C"},{"problemSetProblemId":"2099334496411578419","answer":"B"}]}

而我们要提交的格式是

{"problemType":"MULTIPLE_CHOICE","details":[{"problemId":"0","problemSetProblemId":"2104474233634361382","multipleChoiceSubmissionDetail":{"answer":"B"}},{"problemId":"0","problemSetProblemId":"2104474233634361383","multipleChoiceSubmissionDetail":{"answer":"C"}}]}

这种程度其实脚本调整起来还是非常方便的

TODO

其实还有一些事情没有做的

第一个就是OCR,因为在测试中发现有些题目的代码是写在图片里面的,就很难绷

第二个就是FILTER,虽说前面已经节省了一些token的消耗,但是本着不多浪费一点无用的米,感觉还是有必要写个filter去对题目的内容去做一些清洗

项目已上传github欢迎提出意见呢

https://github.com/kisakiayano/PTA-mcp