
各位读者朋友大家好。在之前的系列笔记中我们已经陆续完成了 Python 爬虫基础环境搭建、简单页面抓取和解析的入门操作。本篇文章作为“数据处理 027”系列的第三篇实战笔记将重点解决一个问题当爬虫从“能跑”到“能稳定、规范、高效地跑”时我们该做哪些事情。这篇文章不会停留在 requests.get() 和 BeautifulSoup 的简单拼接上而是会围绕一个可以直接运行的实战案例完整拆解爬虫开发的常用套路请求头伪装、会话保持、分页抓取、数据清洗、异常重试以及最终将数据落地到本地文件。适合刚学完 Python 基础语法、有一定 requests 库使用经验但想进一步提升爬虫代码质量的读者。如果你正准备用 Python 抓取一些公开网站的数据做统计分析这篇文章可以帮助你少踩不少坑。1. 爬虫开发前必须想清楚的两件事1.1 什么是网络爬虫网络爬虫Web Crawler / Spider本质上是一个自动访问网页并提取信息的程序。它的工作流程可以拆成三步获取网页内容通过 HTTP 请求向目标服务器发送访问请求拿到 HTML、JSON 或二进制文件。解析并提取数据从返回的网页内容中找出自己关心的字段例如标题、时间、价格、正文等。保存与落地将结构化数据写入 CSV、Excel、数据库或继续交给下游数据分析模块。用一张简单的流程来描述输入 URL - 发送请求 - 接收响应 - 解析内容 - 提取字段 - 数据清洗 - 保存结果在实际项目中第三步和第四步之间往往还会插入“判断请求是否成功”“是否需要重试”“是否需要等待”等控制逻辑这个我们会在后面的案例中详细演示。1.2 合法性爬虫绝对不能忽视的边界在动手写任何爬虫之前每位开发者都需要建立一条底线思维。Python 爬虫技术本身是中性的但使用方式必须严格遵守相关法律法规和网站的使用协议。本文中的案例仅针对公开的、允许访问的测试数据源或模拟页面。在实际开发中请大家务必注意以下几点尊重 robots.txt 协议爬取前检查网站的 robots.txt 文件了解该网站允许或禁止抓取的路径。控制请求频率不要对目标网站造成压力设置合理的延时例如每抓取一页后 sleep 1~3 秒。不爬取个人隐私数据不采集涉及个人隐私、非公开的用户信息。仅用于学习研究爬取的数据不要用于商业用途或违法违规场景。记住爬虫技术是数据获取手段使用时的“度”决定了它是否合规。本文所有代码都是教学演示请读者在受控环境中操作。2. 环境准备与版本说明2.1 本文使用的开发环境在开始编写代码之前先明确一下开发环境。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路和开发流程。操作系统Windows 10 / 11 或 macOS / Linux 均可Python 版本3.8 及以上建议 3.10 或 3.11开发工具VS Code 或 PyCharm命令行工具Terminal核心依赖库requests、beautifulsoup4、pandas用于数据处理、lxml解析器如果你还没有安装 Python建议从 Python 官网下载对应操作系统的安装包。安装时务必勾选“Add Python to PATH”选项这是很多新手安装之后命令行找不到 python 命令的常见原因。2.2 安装依赖库我们使用 pip 安装本项目所需的所有依赖库。打开终端或命令行执行以下命令pip install requests beautifulsoup4 lxml pandas如果你想统一管理依赖版本可以创建一个 requirements.txt 文件内容如下requests2.31.0 beautifulsoup44.12.2 lxml4.9.3 pandas2.0.3然后执行pip install -r requirements.txt这里建议在项目目录中创建虚拟环境避免和全局 Python 环境产生依赖冲突。创建虚拟环境的命令如下python -m venv venv# Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活虚拟环境后再执行 pip 安装命令依赖就会被安装到项目独立的环境中不会污染系统 Python。2.3 项目结构规划为了方便管理代码和数据我们先规划好项目目录结构python_spider_demo/ │ ├── venv/ # 虚拟环境目录自动生成 ├── data/ │ └── output/ # 保存爬取结果 ├── spiders/ │ └── demo_spider.py # 爬虫主程序 ├── requirements.txt # 依赖清单 └── README.md # 项目说明在项目根目录下创建 data/output 和 spiders 文件夹。我们后面编写的脚本会统一放在 spiders 目录下数据输出到 data/output 目录。3. 爬虫核心知识点拆解在写完整案例之前先单独讲解几个关键知识点。它们是整个爬虫程序中反复用到的基础构件理解了它们后面的代码读起来就会非常顺畅。3.1 请求头Headers伪装与 UA 设置很多网站会检查 HTTP 请求头中的 User-Agent 字段用来判断请求是来自真实浏览器还是爬虫程序。默认情况下requests 库发送的请求 UA 类似python-requests/2.31.0非常容易被服务器识别并拒绝访问。因此我们需要在请求时手动添加请求头。最常用的是将 UA 设置为一个常见的浏览器标识headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 }除了 UA 之外部分网站还会校验 Referer、Origin 等字段。Referer 表示请求是从哪个页面跳转过来的当爬虫模拟表单提交或翻页请求时服务端可能会校验这个字段。这时我们可以补充headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Referer: https://example.com/list, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }3.2 会话对象 requests.Session()在开发爬虫时有些场景下需要在多个请求之间保持同一状态。典型的场景包括登录后携带 Cookie 访问后续页面。访问某些需要先设置 Cookie 才能进入的页面。保持 Session 中的固定连接提高请求效率。这时候我们不应该每次都使用requests.get()而应该使用requests.Session()。它的用法非常简单import requests session requests.Session() # 在 session 上设置默认请求头之后所有请求都会携带 session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... }) # 第一次请求向目标服务器打招呼 session.get(https://httpbin.org/cookies/set?namepython_spider) # 第二次请求会带着第一次设置的 Cookie response session.get(https://httpbin.org/cookies) print(response.text)使用 Session 的好处是requests 会自动保存服务器返回的 Set-Cookie 信息并在后续请求中自动携带无需手动管理 Cookie 字符串。3.3 BeautifulSoup 解析 HTML 的三种常用定位方式拿到网页内容后我们需要从 HTML 中提取目标数据。BeautifulSoup 是 Python 生态中最常用的解析库之一它支持的定位方式非常灵活其中最常用的三种是1. 根据标签名查找title soup.find(h1)只返回第一个匹配的 h1 标签。2. 根据 class 样式查找items soup.find_all(div, class_list-item)注意这里用的是class_因为class是 Python 的关键字所以 BeautifulSoup 特意使用class_参数来代替。3. 根据属性查找link soup.find(a, href/detail/123)如果想提取标签中的文本内容可以用.get_text()方法text item.get_text(stripTrue)如果想提取某个属性的值可以用中括号下标方式href link[href]3.4 常见反爬手段的应对思路这里需要强调所有应对手段都必须在合法合规前提下使用。以下列举的是教学层面的通用技术方案不代表鼓励对抗网站正常的风控策略。反爬手段常见现象应对思路封禁 UA返回 403 或重定向到验证页设置浏览器 UA 请求头IP 频率限制请求频繁后返回 429 或验证码降低请求频率增加延时动态渲染页面HTML 中无目标数据查找接口地址直接请求 JSON 数据需要登录未登录时跳转登录页模拟登录并保持 Session在绝大多数学习型项目中控制访问频率和设置真实 UA 已经足够。如果遇到更复杂的验证码、登录签名等建议优先考虑官方开放 API而不是硬解反爬。4. 完整实战案例抓取一个新闻列表并保存为 CSV接下来我们结合上面的知识点写一个完整的爬虫程序。我们选择的目标是一个公共测试网站结构简单、响应稳定适合做教学演示。案例目标是抓取一个新闻列表页面的数据提取每条新闻的标题、链接和发布时间并进行简单清洗后保存为 CSV 文件。为了方便读者在自己电脑上运行我这里直接用 Python 标准库中的urllib配合一个简单的本地 HTML 文件来模拟而不是依赖某个具体的外部网站。这样既方便演示又不会因为外部网站改版而导致示例失效。如果你有自己熟悉的公开测试站点也可以把请求 URL 替换掉。4.1 构造测试页面本地模拟为了完全可控地演示爬虫流程我们先在本地创建一个简单的 HTML 文件。这个页面里包含若干条新闻列表每条新闻有标题、链接和日期。在 data 目录下创建sample.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 title测试新闻列表/title /head body div classnews-list div classnews-item h2 classtitlea href/news/101.htmlPython 3.12 新特性解析/a/h2 span classdate2025-01-15/span /div div classnews-item h2 classtitlea href/news/102.htmlrequests 库官方文档中文翻译/a/h2 span classdate2025-01-14/span /div div classnews-item h2 classtitlea href/news/103.htmlBeautifulSoup 实战技巧汇总/a/h2 span classdate2025-01-13/span /div div classnews-item h2 classtitlea href/news/104.htmlpandas 数据清洗入门指南/a/h2 span classdate2025-01-12/span /div /div /body /html在真实项目中我们通常是通过 HTTP 请求获取 HTML但本地读取文件的方式可以让我们更聚焦于解析逻辑避免因为网络问题导致示例跑不通。4.2 爬虫主程序spiders/demo_spider.py下面就是完整的爬虫脚本。我会在代码中通过注释说明每个模块的作用。请将以下代码保存到spiders/demo_spider.py文件中。# 文件路径spiders/demo_spider.py import csv import re import time from pathlib import Path import requests from bs4 import BeautifulSoup # 配置请求头模拟真实浏览器 HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } # 数据输出路径 OUTPUT_PATH Path(__file__).resolve().parent.parent / data / output / news.csv def fetch_html(url: str) - str: 发送 HTTP GET 请求并返回网页 HTML 文本。 这里使用 Session 来保持连接并捕获常见异常。 session requests.Session() session.headers.update(HEADERS) try: response session.get(url, timeout10) response.raise_for_status() # 如果状态码不是 2xx会抛出异常 response.encoding response.apparent_encoding # 自动检测页面编码 return response.text except requests.RequestException as e: print(f[错误] 请求 {url} 失败: {e}) return def local_html_loader(file_path: str) - str: 从本地文件读取 HTML 内容。 如果没有可用的目标网站可以使用这个函数加载本地样例。 with open(file_path, r, encodingutf-8) as f: return f.read() def parse_news_list(html: str) - list: 使用 BeautifulSoup 解析 HTML提取新闻标题、链接和日期。 返回元素为字典的列表。 soup BeautifulSoup(html, lxml) news_items [] # 找到所有新闻条目 for item in soup.select(div.news-item): title_tag item.find(h2, class_title).find(a) date_tag item.find(span, class_date) title title_tag.get_text(stripTrue) link title_tag[href] date_text date_tag.get_text(stripTrue) news_items.append({ title: title, link: link, date: date_text, }) return news_items def clean_data(news_list: list) - list: 对解析出来的数据进行清洗 1. 去掉标题中多余的空格。 2. 统一日期格式。 3. 补全链接为完整 URL。 cleaned [] for item in news_list: title re.sub(r\s, , item[title]).strip() link item[link] if link.startswith(/): link https://example.com link date_text item[date].strip() # 简单的日期格式校验如果格式不对则标记为未知 if not re.match(r\d{4}-\d{2}-\d{2}, date_text): date_text 未知 cleaned.append({ title: title, link: link, date: date_text, }) return cleaned def save_to_csv(data: list, output_path: Path) - None: 将清洗后的数据写入 CSV 文件。 如果文件所在目录不存在自动创建目录。 output_path.parent.mkdir(parentsTrue, exist_okTrue) with open(output_path, w, encodingutf-8-sig, newline) as f: fieldnames [title, link, date] writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(data) print(f[成功] 数据已保存至 {output_path}) def main(): 主流程加载页面 - 解析数据 - 清洗数据 - 保存数据 # 方式一使用本地样例文件推荐可控性强 html local_html_loader(data/sample.html) # 方式二如果你有自己的目标测试网站可以改用这种方式 # html fetch_html(https://example.com/news) if not html: print([错误] 未获取到任何 HTML 内容程序退出。) return raw_news parse_news_list(html) print(f[信息] 解析到 {len(raw_news)} 条原始数据) cleaned_news clean_data(raw_news) # 模拟真实爬虫的访问间隔避免请求过快 time.sleep(1) save_to_csv(cleaned_news, OUTPUT_PATH) # 打印前 3 条结果方便确认 print(前 3 条结果预览) for item in cleaned_news[:3]: print(item) if __name__ __main__: main()4.3 代码运行与预期输出在项目根目录下执行以下命令python spiders/demo_spider.py如果一切正常你应该看到类似下面的输出[信息] 解析到 4 条原始数据 [成功] 数据已保存至 .../data/output/news.csv 前 3 条结果预览 {title: Python 3.12 新特性解析, link: https://example.com/news/101.html, date: 2025-01-15} {title: requests 库官方文档中文翻译, link: https://example.com/news/102.html, date: 2025-01-14} {title: BeautifulSoup 实战技巧汇总, link: https://example.com/news/103.html, date: 2025-01-13}打开data/output/news.csv文件内容应该是titlelinkdatePython 3.12 新特性解析https://example.com/news/101.html2025-01-15requests 库官方文档中文翻译https://example.com/news/102.html2025-01-14BeautifulSoup 实战技巧汇总https://example.com/news/103.html2025-01-13pandas 数据清洗入门指南https://example.com/news/104.html2025-01-12这里使用utf-8-sig编码保存目的是让 Excel 打开 CSV 文件时不会出现中文乱码。如果使用默认的utf-8编码在 Windows 上的 Excel 中打开时经常会显示乱码。4.4 抓取动态页面直接请求接口的实战思路上面的案例演示的是静态 HTML 页面的抓取方式。但很多现代网站的数据并不是直接写在 HTML 里的而是通过 JavaScript 动态渲染的。这种情况下解析 HTML 往往什么都拿不到。解决思路是打开浏览器开发者工具F12切换到 Network 面板刷新页面后找到返回 JSON 数据的 XHR 或 Fetch 请求直接构造请求去访问这个接口。举个例子假设某个新闻网站的数据接口是GET https://example.com/api/news?page1size10返回的 JSON 格式为{ code: 0, data: { list: [ { title: 标题一, url: /news/1.html, publish_time: 2025-01-15 } ] } }那么爬虫核心逻辑就变成了import requests url https://example.com/api/news headers { User-Agent: Mozilla/5.0 ..., Accept: application/json, } params { page: 1, size: 10, } resp requests.get(url, headersheaders, paramsparams, timeout10) data resp.json() # 直接解析 JSON for item in data[data][list]: print(item[title], item[url], item[publish_time])这种方式比解析 HTML 更高效因为接口返回的都是结构化数据省去了大量清洗工作。但要注意很多网站会对接口做签名校验、过期时间限制遇到这种情况时需要再深入分析请求参数。不过对于一个学习型项目掌握“找到数据接口 - 构造参数 - 解析 JSON”这条链路已经能解决 70% 以上的动态页面抓取需求。5. 异常处理与爬虫健壮性设计爬虫程序是网络环境中相对脆弱的程序因为网络波动、目标网站改版、字段缺失等问题层出不穷。一个合格的爬虫代码必须具备健壮性否则跑一半就崩了前功尽弃。下面从几个方面来说明如何提升爬虫的健壮性。5.1 请求异常捕获在上面的示例中我们使用了try...except requests.RequestException来捕获所有请求相关的异常。这样当某个请求失败时程序不会直接退出而是打印错误信息并返回空字符串。try: response session.get(url, timeout10) response.raise_for_status() except requests.RequestException as e: print(f[错误] 请求失败: {e}) return 常见的请求异常包括ConnectionError无法连接目标服务器Timeout请求超时HTTPError返回了 4xx 或 5xx 状态码TooManyRedirects重定向次数过多5.2 解析容错在使用 BeautifulSoup 解析时并不一定每个元素都存在。例如某条新闻缺少日期字段如果我们直接访问date_tag.get_text()就会抛出AttributeError: NoneType object has no attribute get_text。所以更健壮的写法是先判断标签是否存在再进行取值title_tag item.find(h2, class_title) date_tag item.find(span, class_date) if title_tag is None or date_tag is None: print([警告] 跳过一条不完整数据) continue title title_tag.get_text(stripTrue) date_text date_tag.get_text(stripTrue)5.3 重试机制对于偶尔失败的超时请求最简单有效的方法是增加重试机制。可以自己写一个重试循环也可以使用requests库中HTTPAdapter的max_retries参数但自定义重试逻辑更直观一些。def fetch_html_with_retry(url: str, max_retries: int 3) - str: for attempt in range(1, max_retries 1): print(f[信息] 第 {attempt} 次请求 {url}) html fetch_html(url) if html: return html wait_seconds attempt * 2 print(f[警告] 请求失败{wait_seconds} 秒后重试...) time.sleep(wait_seconds) print([错误] 重试次数已用完请求失败。) return 注意重试的间隔时间应该逐次增加避免瞬间连续冲击目标服务器。5.4 数据落地时的去重在实际爬虫项目中同一个页面可能被重复爬取。如果每次都把数据直接写入文件必然会产生大量重复数据。常见的做法是在写入前对数据做一次去重def deduplicate(news_list: list) - list: seen set() unique_list [] for item in news_list: key item[link] # 用链接作为唯一标识 if key not in seen: seen.add(key) unique_list.append(item) return unique_list如果数据量很大更推荐将数据写入数据库并使用唯一索引约束来保证不重复。这里只演示基于内存的简单去重。6. 常见问题与排查思路FAQ下面整理一些 Python 爬虫开发中非常容易踩坑的场景每个问题都附带了排查思路和解决方案。问题现象常见原因解决思路爬取页面返回 403服务端识别到爬虫请求拒绝了访问在请求头中设置真实的浏览器 User-Agent必要时增加 Referer返回的 HTML 中没有目标数据页面数据由 JavaScript 动态加载生成打开 F12 - Network找到真正的数据接口并直接请求中文乱码页面编码和 Python 解析编码不一致使用response.encoding response.apparent_encoding或直接指定字符集保存的 CSV 文件用 Excel 打开乱码CSV 使用了 UTF-8 编码Excel 默认按 ANSI 打开使用utf-8-sig编码写入 CSV 文件爬取到一半程序报错退出某个标签解析不到导致 AttributeError判断标签是否为 None使用 try-except 包围解析逻辑请求频繁后提示访问被限制请求频率过高触发反爬每次请求后使用time.sleep(random.uniform(1, 3))控制频率明明有数据但只抓到几条目标网站做了分页只抓了第一页分析分页参数循环请求每一页的数据6.1 爬虫过程中如何避免被屏蔽在合法合规的前提下给目标网站造成尽可能小的压力是每个爬虫开发者的基本修养。具体建议如下设置请求间隔不要用毫秒级速度疯狂请求。尽可能使用官方 API。不要并发发起大量请求如果需要并发控制在较低数量并设置合理的超时。如果网站明确在 robots.txt 中禁止爬取某个路径请勿强行绕过。6.2 如何调试 BeautifulSoup 解析逻辑当你不确定自己的 CSS 选择器能否匹配到目标元素时可以先写一段临时调试代码把解析结果打印出来看看from bs4 import BeautifulSoup # 假设 html 已经读取 soup BeautifulSoup(html, lxml) items soup.select(div.news-item) print(f匹配到 {len(items)} 个元素) for item in items[:2]: print(item.prettify())prettify()方法可以将标签内容格式化输出非常适合调试时观察 HTML 结构。7. 最佳实践与工程建议写一个能跑的爬虫并不难难的是写一个能持续维护、不容易出错的爬虫。下面这些建议来自实际项目经验希望对你有参考价值。7.1 代码模块化爬虫代码不要所有逻辑都写在一个大函数里。按照职责拆分成函数每个函数只做一件事。例如fetch_html()负责网络请求parse_news_list()负责解析页面clean_data()负责数据清洗save_to_csv()负责数据持久化这样不仅便于单测也方便后续扩展。如果原来的页面结构改了只需要修改parse_news_list()函数即可其他模块不受影响。7.2 配置与代码分离不要将 URL、请求头、参数硬编码在代码中。建议将这些信息提取到配置文件或环境变量中。简单项目可以放在 Python 文件顶部的常量区域复杂项目可以使用 config.py 或 YAML 配置文件。# config.py BASE_URL https://example.com HEADERS { User-Agent: Mozilla/5.0 ..., } TIMEOUT 10 MAX_RETRIES 3 REQUEST_INTERVAL 2这种方式最大的好处是当你需要切换不同的目标源或调整访问频率时不需要改动核心代码。7.3 日志记录print 之外的选择在简单的脚本中使用 print 没有问题但项目复杂度提升后print 输出的信息量有限而且很难持久化。更推荐使用 Python 自带的logging模块import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(spider.log, encodingutf-8), logging.StreamHandler() ] ) logging.info(开始抓取) logging.warning(解析到不完整数据) logging.error(请求失败)有了日志文件即使程序在半夜跑崩了第二天也能通过日志快速定位失败原因。7.4 数据存储的选择如果你只是抓取几百条数据做学习分析CSV 文件完全够用。但如果你要持续抓取大量数据建议尽早切换到数据库。SQLite 是一个非常轻量的选择不需要额外安装数据库服务非常适合小型爬虫项目。import sqlite3 conn sqlite3.connect(news.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, link TEXT NOT NULL UNIQUE, date TEXT ) ) conn.commit()使用数据库后去重可以交给数据库的 UNIQUE 约束同时可以方便地做数据统计和查询。7.5 安全性注意事项不要在代码中硬编码敏感信息例如登录密码、API Key 等应使用环境变量或密钥管理服务。不要编写恶意爬虫不要用爬虫去攻击网站、刷接口、爬取非公开数据。定期检查数据合规性如果数据用于商业分析务必确认数据来源的合法授权。8. 总结与后续学习方向回顾一下本文从 Python 爬虫的概念和合规边界讲起详细介绍了 requests 请求头伪装、Session 会话保持、BeautifulSoup 解析 HTML、数据清洗和 CSV 落地的完整流程并提供了一个可以直接运行的实战案例。同时我们还讨论了异常处理、重试机制、去重逻辑、日志记录和数据库存储等工程化问题。阅读完这篇文章你应该已经能掌握以下能力使用 requests 构造带请求头的 HTTP 请求。使用 BeautifulSoup 定位并提取 HTML 中的结构化数据。对解析出的数据做基础清洗并保存为 CSV 文件。面对常见的动态页面能够通过分析接口地址来抓取 JSON 数据。编写具备基础健壮性的爬虫代码减少因为网络波动或页面改版导致的程序崩溃。下一步你可以从以下几个方向继续深入学习框架学习了解 Scrapy 的基本结构它更适合大规模、分布式的爬虫项目。数据可视化将爬取的数据使用 pandas 和 matplotlib 进行清洗分析并绘制图表。反爬与风控进阶在合法授权的前提下研究验证码识别、请求签名等高级话题。爬虫部署将爬虫程序部署到云服务器上配合定时任务实现每日自动抓取。爬虫是一门实践性极强的技术光看不练很难真正掌握。建议你现在就打开编辑器把文中的示例代码手动敲一遍然后尝试修改选择器、增加分页逻辑观察输出结果的变化。遇到问题不可怕按照本文第六节的排查思路一步步定位你很快就能积累起自己的爬虫实战经验。如果这篇文章对你有帮助可以收藏备用。后续我会继续更新数据处理系列笔记欢迎关注交流一起在爬虫和数据处理的路上共同进步。