Python+AI自动化刷题实战:5分钟搞定实验室安全考试
1. 项目概述与核心思路
最近实验室新进了一批师弟师妹,安全考试成了他们头疼的第一关。题库几百道,题目还经常更新,手动刷题费时费力。正好手头有个阿里云的AI服务资源,结合Python写个自动化脚本,5分钟搞定安全考试,听起来是不是很酷?这其实就是一次典型的“AI+RPA”(机器人流程自动化)的轻量级应用实战。核心思路很简单:用Python模拟浏览器操作,自动登录考试系统,然后调用阿里云的AI大模型接口来识别题目、搜索答案并自动选择。整个过程完全自动化,解放双手。这个项目不仅实用,更能让你深入理解Python网络请求、AI接口调用和自动化控制的结合,是提升效率的绝佳案例。
适合谁呢?如果你是实验室的学生、科研助理,或者任何需要定期参加类似线上安全、规章考试的朋友,这个脚本能帮你节省大量时间。对于Python初学者,这是一个非常好的综合练习项目,涵盖了requests库、正则表达式、JSON数据处理和API调用等核心技能。当然,前提是你得有考试的合法访问权限,并且仅用于个人学习效率提升,切勿用于任何破坏规则或作弊的行为。
2. 核心工具选型与原理拆解
要实现这个“5分钟脚本”,我们需要几个核心组件:一个能模拟浏览器行为的工具、一个强大的“大脑”来理解题目并找到答案,以及将它们粘合起来的Python代码。下面我们来逐一拆解选型背后的考量。
2.1 浏览器自动化:Selenium vs. Playwright
首先,我们需要一个工具来驱动浏览器,完成点击、输入、获取页面元素等操作。这里有两个主流选择:老牌的Selenium和微软推出的新秀Playwright。
我最终选择了 Playwright 。原因有几个:第一,Playwright对现代Web应用(尤其是大量使用JavaScript动态加载内容的单页应用)的支持更好,速度更快。实验室的考试系统很可能就是这种。第二,它的API设计更现代、更简洁,同步和异步操作都支持得很好。第三,它内置了等待机制,能智能地等待元素加载完成,减少了我们写复杂等待逻辑的麻烦。相比之下,Selenium虽然生态庞大,但在处理动态页面时有时需要更多“补丁”代码。
安装Playwright非常简单: pip install playwright ,然后运行 playwright install 来下载它自带的浏览器(Chromium, Firefox, WebKit)。我们主要使用Chromium,因为它最通用。
2.2 AI大脑:为什么是阿里云通义千问?
接下来是最关键的部分——AI。我们需要一个能理解自然语言题目,并从知识库或上下文中推理出答案的模型。市面上有很多选择,比如OpenAI的GPT系列、国内的各种大模型。我选择 阿里云的通义千问 ,主要基于以下几点实战考量:
- 稳定与合规 :对于国内用户,阿里云的API服务访问稳定,无需复杂网络配置,符合内容安全要求。其服务条款对使用场景有明确规范,我们用于教育辅助目的在合理范围内。
- 成本与易用性 :新用户通常有免费额度,足够完成多次考试。阿里云的控制台清晰,API密钥(AccessKey)管理方便,文档也比较齐全。
- 能力足够 :通义千问在中文理解、推理和问答任务上表现优秀,完全能胜任从题干中提取关键信息并匹配答案的工作。
我们需要在阿里云官网开通“灵积模型服务”,创建一个API-KEY。核心是调用其 qwen-max 或 qwen-plus 模型的对话(Chat)接口。
2.3 粘合剂:Python核心库
Python脚本是大脑和手脚的指挥中心。除了Playwright,我们还需要几个库:
- requests 或 aiohttp : 用于向阿里云AI接口发送HTTP请求。如果追求更高性能的并发(比如同时处理多道题),可以使用异步的aiohttp。
- json : 处理API请求和返回的数据。
- re (正则表达式) : 用于从杂乱的HTML或返回文本中,精准提取题目和选项的纯文本内容。
- time : 添加适当的延迟,模拟真人操作,避免请求过快被服务器识别为机器人。
整个工作流原理如下:Playwright控制浏览器打开考试页面,定位到题目和选项的HTML元素,用正则表达式清洗出干净的文本。然后将“题目+选项”组合成一个清晰的Prompt,通过requests库发送给阿里云通义千问API。API返回推理出的答案(例如“A”或“C”),脚本再控制Playwright去点击对应的选项按钮,最后提交试卷。
3. 环境准备与关键配置
工欲善其事,必先利其器。在写代码之前,我们需要把环境搭建好,特别是阿里云AI服务的配置,这是整个脚本的“燃料”。
3.1 Python环境与依赖安装
首先确保你安装了Python 3.7或更高版本。建议使用虚拟环境来管理依赖,避免包冲突。
# 创建并激活虚拟环境(以venv为例)
python -m venv venv
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate
# 安装核心依赖
pip install playwright
pip install requests # 用于同步调用API,简单稳定
# 如果想尝试异步,可以安装 aiohttp
# pip install aiohttp
# 安装Playwright的浏览器
playwright install chromium
3.2 阿里云AI服务配置详解
这一步是重中之重,很多新手在这里会卡住。
- 开通服务 :登录阿里云控制台,在顶部搜索“灵积模型服务”或“DashScope”,进入产品页面。按照指引开通服务。通常新用户会有免费额度。
- 获取API-KEY :在控制台,找到“API密钥管理”页面。创建一个新的API密钥(AccessKey)。你会得到
AccessKey Id和AccessKey Secret。 请像保护密码一样保护它们,千万不要直接硬编码在代码里提交到Git等公共平台。 - 理解计费与模型 :在灵积模型服务页,查看模型列表。对于我们的刷题场景,
qwen-max(能力最强)或qwen-plus(性价比高)都是不错的选择。注意查看它们的计价方式,通常按输入和输出的总token数计费。一次简单的问答消耗极少,免费额度完全够用。
我们需要将API-KEY安全地配置到脚本中。最佳实践是使用环境变量。
# 在终端中设置环境变量(临时,重启终端失效)
export DASHSCOPE_API_KEY='你的API-KEY'
# Windows (Command Prompt):
set DASHSCOPE_API_KEY=你的API-KEY
# Windows (PowerShell):
$env:DASHSCOPE_API_KEY='你的API-KEY'
然后在Python代码中这样读取:
import os
api_key = os.environ.get('DASHSCOPE_API_KEY')
if not api_key:
raise ValueError("请设置环境变量 DASHSCOPE_API_KEY")
3.3 考试页面分析与元素定位
在写自动化脚本前,必须手动操作一遍考试流程,并用浏览器开发者工具(F12)分析页面结构。这是自动化成功的基础。
你需要找到以下关键元素的CSS选择器或XPath:
- 题目文本 :通常在一个
<div>或<p>标签里,id或class可能包含question,title,topic等。 - 选项列表 :可能是多个
<label>、<li>或<div>,里面包含单选按钮<input type="radio">和选项文本。 - “下一题”按钮 :也可能是“提交”或“确定”按钮。
- 登录表单 (如果需要):用户名和密码的输入框。
实操技巧 :使用Playwright的 page.locator() 函数,结合Chrome开发者工具中的“检查”功能,右键点击元素选择“Copy” -> “Copy selector”或“Copy XPath”,可以快速获得定位器。但直接复制的选择器可能很脆弱(依赖于动态生成的id或复杂结构),最好自己编写更稳定、简洁的选择器,例如通过部分class名和文本内容组合定位。
注意 :不同学校的考试系统千差万别,没有通用的选择器。这一步需要你耐心分析和调试。一个稳定的选择器是脚本长期可用的关键。
4. 脚本核心代码实现与分步解析
下面我们进入核心环节,一步步构建脚本。我将代码分成几个功能模块,并详细解释每一部分的作用和注意事项。
4.1 初始化与登录模块
首先,初始化Playwright和浏览器,并处理登录(如果考试系统需要独立登录)。
import asyncio
from playwright.async_api import async_playwright
import requests
import json
import re
import os
class LabExamBot:
def __init__(self, api_key):
self.api_key = api_key
self.api_url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
# 初始化Playwright对象和浏览器
self.playwright = None
self.browser = None
self.page = None
async def init_browser(self, headless=False):
"""初始化浏览器,headless=False表示显示浏览器窗口,方便调试"""
self.playwright = await async_playwright().start()
# 使用Chromium,可配置为有头模式便于观察
self.browser = await self.playwright.chromium.launch(headless=headless)
self.page = await self.browser.new_page()
# 设置超时和视口大小
self.page.set_default_timeout(30000) # 30秒超时
await self.page.set_viewport_size({"width": 1280, "height": 720})
async def login(self, login_url, username, password):
"""处理登录流程,需要根据实际考试系统页面调整选择器"""
await self.page.goto(login_url)
# 示例选择器,需要替换成实际的
await self.page.fill('input[name="username"]', username)
await self.page.fill('input[name="password"]', password)
await self.page.click('button[type="submit"]')
# 等待登录成功,跳转到考试页面或出现特定元素
await self.page.wait_for_selector('#dashboard', state='visible') # 示例
print("登录成功")
关键点解析 :
headless=False在开发阶段非常有用,你能看到浏览器每一步操作,方便调试定位问题。脚本稳定后可以改为True在后台运行。set_default_timeout很重要,避免因为网络或页面加载慢导致脚本无限等待。- 登录部分的选择器
input[name="username"]是相对稳定的写法,比依赖动态id的#id_xxxx更好。你需要根据实际页面调整。
4.2 题目抓取与文本清洗
登录后进入考试界面,我们需要抓取题目和选项的纯文本。
async def get_question_and_options(self):
"""从当前页面提取题目和选项文本"""
# 假设题目在一个id为`question-text`的div里
question_element = await self.page.query_selector('#question-text')
if not question_element:
# 如果id不稳定,尝试用包含‘题目’文本的标签
question_element = await self.page.query_selector('div:has-text("题目")')
question_text = await question_element.inner_text() if question_element else ""
# 清洗文本:去除多余空格、换行符
question_text = re.sub(r'\s+', ' ', question_text).strip()
print(f"捕获题目:{question_text[:50]}...") # 打印前50字符便于观察
# 提取选项,假设每个选项在一个class为`option-item`的label里
option_elements = await self.page.query_selector_all('.option-item')
options = []
option_labels = [] # 用于记录选项字母(A, B, C, D)
for idx, elem in enumerate(option_elements):
option_text = await elem.inner_text()
cleaned_text = re.sub(r'\s+', ' ', option_text).strip()
# 尝试提取选项字母和内容,例如“A. 必须戴防护眼镜”
match = re.match(r'^([A-D])[\.、]?\s*(.+)', cleaned_text)
if match:
label, content = match.groups()
option_labels.append(label)
options.append(content)
else:
# 如果没有明显字母,用索引生成
option_labels.append(chr(65 + idx)) # 65是'A'的ASCII码
options.append(cleaned_text)
print(f"选项:{list(zip(option_labels, options))}")
return question_text, option_labels, options
实操心得 :
inner_text()比text_content()更能获取用户可见的文本,会自动处理一些隐藏元素。- 正则表达式
re.sub(r'\s+', ' ', text)能将所有空白字符(空格、换行、制表符)替换为单个空格,是清理网页文本的利器。 - 选项提取的逻辑需要很强的鲁棒性。有的系统用“A.”,有的用“A、”,有的甚至没有前缀。这里写的正则和备选逻辑(按索引生成字母)能覆盖大部分情况。你可能需要根据实际页面微调这个正则表达式。
4.3 构建Prompt与调用AI接口
拿到干净的题目和选项后,我们需要构造一个清晰的Prompt(提示词)给AI模型,让它扮演一个“实验室安全专家”来答题。
def call_ai_for_answer(self, question, options):
"""调用阿里云通义千问API获取答案"""
# 构建一个清晰的Prompt
prompt = f"""你是一位实验室安全专家。请回答以下单选题,只输出选项字母(例如 A, B, C, D),不要输出任何其他解释。
题目:{question}
选项:
"""
for label, opt in zip(self.current_option_labels, options): # 假设current_option_labels已在上一步赋值
prompt += f"{label}. {opt}\n"
prompt += "\n请根据你的专业知识,选择最正确的一项。只输出字母。"
data = {
"model": "qwen-max", # 或 "qwen-plus"
"input": {
"messages": [
{"role": "system", "content": "你是一个严谨的实验室安全助手,只根据专业知识回答问题。"},
{"role": "user", "content": prompt}
]
},
"parameters": {
"result_format": "message", # 返回消息格式
"temperature": 0.1, # 温度调低,让输出更确定,减少随机性
"top_p": 0.8
}
}
try:
response = requests.post(self.api_url, headers=self.headers, json=data, timeout=10)
response.raise_for_status() # 检查HTTP错误
result = response.json()
# 解析返回结果,不同模型返回结构可能略有差异
answer_text = result.get("output", {}).get("choices", [{}])[0].get("message", {}).get("content", "").strip()
# 使用正则从返回文本中提取大写字母
match = re.search(r'\b([A-D])\b', answer_text.upper())
if match:
predicted_answer = match.group(1)
print(f"AI预测答案:{predicted_answer}")
return predicted_answer
else:
print(f"AI返回内容无法解析为选项:{answer_text}")
return None
except requests.exceptions.RequestException as e:
print(f"调用AI API失败:{e}")
return None
except (KeyError, IndexError, json.JSONDecodeError) as e:
print(f"解析AI响应失败:{e}, 响应内容:{response.text[:200]}")
return None
为什么这样设计Prompt?
- 角色设定 :
“你是一位实验室安全专家”,让模型进入特定领域语境。 - 明确指令 :
“只输出选项字母”,这是最重要的,强制模型输出我们脚本能直接处理的格式,避免它长篇大论。 - 格式化输入 :将题目和选项清晰列出,便于模型阅读。
- 参数调整 :
temperature=0.1让模型的输出更集中、更确定,对于有标准答案的题目非常合适。
注意事项 :
- API的返回结构一定要仔细查阅阿里云官方文档,不同模型版本可能有细微差别。上述解析路径
result["output"]["choices"][0]["message"]["content"]是常见格式。 - 一定要做好异常处理(
try...except)。网络请求可能失败,API可能返回错误,模型也可能“不听话”输出了一段话而不是单个字母。我们的正则提取re.search(r'\b([A-D])\b', ...)是最后一道防线。
4.4 模拟点击与流程控制
拿到AI返回的答案字母后,我们需要控制浏览器点击对应的选项,并进入下一题。
async def select_answer_and_continue(self, answer_letter):
"""根据答案字母选择选项,并点击下一题"""
if not answer_letter:
print("未获得有效答案,暂时跳过此题")
# 可以在这里实现一个备选策略,比如随机选一个,或者标记下来手动处理
# await self.page.click(f'.option-item:nth-child({random.randint(1,4)})')
await self.page.click('#next-btn') # 假设有跳过或下一题按钮
return False
# 寻找对应字母的选项进行点击
# 方法1:如果选项元素有data-value或value属性对应字母
# await self.page.click(f'input[value="{answer_letter}"]')
# 方法2:更通用的方法,通过文本内容匹配
# 注意:这里需要能精准定位到可点击的元素(如label或包含radio的div)
selector = f'.option-item:has-text("^{answer_letter}[\.、]")'
try:
await self.page.click(selector)
print(f"已选择答案:{answer_letter}")
except Exception as e:
print(f"点击选项{answer_letter}失败:{e},尝试备用选择器...")
# 备用方案:通过索引点击
idx = ord(answer_letter) - 65 # A->0, B->1...
all_options = await self.page.query_selector_all('.option-item')
if idx < len(all_options):
await all_options[idx].click()
else:
print("备用方案也失败,无法选择答案。")
# 点击“下一题”或“提交”按钮,需要根据实际页面调整选择器和等待逻辑
next_btn_selector = '#next-question-btn, button:has-text("下一题"), button:has-text("提交")'
await self.page.click(next_btn_selector)
# 等待下一题加载或页面状态变化
# 可以等待题目区域更新,或者一个加载动画消失
await self.page.wait_for_selector('#question-text', state='visible') # 等待新题目出现
# 或者简单等待一小段时间
await self.page.wait_for_timeout(1000) # 等待1秒
return True
流程控制主循环 :
async def run_exam(self, exam_url, total_questions=50):
"""主执行流程"""
await self.page.goto(exam_url)
print(f"开始处理考试,预计{total_questions}题")
for q_num in range(1, total_questions + 1):
print(f"\n--- 正在处理第 {q_num} 题 ---")
# 1. 获取题目和选项
question_text, option_labels, options = await self.get_question_and_options()
self.current_option_labels = option_labels # 存储给AI调用用
# 2. 调用AI获取答案
answer = self.call_ai_for_answer(question_text, options)
# 3. 选择答案并进入下一题
success = await self.select_answer_and_continue(answer)
if not success and q_num < total_questions:
# 如果本题失败,但还有题,等待稍长时间后继续
await self.page.wait_for_timeout(2000)
# 4. 控制速度,避免请求过快
await asyncio.sleep(1) # 每题之间间隔1秒,更拟人
print("所有题目处理完毕!")
# 最后可能需要处理交卷
# await self.page.click('#submit-paper-btn')
核心技巧 :
wait_for_selector和wait_for_timeout结合使用,前者用于等待特定元素出现(更精确),后者用于固定等待(更简单但可能低效)。- 在主循环中增加
await asyncio.sleep(1)是非常重要的一步,这模拟了人的阅读和思考时间,能显著降低被反爬机制检测到的风险。 - 对于点击失败的情况,设计了备用选择器(按索引点击),增强了脚本的容错能力。
5. 完整代码整合与使用指南
将上述所有模块整合,并添加一些必要的配置和主函数入口,就得到了完整的脚本。以下是整合后的核心框架和调用示例。
# lab_exam_bot.py
import asyncio
import re
import os
import requests
from playwright.async_api import async_playwright
class LabExamBot:
# ... 此处整合上述所有方法 __init__, init_browser, login, get_question_and_options, call_ai_for_answer, select_answer_and_continue, run_exam ...
async def close(self):
"""关闭浏览器和Playwright"""
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
async def main():
# 1. 从环境变量获取API Key
api_key = os.environ.get("DASHSCOPE_API_KEY")
if not api_key:
print("错误:请设置环境变量 DASHSCOPE_API_KEY")
return
# 2. 创建机器人实例
bot = LabExamBot(api_key)
# 3. 初始化浏览器(开发阶段建议headless=False)
await bot.init_browser(headless=False)
try:
# 4. 登录(如果考试系统需要独立登录)
login_url = "https://your-lab-exam-system.com/login"
username = "your_student_id"
password = "your_password"
# await bot.login(login_url, username, password)
# 5. 开始刷题
exam_url = "https://your-lab-exam-system.com/exam/123"
total_questions = 50 # 根据实际考试题目数量修改
await bot.run_exam(exam_url, total_questions)
except Exception as e:
print(f"程序运行出错:{e}")
finally:
# 6. 关闭资源
await bot.close()
if __name__ == "__main__":
asyncio.run(main())
使用步骤指南 :
- 环境准备 :按照第3部分安装Python、依赖和浏览器。
- 配置API-KEY :在阿里云开通服务并获取Key,设置为环境变量
DASHSCOPE_API_KEY。 - 修改脚本配置 :
- 将
exam_url替换为你实际考试页面的URL。 - 修改
total_questions为考试总题数。 - 如果考试系统有独立登录页,取消注释
login相关代码,并修改login_url、username、password以及login方法内的选择器。 - 根据你的考试页面HTML结构,仔细调整
get_question_and_options和select_answer_and_continue方法中的所有CSS选择器(如#question-text,.option-item等)。这是脚本能否运行的关键。
- 将
- 测试运行 :首次运行务必在
headless=False模式下,观察浏览器每一步操作是否如预期。可以先将total_questions设为1或2进行测试。 - 正式运行 :测试无误后,可将
headless设为True,让脚本在后台安静运行。
6. 常见问题排查与优化技巧
在实际操作中,你几乎一定会遇到各种问题。下面是我在开发和测试中踩过的坑以及解决方案。
6.1 元素定位失败
这是最常见的问题,浏览器打开了,但脚本找不到题目或按钮。
- 症状 :
TimeoutError: Timeout 30000ms exceeded.或Error: No element found for selector: ... - 排查 :
- 确认页面加载完成 :在
page.goto()后增加await page.wait_for_load_state('networkidle'),等待网络基本空闲。 - 检查选择器 :使用
headless=False模式,在浏览器中打开开发者工具(F12),在Console里输入document.querySelector('你的选择器')看是否能找到元素。Playwright也支持在代码中截图await page.screenshot(path='debug.png')帮助查看页面状态。 - 处理iframe :如果考试内容嵌在
<iframe>里,你需要先定位到iframe,再在iframe的上下文中查找元素。frame = page.frame(name='content-frame') # 通过name或selector # 或者 page.frames 列表 question = await frame.query_selector('#question') - 动态内容 :如果题目是AJAX加载的,可能需要等待特定元素出现。使用
await page.wait_for_selector('#question', state='visible')而不仅仅是query_selector。
- 确认页面加载完成 :在
6.2 AI返回答案格式错误或不准
- 症状 :AI返回了一整段话,或者答案明显错误。
- 排查与优化 :
- 强化Prompt :在Prompt中更严厉地强调输出格式。例如:“你必须且只能输出一个大写字母,代表你认为正确的选项。不要输出任何其他字符、标点或解释。”
- 提供上下文 :如果题库是固定的,可以在Prompt开头加入少量背景知识,例如“以下是关于《高校实验室安全手册》的考试题目...”。
- 后处理校验 :在解析AI返回后,检查提取到的字母是否在预期的选项范围内(如[‘A’, ‘B’, ‘C’, ‘D’]),如果不是,可以记录该题并采用默认策略(如选C或跳过)。
- 模型调参 :尝试将
temperature降得更低(如0.01),并设置top_p=0.1,让模型输出更保守、更确定。也可以换用不同的模型试试(如qwen-turbo速度更快)。 - 人工复核 :对于非常重要的考试,可以设置脚本将AI不确定(例如返回的置信度低)或解析失败的题目记录下来,最后人工处理。
6.3 被反爬机制拦截
- 症状 :脚本运行几题后页面卡住、跳出验证码、或直接提示异常。
- 应对策略 :
- 降低速度 :这是最有效的方法。在每步操作间增加随机延迟,
await page.wait_for_timeout(random.randint(1000, 3000)),模拟真人操作的不规律性。 - 模拟人类行为 :Playwright可以模拟鼠标移动、随机轨迹。虽然我们的场景不一定需要,但更复杂的系统可能需要。
await page.mouse.move(x, y) # 移动到某个坐标 - 使用真实浏览器上下文 :Playwright可以复用已有的Chrome用户数据目录,让浏览器看起来更像一个常驻的真实用户会话。
context = await browser.launch_persistent_context(user_data_dir='./user_data') page = await context.new_page() - 处理验证码 :如果出现简单验证码,可以考虑集成OCR库(如
ddddocr、pytesseract)进行识别。但复杂验证码通常意味着你需要重新评估自动化方案的可行性。
- 降低速度 :这是最有效的方法。在每步操作间增加随机延迟,
6.4 脚本稳定性优化
- 增加重试机制 :对于网络请求或AI调用失败,可以加入重试逻辑。
import time max_retries = 3 for attempt in range(max_retries): try: answer = self.call_ai_for_answer(question, options) if answer: break except Exception as e: print(f"第{attempt+1}次尝试失败:{e}") time.sleep(2) - 详细日志 :将关键步骤(如捕获的题目、AI返回的原始文本、最终选择的答案)记录到文件,方便出错后回溯分析。
- 断点续做 :对于题量很大的考试,可以考虑记录已完成的题号到文件。如果脚本中途崩溃,重启后可以从断点继续,避免重头再来。
这个项目从构思到实现,最深的体会就是“魔鬼在细节中”。一个看似简单的自动化想法,落地时需要仔细处理网页结构的细微差别、API调用的各种异常、以及模拟人类操作的真实感。它不仅仅是一个“偷懒”的工具,更是一个综合性的Python工程实践。当你看到浏览器自动一题题翻页、选择,最终提交成功时,那种效率提升的成就感是非常实在的。最后提醒一句,技术是用来提升学习和工作效率的,请务必在合规和道德的前提下使用它。
更多推荐

所有评论(0)