ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

考试宝试题爬虫实战:Selenium环境搭建、登录态复用与XPath定位全攻略

考试宝试题爬虫实战:Selenium环境搭建、登录态复用与XPath定位全攻略 简介基于Python与Selenium实现的考试宝试题爬虫源码包面向计算机专业毕业设计、课程作业与爬虫实战学习者提供完整可运行的爬虫项目及使用说明。压缩包共6个文件包含两个Python源文件主逻辑与配置文件、Chromedriver执行驱动、Git忽略文件、Markdown说明文档及一张项目结构示意图整体8.2MB轻量易部署。目前已有273人学习下载项目源自评审98分的高分设计源码经本地编译调试确保可运行。通过Selenium实现网页自动化访问与试题数据抓取配套说明对驱动配置、代码组成和运行流程有清晰介绍可帮助读者快速跑通Selenium爬虫开发流程并迁移至其他采集场景。1. 考试宝试题爬虫这件事为什么选 Selenium 而不是 requests想批量把考试宝某个题库的题目抓下来离线练习的人多半会先试 requests BeautifulSoup结果发现页面源码里根本没有题目文本——题干和选项都是前端 JS 动态渲染出来的直接请求 HTML 只能拿到一堆空壳 div。这时候 selenium 就成了最省事的方案它用真实浏览器跑页面等 JS 执行完再抓 DOM比逆向接口加解密省太多时间。这篇笔记就是一套基于 Python selenium 的考试宝试题爬虫完整落地路径从环境搭建、登录态复用、元素定位到翻页落盘全部给出可复制的代码和参数并把最容易翻车的几个点单独拎出来讲透。适合想抓题自用的学生或做题库分析的人照着改改选择器就能用在同类考试站点上。2. 环境搭建与登录态复用Selenium 版本、驱动安装与 cookies 持久化先解决一个最现实的问题Selenium 4 之后 API 变化不小网上大量教程还在用find_element_by_xpath这种旧写法直接复制运行会报AttributeError: WebDriver object has no attribute find_element_by_xpath。新项目建议直接用 Selenium 4.x 配合 ChromeDriver同时用 webdriver-manager 自动拉取驱动省掉手动对版本号这一整类麻烦。2.1 依赖安装与驱动版本最常见的翻车点Python 环境要求很低3.8 往上都行。安装命令两条搞定pip install selenium webdriver-manager如果你的机器上 Python 是通过官网安装包装的建议顺手确认一下 pip 指向的是哪个解释器避免装到了别的环境里。安装完成后先写一段最简启动代码验证驱动能不能起来from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options webdriver.ChromeOptions() options.add_argument(--headlessnew) # 新版无头模式Chrome 109 可用 options.add_argument(--disable-gpu) # 无头模式下关闭 GPU 加速减少渲染报错 options.add_argument(--no-sandbox) # Linux 服务器上必须加否则以 root 运行会直接崩 options.add_argument(--window-size1920,1080) # 固定窗口尺寸避免响应式布局把元素挤到屏幕外 driver webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions ) driver.get(https://www.baidu.com) print(driver.title) driver.quit()这段代码里参数不是随便加的每个都有实际意义。--headlessnew是 Chrome 109 之后的新无头模式行为更接近有头浏览器跑很多带懒加载的页面比旧版 headless 稳定得多。--window-size值得专门说一下考试宝这类站点对不同宽度屏幕会切换布局宽度不够时题目选项可能渲染成纵向排列或者某些元素被折叠进弹层里固定 1920 宽能规避一大半定位问题。版本错位是新手遇到最多的坑Chrome 浏览器自动升级到了 114但你系统里装的 chromedriver 还是 110启动时直接报session not created: This version of ChromeDriver only supports Chrome version 110。webdriver-manager 的原理就是启动前先检测本机 Chrome 版本然后去下载对应驱动基本一劳永逸。它的install()默认走的是 Chrome 官方渠道如果你在公司内网、连不上外网就得手动下载驱动放到项目目录再用Service(executable_path./chromedriver)指定路径。2.2 登录态复用让爬虫不用每次扫码考试宝的很多题库需要登录才能看完整题目而登录方式是扫码或者手机验证码爬虫没法自动化完成。所以正确策略是第一次手动登录把浏览器 cookies 存下来之后每次启动爬虫直接加载 cookies跳过登录环节。import json from pathlib import Path COOKIE_FILE cookies.json def save_cookies(driver, pathCOOKIE_FILE): 把当前浏览器所有 cookies 写入 JSON 文件 cookies driver.get_cookies() with open(path, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) print(f已保存 {len(cookies)} 条 cookies 到 {path}) def load_cookies(driver, pathCOOKIE_FILE): 从 JSON 文件读取 cookies 并挂到当前浏览器 if not Path(path).exists(): return False with open(path, r, encodingutf-8) as f: cookies json.load(f) for cookie in cookies: driver.add_cookie(cookie) return True def login_once(driver): 第一次登录打开登录页等用户扫码完成再保存 cookies driver.get(https://www.example.com/login) # 换成考试宝实际登录地址 input(扫完码、确认登录成功后按回车继续...) # 登录成功后页面会跳转此时 cookies 已经写入浏览器 save_cookies(driver)这里有个关键顺序问题add_cookie之前必须先driver.get()访问一次目标域名。因为浏览器只接受归属当前域名的 cookie你还没打开过这个站点就直接塞 cookieSelenium 会抛InvalidCookieDomainException。所以加载 cookies 的正确姿势是driver.get(https://www.example.com) # 先建立域 load_cookies(driver) # 再挂 cookie driver.refresh() # 刷新让登录态生效另外提醒一点cookies 是有时效的考试宝的登录态一般能撑几天到几周。如果发现抓着抓着突然全部返回请登录页面那就是 cookie 过期了重新跑一次登录流程即可。还有一坑是 cookies 文件里可能包含expiry、sameSite这类字段Selenium 的add_cookie对字段有校验个别字段格式不对会报错。稳妥的做法是保存前先做一次清洗只保留name、value、domain、path这四个必填字段clean [] for c in driver.get_cookies(): clean.append({ name: c[name], value: c[value], domain: c.get(domain), path: c.get(path, /), })这个细节是我自己踩过的有一次保存了完整 cookie 结构第二天加载时 Selenium 报invalid argument: cannot parse cookie排查半天发现是expiry字段类型问题。清洗之后再没出过事。3. 元素定位与等待策略考试宝页面上怎么找准题目跑通环境之后真正的技术活开始了。爬虫能不能稳定抓到题目取决于两件事等得够不够准、选得够不够稳。很多新人上来就time.sleep(3)页面慢一点就抓空快一点又白等这种写法注定不长久。这一章把等待策略和 XPath 定位讲透代码可以直接抄。3.1 用 WebDriverWait 而不是 sleep超时与轮询参数怎么设页面里的题目是 JS 异步渲染的driver.get()返回时 DOM 可能还是空的。Selenium 官方推荐的方案是显式等待明确告诉浏览器你等到某个元素出现再继续执行。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, timeout10) # 等题目容器出现最多等 10 秒 question_items wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, div.question-item)) ) # 等翻页按钮可点击再去点击 next_btn wait.until( EC.element_to_be_clickable((By.XPATH, //button[contains(text(), 下一页)])) )presence_of_element_located表示元素出现在 DOM 树里就算满足哪怕它还藏在屏幕外element_to_be_clickable则要求元素可见且可交互适合按钮、链接这类要点击的对象。另一个常用的是visibility_of_element_located用于需要读取文本内容的元素——如果只是判断存在但页面还没渲染完text可能拿到空字符串。关于超时参数我的习惯是页面初次加载给 10 秒点击后二次渲染给 5 秒碰到需要额外校验的场景比如提交后出现结果给到 30 秒。超时会抛TimeoutException所以要配合 try/except 做重试而不是让它直接中断整个爬虫from selenium.common.exceptions import TimeoutException try: wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, div.question-item))) except TimeoutException: print(题目元素加载超时尝试滚动页面触发懒加载) driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(2)考试宝这类题库站通常用懒加载页面底部一滚才往 DOM 里塞下一批题目。所以光等还不够得先触发滚动。这个组合拳我在多个站点上都验证过是有效的。3.2 XPath 与 text 函数提取题干和选项的标准写法定位到题目容器之后要在容器内部再找题干、选项和答案。下面的 HTML 结构是典型考试站点的题目骨架具体 class 名你 F12 看一眼就能对上div classquestion-item div classstemPython 中用于定义函数的关键字是 /div div classoptionA. class/div div classoptionB. def/div div classoptionC. function/div div classoptionD. lambda/div div classanswerB/div /div用 XPath 提取时核心写法是这样的def parse_question(item): # 题干定位 stem 类名的 div取文本 stem_el item.find_element(By.XPATH, .//div[contains(class, stem)]) stem stem_el.text.strip() # 选项可能有多个用 find_elements 返回列表 option_els item.find_element(By.XPATH, .//div[contains(class, option)]) options [o.text.strip() for o in option_els] # 答案有的站点直接渲染有的要点击“查看答案”后才出现 try: answer_el item.find_element(By.XPATH, .//div[contains(class, answer)]) answer answer_el.text.strip() except Exception: answer item.get_attribute(data-answer) # 兜底读自定义属性 return {stem: stem, options: options, answer: answer}这里有两个必须避开的坑。第一find_element不能直接选text()节点——很多人从网上抄到类似.//div/text()的写法Selenium 会直接抛The result of the xpath expression is: [object Text]。正确做法是先定位到元素再通过.text属性拿文本。第二contains(text(), xxx)这个函数只匹配元素的直接文本子节点不递归匹配子元素。如果题干里某个关键词包在span里你得写成.//div[contains(., 题型)]用.代替text()表示匹配整个元素的所有文本内容。这俩坑我见人踩过无数回。定位元素还有一种常见场景是按文本内容找节点比如找下一页按钮next_btn driver.find_element( By.XPATH, //button[contains(text(), 下一页) or contains(.//span, 下一页)] )contains(text(), ...) or contains(.//span, ...)这种写法覆盖了文本直接写在按钮里和文本包在 span 里两种情况实战里非常管用。如果按钮文本带首尾空格text()匹配不上normalize-space()可以解决//button[normalize-space() 下一页]关于选择器选型我的建议是能用 XPath 用 XPath。CSS 选择器适合按 class 精确定位但考试类站点前端改版频繁class 名经常带随机后缀或者改名contains(class, stem)这种模糊匹配的容错度比完整 class 高得多。4. 组装一个能跑的考试宝试题爬虫翻页、解析与落盘前面几章把零件备齐了这一章把它们组装成一台完整机器。我会给出一份可以直接保存运行的脚本包含登录、抓取、解析、落盘四个模块并解释每段代码在流程里的作用。标题里提到的使用说明在这里落到实处脚本跑完你会得到一份结构化的 JSON 题库文件。4.1 主流程代码最小可用的单套题库爬虫# exam_crawler.py # 使用说明 # 首次使用python exam_crawler.py --login 手动登录并保存 cookies # 日常抓取python exam_crawler.py --crawl --start 1 --end 10 # 参数说明 # --start 起始页码--end 结束页码页码从 1 开始 import argparse import json import time from pathlib import Path from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager # 配置区 BASE_URL https://www.example.com/question/list?page # 换成题库实际列表页地址 COOKIE_FILE cookies.json OUTPUT_FILE questions.json # 题干和选项的选择器按你 F12 看到的实际 class 修改 SEL_QUESTION div.question-item SEL_STEM .//div[contains(class, stem)] SEL_OPTION .//div[contains(class, option)] SEL_ANSWER .//div[contains(class, answer)] # def get_driver(headlessTrue): options webdriver.ChromeOptions() if headless: options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--window-size1920,1080) options.add_argument(--langzh-CN) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) return webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions ) def save_cookies(driver): clean [] for c in driver.get_cookies(): clean.append({ name: c[name], value: c[value], domain: c.get(domain), path: c.get(path, /), }) with open(COOKIE_FILE, w, encodingutf-8) as f: json.dump(clean, f, ensure_asciiFalse, indent2) def load_cookies(driver): if not Path(COOKIE_FILE).exists(): return False with open(COOKIE_FILE, r, encodingutf-8) as f: cookies json.load(f) for cookie in cookies: driver.add_cookie(cookie) return True def is_logged_in(driver): 检测登录态找一个登录后才会出现的元素做判断 try: driver.find_element(By.CSS_SELECTOR, div.user-info) # 换成实际元素 return True except Exception: return False def parse_page(driver): questions [] items driver.find_elements(By.CSS_SELECTOR, SEL_QUESTION) for item in items: stem item.find_element(By.XPATH, SEL_STEM).text.strip() options [o.text.strip() for o in item.find_elements(By.XPATH, SEL_OPTION)] answer try: answer item.find_element(By.XPATH, SEL_ANSWER).text.strip() except Exception: answer item.get_attribute(data-answer) or questions.append({ stem: stem, options: options, answer: answer, }) return questions def crawl_pages(driver, start, end): all_questions [] for page in range(start, end 1): driver.get(f{BASE_URL}{page}) wait WebDriverWait(driver, 10) try: wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, SEL_QUESTION))) except Exception: print(f第 {page} 页加载超时可能是最后一页或结构变了) break # 触发懒加载滚动两次 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(1) driver.execute_script(window.scrollTo(0, 0)) page_questions parse_page(driver) if not page_questions: print(f第 {page} 页解析到 0 题提前结束) break all_questions.extend(page_questions) print(f第 {page} 页{len(page_questions)} 题累计 {len(all_questions)} 题) time.sleep(2) # 礼貌限速别把对方服务器打得太狠 return all_questions def main(): parser argparse.ArgumentParser() parser.add_argument(--login, actionstore_true, help手动登录并保存 cookies) parser.add_argument(--crawl, actionstore_true, help开始抓取) parser.add_argument(--start, typeint, default1) parser.add_argument(--end, typeint, default10) args parser.parse_args() driver get_driver(headlessFalse if args.login else True) if args.login: driver.get(https://www.example.com/login) input(登录成功后按回车保存 cookies...) save_cookies(driver) driver.quit() return if args.crawl: driver.get(BASE_URL.split(page)[0]) # 先访问根域 load_cookies(driver) driver.refresh() if not is_logged_in(driver): print(登录态失效先执行 python exam_crawler.py --login) driver.quit() return questions crawl_pages(driver, args.start, args.end) with open(OUTPUT_FILE, w, encodingutf-8) as f: json.dump(questions, f, ensure_asciiFalse, indent2) print(f完成共 {len(questions)} 题已写入 {OUTPUT_FILE}) driver.quit() if __name__ __main__: main()这段代码的流程逻辑是--login模式用有头浏览器打开登录页等人扫完码后把 cookies 落盘--crawl模式启动无头浏览器先访问根域挂 cookies再逐页抓取。翻页方式这里用的是最简单的一种——直接修改 URL 里的 page 参数。这是因为考试宝这类题库站的列表页通常支持 URL 传参翻页比模拟点击下一页按钮稳定得多。如果目标站点不支持 URL 翻页就得走按钮点击代码要改成这样while True: # 抓当前页... try: next_btn wait.until(EC.element_to_be_clickable( (By.XPATH, //button[contains(text(), 下一页)]))) next_btn.click() time.sleep(1) except Exception: print(没有下一页按钮抓取结束) break按钮点击有两个隐患一是点击后页面如果是 AJAX 局部刷新Selenium 拿着旧元素句柄去操作会报StaleElementReferenceException解决办法是每次循环重新查找按钮二是按钮离屏时点击会失效需要先driver.execute_script(arguments[0].scrollIntoView(true), next_btn)把按钮滚进视口再点。4.2 数据结构与落盘JSON 与 CSV 怎么选、答案如何对齐落盘格式我优先推荐 JSON。原因很简单题库数据的自然形态是嵌套的——一道题包含题干、多个选项、答案、可能还有解析。用 CSV 存长这样stem,option1,option2,option3,option4,answer,analysis选项数量一变化就得改表头同时题干里如果有逗号、换行还得转义非常痛苦。JSON 则天然支持任意结构[ { stem: Python 中用于定义函数的关键字是, options: [A. class, B. def, C. function, D. lambda], answer: B } ]写文件时两个参数必须记住ensure_asciiFalse保证中文原样输出而不是变成\uXXXX转义序列indent2让文件可读方便人工检查。如果你后续要用 Excel 或者 pandas 做数据分析CSV 更顺手。转换逻辑很简单选项列表用|分隔拼进单列import csv with open(questions.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([题干, 选项, 答案]) for q in questions: writer.writerow([ q[stem], | .join(q[options]), q[answer], ])这里编码用utf-8-sig而不是utf-8是因为 Excel 打开 UTF-8 文件默认不识别中文会乱码带 BOM 的utf-8-sig才能让 Excel 直接正常显示。关于答案对齐考试宝有些题目的答案不是直接渲染在 DOM 里的而是藏在点击查看答案之后才展开的区域内。遇到这种情况解析时得模拟点击# 点击题目里的“查看答案”按钮 try: btn item.find_element(By.XPATH, .//button[contains(text(), 查看答案)]) btn.click() time.sleep(0.3) # 展开动画或异步渲染需要一点时间 answer item.find_element(By.XPATH, .//div[contains(class, analysis)]).text.strip() except Exception: answer 注意time.sleep(0.3)在这里是故意的答案区域展开通常有 CSS 动画等 0.3 秒成本极低但能显著降低元素存在但文本还没渲染完的概率。等待不是不能用而是要用在明确知道等待什么、等多久的场合。另外提醒一句合规底线这套爬虫只适用于抓取你自己有权限访问的题目用于个人学习或内部题库整理。不要用它去高频请求把对方服务器打挂不要绕过付费墙去抓收费内容抓取频率控制在每页 2 秒以上是基本礼貌。5. 考试宝爬虫高频翻车点排查定位失效、滑块验证与长时间运行这一章是把前面所有坑集中爆出来。每条都是现象 → 原因 → 解决的结构都是我实际处理过或者见过别人踩的典型场景。写爬虫的人最怕的不是代码报错而是代码不报错但结果全错——所以这一章值得多看两遍。5.1 选择题干元素突然失效页面改版与登录态页面差异现象爬虫昨天还能跑今天启动后每一页都打印解析到 0 题或者直接报NoSuchElementException。原因两种情况最常见。一是站点前端改版CSS 类名变了比如question-item改成了question-card二是登录和未登录渲染的页面结构不一样——未登录时显示的是诱导登录的遮罩层题目 DOM 根本不存在。解决先不要改代码打开浏览器手动访问一页按 F12 看实际 DOM。确认是登录态问题还是结构变化。如果是结构变化用多级选择器做兜底我一般会维护一个候选列表QUESTION_SELECTORS [ div.question-item, div.question-card, div.exam-question, div[class*question], # 最后兜底只要 class 里含 question 就抓 ] def find_question_items(driver): for selector in QUESTION_SELECTORS: items driver.find_elements(By.CSS_SELECTOR, selector) if items: return items return []这样即使主选择器失效爬虫还能继续跑同时打印一条警告提示你手动核验。比直接崩掉强太多。5.2 无头模式触发滑块验证码自动化特征太明显现象headless 模式一打开题库页面还没开始抓页面就弹出滑块验证或者提示访问环境异常请完成验证。原因无头模式的浏览器指纹特征非常明显window.navigator.webdriver标志为 true加上请求频率过快、IP 段异常很容易被风控系统识别。滑块验证不是针对你的爬虫而是针对所有异常访问的统一防护。解决分三层处理。第一层降低触发概率去掉 headless 改有头模式、把每页间隔从 2 秒提到 4 秒、同一时间只跑一个爬虫实例。第二层处理触发后的验证有头模式下滑块的拖动可以用ActionChains模拟但成功率看网站风控强度不保证每次都过。第三层最实际的方案是——账号被风控之后就停手等半小时一小时的冷却期再继续别硬刚。另外有几个减少指纹暴露的常见参数options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False)这三个参数能去掉一部分自动化标志但实话讲现在主流站点的风控早就不只看这个标志了。更有效的是控制节奏和总量把自己伪装成正常但有点急性子的用户而不是机器人在疯狂刷页面。5.3 登录态静默失效cookies 过期但不报错现象抓了 300 页一直正常突然某页返回的数据变成请先登录的提示页但因为页面里没有题目元素爬虫直接判定解析到 0 题并退出你回头看日志才发现数据缺了一大截。原因登录 cookies 在抓取中途过期或者服务端检测到频繁请求强制下线。Selenium 不会主动告诉你登录态没了只有等你发现页面内容不对才知道。解决在每页抓完后的翻页间隙做一次登录态校验把问题提前暴露出来def check_login(driver): 返回 True 表示还在登录态False 表示已失效 try: # 找一个只有登录后才能看到的元素比如用户名、头像、退出按钮 driver.find_element(By.CSS_SELECTOR, a.logout-btn) return True except Exception: return False # 在 crawl_pages 的循环里 for page in range(start, end 1): driver.get(f{BASE_URL}{page}) if not check_login(driver): print(f第 {page} 页检测到登录态失效停止抓取) break # 后续正常解析...检测到失效之后不要自动重新登录直接停。因为自动登录需要扫码爬虫没法自己完成停下来人工处理是唯一靠谱的路。损失可控至少你知道断在哪一页补抓的时候从那页继续就行。5.4 抓了几千题之后浏览器越来越慢直至崩溃现象爬虫跑到 2000 题以后每页响应时间从 3 秒涨到 10 秒最后浏览器标签页直接变成无响应进程被杀。原因Selenium 长时间运行不重启浏览器进程内积累了大量 DOM 节点和渲染资源。虽然每页解析完题目对象就释放了但 Chrome 渲染进程的内存不会立刻还给系统。无头模式下这个问题更严重。解决定时重启 driver 是最简单有效的方案。每抓 50 页左右杀掉浏览器重新起一个代价是重新加载 cookies但换来的是稳定def crawl_with_restart(start, end, restart_every50): all_questions [] page start while page end: driver get_driver(headlessTrue) driver.get(BASE_URL.split(page)[0]) load_cookies(driver) driver.refresh() batch_end min(page restart_every - 1, end) all_questions.extend(crawl_pages(driver, page, batch_end)) driver.quit() # 强制释放内存 page batch_end 1 return all_questions重启的代价很小因为每页的数据已经落盘过一轮重启后只是接着上次的页码继续。内存相关的排查也有个实用技巧抓取过程中用系统资源监视器盯着 Chrome 的进程占用如果单进程内存超过 1.5GB 还不回落基本就是该重启的信号。6. 让爬虫长期稳定跑的三件事失败重试、增量抓取与运行日志爬虫写出来能跑一次不算本事能周周跑、月月跑才算数。因为题库站会更新题目、改版页面、清理异常账号一套裸爬虫在真实环境里的存活周期通常不超过两周。我自己的经验是把三件事做进代码里失败重试、增量抓取、运行日志。失败重试要覆盖两种异常。一种是网络层的TimeoutException页面加载超时重试大概率能过一种是解析层的页面有结构但抓不到数据重试两次还有问题就直接跳过不要卡死整个任务。def get_page_with_retry(driver, url, retries3): for attempt in range(retries): try: driver.get(url) WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, SEL_QUESTION))) return True except Exception: print(f第 {attempt 1} 次加载失败: {url}) time.sleep(3 * (attempt 1)) # 退避等待3 秒、6 秒、9 秒 return False增量抓取解决的是题目太多、一次抓不完的问题。思路很简单启动时把已抓题目的题干哈希值载入一个 set每抓一道就核对一下重复的直接丢弃。考试类站点的题干长度稳定适合做去重键。这样下次运行从断掉的页码继续而不是从头再来。import hashlib def stem_key(stem): # 取题干前 50 个字符做哈希兼顾去重效率和区分度 return hashlib.md5(stem[:50].encode(utf-8)).hexdigest() seen set() if Path(OUTPUT_FILE).exists(): for q in json.loads(Path(OUTPUT_FILE).read_text(encodingutf-8)): seen.add(stem_key(q[stem])) # 抓取时 if stem_key(q[stem]) not in seen: seen.add(stem_key(q[stem])) all_questions.append(q)运行日志是最后一道保险。我习惯用 Python 标准库的logging写文件日志每页记一行页码、抓题数、耗时。出问题的时候直接tail日志文件看最后的页码就知道断在哪不用凭记忆猜。import logging logging.basicConfig( filenamecrawler.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) # 每页结束后 logging.info(fpage{page} ok{len(page_questions)} total{len(all_questions)})我做这类爬虫项目代码里永远给日志留位置。原因很简单爬虫这东西跑得顺的时候你不需要看它跑得不顺的时候你只能靠日志查案。早期我自己犯过一个低级错误把所有输出全打在控制台结果半夜跑挂了第二天只剩一个黑乎乎的终端窗口连挂在哪一页都不知道只能从头再扫一遍。后来养成了日志先行的习惯先写日志模块再写抓取逻辑。如果你准备拿这份代码去改造成自己的题库爬虫我最后唠叨一句经验把第 4 章的选择器配置区当成全项目最核心的部分维护。每次页面改版最先要改的永远是那几行选择器而不是翻页逻辑、落盘逻辑。把选择器集中放在文件顶部加上注释说明抓取的是什么站的什么页面三个月后你回来维护时会感激当初这个决定。希望帮到你。本文还有配套的精品资源点击获取
返回列表