做爬虫这件事,最初往往源于一句很朴素的抱怨:“这网站上明明有我要的数据,为什么不能直接复制下来?”我第一次写Python爬虫,就是帮朋友整理一个行业网站上的商品价格信息,手动复制粘贴到第两百条的时候,眼睛都快花了。后来花了一个下午写了个脚本,十分钟就把三千多条数据整整齐齐地存进了表格里。从那一刻我意识到,所谓“用Python爬取想要的网页信息”,本质上就是把自己从重复劳动里解放出来的过程。
我见过很多人把爬虫想得很玄,觉得要懂一堆底层协议、浏览器内核,其实大部分日常需求根本用不上那么深。你只需要搞清楚三件事:怎么把网页请求回来,怎么把网页里有用的内容挑出来,怎么把挑出来的内容存好。Python在这条链路里有非常成熟的工具链,只要按部就班地走一遍,一个能用的爬虫脚本大概率能在半天内跑起来。这篇文章就从我实际做项目的角度,把整个过程拆开揉碎讲一遍,内容包括工具选型、请求写法、解析技巧、数据清洗、常见坑的排查,以及我自己反复踩过的几个雷。不管你是刚接触Python的小白,还是已经有基础想系统梳理一下,这篇文章应该都能给你一些能直接上手的参考。
1. 动手前先想清楚:爬虫项目的第一步是需求拆解
很多人拿到一个爬虫需求就急着写代码,结果写一半发现目标页面的数据是动态加载的,或者目标数据散落在好几个页面里,又或者根本不需要写爬虫,网站自己就提供了导出功能。所以我习惯先把需求拆清楚,这比选什么工具都重要。
1.1 明确目标数据:你要抓的到底是什么
说白了,你得先回答这几个问题:
- 目标网站是哪个?页面结构大概什么样?
- 要抓的数据字段有哪些?比如价格、标题、发布时间、评论数。
- 数据量有多大?是几十条、几千条、还是每天要持续抓几万条?
- 抓取频率是多少?一次性跑完,还是需要定时增量更新?
- 数据最终存到哪里?Excel、CSV、数据库,还是直接推到API接口?
别小看这些问题。有一次我接了个需求,对方说要抓某论坛的帖子标题,我按常规写了列表页遍历,结果跑起来发现他想要的其实是帖子里某个隐藏板块的附件链接,整个页面结构完全不一样,等于返工。后来我养成了习惯:先手动打开目标页面,用浏览器开发者工具看一下数据长在哪里,再动手写代码。这一步花不了几分钟,但能省下后面大半天的调试时间。
1.2 协议与合规:抓取边界先摸清楚
每次讲爬虫我都必须提一句合规。虽然本文是技术分享,但边界得先说清楚:不抓取个人隐私数据、不抓取需要登录才能访问的非公开数据、不绕过网站的技术访问控制。你可以查看目标网站的robots.txt,里面会告诉你哪些路径允许爬虫访问。虽然robots.txt没有法律强制力,但尊重它是行业里的基本礼貌。
实际操作中我一般这么判断:如果能通过浏览器直接看到的数据,正常的、低频率的抓取通常问题不大;但如果数据需要通过账号登录后才能看,或者网站明确有付费墙,那就别碰。另外,抓下来的数据如果涉及个人可识别信息,记得做脱敏处理,别原样入库。这些都是从业者应该有的自觉。
1.3 工具选型:requests + BeautifulSoup 还是 Scrapy 还是 Selenium
Python爬虫生态里,工具选型是第一步,选错了后面会很难受。我按自己的使用经验做个对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| requests + BeautifulSoup | 静态页面、中小规模抓取 | 轻量、上手快、依赖少 | 面对动态页面无能为力 |
| Scrapy | 大规模、多页面、需要并发抓取 | 框架化、扩展性强、自带去重和调度 | 学习曲线陡,项目结构比脚本复杂 |
| Selenium | 动态渲染页面、需要模拟点击 | 所见即所得,几乎能操作任何页面 | 慢、吃内存、需要维护浏览器驱动 |
| Playwright | 动态渲染页面、自动化操作 | 比Selenium快,支持多浏览器 | 同样有环境依赖,需要装浏览器内核 |
| requests-html | 轻量动态页面 | API友好,支持JavaScript渲染 | 底层封装较重,出问题排查难 |
我的选择逻辑很直接:能静态抓就静态抓,一个requests就搞定的事没必要上Scrapy。只有遇到需要渲染JavaScript的页面时,我才会考虑Selenium或者Playwright。Scrapy适合那种需要长期跑、数据量达到几十万上百万、还要做增量采集的项目,普通的个人小项目用它反而有点杀鸡用牛刀。
2. 环境准备与基础请求:让脚本先学会“打开网页”
工具链选好之后,第一件事是把环境搭起来。Python版本建议直接上3.10以上,现在新项目真的没必要再考虑2.x了。虚拟环境这个东西,我一开始觉得麻烦,后来被依赖冲突教育过几次之后,发现真香。
2.1 环境准备与依赖安装
创建一个项目目录,然后建虚拟环境:
mkdir my_spider cd my_spider python3 -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate虚拟环境的好处是会为当前项目隔离出一套独立的第三方库环境,不同项目之间互不污染。我见过太多“在我电脑上能跑”的惨案,就是因为全局环境里装了一堆不同版本的库,换台机器就崩了。
接下来安装核心依赖:
pip install requests beautifulsoup4 lxml pandasrequests用来发HTTP请求,beautifulsoup4用来解析HTML,lxml是指定的解析器引擎,比Python内置的html.parser快得多,pandas用来做数据清洗和存储。这几个组合足够应付绝大多数静态页面的抓取场景了。
2.2 requests 的基本用法:从GET开始
请求一个页面的最小代码是这样的:
import requests url = "https://example.com/list.html" resp = requests.get(url, timeout=10) print(resp.status_code) print(resp.text[:500])这里有几个细节需要说明。timeout参数很重要,我见过很多新手不设置这个参数,结果目标网站响应慢的时候,脚本就一直挂在那里,看起来像是卡死了。设置一个合理的超时时间(我一般设10秒),至少能让脚本在出错的时候快速失败,而不是无限等待。
其次,拿到响应之后先看status_code,如果是200就继续往下走,如果是404、403或者500,你就要停下来排查了,而不是盲目地往解析器里扔数据。
2.3 请求头与反爬初步:别裸奔
第一次用requests跑某个站点的时候,我收到过403 Forbidden。原因很简单,就是没有设置User-Agent。网站一看这个请求连浏览器标识都没有,直接当成非浏览器请求给拒了。
所以只要写爬虫,我几乎都会带上一些常见的请求头:
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/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } resp = requests.get(url, headers=headers, timeout=10)User-Agent可以从自己浏览器的开发者工具里复制,不同网站的检测策略不一样,有些还会检查Referer、Accept-Language这些字段。我通常的做法是先用浏览器打开目标页面,看看Network面板里实际发出的请求头是什么样的,然后把关键的几个字段复制到代码里。这样写出来的请求和浏览器发出来的请求长得几乎一样,能少很多麻烦。
另外,很多网站对请求频率很敏感。如果你一口气发几十个请求,对方可能直接把你的IP封了。我在代码里习惯用time.sleep控制请求间隔,比如:
import time from random import uniform # 每次请求后随机休眠1到3秒,模拟人类浏览节奏 time.sleep(uniform(1, 3))这样做的目的不是“做贼心虚”,而是降低对目标服务器的压力,避免给对方造成不必要的负担。很多爬虫被拉黑,不是技术问题,是礼貌问题。
3. 页面解析与数据提取:从HTML中“抠”出真正有用的信息
请求拿到HTML之后,下一步就是解析。这一步做得不好,后面全白搭。我从第一次写爬虫到现在,用过正则、BeautifulSoup、XPath、甚至手写字符串查找函数,一路走过来,现在90%的静态页面我都用BeautifulSoup解决。
3.1 BeautifulSoup 定位与选择器
BeautifulSoup的核心用法是定位节点。拿到一个HTML文档之后,你可以先用find或者find_all来找标签,但更高效的方式是用CSS选择器。
比如你想抓一个列表页里所有商品的标题和价格,HTML结构可能是这样的:
<ul class="product-list"> <li class="product-item"> <h2 class="title">商品A</h2> <span class="price">99.00</span> </li> <li class="product-item"> <h2 class="title">商品B</h2> <span class="price">129.00</span> </li> </ul>对应代码:
from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "lxml") items = soup.select("ul.product-list li.product-item") for item in items: title = item.select_one("h2.title").text.strip() price = item.select_one("span.price").text.strip() print(title, price)select_one只返回第一个匹配结果,select返回所有匹配结果,这个要分清。我见过有人用select_one拿所有列表项,结果只拿到第一条,愣是在那儿debug了半天。
3.2 正则与XPath对比
正则适合什么样的场景?比如你想从一段JavaScript代码里抠出某个变量,或者某个数据是嵌在一个很长的文本串里,有明显的格式特征。但我不推荐用正则去解析HTML本身,因为HTML的标签结构会有各种嵌套和属性顺序变化,正则表达式写起来会越来越复杂,最后变成一坨只有自己能看懂的“神秘代码”。
XPath是另一种定位方式,主要用在lxml里。它比CSS选择器更灵活,功能也更强大,尤其是在处理“按文本内容定位”“按元素位置定位”的时候,写起来比CSS选择器直观。比如:
from lxml import html tree = html.fromstring(resp.text) titles = tree.xpath('//h2[@class="title"]/text()')如果你对CSS选择器更熟悉,就用CSS;如果你更适应XPath的写法,那就用XPath。工具无高下,顺手就行。我个人的建议是,新手优先掌握BeautifulSoup的CSS选择器,因为它最贴近日常的HTML理解方式,等熟练之后再去学XPath做补充也不迟。
3.3 数据清洗与结构化存储
解析出原始字符串之后,往往还要做清洗。网页里的文本经常带着多余的空白、换行、制表符,价格字段可能带特殊符号,还有一些字段可能为空。我的习惯是每一步都写成函数,这样既方便测试,也方便复用。
比如清洗价格:
def parse_price(raw): # 把价格字符串中的非数字字符去掉,只保留小数点和数字 cleaned = re.sub(r"[^\d.]", "", raw) if not cleaned: return None return float(cleaned)而数据存储,我一般分两种。数据量小、结构简单的,直接存CSV;数据量大、需要频繁查询的,存SQLite或者MySQL。存CSV用pandas一行就行:
import pandas as pd data = [ {"title": "商品A", "price": 99.0}, {"title": "商品B", "price": 129.0}, ] df = pd.DataFrame(data) df.to_csv("products.csv", index=False, encoding="utf-8-sig")这里有个细节:编码字段我用的是utf-8-sig而不是utf-8。因为Excel在打开CSV的时候,默认按GBK编码解析,不带BOM的UTF-8文件会被识别成乱码。加了个BOM头之后,Excel就能正确识别了。这个坑我踩过一次之后,所有CSV都会加上这个参数。
3.4 动态加载页面的处理思路
静态页面毕竟只是多数网站的一部分,很多现代网站的数据是通过JavaScript动态渲染的。你requests拿回来的HTML源码里可能只有一个空壳框架,真正的数据是通过额外的接口请求加载的。
遇到这种情况,我一般按优先级尝试这么几个方案:
- 先用开发者工具打开页面的Network面板,看看页面加载过程中有没有XHR请求返回JSON数据。如果有,直接请求那个JSON接口,解析JSON比解析HTML省事一万倍。
- 如果接口被加了复杂签名,可以考虑直接模拟浏览器渲染,上Playwright或者Selenium。
- 最后一个备选方案是看页面里有不有内嵌的JSON数据(比如