Python网络爬虫技术实战:从HTTP请求到数据存储的完整指南
简介Python网络爬虫技术分章习题答案资料包面向高校爬虫课程学习者与自学人群主要解决课后选择题无答案、操作题缺乏参考实现的问题。内容覆盖网络请求、HTML解析、数据存储、Scrapy框架等章节适合用于巩固基础并对照练习。资源共47个文件压缩包整体3.4MB包含16个py源码脚本、7个txt章节答案、6个pyc缓存文件以及请求/响应头、body、meta等调试文件另有少量png、jpg与csv演示数据类型均衡便于边看边练。已有2865人学习下载。通过各章选择题答案可快速检验知识掌握度操作题代码涵盖UDP/TCP客户端、爬取豆瓣接口、爬取人民日报首页、将数据存储到MongoDB等真实场景同时保留Scrapy工程配置与TipDM数据文件能够帮助理解从请求构造到数据落库的完整流程。章节目录按第1章至第7章组织定位清晰是一份兼顾习题解析与实战示例的教学补充资料。1. 用习题答案反向梳理 Python 网络爬虫知识体系看到「Python网络爬虫技术_习题答案.rar」这个标题第一反应可能是「又是一份课后题答案」。但如果你工作三五年后还只会对着答案抄那这份压缩包对你的价值约等于零。真正值得做的事是把习题当作一份知识地图每一道题对应一个爬虫开发中的真实决策点——用 requests 还是 httpx、解析用 XPath 还是 CSS Selector、遇到动态渲染数据该不该上 Selenium、被反爬拦截时先看状态码还是响应头。这些决策不是背出来的是在写代码、跑任务、看报错的过程中长出来的。这篇文章不替你去解某本特定教材的题而是按爬虫从业者处理习题的常见思路把「Python 网络爬虫技术」这类课程里最常考的几大模块拆开HTTP 请求库选型、页面解析、动态渲染处理、数据存储、反爬对抗与异常处理。每一章都给出能直接跑的命令和代码并解释为什么这么写、参数怎么调、报错怎么看。无论你是刚学完 Python 基础准备入坑爬虫还是工作多年想系统补一遍底层细节都可以顺着这个结构把答案变成自己的工程能力。2. 夯实请求层用 requests 与 httpx 完成 HTTP 请求的收发2.1 爬虫请求的本质HTTP 协议下的资源获取网络爬虫web crawler的核心工作说到底是模拟浏览器向服务器发送 HTTP 请求接收响应后从 HTML、JSON 或其他格式中提取目标数据。理解这一点你就能明白为什么所有爬虫课程的习题里前几道总是围绕着 URL 构造、请求头设置、GET 与 POST 的区别、会话维持展开——这些不是死知识点而是你写第一个可用爬虫前必须跨过的门槛。写请求代码之前先明确选型requests 是 Python 爬虫最主流的 HTTP 库API 简洁、文档完善、社区资料多适合绝大多数静态页面抓取httpx 是后起之秀同时支持 HTTP/1.1 与 HTTP/2且提供了与 requests 几乎一致的接口最重要的是它原生支持异步适合在高并发抓取场景中替代 requests。初学者优先掌握 requests等你要写大规模采集任务时再切到 httpx两者的学习成本几乎是平移的。安装环节没什么玄学直接用 pip 装即可pip install requests httpx如果你在 Linux 系统上部署爬虫注意区分系统自带的 Python 与虚拟环境中的 Python。常见的坑是「python was not found; run without arguments to install from the microsoft store」这类提示——那是 Windows 上没把 Python 加入 PATH 的表现而在 Linux 下执行python3才是标准的进入解释器的命令。建议所有爬虫项目都建虚拟环境python3 -m venv venv source venv/bin/activate pip install requests httpx这里每条命令的含义再解释一下python3 -m venv venv是在当前目录创建一个名为 venv 的虚拟环境source venv/bin/activate是激活该环境让后续的 pip 安装都只作用于这个项目内避免污染系统全局 Python 环境。2.2 用 requests 发送 GET 与 POST 请求的最小代码学习网络爬虫技术最忌讳的就是只背 API 不写代码。下面这段代码是 GET 请求的标准范式几乎所有习题里的「抓取某网页标题」「获取某接口返回的 JSON」都能套用import requests url https://httpbin.org/get headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } params {page: 1, size: 20} resp requests.get(url, headersheaders, paramsparams, timeout10) print(resp.status_code) print(resp.url) # 实际请求的完整 URL data resp.json() # 接口返回 JSON 时直接用 .json()这段代码的逻辑说明requests 库的get方法接收四个关键参数——url是目标地址headers携带请求头其中User-Agent的作用是告诉服务器你用的是哪种客户端很多网站对非浏览器 UA 直接拒绝响应params会自动把字典拼接到 URL 查询字符串中timeout是超时秒数不设置的话请求可能无限等待。参数层面的经验timeout建议设成(3.05, 10)这种元组形式第一个值是连接超时第二个是读取超时分别控制建立连接和接收数据的最长等待时间。至于resp.json()依赖响应头里的Content-Type: application/json如果对方返回的虽是 JSON 格式但 Content-Type 写的是text/htmlrequests 会抛requests.exceptions.JSONDecodeError这时候改用自己的json.loads(resp.text)更稳。POST 请求通常用于表单提交或接口调用代码骨架如下import requests login_url https://httpbin.org/post form_data {username: admin, password: 123456} headers {User-Agent: Mozilla/5.0, Content-Type: application/x-www-form-urlencoded} resp requests.post(login_url, dataform_data, headersheaders, timeout5) print(resp.status_code) print(resp.text[:500])POST 与 GET 的差异体现在三个地方参数放在data或json参数中而不是params请求头需要指定Content-Type指明消息体的编码方式某些服务端会用Referer或Origin校验请求来源遇到 403 时优先检查这两个头。当你需要模拟登录后的状态时用requests.Session()创建一个会话对象同一个 Session 发出的所有请求会自动携带 cookies这是处理需要登录的爬虫习题的标准做法s requests.Session() s.post(login_url, data{username: user, password: pass}) # 之后的请求自动带登录 cookie resp s.get(https://httpbin.org/cookies, timeout5) print(resp.json())Session 对象的优势在于把 cookies、headers、代理配置统一管理起来避免每次请求都手动从上次响应里提取 Cookie 再塞到 headers 中这在多页面抓取场景下能省下大量重复代码。2.3 requests 高频参数对照与常见报错处理requests 的参数体系不复杂但每个参数都有它对应的坑。我把习题和实战中最常遇到的高频参数整理成一份对照表方便你在写代码时快速查阅参数作用常见坑headers设置请求头漏设 User-Agent 会被某些服务器拒绝Referer 不匹配会 403params拼接 URL 查询参数列表值需要重复 key 时用params[(k, 1), (k, 2)]data发送表单数据值为字典时 requests 自动做 urlencode传字符串时不会自动编码json发送 JSON 数据同时设置 data 与 json 时 data 优先后者被忽略timeout超时控制设为单体数字时仅代表连接超时读取超时可能无限等待proxies设置代理爬虫项目里格式为{http: http://127.0.0.1:7890}注意协议前缀verifySSL 证书验证遇到证书过期可设 False但会有 InsecureRequestWarning 警告stream流式下载大文件下载用streamTrue配合迭代否则一次性加载到内存关于verifyFalse要补一句关闭 SSL 验证虽然能绕过证书报错但同时也让连接处于明文可被劫持的状态。生产环境更推荐用certifi库里的证书或单独指定verify/path/to/ca.pem开发调试阶段图省事再关掉。requests 的异常体系也值得记住。requests.exceptions.Timeout表示超时requests.exceptions.ConnectionError表示网络不通或连接被重置requests.exceptions.TooManyRedirects表示重定向次数超限requests.exceptions.HTTPError通常由resp.raise_for_status()在状态码为 4xx/5xx 时抛出。正确的异常处理不是包一层try except Exception就完事而是分别捕获、分别处理超时重试连接错误换代理HTTPError 打日志后跳过该 URL。3. 页面解析实战XPath、CSS Selector 与正则的选型边界3.1 HTML 解析三件套的适用场景拿到响应内容之后爬虫的下一站就是解析。习题里最常见的三类解析工具是 XPath、CSS Selector、正则表达式但很多初学者看到 HTML 就无脑上正则最后被嵌套标签和转义字符折磨得死去活来。解析之前先想清楚页面结构才能选对工具。XPath 是在 XML 与 HTML 文档中查找节点的语言表达能力强能处理复杂层级关系和条件筛选是爬虫从业者的首选工具。CSS Selector 语法更紧凑适合那些结构规整的页面在 Scrapy 框架中大量使用。正则表达式适合从 HTML 中提取纯字符串模式比如从一段 JS 变量里抠出 JSON 数据或者在文本中匹配邮箱、手机号、日期等固定格式。三者不是互斥关系实际项目中经常组合使用——先用 XPath 定位节点区域再用正则精提取。为了用上这些工具你需要安装两个解析库pip install lxml parsellxml 是 C 语言实现的 libxml2 的 Python 绑定解析速度极快XPath 与 CSS Selector 都建立在它之上。parsel 是 Scrapy 团队单独抽出来的解析库API 设计得更贴合爬虫场景同时暴露了 XPath 与 CSS 两套接口。我个人在普通 requests 项目中更常用 lxml 的etree在 Scrapy 中直接用自带的 Selector两者的核心逻辑是共通的。3.2 用 lxml 的 XPath 提取数据的标准步骤下面这段代码演示了从 HTML 中提取书籍标题与价格的完整流程它对应习题里最常见的一类题「抓取列表页中所有商品的名称与价格」from lxml import etree html_text html body div classbook-list div classbook-item h2 classtitlePython网络爬虫技术/h2 span classprice79.00/span /div div classbook-item h2 classtitle流畅的Python/h2 span classprice129.00/span /div /div /body /html root etree.HTML(html_text) items root.xpath(//div[contains(class, book-item)]) for item in items: title item.xpath(.//h2[classtitle]/text())[0] price item.xpath(.//span[classprice]/text())[0] print(title, price)逻辑说明etree.HTML()会把 HTML 字符串解析成一棵节点树即使原始 HTML 有闭合标签缺失lxml 也会自动修正。root.xpath(//div[contains(class, book-item)])返回所有 class 属性中包含 book-item 的 div 节点列表。对每个 item 再使用.//h2[classtitle]/text()获取子节点的文本——开头的.表示相对当前节点查找//表示从当前节点的后代中任意层级查找text()取出该节点的纯文本内容。这里有几个 XPath 细节值得单独列出来//是从整个文档任意位置查找./是从当前节点直接子级查找//会跨越任意层级的中间节点contains(class, book-item)用于处理 class 多值的情况如果写成classbook-item遇到classbook-item active就匹配不上了text()取出的是当前节点的直接文本内容不包含子节点内部的文本需要取整个段落所有文本时用string()函数属性选择用属性名索引从 1 开始如(//div)[1]遇到 HTML 里没有 class 或 id 可用的情况XPath 的轴axis定位就派上用场了。比如获取某节点前一个弟弟节点写法是//h2/following-sibling::span[1]这比写一串复杂的正则靠谱得多。3.3 CSS Selector 的等效写法与正则补充parsel 库的 CSS Selector 写法与前端开发几乎一致像div.book-item h2.title::text就能直接选中标题节点并取其文本。下面用 parsel 重写一遍上面那个例子from parsel import Selector sel Selector(texthtml_text) for item in sel.css(div.book-item): title item.css(h2.title::text).get() price item.css(span.price::text).get() print(title, price)::text是 parsel 中获取文本内容的伪元素.get()返回第一个匹配结果.getall()返回全部匹配结果组成的列表。CSS Selector 的好处是阅读成本低缺点是表达复杂层级关系时不如 XPath 灵活比如按文本内容反选父节点、查找兄弟节点这类操作CSS 写起来会很别扭。正则表达式在爬虫解析中的定位是「补刀」。当目标数据不在 HTML 标签中而是藏在script标签里的 JS 变量中时XPath 和 CSS Selector 都只能把整个 script 标签内容作为一个字符串取出这时再用正则精准切片import re import json script_text var bookInfo {title: Python网络爬虫技术, price: 79.0}; match re.search(rvar bookInfo (\{.*?\});, script_text) if match: data json.loads(match.group(1)) print(data[title], data[price])正则里.*?是非贪婪匹配遇到第一个};就停防止匹配范围蔓延到后续其他数据。re.search在全文查找第一个匹配位置re.findall返回所有匹配结果。解析这部分的习题里还有一个隐藏考点数据在 HTML 里是「展示层」还是「接口层」。很多网站的商品价格、库存等信息是由 JavaScript 异步加载后渲染到页面的打开浏览器能看到直接 requests 抓回来的 HTML 里根本没有这些字段。遇到这类页面解析工具再熟练也没用得进入下一章讲的动态渲染处理。4. 动态渲染页面Selenium 与 Playwright 的取舍及实操4.1 为什么直接 requests 抓不到数据不少爬虫技术教材都把动态渲染放得很靠后但实际工作中它比静态解析更常见。现在的站点大量使用 Vue、React 这类前端框架页面初始 HTML 只有一个空壳 div真正的数据是页面加载后通过 XHR 或 Fetch 从后端接口拿到的再借助 JS 渲染到 DOM 上。你直接用 requests 去 GET 那个页面 URL返回的是空壳自然提取不到商品列表、用户评论、行情数据这些关键信息。处理思路有两条选哪条取决于你能否直接找到底层接口。打开浏览器开发者工具的 Network 面板刷新页面过滤 XHR 或 Fetch 类型的请求如果你能看到某个返回 JSON 数据的接口而且它没有复杂的签名或加密参数那直接用 requests 模拟它——这是效率最高的做法也是爬虫从业者口中的「直连接口」路线。如果接口参数里有动态 token、加密签名或者前端代码做了混淆导致逆向成本过高这时候才轮到浏览器自动化工具出场。4.2 用 Selenium 驱动浏览器抓取动态内容的完整示例Selenium 是老牌浏览器自动化工具原理是通过 WebDriver 协议控制真实浏览器内核执行页面加载与 JS 渲染。虽然它在速度和资源占用上不占优势但胜在生态成熟、资料多、兼容性好是习题与中小型项目里最稳妥的选择。安装需要两步pip install selenium然后你需要一个浏览器驱动。Chrome 对应 chromedriverEdge 对应 msedgedriver。新版 Selenium 支持通过 Selenium Manager 自动管理驱动只要你把浏览器升级到较新版本webdriver.Chrome()会自动下载匹配的驱动省去手动配置的痛苦。下面这段代码演示了用 Selenium 等待页面加载完成后提取数据的标准流程import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options Options() options.add_argument(--headlessnew) # 无头模式不显示浏览器界面 options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) # 窗口分辨率影响响应式布局 options.add_argument(--disable-blink-featuresAutomationControlled) # 弱化自动化特征 driver webdriver.Chrome(optionsoptions) try: driver.get(https://example.com) # 显式等待目标元素出现最多等 10 秒 element WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, div.book-item)) ) items driver.find_elements(By.CSS_SELECTOR, div.book-item) for item in items: title item.find_element(By.CSS_SELECTOR, h2.title).text print(title) finally: driver.quit()这段代码的关键点--headlessnew是 Chrome 109 之后的新无头模式行为与有头版更一致减少被网站识别的概率EC.presence_of_element_located是显式等待比time.sleep()靠谱得多——固定等待 5 秒可能等不到数据就报错也可能页面早就加载完却白白浪费 5 秒显式等待是「等条件成立」机制上没有这些毛病。driver.quit()放在finally里保证无论代码是否报错浏览器进程都被关闭避免内存泄漏。除了 SeleniumPlaywright 是近几年热度上升非常快的替代方案。它由微软维护API 设计更现代原生支持异步默认自动等待机制也更智能。从 Selenium 切到 Playwright 的成本不低但它处理复杂单页应用SPA的能力更强适合做高难度的动态渲染采集。如果你刚开始接触动态页面处理我建议先玩熟 Selenium 把原理搞懂再按需换到 Playwright。4.3 识别网站使用的渲染技术栈拿到一个页面先别急着写代码花两分钟判断它是服务端渲染还是客户端渲染能避免后面走大弯路。方法很简单在浏览器里右键查看网页源代码CtrlU 或 CommandU如果源代码里直接能看到正文内容就是服务端渲染如果源代码里只有 script 标签和空 div那就是客户端渲染。另外在 Network 面板里观察请求列表如果文档请求Document 类型返回的 HTML 很小随后跟着一大批 XHR JS 请求基本可以断定是动态渲染。对于纯静态页面requests 加 XPath 是最优解单位时间能抓取的数量远超浏览器自动化。对于动态渲染页面优先找 JSON 接口找不到再上 Selenium。这条顺序在真实项目中能帮你省下最多的服务器资源和时间成本。还有一个常见误用要提醒不是所有 Selenium 抓取都要一个个元素定位。如果只是要拿整个页面渲染完成后的 HTML直接driver.page_source一次性取回字符串再交给 lxml 解析比在 Selenium 里逐个找元素快得多。页面结构复杂时这种「浏览器渲染解析器解析」的组合方式反而更稳。5. 数据落库与反爬对抗从 CSV 到 MySQL 的工程化改造5.1 数据存储选型CSV、SQLite 还是 MySQL爬虫拿到的数据最终要落盘存储方案直接影响到后续的数据处理效率。习题里最常要求的是保存到 CSV 或 JSON 文件这类方案零配置、开箱即用适合数据量在万级以下、只做一次性分析的场景。但当你采集的数据达到百万级或者需要按条件查询、去重、增量更新时文件存储的性能和灵活性就跟不上了。我的建议是分三个梯度小规模练习数据用 CSV单机爬虫项目用 SQLite生产环境多机协作用 MySQL 或 PostgreSQL。SQLite 是 Python 标准库自带的关系型数据库只用一个文件就能存储全部数据支持 SQL 查询、事务、索引非常适合单机爬虫做数据中转。MySQL 适合需要并发写入、远程访问、备份恢复的正式业务。5.2 Python 操作 SQLite 与 MySQL 的代码骨架SQLite 的操作不依赖外部服务代码也最简洁import sqlite3 conn sqlite3.connect(books.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, price REAL, crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) books [(Python网络爬虫技术, 79.0), (流畅的Python, 129.0)] cursor.executemany(INSERT INTO books (title, price) VALUES (?, ?), books) conn.commit() conn.close()这里值得解释的是占位符?。它替代了 f-string 字符串拼接作用是防止 SQL 注入——爬到的数据里可能包含单引号、分号等 SQL 特殊字符直接拼接会语法报错甚至被恶意利用。executemany一次性插入多行数据比循环执行execute效率高得多。写完要记得conn.commit()提交事务否则数据不会真正落盘。连接 MySQL 需要在虚拟环境中安装 PyMySQL连接参数包括主机地址、端口、用户名、密码、数据库名pip install pymysqlimport pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasespider_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, ) with conn.cursor() as cursor: cursor.execute(INSERT INTO books (title, price) VALUES (%s, %s), (Python网络爬虫技术, 79.0)) conn.commit() conn.close()PyMySQL 的占位符是%s这与 sqlite3 的?不同跨库切换时容易写错。charsetutf8mb4必须设置否则存 emoji 或生僻字时会报Incorrect string value错误。DictCursor让查询结果变成字典列表字段名直接作为 key比默认的元组结构可读性高很多。数据入库时还有两个工程化细节一是建唯一索引做去重比如把商品链接设成唯一键插入时用INSERT ... ON DUPLICATE KEY UPDATE实现「存在则更新、不存在则插入」的幂等逻辑二是用time字段记录采集时间为后续的数据增量更新留后路。只写INSERT不带去重的爬虫跑两天必然出现大量重复数据清洗时痛苦加倍。5.3 反爬对抗的常见手法与应对策略习题里可能出现「模拟登录」「处理验证码」「绕过 IP 限制」这类题但实际项目中反爬对抗是一条红线原则是只采集公开可访问的数据不做绕过付费墙或破坏服务的行为。这里讨论的应对策略只针对正常访问被误伤的情况比如请求频率过高被临时封禁。最常见的反爬手段是 User-Agent 检测。服务器检查请求头里的 User-Agent发现不是常见浏览器就拒绝。应对方法是准备一个 UA 池每次请求随机替换import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, ] def get_headers(): return { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, }更隐蔽的反爬是请求频率检测。同一个 IP 在短时间内发出大量请求服务器会直接封 IP。主流应对方案是限速与重试机制。限速方案很简单每次请求之间time.sleep(random.uniform(1, 3))制造随机间隔更工程化的做法是引入 Tenacity 或 backoff 库遇到 429 状态码Too Many Requests或连接异常时自动退避重试pip install tenacityfrom tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import requests retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10), retryretry_if_exception_type((requests.exceptions.Timeout, requests.exceptions.ConnectionError)), ) def fetch_with_retry(url, **kwargs): resp requests.get(url, timeout10, **kwargs) if resp.status_code 429: raise requests.exceptions.ConnectionError(frate limited: {resp.status_code}) return resp这段代码的逻辑retry装饰器给函数加了重试能力——stop_after_attempt(3)表示最多尝试 3 次wait_exponential表示每次重试的等待时间按指数增长从 2 秒开始翻倍最多 10 秒retry_if_exception_type指定只有遇到超时和连接错误才重试避免在 HTTP 404 这种确定错误上浪费重试次数。我在函数内部手动将 429 状态码转成 ConnectionError是为了让重试逻辑能覆盖「被限流」这个场景。如果单 IP 采集量确实超出阈值代理池是最后的解决方案。requests 的proxies参数传入{http: http://代理IP:端口, https: http://代理IP:端口}即可。但代理池的质量参差不齐失效代理会让你的错误处理逻辑频繁触发反而降低整体采集效率。成熟的爬虫框架 Scrapy 自带了RetryMiddleware和ProxyMiddleware把这两块能力统一管理起来这也是为什么大型采集项目一般默认选 Scrapy 而不是裸 requests。6. 并发提速与断点续抓的工程化收尾技巧6.1 多线程并发爬虫的最小可用实现爬虫跑通之后下一个关注点必然是速度。单线程逐个请求遇到高延迟的站点千条 URL 可能要跑几十分钟。用多线程并发能成倍缩短时间但要控制并发度避免对目标服务器造成压力也避免自己被封。Python 的concurrent.futures.ThreadPoolExecutor是标准库自带的最简单方案import time import requests from concurrent.futures import ThreadPoolExecutor, as_completed URLS [fhttps://httpbin.org/delay/1?index{i} for i in range(20)] def fetch_one(url): resp requests.get(url, timeout10) return resp.status_code, url with ThreadPoolExecutor(max_workers5) as executor: future_to_url {executor.submit(fetch_one, url): url for url in URLS} for future in as_completed(future_to_url): status, url future.result() print(status, url)max_workers参数控制同时运行的线程数一般设为 5 到 10 即可太高的并发度在访问方看来等同于攻击。executor.submit把任务提交给线程池返回一个 Future 对象as_completed按任务完成顺序依次产出 Future避免等待最慢的那个任务拖慢整体。如果你需要按提交顺序获取结果就改用executor.map。协程是比多线程更轻量的并发方案asyncio 配合 httpx 的异步接口能在一台机器上维持数万并发连接。但协程的代码结构相对复杂调试也更费力初学者先把多线程跑熟更实际。等到你要做全站采集或需要同时监听大量请求时再研究async def与await的组合。6.2 断点续抓让爬虫中途崩溃不重来没有断点续抓能力的爬虫跑一半因网络闪断或服务器重启而中断重启后只能从头开始之前抓的数据虽然落库了但大量重复请求浪费了时间和带宽。断点续抓的核心思路是记录每个 URL 的抓取状态重跑时跳过已成功处理的条目只处理失败的。一套轻量实现是用 SQLite 维护任务队列import sqlite3 from datetime import datetime class UrlStore: def __init__(self, db_pathtasks.db): self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS tasks ( url TEXT PRIMARY KEY, status TEXT DEFAULT pending, retries INTEGER DEFAULT 0, updated_at TEXT ) ) def add_urls(self, urls): now datetime.now().isoformat() for url in urls: self.conn.execute( INSERT OR IGNORE INTO tasks (url, status, updated_at) VALUES (?, pending, ?), (url, now), ) self.conn.commit() def get_pending(self, limit10): rows self.conn.execute( SELECT url FROM tasks WHERE status pending LIMIT ?, (limit,) ).fetchall() return [row[0] for row in rows] def mark_done(self, url): self.conn.execute( UPDATE tasks SET status done, updated_at ? WHERE url ?, (datetime.now().isoformat(), url), ) self.conn.commit() def mark_failed(self, url): self.conn.execute( UPDATE tasks SET retries retries 1, updated_at ? WHERE url ?, (datetime.now().isoformat(), url), ) self.conn.commit()以上代码在UrlStore类中封装了三个操作方法add_urls用INSERT OR IGNORE把新 URL 写入任务表已存在的 URL 不会重复插入天然去重get_pending每次只取 limit 条状态为 pending 的记录保证并发场景下多个 worker 不会重复取到同一个 URLmark_done把 URL 标记为已完成mark_failed增加重试计数但不改变状态失败任务会进入下一轮循环重试。爬虫主循环里每次取一批 pending 任务抓取成功就 mark_done失败就先 mark_failed等重试到一定次数后再置为 failed 不再处理。加上断点续抓后爬虫的语义从一个「一次性脚本」升级成了「可持续运行的采集任务」。即使中途崩了重启脚本后get_pending能准确拿到没跑完的 URL已完成的自动跳过。这套机制的另一个好处是方便做增量采集——每天定时跑一次新 URL 走add_urls覆盖旧 URL 通过唯一约束自动被忽略。6.3 验证爬虫正确性的三条判断路径写完爬虫后别急着宣布大功告成先把正确性验证跑一遍。第一路径是自己当评审员随机挑几个已入库的条目回到源站点手动核对数据是否一致重点关注价格字段、日期格式、数量单位是否对得上。第二路径是看日志给每个请求和解析步骤加上日志输出记录状态码、解析到的字段数量、异常堆栈日志能帮你快速定位是请求阶段出错还是解析阶段出错。第三路径是对比采集量与源站页面显示的总数。如果列表页显示共 500 条数据你入库只有 320 条说明存在漏抓——可能发生在翻页逻辑、分页参数边界、重复元素过滤这几个环节。此时回到原始 HTML用 XPath 数一下单页实际渲染出的条目数再检查循环的页码范围是否正确。把这些技巧整合到一起你会发现爬虫开发的核心不是「会不会写 requests」而是「能不能稳定、可控、可恢复地跑完整个采集任务」。习题答案能告诉你每道题的解法但只有亲手跑通一遍、踩过几个坑那些参数和异常处理才能长在你身上成为下次遇到相似页面时脱口而出的条件反射。本文还有配套的精品资源点击获取