Python批量查分爬虫实战:从登录验证到数据解析的完整解决方案 1. 项目概述当“查分”遇上“批量”一个刚需场景的自动化实践又到了一年一度的各类考试、资格认证成绩集中发布期。无论是学生查询期末考、四六级、考研分数还是职场人查询职业资格、职称评审结果面对成百上千个考生信息手动一个个输入准考证号、身份证号去查询不仅效率低下还极易出错。这种重复、机械、高并发的查询需求催生了“批量查分爬虫”这个看似简单、实则充满细节考量的自动化工具。我把它称为“刚需场景下的效率倍增器”。它的核心目标非常明确自动化地、准确地、高效地从指定的官方成绩查询网站批量获取大量考生的成绩信息并整理成结构化的数据如Excel或CSV文件。这背后涉及的核心技术点远不止是写个爬虫脚本那么简单。你需要考虑目标网站的反爬策略验证码、IP限制、请求频率、数据解析的稳定性网页结构可能变动、以及整个流程的健壮性和易用性。过去几年我帮不少教育机构、培训中心和企业的HR部门搭建过类似的系统踩过不少坑也总结了一套相对成熟的实践方案。这篇文章我将以一个典型的、需要登录且可能有简单图形验证码的考试成绩查询网站为例拆解构建一个健壮的“批量查分爬虫”的完整思路、技术选型、核心实现细节以及那些只有实操过才知道的“坑”和技巧。无论你是Python初学者想解决自己的实际问题还是有一定经验的开发者需要为团队构建工具都能从中找到可直接复用的代码和思路。2. 整体设计与核心思路拆解在动手写代码之前理清思路和设计架构至关重要。一个鲁棒的批量查分系统不能是简单的for循环加requests它需要模块化、可配置、易维护。2.1 核心流程与模块划分整个爬虫的工作流可以抽象为以下几个核心模块形成一个清晰的管道Pipeline数据输入模块负责读取待查询的考生信息。通常是一个Excel或CSV文件包含姓名、准考证号、身份证号等关键字段。会话管理与登录模块模拟浏览器处理网站登录。这是第一个难点可能涉及会话Session保持、验证码识别简单的图形码或滑动验证、以及登录态Cookies的持久化。查询请求构造与发送模块根据每个考生的信息构造符合目标网站接口要求的查询请求可能是GET或POST表单。响应解析与数据提取模块从返回的HTML页面或JSON数据中精准提取出成绩字段如科目一、科目二、总分等。这里需要应对网页结构变化的风险。数据存储与输出模块将提取到的成绩信息与原始考生信息关联并写入到新的Excel或CSV文件中通常一行对应一个考生的完整信息。调度与容错模块控制查询频率避免被封IP处理网络异常、解析失败等错误实现断点续查并生成详细的运行日志。2.2 技术选型与工具栈基于上述模块我的技术选型如下这套组合在易用性、功能和稳定性上取得了很好的平衡编程语言Python 3.8。毋庸置疑的选择丰富的生态库Requests, BeautifulSoup, Selenium, Pandas让爬虫开发事半功倍。HTTP请求库Requests。简单易用是处理大多数HTTP交互的首选。配合Session对象可以自动管理Cookies。HTML解析库BeautifulSoup4 (bs4)或lxml。对于静态页面BeautifulSoup的API更友好lxml解析速度更快。我通常根据复杂度选择简单页面用bs4复杂或需要高性能时用lxml。浏览器自动化备选Selenium。当目标网站登录或查询逻辑严重依赖JavaScript动态渲染且无法通过分析网络请求轻松模拟时Selenium是终极武器。但它速度慢、资源占用高应作为保底方案。验证码处理第三方OCR服务或机器学习库。对于简单的数字、字母图形验证码可以使用ddddocr、pytesseractTesseract-OCR的Python封装尝试识别。复杂验证码或性价比考虑可接入打码平台如超级鹰、图鉴。数据操作与存储Pandas。用于读写Excel/CSV文件、数据清洗和合并极其高效。并发处理可选concurrent.futures.ThreadPoolExecutor或asyncio aiohttp。当考生数量巨大如上万为提高效率可以考虑使用线程池进行并发查询。但必须谨慎控制并发数避免对目标服务器造成过大压力或触发反爬。配置管理JSON/YAML配置文件或Python字典。将目标URL、请求头Headers、字段映射关系、查询间隔等参数外部化提高脚本的可配置性。注意在技术选型上一个核心原则是“由简入繁”。优先尝试用Requests模拟请求不行再考虑Selenium。并发也应在单线程稳定运行后再考虑加入。3. 核心细节解析与实操要点这一部分我们深入每个模块看看具体怎么做以及有哪些必须注意的细节。3.1 数据输入不仅仅是读取文件输入文件的设计直接影响后续流程。我建议的格式是一个标准的Excel文件.xlsx包含以下列id(序号可选)name(姓名)exam_no(准考证号关键查询条件)id_card(身份证号另一个关键查询条件)其他可能需要的辅助信息使用Pandas读取非常简单import pandas as pd input_df pd.read_excel(考生名单.xlsx) # 检查必要列是否存在 required_columns [exam_no, id_card] if not all(col in input_df.columns for col in required_columns): raise ValueError(输入文件缺少必要的列)实操要点1数据清洗前置。读取后立即进行简单的数据清洗去除考生号、身份证号两端的空格检查是否有空值。一个干净的输入能避免很多运行时错误。input_df[exam_no] input_df[exam_no].astype(str).str.strip() input_df[id_card] input_df[id_card].astype(str).str.strip() # 过滤掉关键信息为空的行 input_df input_df.dropna(subsetrequired_columns)3.2 登录模块攻克第一道防线登录是自动化的第一个拦路虎。首先你需要手动在浏览器中完成一次登录并用开发者工具F12的“网络”(Network)选项卡记录整个过程。分析请求找到提交登录信息的那个POST请求。查看它的Form Data或Payload通常包含username、password和一个验证码字段还可能有一些隐藏的token或csrf_token。获取验证码如果登录需要验证码你需要先发送一个GET请求到验证码图片的URL这个URL通常在登录页的HTML里或者是一个独立的接口将图片下载到本地或内存中然后进行识别。识别验证码对于简单的验证码可以尝试用ddddocr。import ddddocr ocr ddddocr.DdddOcr() with open(captcha.png, rb) as f: img_bytes f.read() captcha_code ocr.classification(img_bytes)如果识别率低考虑接入付费打码平台它们的API通常能返回更准确的结果。构造登录请求使用requests.Session()对象它会在后续请求中自动携带本次登录获得的Cookies。import requests session requests.Session() # 首先可能要先访问登录页获取一些初始的token或cookies login_page_resp session.get(login_url) # 这里可能需要从login_page_resp.text中解析出csrf_token (用BeautifulSoup) # ... # 然后构造登录数据 login_data { username: your_username, password: your_password, captcha: captcha_code, # csrf_token: extracted_token } login_resp session.post(login_api_url, datalogin_data) # 检查登录是否成功通常看返回的JSON状态码或页面重定向 if 登录成功 in login_resp.text or login_resp.json().get(code) 200: print(登录成功) else: print(登录失败, login_resp.text)实操要点2会话持久化。如果批量查询耗时很长登录会话可能过期。一种策略是定期检查会话是否有效例如访问一个需要登录的页面如果失效则重新登录。更稳妥的做法是成功登录后将session.cookies用pickle库保存到文件下次启动时直接加载避免频繁触发登录流程和验证码。3.3 查询与解析稳定性的关键登录成功后就可以进行查询了。同样先手动查询一个考生分析网络请求。构造查询请求找到查询接口观察它是如何传递查询参数的通常是exam_no和id_card。可能是GET请求的参数也可能是POST的表单数据。务必模拟得和浏览器一模一样包括Headers里的User-Agent、Referer等。query_headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Referer: https://exam-query-site.com/score.html, # 其他必要的Headers } query_params { action: query, zkzh: current_exam_no, # 准考证号 sfzh: current_id_card # 身份证号 } query_resp session.post(query_api_url, dataquery_params, headersquery_headers)解析响应数据这是最容易出问题的地方。如果返回的是JSON皆大欢喜直接用query_resp.json()解析。但很多老网站返回的是HTML。使用BeautifulSoup定位成绩所在的HTML元素。你需要找到包裹成绩信息的唯一特征比如一个具有特定id或class的div或者一个table。绝对不要依赖易变的样式类名优先使用id、name属性或者通过标签结构关系来定位例如找到“英语”文本所在的td然后取它相邻的下一个td里的分数。写一个健壮的解析函数对可能缺失的字段进行处理返回None或空字符串。from bs4 import BeautifulSoup def parse_score_html(html_text): soup BeautifulSoup(html_text, html.parser) score_dict {} # 示例假设成绩在一个id为‘scoreTable’的表格里 score_table soup.find(table, {id: scoreTable}) if not score_table: return None # 或者抛出异常 rows score_table.find_all(tr) for row in rows[1:]: # 跳过表头 cols row.find_all(td) if len(cols) 2: subject cols[0].text.strip() score cols[1].text.strip() score_dict[subject] score return score_dict实操要点3防御式解析与数据验证。解析函数里要有充分的异常处理try...except并验证提取到的数据是否合理例如分数是不是数字是否在0-150之间。对于解析失败或数据异常的记录要记录下来方便后续人工复核。4. 实操过程与核心环节实现让我们把这些模块组合起来形成一个完整的、可运行的脚本框架。为了清晰我将核心逻辑封装成类。4.1 构建核心爬虫类import pandas as pd import requests import time import json import logging from bs4 import BeautifulSoup from typing import Optional, Dict, Any # 配置日志便于调试和追踪 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) class BatchScoreQuerySpider: def __init__(self, config_path: str): 初始化爬虫加载配置 with open(config_path, r, encodingutf-8) as f: self.config json.load(f) self.session requests.Session() self.session.headers.update(self.config.get(common_headers, {})) self.results [] # 存储查询结果 def login(self) - bool: 登录到成绩查询网站 login_cfg self.config[login] # 1. 获取登录页如果需要token if login_cfg.get(need_init_page, False): self.session.get(login_cfg[init_page_url]) # 这里可以解析token简化起见假设不需要 # 2. 处理验证码如果存在 captcha_code None if login_cfg.get(has_captcha, False): captcha_url login_cfg[captcha_url] # 下载验证码图片 captcha_resp self.session.get(captcha_url) # 这里可以调用OCR识别示例中我们手动输入适用于调试 with open(temp_captcha.png, wb) as f: f.write(captcha_resp.content) print(请查看当前目录下的 temp_captcha.png 并输入验证码) captcha_code input().strip() # 3. 构造登录数据 login_data login_cfg[login_data_template].copy() login_data[login_cfg[username_field]] login_cfg[username] login_data[login_cfg[password_field]] login_cfg[password] if captcha_code: login_data[login_cfg[captcha_field]] captcha_code # 4. 发送登录请求 logger.info(正在尝试登录...) resp self.session.post(login_cfg[login_api_url], datalogin_data) # 根据实际响应判断登录成功 if login_cfg[success_indicator] in resp.text: logger.info(登录成功) return True else: logger.error(f登录失败响应{resp.text[:200]}) return False def query_single(self, exam_no: str, id_card: str) - Optional[Dict[str, Any]]: 查询单个考生的成绩 query_cfg self.config[query] # 构造查询参数 params_or_data query_cfg[params_template].copy() # 替换占位符 for key, value in params_or_data.items(): if value {exam_no}: params_or_data[key] exam_no elif value {id_card}: params_or_data[key] id_card try: logger.debug(f查询考生{exam_no}) if query_cfg[method].upper() GET: resp self.session.get(query_cfg[api_url], paramsparams_or_data, headersquery_cfg.get(headers, {})) else: # POST resp self.session.post(query_cfg[api_url], dataparams_or_data, headersquery_cfg.get(headers, {})) resp.raise_for_status() # 检查HTTP错误 # 根据配置的响应类型进行解析 if query_cfg.get(response_type) json: data resp.json() # 这里需要根据实际JSON结构提取成绩假设成绩在data[scores]里 scores data.get(scores, {}) else: # 默认为HTML scores self._parse_html_score(resp.text, query_cfg.get(parse_rules)) if scores: # 如果成功解析到成绩 return { exam_no: exam_no, id_card: id_card, **scores, # 将成绩字典展开 query_status: 成功, error_msg: } else: return { exam_no: exam_no, id_card: id_card, query_status: 解析失败, error_msg: 未能从响应中解析出成绩 } except requests.exceptions.RequestException as e: logger.error(f网络请求失败 [{exam_no}]: {e}) return { exam_no: exam_no, id_card: id_card, query_status: 请求失败, error_msg: str(e) } except Exception as e: logger.error(f查询过程异常 [{exam_no}]: {e}) return { exam_no: exam_no, id_card: id_card, query_status: 异常, error_msg: str(e) } def _parse_html_score(self, html: str, parse_rules: Dict) - Dict: 根据规则解析HTML中的成绩 # 这是一个示例解析函数实际规则需要根据目标网站定制 soup BeautifulSoup(html, html.parser) scores {} # 示例规则parse_rules 可能包含选择器 # 例如{total_score: #scoreTotal, subject1: .subject-row:nth-child(1) .score} for field, selector in parse_rules.items(): element soup.select_one(selector) scores[field] element.text.strip() if element else N/A return scores def run(self, input_file: str, output_file: str): 主运行流程 # 1. 登录 if not self.login(): logger.error(登录失败程序退出。) return # 2. 读取输入数据 logger.info(f正在读取输入文件{input_file}) df_input pd.read_excel(input_file) df_input[exam_no] df_input[exam_no].astype(str).str.strip() df_input[id_card] df_input[id_card].astype(str).str.strip() total len(df_input) logger.info(f共需查询 {total} 条记录。) # 3. 逐条查询 for idx, row in df_input.iterrows(): exam_no row[exam_no] id_card row[id_card] result self.query_single(exam_no, id_card) self.results.append(result) logger.info(f进度 [{idx1}/{total}] - {exam_no}: {result[query_status]}) # 4. 请求间隔避免被封 time.sleep(self.config.get(request_interval, 2)) # 5. 保存结果 logger.info(查询完毕正在保存结果...) df_results pd.DataFrame(self.results) # 将结果与原始输入合并如果需要保留原始所有信息 df_output pd.merge(df_input, df_results, on[exam_no, id_card], howleft) df_output.to_excel(output_file, indexFalse) logger.info(f结果已保存至{output_file}) # 6. 打印简要统计 success_count (df_output[query_status] 成功).sum() logger.info(f任务完成。成功{success_count}失败{total - success_count}) # 配置文件示例 (config.json) { common_headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... }, login: { need_init_page: true, init_page_url: https://example.com/loginPage, has_captcha: true, captcha_url: https://example.com/captcha.jpg, login_api_url: https://example.com/doLogin, username_field: user, password_field: pass, captcha_field: code, username: your_username, password: your_password, login_data_template: { user: , pass: , code: }, success_indicator: 登录成功 }, query: { method: POST, api_url: https://example.com/queryScore, headers: { Referer: https://example.com/queryPage }, params_template: { zkzh: {exam_no}, sfzh: {id_card}, submit: 查询 }, response_type: html, parse_rules: { name: #nameSpan, total: #totalScore, subject1_score: table.scoreTable tr:nth-of-type(1) td:nth-of-type(2) } }, request_interval: 3 } 4.2 运行与配置准备配置文件根据目标网站的分析结果仔细填写config.json。这是整个脚本的“指挥中心”。准备输入文件按照格式准备好考生名单.xlsx。运行脚本if __name__ __main__: spider BatchScoreQuerySpider(config.json) spider.run(考生名单.xlsx, 查询结果.xlsx)实操要点4请求间隔与礼貌爬取。request_interval参数至关重要。设置一个合理的间隔如3-5秒是对目标网站服务器的基本尊重也能有效降低IP被封的风险。对于非常重要的查询我甚至建议间隔设置在10秒以上。5. 常见问题与排查技巧实录在实际操作中你一定会遇到各种各样的问题。下面是我总结的“排坑指南”。5.1 登录总是失败问题脚本模拟登录但服务器总是返回错误或跳回登录页。排查检查网络请求用Fiddler、Charles或浏览器开发者工具对比你的脚本请求和浏览器手动登录的请求。确保每一个Header尤其是User-Agent,Content-Type,Cookie、每一个表单字段包括隐藏的__VIEWSTATE、token都完全一致。差一个字符都可能失败。验证码问题验证码识别错误是最常见的原因。先注释掉自动识别将验证码图片保存下来手动输入到脚本中看能否登录成功。如果手动输入可以说明是OCR识别率问题需要更换识别方案或接入打码平台。会话与Cookie确保使用了requests.Session()并且获取验证码和提交登录使用的是同一个session对象。有些网站需要先访问登录页获取一个初始Cookie然后再用这个Cookie去请求验证码和登录。加密参数越来越多的网站会对密码甚至整个登录包进行前端加密如RSA AES。如果你在Form Data里看到一堆乱码似的encryptedPassword那就需要分析前端JavaScript的加密逻辑并用Python如execjs库模拟执行。这是爬虫中最难的部分有时不得不祭出Selenium。5.2 查询不到数据或解析出错问题登录成功但查询请求返回空页面、错误提示或者解析不到成绩。排查检查查询参数同样对比浏览器查询时的请求。确认准考证号、身份证号的参数名和格式是否有空格、是否需要URL编码。检查登录状态查询时是否携带了有效的登录Cookie可以在查询前让脚本访问一个需要登录的个人中心页面看是否能成功获取内容以验证会话有效性。处理重定向有些网站查询后会重定向。确保你的脚本能跟随重定向requests默认是跟随的或者手动处理重定向后的最终页面。应对动态内容如果成绩是JavaScript异步加载的即页面框架先出来成绩后通过AJAX请求填充那么直接请求查询URL得到的是空壳。你需要找到那个真正的数据接口XHR请求直接模拟请求那个接口它通常返回JSON。解析规则失效这是高频问题。网站改版HTML结构变了你的CSS选择器或查找方法就失效了。防御性编程在解析函数里增加多重try-except对关键元素进行if element is None判断并记录下解析失败的原始HTML片段方便你更新规则。5.3 被封IP或访问频率限制问题运行一段时间后返回403 Forbidden、验证码频繁出现或直接无法访问。应对策略降低频率首要措施增大request_interval。这是最有效、最道德的方法。使用代理IP池如果需要高速查询必须使用代理IP。可以从付费代理服务商购买或者自建代理池。在requests中设置代理很简单proxies {http: http://ip:port, https: https://ip:port}然后在请求时传入。需要实现IP轮换逻辑。模拟更真实的浏览器完善Headers包括Accept,Accept-Language,Connection等。甚至可以随机切换不同的User-Agent。识别验证码并处理如果被限制后出现验证码需要将验证码识别逻辑集成到查询流程中而不仅仅是登录环节。5.4 数据错乱与存储问题问题最终生成的Excel文件里成绩和考生对不上号或者有大量空行。排查严格的数据关联在query_single函数返回的结果中必须包含能唯一标识该记录的字段如exam_no和id_card。在最后用Pandas合并时就以此为准进行关联merge函数的on参数而不是依赖行顺序。实时保存与断点续查对于海量数据不要等全部查完再保存。可以每查询N条比如50条就将self.results列表保存到磁盘如JSON Lines格式。如果程序中途崩溃重启后可以加载已保存的结果跳过已查询的考生。这是生产级脚本的必备特性。日志记录详细的日志logging模块是你的眼睛。记录下每一条记录的查询状态、耗时、错误信息。出现问题时翻看日志是最高效的定位方法。一个实用的调试技巧在开发阶段将关键步骤的HTTP请求和响应内容保存到文件。例如将登录成功后的session.cookies、第一次查询的请求头和响应HTML保存下来。这样你可以在浏览器开发者工具的“网络”选项卡中通过“导入HAR文件”或直接修改Cookie来完全复现脚本的行为进行对比调试。最后请始终牢记批量查分爬虫是一个工具其使用应在合法合规的范围内尊重网站的服务条款不要用于恶意爬取或对服务器造成攻击性压力。将请求频率控制在合理范围既是技术上的稳健策略也是对数据提供方的基本尊重。