做影视数据分析的朋友,基本都绕不开国际电影节的入围名单。每年戛纳、柏林、威尼斯三大影展的入围名单一公布,紧接着就是各种版本的片单整理。最笨的办法是一个一个官网去复制粘贴,费时间不说,片名中英文对照、制作国家/地区、竞赛单元、导演信息这些字段还特别容易漏。这份指南里,我用 Python 爬虫把三家官网的采集逻辑彻底跑通了一遍,哪些页面可以直接解析 HTML,哪些数据其实藏在接口返回的 JSON 里,中英文片名怎么对齐,最后怎么导出成干净的 CSV 文件,都会一步步拆开讲清楚。
这个项目适合三类人:一是需要定期追踪电影节片单的影评人、选片人、策展助理,二是做影视数据库或文化数据分析的开发者,三是刚学完 requests 和 BeautifulSoup、想找个真实项目练手的爬虫新手。文章按“需求拆解 → 页面分析 → 代码实现 → 问题排查”的顺序展开,你可以直接照着操作,也可以只挑自己关心的部分看。
1. 需求拆解:这份名单数据,到底该从哪儿下手
1.1 明确字段与数据来源
先把需求讲清楚。我们要采集的核心字段有四个:片名(中英文)、制作国家/地区、竞赛单元、导演信息。看起来简单,真去官网翻一圈就会发现有很多坑。
先说片名。三大影展的官网通常只提供原始片名和英文译名,中文名基本不会出现在官网上。比如一部法语影片,官网给出的是法文原名 Le Voyage 和英文名 The Journey,中文名叫什么需要自己想办法处理。这是整个项目里最耗精力的部分,后面我会单独讲对齐策略。
再说制作国家/地区。合拍片非常常见,一部影片可能同时标注法国、比利时、卢森堡三个国家,采集的时候要考虑是用短横线分隔还是用逗号连接,否则后面做统计时会很痛苦。
竞赛单元字段相对明确,但不同影展的单元名称差异很大。戛纳有“主竞赛”“一种关注”“午夜展映”,柏林有“主竞赛”“遇见”,威尼斯有“主竞赛”“地平线”“威尼斯日”。同一个单元在不同官网上的分类层级也不同,有的在列表页直接可见,有的需要点进详情页才能确认。采集结构设计时必须把这个层级差异考虑进去。
导演信息同样不省心。导演姓名在官网里可能是“Jean Dupont”这种格式,也可能在接口里拆成 first_name 和 last_name 两个字段,甚至遇到双导演时会以数组形式返回。解析的时候要统一成“导演A / 导演B”的格式,避免一条数据多个导演时把字段撑破。
数据来源锁定在两个语言体系、三种页面结构上:戛纳以法语内容为主,柏林以德语为主,威尼斯以意大利语为主。三家官网的页面语言、渲染方式、数据结构各有不同,这正是这个项目最有价值的地方——你不是在写一个只适配某个网站的爬虫,而是在练习一套能快速迁移到任意多语言官网的方法论。
1.2 技术选型:为什么先用 requests 而不是 Scrapy
很多新手一看要抓三个网站,第一反应是上 Scrapy。我的建议是:这个量级完全没必要。
Scrapy 的优势在于分布式、并发调度、中间件生态,适合大规模、持续性、需要增量更新的爬虫项目。而电影节入围名单一年就公布几次,数据量撑死几百条,用 Scrapy 属于杀鸡用牛刀,光配置 Item Pipeline 和中间件的时间就够把整个项目写完了。
我的技术路线是分层的:
- 第一优先级:requests + BeautifulSoup 直接解析静态 HTML。结构规整的页面用这种方式最直接,代码可读性也最好。
- 第二优先级:寻找页面背后的 XHR 接口,用 requests 直接请求 JSON。很多动态渲染的官网,底层数据接口反而是最稳定的入口,比解析渲染后的 DOM 更可靠。
- 第三优先级:Playwright 做浏览器渲染兜底。只有接口有签名校验、或者页面结构过于复杂时才上这种重型方案。
这个优先级设计的核心逻辑是:能用简单方法解决的,绝不上复杂工具。我见过不少爬虫项目,最后死在过度设计上——用 Scrapy 写了大量中间件,结果目标网站改一次版,整套配置就要重写。用 requests 加 BeautifulSoup,碰到改版时改几行选择器就能恢复工作。
1.3 这份数据抓下来能做什么
这个项目看起来只是抓一份名单,但它真正能延伸的应用场景很多。
历年入围名单做趋势分析是最直接的玩法。把 2015 年到最近一届的数据全部采集下来,按国家/地区、竞赛单元做透视,能直观看到各国影片的入围数量变化。比如某一年某个国家的入围数量突然增加,背后可能反映的是当地电影工业的活跃度提升,或者电影节策展方向的调整。
还可以做多语言片名数据库。把原文片名、英文片名、中文译名对齐后建一张表,做成公开的影展数据库,后续做影评分析、影片关联推荐、搜索匹配都有用。很多影视社区的片单功能,底层数据就是这么积累起来的。
对影评人来说,完整的入围名单配合导演过往作品信息,能快速生成一篇带数据分析的选片观察文章。这也是一份名单数据最有价值的地方——它不只是几十个片名,而是一个可以进行多维度分析的结构化数据集。
2. 页面结构分析:三家官网的数据定位实战
2.1 用浏览器开发者工具,找到数据的真实位置
拿到一个官网页面,第一件事不是写代码,而是打开浏览器开发者工具,先搞清楚数据到底以什么形式存在。
判断逻辑很简单:右键查看网页源代码,搜索页面上一眼能看到的文字。如果源码里能搜到,说明是服务端渲染的静态 HTML,直接用 requests 请求再解析即可。如果源码里搜不到但页面上能看到,说明数据是 JavaScript 动态加载的,这时就要切到 Network 面板,筛选 Fetch/XHR 请求,刷新页面,观察哪个接口返回的数据里包含了目标字段。
这个判断动作很关键。我见过有人对着一个纯动态页面硬写 CSS 选择器,折腾了半天拿不到数据,最后发现页面元素是 JS 渲染的,真正的数据在一个返回 JSON 的接口里。用生活化类比来说,静态页面相当于把菜直接摆在货架上,你拿着购物篮去挑就行;动态页面相当于货架是空的,但仓库管理员手上有完整的库存清单,你得找到沟通窗口去问。
定位接口时重点看三类请求:返回 JSON 的 XHR 请求、返回 JSONP 的脚本请求、以及静态资源里引用的数据文件。找到可疑的接口后,直接在浏览器新标签打开,看返回的数据结构里有没有目标字段。如果有,记录下来备用;如果没有,继续往下翻其他请求。
2.2 戛纳官网:静态列表页的解析套路
戛纳官网的入围名单页,结构上更接近传统门户风格。以法语为主,但很多栏目有英文切换链接。页面 HTML 里能看到规整的条目容器,每个入围影片通常对应一个带有统一 class 名称的区块。
我用一个结构示意来说明:
<div class="film-item"> <h3 class="film-title">Le Voyage</h3> <p class="film-title-en">The Journey</p> <p class="director">Jean Dupont</p> <p class="country">France, Belgium</p> <span class="section">Competition</span> </div>实际页面里标签结构会有差异,但核心思路是一致的:先定位条目容器,再从容器内部提取各字段。这种页面的优势是直观,麻烦在于官网经常对 HTML 结构调整,每次改版都要重新确认选择器。我在后面会给出一套配置化选择器的解决方案,避免每次都改代码。
戛纳官网的反爬策略相对温和,只要 User-Agent 设置正确,请求频率控制在 1 秒以上,基本不会触发拦截。但要注意 Accept-Language 请求头,如果你的请求头声明的是 zh-CN,而页面内容以法语为主,有些 CDN 节点可能返回异常的响应。建议把 Accept-Language 设为 fr-FR 或 en-US,与目标页面语言保持一致。
2.3 柏林官网:动态渲染页面的接口挖掘
柏林官网是我在三个网站里踩坑最多的一个。页面本身是典型的动态渲染架构,列表区域的内容是通过 JavaScript 从后端接口拉取后渲染出来的。直接请求页面 URL 拿到的 HTML 里,根本找不到影片条目信息。
解决办法是切到 Network 面板,筛选 Fetch/XHR,刷新入围名单页面。这时能看到一个返回 JSON 的接口,响应数据里包含了完整的影片数组。每个影片对象里有原文片名、英文片名、导演数组、制作国家数组、竞赛单元字段,甚至还有影片海报链接。
这个接口的价值在于:它比渲染后的 DOM 结构更稳定,字段命名更规范,数据量也更完整。用 requests 直接请求这个 JSON 接口,解析效率远高于处理 HTML。
柏林官网还有一个特点:接口的请求头里需要带 Referer 字段,否则服务器会直接拒绝响应。这在动态页面里很常见,因为接口设计时通常会校验来源页面。实操中我见过不少初学者,UA 设置了,Accept 设置了,唯独忘了 Referer,结果请求返回 403。这个问题我会在第四章的排查表里重点写。
2.4 威尼斯官网:多语言页面的字段识别
威尼斯官网的难度不在反爬,而在语言。页面默认意大利语,字段名称全是意语缩写:Titolo 指片名,Regista 指导演,Paese 指制片国家,Sezione 指竞赛单元。
这里有一个很实用的小技巧:威尼斯官网的 URL 通常支持语言切换,把路径中的 it 替换为 en,页面就会切换到英文版。但接口返回的 JSON 字段名未必会跟着语言切换,可能是因为底层数据模型统一用意大利语命名,也可能是因为接口只返回固定字段名。所以要在代码里做一层字段映射,而不是指望官网帮你翻译好。
威尼斯官网的页面结构改动比较频繁,像是在持续重构。去年能用的一批选择器,今年可能就失效了。这种情况下,接口方案比 HTML 解析方案更有优势——官网改版通常不会动底层接口,就算动了,JSON 字段的调整也往往比 HTML 结构调整更容易适配。
3. 核心代码实现:从请求到 CSV 导出的全流程
3.1 项目初始化与请求模块
先建立项目结构。我的习惯是分成三个文件:spider.py 放主流程逻辑,parsers.py 放各网站的解析函数,output.py 负责清洗和导出。如果你的项目规模不大,也可以全部写在一个文件里,但至少要把请求函数、解析函数、导出函数分开定义,方便后续单独维护。
依赖安装只需要四个库:
pip install requests beautifulsoup4 pandas lxml其中 lxml 不是必须的,但它的解析速度比 Python 标准库解析器快不少,处理大页面时体感差异明显,建议顺手装上。
请求模块的核心是请求头设置,这段代码是每个爬虫项目的地基:
import random import requests UA_POOL = [ "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", ] def build_headers(lang="fr-FR"): return { "User-Agent": random.choice(UA_POOL), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": f"{lang},{lang.split('-')[0]};q=0.9,en;q=0.7", "Connection": "keep-alive", } def fetch(url, lang="fr-FR", referer=None): headers = build_headers(lang) if referer: headers["Referer"] = referer try: resp = requests.get(url, headers=headers, timeout=15) resp.raise_for_status() return resp except requests.RequestException as e: print(f"[请求失败] {url} -> {e}") return None注意几个细节:User-Agent 池用随机方式轮换,是为了避免每次请求都暴露完全相同的浏览器特征;Accept-Language 根据目标网站语言动态设置;Referer 只在需要时添加。这个模块足够应付三大影展官网的基础请求需求。
3.2 静态页面解析:以戛纳官网为例
拿到 HTML 之后,用 BeautifulSoup 定位入围影片的条目容器。假设容器 class 是 film-item,解析逻辑可以这样写:
from bs4 import BeautifulSoup def parse_cannes(html): soup = BeautifulSoup(html, "lxml") films = [] for block in soup.select(".film-item"): film = { "title_original": block.select_one(".film-title").get_text(strip=True), "title_en": block.select_one(".film-title-en").get_text(strip=True), "director": block.select_one(".director").get_text(strip=True), "country": block.select_one(".country").get_text(strip=True), "section": block.select_one(".section").get_text(strip=True), } films.append(film) return films这段代码里最需要注意的是 .get_text(strip=True) 这个组合。不做 strip 的话,从 HTML 里取出来的字符串会带大量换行和空格,后续存进 CSV 还得二次清洗。select_one 返回的是第一个匹配元素,如果某个字段不存在会返回 None,这时再调用 get_text 就会报错。正式代码里要给每个字段加一层 try/except 或者使用 getattr 保护,避免单条数据解析异常导致整个流程崩溃。
写解析函数时还有一个经验:先在命令行里把 HTML 片段打印出来,肉眼确认选择器能匹配到目标元素,再批量运行全量解析。直接跑全量容易在某个未知页面结构上踩坑,调试成本反而更高。
3.3 动态接口采集:以柏林官网为例
柏林官网的接口方案,核心是找到那个返回影片数组的 JSON 接口。假设接口地址结构为:
api_url = "https://example-berlinale-api.org/api/selection" params = { "edition": "2025", "type": "competition" } headers = build_headers(lang="de-DE") headers["Referer"] = "https://example-berlinale.org/en/films" resp = requests.get(api_url, headers=headers, params=params) data = resp.json() films = [] for item in data.get("data", []): films.append({ "title_original": item.get("originalTitle"), "title_en": item.get("englishTitle"), "director": " / ".join(item.get("directors", [])), "country": ", ".join(item.get("countries", [])), "section": item.get("section"), })这里有几个关键点。directors 和 countries 字段在接口里通常是数组,必须用 join 方法拼接成字符串,否则存到 CSV 里会变成 Python 的 list 表达形式,看起来很奇怪。用逗号连接国家、用顿号连接导演,是我在实践里试出来的比较顺眼的格式,你可以按自己习惯调整。
动态接口方案远比你想象的可靠。官网改版时可能改页面模板、改 CSS 类名、改前端框架,但底层接口的数据模型往往保持稳定。特别是电影节官网这种项目,接口后面连着的是数据库,研发团队不会因为首页改版就轻易改动数据库字段命名。所以只要有可用的接口,优先走接口方案。
如果接口请求失败,比如返回 403 或者数据结构发生变化,才需要退回 Playwright 方案。用 Playwright 打开页面,等待影片列表渲染完成后,直接读取渲染后的 DOM。这个方案成本高、速度慢,但作为兜底手段,能让爬虫在极端情况下仍然拿到数据。
3.4 中英文片名对照:最容易被低估的环节
官网能提供原文片名和英文片名,但中文译名几乎不会出现在官网里。这是整个项目里最需要人工参与的部分。
我的处理策略是“映射表为主,人工校对兜底”。先建立一张 Python 字典作为本地映射表:
CN_TITLES = { "The Journey": "旅程", "Land of Silence": "静默之地", } def fill_cn_title(row): return CN_TITLES.get(row.get("title_en"), row.get("title_en"))运行完主体爬虫后,检查输出 CSV 里 title_cn 列中与 title_en 相同的记录,这些就是映射表缺失的影片,集中处理补上译名。这种做法的好处是:第一次跑完你可能需要人工补几十条,但后续每年新增名单时,大部分片名都能命中已有映射表,需要人工处理的数量会越来越少。
不建议依赖翻译 API 做全自动翻译。影片译名讲究约定俗成,很多国际影片有固定的中文通译名,直接直译出来的结果在豆瓣、IMDb 等平台对不上号,反而给后续关联数据造成麻烦。翻译 API 可以作为一种辅助手段,但最终要以人工校对为准。
3.5 CSV 导出与数据清洗
爬到原始数据只是第一步,导出的 CSV 要让别人能直接打开使用,这才是完整交付。
import pandas as pd def save_to_csv(films, filename): df = pd.DataFrame(films) df = df.drop_duplicates(subset=["title_original", "director"]) df = df.fillna("未知") df.to_csv(filename, index=False, encoding="utf-8-sig")这里有两个关键设置。
第一个是 encoding="utf-8-sig"。很多初学者导出 CSV 后,用 Excel 打开发现中文全部变成乱码,原因就在这里。Excel 默认按本地编码(在中文系统里通常是 GBK)解析 CSV 文件,而 Python 写入时如果用 UTF-8 且不带 BOM,Excel 就会识别失败。utf-8-sig 编码会在文件头部写入 BOM 标记,Excel 看到这个标记就能正确识别为 UTF-8 编码。
第二个是 drop_duplicates 的去重逻辑。同一个影片可能在官网的多个子页面出现,比如既出现在主竞赛单元列表里,又出现在某场新闻发布会新闻稿里。用 title_original 加 director 的组合去重,能有效避免最终名单里的重复记录。
数据清洗还要注意字符串里的隐藏字符。HTML 里常见的不换行空格是 \xa0,UTF-8 里有时会出现零宽空格(\u200b),这些肉眼看不到的字符会让你的文本匹配失败。清洗时统一用正则把它们替换成普通空格。
4. 实战问题档案:爬虫跑不起来时,先查这几件事
4.1 请求被拒绝(403/406)时的排查顺序
遇到 403 Forbidden 或者 406 Not Acceptable,按照下面的检查清单逐项排查:
- 检查 User-Agent 是否看起来像一个真实浏览器。放在 Python 默认的 requests.user-agent 很容易被识别,换成一个完整的浏览器 UA 能解决大部分问题。
- 检查 Accept-Language 是否与目标网站语言一致。请求法语官网却声明中文语言偏好,可能会被 CDN 判定为异常请求。
- 检查是否需要 Referer 头。动态接口场景下尤其常见,服务器会校验请求来源页面是否合法。
- 检查请求频率是否过高。用脚本连续不停发送请求,触发基于速率限制的反爬策略是最常见的情况。每次请求之间加 1 到 3 秒的 sleep 很有必要。
这里我踩过一个印象深刻的坑:有一次柏林官网的接口突然大面积返回 403,排查了半天,发现是自己在代码里使用了某个已失效的旧 UA 字符串。换了一批新 UA 后立刻恢复。UA 池里的字符串需要定期更新,尤其是过了一两年后,旧版本浏览器的 UA 被服务端列入黑名单的情况并不少见。
4.2 页面结构变化导致解析失败怎么办
官网改版是每个爬虫项目都会遇到的问题,电影节官网也不例外。最直观的表现就是 BeautifulSoup 选择器匹配不到任何元素,返回结果为空。
我推荐把选择器从代码逻辑中剥离出来,集中维护在一个配置字典里:
SELECTORS = { "cannes": { "container": ".film-item", "title_original": ".film-title", "title_en": ".film-title-en", "director": ".director", "country": ".country", "section": ".section", }, "berlinale_api": { "title_original": "originalTitle", "title_en": "englishTitle", "director": "directors", "country": "countries", "section": "section", }, }这样改版时只需要更新配置文件里的选择器,不必改动主逻辑代码。解析函数里找不到元素时,抛出的异常会被统一捕获并记录到日志文件中,而不是直接让整个爬虫崩溃。类似这样:
def safe_extract(block, selector): elem = block.select_one(selector) return elem.get_text(strip=True) if elem else ""用这个保护函数替代直接调用 select_one(...).get_text(...),可以确保某一条数据缺字段时不至于中断全流程。
4.3 CSV 导出后 Excel 打开乱码怎么办
这个问题前面提过解决方案,这里展开说一下原因。Python 默认写文件的编码是 UTF-8,但写成 UTF-8 无 BOM 格式时,Excel 在 Windows 上会尝试用 ANSI 编码(中文系统即 GBK)解析,从而产生乱码。
解决方式即 CSV 导出时指定 encoding="utf-8-sig"。这是唯一的推荐方案,不建议改为 encoding="gbk",因为 GBK 无法覆盖所有 Unicode 字符,一旦片名包含生僻字或特殊符号,写入就会直接报错。
另外要注意的是,pandas 的 to_csv 会默认对内容进行转义。如果片名里包含逗号,字段会被自动用双引号包裹,这是正常现象,不要手动去掉引号,否则 CSV 结构会出错。
4.4 关于采集规范与请求频率的提醒
最后说点偏规范的内容。虽然电影节官网的公开数据理论上是可以访问的,但我们做爬虫时要守住几条底线。
第一,控制请求频率。单线程加随机延迟是最稳妥的方案,每个请求之间至少间隔 1 秒。电影节名单一年就更新几次,完全不存在需要高并发快速抓取的场景,并发带来的收益微乎其微,风险却会放大很多。
第二,尊重站点的访问条款。采集前可以在官网站点查一下站点条款,特别是有没有明确禁止自动抓取的说明。个人学习和研究用途的数据采集,通常问题不大,但不要拿去做什么商业化服务。
第三,注意数据发布时的版权边界。即便只是名单,也可能包含官网的排版、描述文字等信息。做数据分析时尽可能只保留结构化字段,减少对外分发原始页面内容的可能性。
我在实际做这个项目时,最大的体会是:真正花时间的不是 requests 怎么写、BeautifulSoup 怎么解析,而是官网改版后的适配工作和片名译名的校对工作。这两件事没有一劳永逸的解法,只能靠“配置化 + 日志排查 + 人工校对”这套流程去持续维护。如果你准备自己动手做一遍,我的建议是先启动一个小规模试点,抓一个单元、几十条数据跑通全流程,再扩展去抓整个入围名单。一步到位往往意味着遇到问题时排查范围太大,反而拖慢进度。片名映射表也建议从第一天就建好,后续每年增量更新时,你会发现前一年的人工积累能节省大量重复劳动。