
拿电影网站当入门实战是我带新人时最常用的案例之一。原因很简单电影数据表结构清晰、页面信息丰富、请求方式典型而且采集到的结果一眼就能判断对不对。这篇就完整拆解一个真实案例——从一个电影网站抓取电影名称、年份、评分和链接全程走完抓包、分析、写代码、跑结果四步。我会把当时的截图思路、抓包操作、完整代码和注释都整理出来尽量让你看完就能照着写。适合谁来参考刚学完Python基础语法、准备接触爬虫但不知道从哪下手的初学者以及会写简单requests请求但对抓包一头雾水的同学。如果你已经能熟练处理反爬这篇的节奏可能偏慢但排查思路部分依然值得扫一眼。1. 项目选型与整体思路为什么用电影网站当入门实战1.1 电影数据有什么特殊优势选电影网站做爬虫入门有三点别的项目给不了的好处。第一数据结构规整。一部电影通常有标题、年份、地区、类型、评分、简介、链接这些字段几乎都排布在固定HTML结构中非常适合用XPath提取。对比一下新闻网站的动态加载和论坛的反爬强度电影站对新手相当友好。第二页面特征容易判断。你把DevTools打开看到div classmovie-item这种语义化类名基本就能猜到怎么提取。不像某些网站用随机class名称光定位元素就折腾半天。第三结果可验证性强。抓下来50条数据其中某部电影的年份是2019评分8.1你一眼就能判断解析逻辑对不对。这种即时反馈对学习很重要——写爬虫最怕的就是跑完不知道结果对不对而电影数据天然适合验证。1.2 整个项目的流程拆解我把这个项目拆成了六个环节你后面看代码时对照这个流程就不会乱确定目标URL搞清楚电影列表页是哪个地址。用浏览器开发者工具或Fiddler“抓包”找到真正返回电影数据的请求。分析请求头和响应体确认需要什么参数、返回的是HTML还是JSON。用requests构造请求模拟浏览器访问拿到HTML源码。用lxml加XPath从HTML里提取电影字段。清洗数据并存储最后跑一遍验证结果。这六步里第2步对很多新手来说是盲区。大家习惯性直接对着页面源代码写XPath结果一运行就发现元素找不到——因为你看到的网页源码可能是JavaScript动态渲染之后的根本不是requests默认拿到的那个版本。抓包能帮你看清请求到底拿了什么数据、以什么形式返回这一点后面会详细说。1.3 工具选型requests、lxml、XPath怎么搭配这个项目的技术栈我选的是requests发HTTP请求。相比urllib它语法更简洁处理cookies、headers、超时都很方便属于爬虫入门第一课必会的东西。lxml解析HTML/XML。底层是C语言实现解析速度比BeautifulSoup的默认解析器快不少。用它配合XPath定位元素非常精准。XPath一种在XML/HTML文档中查找节点的语言。你可以把它理解成“给HTML元素写地址”。你可能问为什么不用BeautifulSoup不是不行而是对于这种结构清晰的列表页XPath的表达能力更直接。比如想提取所有电影标题XPath写//div[classmovie-item]/a/text()就够了。写多了你会发现XPath在应对嵌套结构时比BeautifulSoup的find系列方法更省事。安装依赖就一行命令pip install requests lxml我建议在虚拟环境里装避免把系统Python环境搞乱。如果你要用Anaconda直接在Jupyter里跑代码块也能顺利运行但发布脚本时还是建议用python script.py的普通文件方式。2. 抓包入门从浏览器到工具看清每一次请求2.1 抓包到底是什么爬虫为什么必须先学它很多教程一上来就教写代码忽略了抓包这个前置步骤导致新手经常遇到一个问题为什么我在浏览器里能看到数据用requests就抓不到答案在于浏览器和代码“看到”的东西不一样。浏览器访问网页时会发出很多个HTTP请求HTML文档、CSS样式、JavaScript脚本、图片资源还有可能是后端接口返回的JSON数据。最终页面渲染出来的内容往往是这些请求共同作用的结果。而requests默认只会完成“你指定的那个请求”拿到的只是一个未经渲染的HTML文档。如果目标页面的数据是JavaScript动态加载的那么HTML文档里根本看不到数据你的XPath自然什么都匹配不到。抓包的作用就是把这些HTTP请求全部拦截下来让你看清楚页面真正从后端请求了哪些数据、请求地址是什么、带了哪些参数、返回了什么格式。搞清楚了这些你的爬虫才能做到“精准打击”。我习惯把抓包比作“抄作业之前先看懂解题过程”。你只知道最后的答案渲染后的页面还不够你得知道步骤请求和响应才能自己写出解题过程爬虫代码。2.2 先用浏览器开发者工具快速验包不一定一上来就上Fiddler浏览器自带的开发者工具已经能完成大部分工作。以Chrome为例打开目标电影网站后按F12切到Network面板再刷新页面。你会看到所有资源请求按时间顺序列出来。点击任意一个请求右侧能看到Headers请求头、Payload提交参数、Response响应内容等标签页。此时你要重点找两类请求文档类请求Document通常是type为document的那个URL是你访问的页面地址Response里是完整HTML。XHR/Fetch请求动态加载数据的请求。如果列表页大量数据是通过Ajax加载的你会看到返回JSON或一段HTML片段的接口请求。我通常会先点Document请求在它的Response里搜索电影名称。如果搜得到说明数据直接嵌在HTML里requests一步就能拿到。搜不到就切到XHR标签页逐个看找出那个返回了电影名称的接口。一个实操技巧在Network面板里按下CtrlFMac上是CmdF搜索你要采集的电影名称或关键字段就能立刻定位到是哪个请求包含了它。这是排查速度最快的方法。2.3 用Fiddler捕获带完整请求细节的包浏览器开发者工具适合快速排查但遇到需要分析App请求、或者想看清HTTPS解密后的内容时Fiddler这种独立抓包工具更合适。Fiddler的本质是一个带证书的本地代理服务器所有HTTP/HTTPS流量都先经过它你就能看到明文请求和响应。Fiddler抓包的基本步骤从官网下载安装Fiddler ClassicWindows平台用的多。打开后菜单栏Tools Options HTTPS选项卡勾选“Capture HTTPS CONNECTs”和“Decrypt HTTPS traffic”为抓取HTTPS流量做准备时按提示信任证书。在左下角确认Capturing状态是开启的。然后打开浏览器访问电影网站。Fiddler主界面会刷新出大量请求记录。按CtrlX清空一次再刷新页面方便从干净状态观察。抓包后关注以下内容请求行的GET /movie/list?page1 HTTP/1.1这就是接口路径和参数。User-Agent和Referer——很多请求头字段决定了服务器如何看待你的身份。响应体的Content-Type比如text/html还是application/json决定了你要用HTML解析还是JSON解析。我第一次用Fiddler的时候最直观的感受是“原来浏览器背后做了这么多请求”。等你能在Fiddler里从几十个请求里准确找出数据接口你对爬虫的理解就已经跨过一个小门槛了。2.4 抓包视角下的请求头哪些字段值得特别留意不管用浏览器还是Fiddler你最终要看的都是请求头Request Headers。下面这几个字段对爬虫尤其重要字段含义对爬虫的影响User-Agent客户端标识如浏览器版本、操作系统服务器靠它判断你是不是浏览器写错了容易被拒Referer请求来源页面有些网站会校验Referer是否合法防盗链逻辑常用Cookie登录凭证、会话标识涉及登录或访问次数限制时需要动态携带X-Requested-With是否为Ajax请求部分接口对非Ajax请求返回不同内容入门阶段你至少要把User-Agent设置成真实浏览器的完整值比如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36写死这个值够用一阵子。等项目做大了需要用fake_useragent库随机变化时再考虑更复杂的方案。核心逻辑很简单让服务器觉得你是一个正常访客而不是一段冷冰冰的代码。3. 核心代码实现完整爬虫代码加逐行注释3.1 确认目标页面和数据结构我这边教学用的电影列表页面结构大致是这样的div classmovie-list div classmovie-item a href/movie/123 span classtitle盗梦空间/span span classyear2010/span span classscore9.3/span /a /div !-- 更多 movie-item -- /div实际网站可能更长更复杂但抓包的思路一样看Document请求的响应里是否直接包含这些movie-item节点。如果包含那requests加lxml就能完成。我在写代码前会先打印一小段响应文本确认数据结构然后再动手写解析逻辑。目标URL我用示例地址演示你替换成实际网站时注意同样处理https://example-movie-site.com/movie/list?page1如果你发现数据是通过JSON接口返回的也没关系把lxml解析换成json解析即可。思路完全相同只是解析方式不同。3.2 构造请求会话与请求头写爬虫的第一步不是解析而是让你的请求尽量接近浏览器。建议用requests.Session()而不是直接requests.get()。Session对象会在多次请求间自动保持cookies而且它管理HTTP连接的方式更高效访问一个网站的多个页面时尤其明显。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://example-movie-site.com/ } session requests.Session() resp session.get(https://example-movie-site.com/movie/list?page1, headersheaders, timeout10) resp.encoding resp.apparent_encoding # 处理中文乱码这里有两个新手最容易忽略的细节。第一timeout参数一定要加。不加timeout如果目标服务器慢程序可能卡住几分钟甚至报连接挂起。加了timeout脚本能快速失败并让你继续排查。第二resp.apparent_encoding是根据响应内容推测的编码格式。很多网站声明的是utf-8但实际返回的是gbk或gb2312编码如果不处理中文必然乱码。3.3 用lxml和XPath提取电影字段拿到底层HTML后用lxml解析。核心代码就三行from lxml import html doc html.fromstring(resp.text) items doc.xpath(//div[classmovie-item])然后遍历items用相对XPath继续提取每个节点的字段for item in items: title item.xpath(.//span[classtitle]/text())[0] year item.xpath(.//span[classyear]/text())[0] score item.xpath(.//span[classscore]/text())[0] link https://example-movie-site.com item.xpath(.//a/href)[0]这里必须说明一个小细节如果xpath返回列表为空你用[0]直接取会报IndexError。冒烟测试时可以先加一个判断title_list item.xpath(.//span[classtitle]/text()) year_list item.xpath(.//span[classyear]/text()) if title_list and year_list: title title_list[0].strip() year year_list[0].strip() else: continue实际项目中几十万个页面的标签不可能完全一致总有某个节点缺失这种防御性写法能避免整个程序因为一个脏数据崩溃。3.4 完整代码带详细注释的采集脚本下面是我整理好的完整代码可直接保存为movie_spider.py运行。注释我写得比较啰嗦主要是为了让新手看懂每一行在干什么。import time import csv import requests from lxml import html headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://example-movie-site.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def fetch_page(session, url): 请求单页HTML并返回解析后的lxml元素对象。 try: resp session.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding if resp.status_code ! 200: print(f请求失败状态码{resp.status_code}URL{url}) return None return html.fromstring(resp.text) except requests.RequestException as e: print(f请求异常{e}URL{url}) return None def parse_movie_items(doc): 从HTML文档中提取电影信息列表。 results [] items doc.xpath(//div[classmovie-item]) for item in items: title_list item.xpath(.//span[classtitle]/text()) year_list item.xpath(.//span[classyear]/text()) score_list item.xpath(.//span[classscore]/text()) link_list item.xpath(.//a/href) if not (title_list and year_list and score_list and link_list): print(跳过一条不完整的电影节点) continue title title_list[0].strip() year year_list[0].strip() score score_list[0].strip() # 很多网站给的是相对地址需要补全为绝对地址 link link_list[0] if link.startswith(//): link https: link elif link.startswith(/): link https://example-movie-site.com link results.append({ title: title, year: year, score: score, link: link, }) return results def save_to_csv(movies, filenamemovies.csv): 将电影数据写入CSV文件。 with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, year, score, link]) writer.writeheader() writer.writerows(movies) print(f已保存 {len(movies)} 条数据到 {filename}) def main(): session requests.Session() all_movies [] # 这里以3页为例你可以按需调整页码范围 for page in range(1, 4): url fhttps://example-movie-site.com/movie/list?page{page} print(f正在抓取第 {page} 页...) doc fetch_page(session, url) if doc is not None: all_movies.extend(parse_movie_items(doc)) # 控制请求频率不要太快 time.sleep(2) save_to_csv(all_movies) for movie in all_movies[:5]: print(movie) if __name__ __main__: main()3.5 代码里值得细说的三个设计点第一个是fetch_page单独抽出来。很多新手喜欢把所有逻辑塞进main函数写着写着自己都分不清哪一段在干什么。把“发请求”和“解析页面”拆开后后续要加代理、加重试、加日志都只需要改一个函数。第二个是resp.encoding resp.apparent_encoding。如果你不加这行中文大概率乱码。乱码不是代码逻辑错误而是编码识别错误但它比逻辑错误更难排查。我踩过这个坑之后每次拿响应体都会先处理编码。第三个是time.sleep(2)。这个等待不是随便加的它会避免你的请求频率过高触发网站反爬。虽然电影站反爬不严但养成控制频率的好习惯能让你在更复杂的项目里少踩很多坑。一般入门项目间隔1到2秒即可数据量大的话用3到5秒更稳妥。3.6 运行效果与验证运行时你会看到类似输出正在抓取第 1 页... 正在抓取第 2 页... 正在抓取第 3 页... 已保存 60 条数据到 movies.csv {title: 盗梦空间, year: 2010, score: 9.3, link: https://example-movie-site.com/movie/123}拿到数据后别急着关终端。做两个验证数据量对不对。URL一页有20条电影3页应该是60条。差太多说明部分节点没解析到。抽样看数据内容。打开CSV随机抽几行看年份和评分是否合理。如果发现某一列全是空多半是XPath写错了或者是HTML结构在不同页面上有变化。我见过太多人跑完代码看到几行数据就算“成功”完全不验证结果质量。爬虫项目里结果正确率比能否跑通重要一百倍。4. 数据存储与结果落地4.1 为什么入门阶段用CSV而不是数据库不是所有爬虫数据都需要MySQL或MongoDB。这个项目的目标是“练手”数据量也就是几十上百条用CSV最合适。原因有三数据可以直接用WPS或Excel打开肉眼验证结果非常直观。不需要额外安装数据库、不需要写建表SQL把注意力全部放在爬虫逻辑上。后续要导入数据库pandas一行read_csv就能读到DataFrame里。CSV写入时有一个容易踩的坑编码要用utf-8-sig而不是utf-8。utf-8-sig会在文件头部写入BOM标记Excel打开时才能正确识别编码避免中文变成乱码。不知道这个细节的话你用记事本打开CSV可能看着正常用Excel打开却乱成一团。4.2 数据去重爬取多页时的一个隐藏坑分页抓取时经常遇到顶部“置顶推荐”和列表内容重复的情况。同一部电影出现在第1页和第3页直接写入CSV就会出现重复记录。去重可以很简单——在写入前用一个set做判断seen set() unique_movies [] for m in all_movies: if m[title] in seen: continue seen.add(m[title]) unique_movies.append(m)按电影标题去重对入门项目已经够用。更严格的去重应该用ID或URL但标题去重能解决90%的问题。养成收集完数据先看重复率的习惯你的数据质量会明显提高。4.3 除了CSV你还可以顺手输出一份简易统计既然都抓下来了做点简单统计也花不了几行代码。比如算一下平均评分scores [float(m[score]) for m in unique_movies if m[score].replace(., ).isdigit()] avg_score sum(scores) / len(scores) if scores else 0 print(f抓取电影平均评分{avg_score:.2f})这个操作看起来不起眼但它能快速帮你判断数据有无异常。如果评分字段有很多非数值内容float()转换会报错你就知道解析阶段有遗漏需要回去修XPath。用这种“自检”倒逼解析逻辑完善是写爬虫很好用的一招。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因解决办法返回的HTML里找不到电影数据数据是异步加载的Document请求里没有去Network里的XHR标签页找接口中文乱码响应编码识别错误用resp.apparent_encoding覆盖XPath匹配出一堆空列表class属性值写错或标签嵌套层级和自己想的不一样在Chrome里点击元素右键Copy XPath参照请求被拒绝状态码403/429缺少User-Agent或请求太频繁补全请求头加延迟降低频率报错IndexError: list index out of range某个节点的xpath返回为空先判断列表是否为空再取[0]数据部分缺失但程序没报错页面结构在不同区块有差异打印分页结果定位差异节点并补充兼容5.2 爬取结果为空先确认数据在哪一层遇到“爬下来是空”这个问题我建议按这个顺序排查第一步确认requests拿到的HTML里有没有目标数据。可以打印前1000个字符看一下。如果连电影标题都没出现在响应里那就不是XPath的问题是页面源码根本不含数据。这时候回抓包工具重点找XHR/Fetch接口。第二步如果数据在响应里但XPath提取不到用浏览器开发者工具在Elements面板里检查目标元素的结构对比你的XPath。我见过最多的问题是class名拼错或者目标logo是一个img标签你却用text()去提取。第三步如果提取到部分数据说明某些页面结构不一致。比如第一页第一屏是推荐位class名和列表项不同。这时候把特殊结构单独处理或者做防御性跳过。5.3 抓包时看到几十个请求怎么快速定位数据接口这是新手最头疼的打开Fiddler或Chrome Network满屏都是请求不知道哪个才和电影数据有关。分享一个我自己常用的方法清空请求记录然后在页面上执行一次能引起数据变化的操作——比如翻到第2页。这时候只观察新增的那些请求因为别的资源大多会被浏览器缓存。新增的请求里凡是返回JSON或包含“movie”“list”“page”关键词的基本就是数据接口。还有一种笨办法但很有效在Network面板里直接搜索你看到的电影名称。数据虽然可能在JS里被转义但中文标题往往直接以明文出现。搜到了你就找到了数据所在的请求。5.4 请求正常但频繁超时学会加重试和超时控制入门爬虫常忽略异常处理。一个页面超时整个脚本可能一直卡住意外断网程序直接抛异常退出。我给你一个简单但实用的模式def get_with_retry(session, url, retries3): for attempt in range(retries): try: resp session.get(url, headersheaders, timeout10) if resp.status_code 200: return resp except requests.RequestException as e: print(f第{attempt 1}次请求失败{e}) time.sleep(1 * (attempt 1)) # 递增等待 return None重试时间用1 * (attempt 1)即指数退避的简化版第一次失败等1秒第二次等2秒第三次等3秒。这比固定等1秒重试三次更不容易被服务器拒绝也更符合实际网络波动规律。5.5 反爬不是洪水猛兽入门阶段只要守规矩很多新手看多了“反爬与反反爬大战”的帖子还没开始写就怕了。实际上对于这个电影站项目你只要做到三件事就不会触发反爬设置浏览器User-Agent、请求间隔保持在2秒以上、不要一次性狂抓几千个页面。这三件事做完大部分入门级别站点都不会为难你。另外务必注意爬虫要尊重目标网站的robots.txt和使用条款。很多网站会明确说明哪些路径允许抓取、抓取频率限制是多少。作为练习项目采集公开的列表数据没问题但请不要将采集到的数据用于商业用途也不要绕过登录系统去抓取非公开内容。技术的价值在于学习和解决问题而不是破坏规则。6. 再进阶一步这个项目还能怎么扩展跑完基础版之后你可以试着加四个升级方向难度依次递增多线程采集。现在一页一页抓太慢你可以用ThreadPoolExecutor同时请求多页把每页的耗时从“串行”变成“并行”。注意控制线程数量我建议最多5个并发不然容易被封。抓取详情页。列表页只有基本信息点进每部电影的详情页还能拿演员、简介、片长。这需要二次请求能顺带练习如何管理URL队列和去重。增量更新。每天跑一次脚本把新电影插进现有数据而不是全量重抓。这个功能需要你对标题做唯一性判断也可以去学一学SQLite的INSERT OR IGNORE。对接数据可视化。抓完的数据用pandas整理一下画个评分分布直方图或年份趋势图项目看起来立刻高大上还能顺便复习数据分析技能。个人建议走“多线程采集”和“详情页抓取”这两个方向。它们不引入太复杂的新框架但对理解网络请求并发控制、URL队列管理这些核心概念帮助很大。把这两个做熟你再去接触Scrapy框架会发现框架里很多设计都是有原因的——因为你在手写爬虫时已经踩过那些坑了。我在带新人的时候常说一句话爬虫实战项目别贪多把一个案例做到能拆、能改、能扩展比草草刷十个案例有用得多。电影站这个项目虽然简单但它覆盖了“抓包分析、请求构造、HTML解析、数据存储、异常处理”这五个爬虫基本功。把这些基础打牢后面遇到App抓包、接口签名、验证码识别这些更复杂的场景时你就不会慌——因为解决问题的思路是相通的。