☰
requests+XPath实战:电影网站数据采集全流程解析
2026/10/7 11:42:14 网站建设 项目流程

学爬虫这件事,很多人卡在第一步:教材看到一半,代码也跟着敲了,但总觉得“会跑但不理解”,换个网站就不知道从哪下手。我个人的建议是,别一上来就研究分布式、代理池、验证码识别这些花活,先找一个数据结构规整的小型网站,老老实实走完一遍“抓包 → 分析 → 请求 → 解析 → 保存”的全流程,把爬虫的核心套路吃透。

这个电影网站数据采集案例,就是照着这条路线设计的。它的目标很朴素:用最基本的requests加XPath,把一个电影列表页里的电影名称、评分、主演、类型、简介、上映日期提取出来,最终落成一张CSV表。整个项目不涉及登录、不用处理JS渲染,也不需要Selenium这类重型工具,但对小白来说,它涵盖了爬虫入门最关键的几个能力点:怎么抓包定位真实数据、怎么写请求头伪装、怎么用XPath精准取值、怎么处理编码和清洗字段。如果你刚学完Python基础,想动手写第一个正经爬虫,或者已经有爬虫经验但流程不系统,这篇都很适合参考。

整个案例的演示逻辑是:先看页面长什么样,再抓包看数据在哪,然后写代码逐步实现,最后把实操中会踩的坑列出来。下面直接进正题。

1. 先理清思路再写代码:项目整体设计与方案选型

1.1 采集目标拆解

拿到一个网站,第一件事不是急着写代码,而是把“要什么数据”想清楚。这个案例的目标站点是一个电影列表页,页面上一部电影占一个卡片,包含的信息有:

  • 电影名称(标题链接里的文字)
  • 评分(一个带class的span标签)
  • 类型(科幻/冒险这种多标签组合)
  • 主演(多个演员名,用逗号或空格分隔)
  • 简介(一段简短的剧情说明)
  • 上映日期

这其实是一个非常典型的“列表页抓取”场景。把字段确定下来之后,后续的解析逻辑才有方向。我习惯把字段先写在纸上,顺便标注每个字段在页面里大概的标签特征,比如“评分在 span.score 里”、“主演在 span.actors 里”,这样待会写XPath的时候不用来回翻页面。

另外一个值得提前做的事是确认页码范围。通常这类站点底层是一个列表接口,通过page参数翻页,比如list?page=1、list?page=2。我在设计时直接把“分页采集”作为功能之一做进去,而不是只抓第一页,这样最终得到的数据量足够后续做分析或练手。

1.2 技术方案对比

我当时选技术方案的时候,在“requests + lxml”和“Selenium模拟浏览器”之间犹豫了一下。后来还是果断选了前者,原因很简单:这套站点的数据是直接写在HTML里的,不需要等JS渲染,用requests直接请求就能拿到完整源码。

方案优点缺点适用场景
requests + lxml/XPath轻量、快速、代码直观遇到JS动态渲染的页面拿不到数据数据在HTML源码中的传统网页
Selenium能模拟真实浏览器,支持JS渲染占用资源大、速度慢、维护成本高页面靠JS动态加载、点击翻页等场景
Scrapy框架功能全、性能好、支持分布式学习曲线陡,对新手不友好大规模采集、需要调度和去重的场景

入门阶段如果一上来就用Selenium,所有数据都靠浏览器渲染后再去提取,反而会把“抓包分析”这个核心能力给掩盖掉,出了问题也不知道是网站问题还是自己操作问题。先把requests这套轻量打法练熟,后续再学Selenium和Scrapy都会顺畅得多。

整个采集流程可以概括成一条线:发送HTTP请求拿HTML源码 → 用etree解析成结构化对象 → 通过XPath定位节点提取字段 → 清洗数据 → 保存到CSV。接下来先从抓包这一步说起,因为这是整个案例里最有含金量、也最容易被新手跳过的一环。

2. 抓包实战:搞懂数据到底从哪来

2.1 为什么写爬虫前一定要先抓包

很多新手有个直觉误区:既然爬虫是“获取网页数据”,那直接requests.get(网址)然后解析不就行了。这句话在十年前大致成立,但现在的网站早就没那么单纯了。

网页数据现在分两种加载方式。一种是服务端渲染,服务器直接把数据拼在HTML里返回,爬虫拿到的源代码里就有一切。另一种是客户端渲染,服务器先返回一个HTML空壳,浏览器再通过JavaScript在页面加载后偷偷发若干个HTTP请求(行话叫XHR或Ajax)去拉数据,然后动态渲染出来。你右键“查看网页源代码”看到的,往往只是那个空壳,真正有价值的数据在一个你肉眼根本看不到的接口里。

抓包做的事情,就是把浏览器发出的每一个请求都“截获”下来,看看它到底请求了哪个URL、带了哪些参数、返回了什么数据。这个动作非常像拆快递:你看到的是已经摆好盘的菜,但你需要找到点单记录,才知道这道菜是从哪个后厨端出来的、配方是什么。对爬虫来说,“点单记录”就是那个包含数据的真实URL和参数。

这个案例里的电影站点,虽然数据是服务端渲染的,但我依然强调要抓包,因为抓包过程能帮你确认“数据到底在HTML里还是在接口里”。不经过这一步直接写代码,后面遇到问题根本没法定位。

2.2 浏览器开发者工具和Fiddler怎么选

抓包有两个常用工具:浏览器自带开发者工具(F12)和独立的抓包工具(Fiddler、Charles等)。这个案例用F12就够了,但为了照顾以后遇到App、小程序等场景,我把两者的选择逻辑一起讲清楚。

浏览器F12的操作步骤非常简单:

  1. 打开目标页面,按F12或者右键选择“检查”。
  2. 切到Network(网络)标签页,刷新页面。
  3. 在筛选栏里点击Fetch/XHR,这一步会把绝大部分动态接口过滤出来。
  4. 逐个点击请求,在Response(响应)标签里查看返回内容,找到包含电影数据的那一个。
  5. 记录下这个请求的URL、Method、请求头和Query参数。

整个过程大概几十秒,但信息量非常大。你不仅能确认数据的真实来源,还能看到翻页参数的名字,比如page、offset、size这些,这些参数后面写分页循环时直接复用。

Fiddler这类独立工具,什么时候才需要?当你抓的是手机App、微信小程序、或者需要解密HTTPS流量的时候,浏览器F12就力不从心了。因为App里没有开发者工具,你必须把手机的流量代理到电脑上,由Fiddler或者Charles来做中间人解密。它的核心思路和F12一样:看请求、看响应、找参数。区别在于一个是浏览器内部视角,一个是所有经过电脑网卡的流量都得从那里过。

对比项浏览器F12Fiddler/Charles
使用成本零安装,直接可用需要安装并配置代理证书
抓取范围仅当前浏览器页面手机App、小程序、任意进程的HTTP/HTTPS流量
HTTPS解密浏览器内自动处理需要在电脑和手机两端安装根证书
新手友好度高中等,配置代理时容易出错

我个人的经验是:做网页爬虫,95%的场景用F12就能解决,不需要额外装工具。你要是花一晚上捣鼓Fiddler的证书配置,到头来只是为了抓一个网页接口,那就本末倒置了。

2.3 从抓包结果中定位目标数据

这次抓包我们需要重点确认三样东西:真实请求URL、翻页参数名、请求头字段。

模拟一次抓包过程:打开电影列表页后,按F12进入Network,刷新页面,然后在Fetch/XHR筛选项里看到一个名字类似getMovies或dataList的请求。点击它的Response,里面是一段JSON或HTML片段,正好是页面上展示的那批电影数据。这就说明,真正的数据来源并不是当前这个列表URL本身,而是这个API或数据接口。

再看它的Headers,里面有个Query String Parameters区域,写着page=2。这说明翻页就是通过page这个参数控制的。把这个URL和参数记下来,后面代码里就是:

url = "https://movie.example.com/list" params = {"page": 2} resp = requests.get(url, params=params, headers=headers)

如果你在抓包时发现数据是JSON数组,那就更简单了,直接用resp.json()就能解析,连XPath都不用写。这个案例里我们假设目标站点的数据还是服务端渲染,所以继续走HTML解析路线。抓包这一步做完,代码怎么写就八九不离十了。

3. 代码落地:完整爬虫逐段拆解

3.1 环境准备与依赖安装

写代码之前先把环境备好。Python版本建议3.8以上,推荐直接去Python官网下最新稳定版,安装时勾选“Add Python to PATH”。这一步漏了后面命令行里敲python会提示找不到命令,属于入门最常见的坑之一。

我们只依赖两个第三方库,安装命令如下:

pip install requests lxml

requests是发HTTP请求用的,比Python自带的urllib好用太多,代码简洁、语义清晰。lxml是解析HTML/XML用的,它的底层是C语言实现,解析速度比BeautifulSoup默认的html.parser快很多,而且XPath支持非常完整。

安装完成后可以在命令行里验证一下:

python -c "import requests; import lxml; print('ok')"

如果之前装过其他版本,建议顺手把库升级到较新版本,避免一些旧版接口不兼容的怪问题。

3.2 请求模块:伪装请求头和编码处理

3.2.1 为什么要伪装请求头

直接请求网站会返回403或者被重定向到验证页。原因很简单:服务器会检查请求头里的User-Agent,如果识别出不是正常浏览器,就认为你是爬虫,直接拒之门外。

解决方式非常粗暴但有效:把浏览器自己的请求头完整抄过来。在F12的Network面板里随便点一个请求,找到Request Headers,复制里面常用字段,在代码里用一个字典存起来:

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", "Connection": "keep-alive", }

注意一点:User-Agent的格式里通常包含完整的操作系统和浏览器版本信息,不要自己瞎编,直接从浏览器里复制最稳妥。这一步可以解释为什么你看到的很多爬虫代码第一屏都是headers,它是和服务器打交道的“身份证”。

3.2.2 编码问题别忽视

网页的中文乱码是新手很容易卡住的地方。不同的网站可能用utf-8、gbk、gb2312等不同编码,如果直接用resp.text解析,遇到编码不匹配就会出现一堆乱码。

requests库默认会根据响应头里的charset字段推断编码,但很多网站的charset写得不规范,推断结果不准。更稳妥的做法是手动指定编码:

resp = requests.get(url, headers=HEADERS, params=params, timeout=10) resp.encoding = resp.apparent_encoding

apparent_encoding是requests根据网页内容自动探测出来的编码,对中文页面尤其好用。实测下来大多数情况能直接解决乱码。要是还会乱,就再从页面HTML的<meta charset="...">标签里手动取值写死。

3.3 解析模块:用XPath精准定位电影数据

3.3.1 XPath基础语法速记

解析HTML的办法有很多,正则、BeautifulSoup、XPath都能干。这个案例我选XPath,是因为它的表达能力最强,写起来也最像“画路径”:你想定位页面里的哪一块,就直接描述这条路径。

几个必须记住的基础语法:

  • //表示在当前文档任意位置查找,相当于“全页面搜索”
  • .//表示在当前节点下面搜索
  • [@class="movie-item"]表示筛选带有指定class属性的节点
  • /text()表示取出节点里的纯文本
  • contains(@class, "item")表示class属性里包含某个字符串时匹配
  • 下标[1]表示第几个同类节点,注意是从1开始

比如定位所有包含电影信息的卡片节点,XPath可以写成:

//div[contains(@class, "movie-item")]

关键技巧是contains。实际页面的class属性经常不止一个值,比如class="movie-item hot",如果你写@class="movie-item"就匹配不到,但用contains(@class, "movie-item")就能命中。这是一个非常常见的踩坑点。

3.3.2 XPath定位的完整思路

拿到HTML源码后,先用etree.HTML(html)把它解析成可查找的节点树,然后就是一路定位。

假设页面里每部电影的结构大致如下:

<div class="movie-item"> <h3><a href="/movie/123">流浪地球2</a></h3> <span class="score">9.3</span> <span class="type">科幻/冒险</span> <span class="actors">吴京, 刘德华, 李雪健</span> <p class="desc">太阳即将毁灭,人类在地球表面建造出巨大的推进器……</p> <span class="date">2023-01-22</span> </div>

对应的XPath就非常直白:

  • 标题:h3/a/text()
  • 评分:span[@class="score"]/text()
  • 类型:span[@class="type"]/text()
  • 主演:span[@class="actors"]/text()
  • 简介:p[@class="desc"]/text()
  • 上映日期:span[@class="date"]/text()

这里要注意一个细节:XPath里的路径是相对当前节点来写的,所以先循环拿到每个movie-item卡片,再在卡片内部用.//查找字段,这样代码既直观又不容易错。

刚开始写XPath的时候,我强烈建议先在浏览器console里验证。在F12的Elements面板里按Ctrl+F,可以直接输入XPath测试,页面会高亮匹配到的节点。等确认能匹配到,再复制到Python代码里,能省掉大量调试时间。

3.4 清洗落盘:CSV存储与分页采集

3.4.1 数据清洗

XPath拿到的字段大多是字符串,但实战中必须处理三类脏数据:首尾空格、空字段、字段里有多个值。

  • 用strip()去掉首尾空格和换行符。
  • 用“空值兜底”逻辑防止字段缺失导致程序崩溃,比如score = score[0].strip() if score else "暂无评分"。
  • 如果主演列表是用逗号分隔的,拿到的是一个整体字符串,按项目需求决定是保留原始字符串还是拆成列表。这个案例里保留原字符串,方便后面保存。

清洗逻辑写进解析函数里,每提取一个字段就顺手处理掉,不要等全部提取完再统一洗,那样代码容易乱。

3.4.2 保存CSV的编码坑

把数据写入CSV的时候,最典型的坑是编码。如果直接写成encoding="utf-8",你用记事本打开没问题,但用Excel打开就是一片乱码。正确做法是用utf-8-sig:

with open("movies.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["电影名称", "评分", "类型", "主演", "上映日期", "简介"]) writer.writerows(all_data)

newline=""也是必须的,否则写入CSV时每行之间会多出一个空行,这是Windows平台上的老毛病了。这个细节我建议直接背下来,凡是Python写CSV,固定组合就是encoding="utf-8-sig"+newline=""。

3.4.3 分页采集与频率控制

分页采集就是一个for循环,配合上一节抓包发现的page参数:

for page in range(1, 6): data = crawl_page(page) save_to_csv(data) print(f"第{page}页采集完成,共{len(data)}条") time.sleep(2)

time.sleep(2)这行作用很大。一是给目标站点留出喘息时间,不要几秒钟内把对方服务器请求爆炸,这是爬虫的基本礼貌;二是避免触发反爬机制的频率检测。很多网站的封禁策略就是“同一IP短时间请求次数过多”,加个延迟是最简单有效的规避方式。

另外建议把每一页的采集结果打印出来,方便实时观察进度。如果中途哪一页请求失败,也能通过日志快速定位。

3.5 完整参考代码

把上面的模块整合起来,就是一个可以直接跑通的入门级爬虫脚本。代码里的URL和页面class是示例,正式使用时按你自己抓包得到的结果替换即可。

import requests from lxml import etree import csv import time # 请求头伪装,从浏览器开发者工具里复制 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", } BASE_URL = "https://movie.example.com/list" # 替换成你抓包得到的真实URL def fetch_page(page_num): """请求指定页码的HTML,返回文本""" params = {"page": page_num} try: resp = requests.get(BASE_URL, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except requests.RequestException as e: print(f"第{page_num}页请求失败: {e}") return None def parse_movies(html): """从HTML中提取电影字典列表""" if not html: return [] selector = etree.HTML(html) items = selector.xpath('//div[contains(@class, "movie-item")]') movies = [] for item in items: # 用xpath提取字段,空值时给默认值 title = item.xpath('.//h3/a/text()') title = title[0].strip() if title else "未知片名" score = item.xpath('.//span[@class="score"]/text()') score = score[0].strip() if score else "暂无评分" genre = item.xpath('.//span[@class="type"]/text()') genre = genre[0].strip() if genre else "未知类型" actors = item.xpath('.//span[@class="actors"]/text()') actors = actors[0].strip() if actors else "未知主演" desc = item.xpath('.//p[@class="desc"]/text()') desc = desc[0].strip() if desc else "暂无简介" date = item.xpath('.//span[@class="date"]/text()') date = date[0].strip() if date else "未知日期" movies.append({ "title": title, "score": score, "genre": genre, "actors": actors, "date": date, "desc": desc, }) return movies def save_to_csv(all_movies): """把电影数据追加写入CSV""" file_exists = False try: with open("movies.csv", "r", encoding="utf-8-sig") as f: file_exists = bool(f.readline()) except FileNotFoundError: pass with open("movies.csv", "a", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) if not file_exists: writer.writerow(["电影名称", "评分", "类型", "主演", "上映日期", "简介"]) for m in all_movies: writer.writerow([m["title"], m["score"], m["genre"], m["actors"], m["date"], m["desc"]]) def main(): all_data = [] for page in range(1, 6): # 采集前5页 html = fetch_page(page) movies = parse_movies(html) if movies: all_data.extend(movies) print(f"第{page}页解析完成,新增{len(movies)}条") time.sleep(2) # 请求间隔,避免给服务器压力过大 if all_data: save_to_csv(all_data) print(f"全部完成,共保存{len(all_data)}条数据到 movies.csv") else: print("没有采集到任何数据,请检查页面结构是否变化") if __name__ == "__main__": main()

这段代码在结构上已经具备了一个合格爬虫的基本素养:请求单独封装、解析单独封装、存储单独封装、入口函数统一调度。你以后在它基础上扩展代理、多线程、去重,都只需要改对应模块,不用推倒重来。

4. 常见问题排查:采集路上的坑与对策

4.1 高频问题速查表

我把自己实操过程中遇到过的典型问题整理成了一张速查表,覆盖了入门阶段99%的报错场景。

现象可能原因解决方案
请求返回403或418请求头不完整,被反爬识别补全headers,特别是User-Agent和Referer
中文乱码编码推断错误设置resp.encoding = resp.apparent_encoding
XPath返回空列表选择器写错或class匹配不准确用F12的Ctrl+F先验证XPath
XPath能匹配但取不到text文本在子节点/a标签里加/text()或改用.//text()
采集到的数据只有第一页翻页参数没生效去抓包里确认page参数名,使用params传递
数据全有但缺某几个字段单条数据可能字段缺失写空值兜底逻辑,if score else "暂无"
中途请求超时网络波动或频率太高增加timeout超时参数,加异常重试机制

这里面最隐蔽的是第三行:XPath返回空列表。很多新手一看到空列表就认为是页面结构问题,其实更常见的是class值匹配方式不对,或者class属性里还有别的值。比如class="movie-item active"用@class="movie-item"就匹配不到,必须用contains(@class, "movie-item")。这个教训我用无数次报错换来的,写进代码里能省一半调试时间。

4.2 遇到反爬拦截怎么处理

小型电影站点的反爬通常不会太复杂,常见的就那么三招:检查User-Agent、检查请求频率、检查Cookie。对应的应对思路也很成熟。

第一,伪装请求头。这个在代码里已经做了,但要注意不同浏览器版本的User-Agent格式不同,如果一个不行,换个Chrome或Edge的新版UA试试。

第二,控制频率。不要用多线程无脑并发请求,先用单线程加time.sleep(2)跑通,再想提速的事。如果发现请求被限流,把sleep时间拉长到5到10秒,通常就能恢复。

第三,补全Cookie和Referer。如果某些页面请求需要登录态或者来源校验,直接在代码的headers里面加上从浏览器开发者工具里复制的Cookie字段和Referer字段。具体的做法是:先在浏览器里正常访问一次页面,然后从F12的请求头里复制Cookie值粘贴到代码里。这种Cookie一般有过期时间,等失效了再重新复制一次就行。

这几招只针对入门级反爬。如果是遇到需要滑块验证、字体反爬、JS加密参数的站点,那属于进阶内容,不是这个案例的重点。把基础玩法练扎实后,再去研究那些也不迟。

4.3 网站改版导致爬虫失效怎么办

网页改版是爬虫的大敌。今天写好的XPath,明天可能因为前端工程师改了个class名,就全部失效。应对方法就一句话:先看页面,再改选择器,不要盲调代码。

我遇到网站改版时的排查步骤基本固定:

  1. 打开页面,按F12,找到对应数据结构,重新确认标签和class是否有变化。
  2. 在Elements面板里用Ctrl+F验证新的XPath能否命中目标。
  3. 如果只是class名变化,替换代码里对应的XPath字符串即可。
  4. 如果是整个数据加载方式从头到尾变了,比如从服务端渲染改成接口渲染,那就要重新抓包,可能还得换解析方式。

每次改版都是对前期“思路清晰”程度的一次考验。如果你当初抓包认真记了参数名和URL结构,现在只需要小修小补;如果当初是抄的别人代码,没有自己的理解,那么改版之后基本就要推倒重写。这也是我一直强调“先抓包、后写码”的原因,它本质上是在帮你建立对目标站点的地图,而不是只抄一句咒语。

5. 避坑经验与后续扩展方向

5.1 入门阶段最容易忽略的点

有几个细节,代码能跑通之后回头再看特别值得注意:

  • 合法合规意识。爬虫采集的目标必须是公开的、允许访问的数据。动手之前先看目标网站的robots.txt规则,君子协议能帮你避开很多麻烦。只采集公开信息,不做批量抓取个人信息、付费内容等越界操作,这是底线。
  • 日志输出比想象中重要。入门阶段很多人图省事不写print,出了问题根本不知道脚本跑到哪一步,排查全靠猜。加几行打印不费事,关键时刻能救命。
  • 异常捕获必须写。网络请求这东西,没人敢保证每次都成功。超时、断网、被中断,任何一个都可能在爬取到第200条数据时突然发生。用try/except包住关键的请求逻辑,失败了记录日志、跳过继续,是爬虫健壮性的基础。
  • 先跑一页再跑全部。调试阶段把range从1改成1,先验证单页数据正确了,再放开全量采集。不要一上来就写for page in range(1, 100),万一解析逻辑有错,写进CSV的全是脏数据,还得回头清。

5.2 案例完成之后还能怎么玩

把这个案例跑通之后,你其实已经掌握了爬虫最核心的“三板斧”:请求、解析、存储。接下来有几个非常顺滑的升级方向。

第一个方向是存储升级。CSV适合做练习,但不适合做频繁查询。可以试试把数据存入SQLite或者MySQL,用SQL做筛选和排序,这时候你会自然接触到数据库操作,为后面的数据清洗和分析铺路。

第二个方向是并发提速。单线程爬100个页面需要几分钟,用concurrent.futures线程池可以让多个网络请求同时发出,速度能提升一个数量级。但要加频率控制,不然容易被封。这是从“爬虫脚本”走向“爬虫程序”的关键一步。

第三个方向是可视化展示。采集了一批电影数据之后,可以做评分分布统计、演员合作网络图、类型词云。用pandas加matplotlib就能完成简单版本,既有成就感,又能把“数据采集”和“数据分析”串起来。

第四个方向是应对更复杂的站点。当你发现目标网站用了JavaScript渲染数据,用requests拿不到完整HTML时,就该学习Selenium和Playwright了。掌握了动态页面的处理,爬虫的适用范围就会瞬间扩大。

我个人在实际操作中的体会是:爬虫真正的难点从来不是写代码,而是分析目标网站的思路。每次拿到一个新站点,先抓包看流量、再理结构、最后才写代码,这个顺序千万不要反。把上面这个案例完整走一遍以后,你会发现换一个网站采集基本就是同一套打法,不同的只是URL和选择器。后面再遇到什么样的站点,心里都不会虚了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询