每年奥斯卡颁奖礼结束后,社交平台上总会冒出各种“明星关系图”、“合作网络图”。但你有没有想过,如果把过去将近一百年的所有获奖者放到同一张图里,用他们共同参与过的作品去连线,最终会得到一张什么样的“人脉网络”?这个项目的标题叫“绘制奥斯卡获奖者之间的联系”,我前后花了一周时间把它完整跑通:数据采集、关系建模、交互可视化,每一步都踩了不少坑。这篇博文不是给一份静态成品图,而是把整个可复现的流程、代码思路和失误盘点都写出来,适合正在学习网络爬虫和关系图谱的数据分析爱好者、影视数据研究者,以及想拿真实数据做可视化作品的人参考。
1. 项目想解决什么问题:从“获奖名单”到“关系图谱”
1.1 为什么值得画一张“获奖者关系网”
很多人的第一反应是:奥斯卡获奖名单不就是一份表格吗,把名字列出来不就行了,为什么要画成图?这个思路能回答“谁在哪一年得了什么奖”,但回答不了几个更有意思的问题:
- 哪些获奖者在拿奖之前就已经合作过?
- 哪位获奖者是整个网络里的“桥梁人物”,连接着多个原本不相干的圈子?
- 某个年代的获奖者群体是不是特别抱团,比如新好莱坞时期那批人频繁出现在彼此的作品里?
- 从数据上看,是否存在“铁打的配角,流水的影帝”这种结构性现象?
这类问题天然适合用图来承载。人与人之间的合作关系本质上就是图结构,节点是获奖者,边是合作经历。用表格查询“A和B有没有关系”很容易,但想回答“A和B之间隔着几个人”、“谁在中间起连接作用”,表格就力不从心了。这个项目要做的,就是把奖项数据从二维表转换成语义丰富的网络图,让用户通过拖拽、缩放、点击节点直观地观察这些关系。
1.2 核心概念:节点、边和权重
在动手写代码之前,得先把“关系”这个词翻译成数据模型。我采用最经典的图定义:
| 要素 | 含义 | 我使用的数据 |
|---|---|---|
| 节点 | 一个人,即获奖者 | 姓名 + 出生年做唯一ID |
| 边 | 两个人之间发生过一次合作 | 共同出现在同一部影片的演职员表中 |
| 权重 | 合作次数或合作紧密程度 | 合作次数越多,边越粗 |
| 类别 | 获奖者身份类型 | 演员、导演、制片人、编剧、摄影指导等 |
这里有个容易忽略的细节:边不一定非要在“同一场戏”里。只要两个人都出现在同一部影片的演职员表里,我就认为他们之间产生了一次合作连接。这种定义有点粗,但好处是数据收集成本低、覆盖范围广。如果你想做得更精细,可以再引入“同届获奖”关系,即两个获奖者在同一届颁奖典礼上获奖,这也是一种弱连接,在后续扩展里我会讲。
1.3 方案选型:用 Python 全家桶还是图数据库
最初我犹豫过要不要上 Neo4j,毕竟图数据库是处理这类问题的“正统”工具。但后来发现,这个项目的数据量其实远没有想象中那么大:历届获奖者加在一起也就是一千人左右,合作边撑死几万条,主流配置的电脑完全能扛住。直接上数据库反而是杀鸡用牛刀,还得多学一套查询语言。
因此最终方案是:requests + BeautifulSoup 负责抓公开页面数据,pandas 做清洗,NetworkX 在内存里建图,最后用 pyecharts 输出一个 HTML 交互页面。整条链路都用 Python,安装依赖只需要一条命令:
bash复制pip install requests beautifulsoup4 pandas networkx pyecharts
这套组合的好处是上手门槛低。如果你只对可视化感兴趣,可以直接跳过爬虫部分,拿公开数据集构建 NetworkX 图,再用 pyecharts 渲染;如果你只想练爬虫,也可以只做到 CSV 清洗那一步。每个环节都能独立验证,不用一条路走到黑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据采集:从零构建获奖者数据集
2.1 数据源和字段设计
做任何数据项目,第一步不是写爬虫,而是想清楚要哪些字段。我的目标很简单:拿到“谁在什么时候、凭借什么作品、在哪个奖项上获奖”,再拿到“这个获奖者和哪些其他获奖者合作过”。
我建议的字段表如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| person_id | 字符串 | 唯一ID,通常用“姓名+生年”避免重名 |
| name | 字符串 | 获奖者姓名 |
| birth_year | 整数 | 出生年份,用于消歧 |
| award_year | 整数 | 获奖年份,即第几届 |
| category | 字符串 | 奖项类别,如“最佳男主角” |
| film | 字符串 | 获奖作品名称 |
| role_type | 字符串 | 演员/导演/制片人/编剧等 |
数据源选择上,我优先考虑“公开可访问的奖项资料站”,而不是某一家商业平台的页面,理由有三个:结构相对稳定、历史数据完整、没有强登录限制。实际操作时,我会先去奖项官网的历年获奖列表,再对照影视数据平台补全每部作品的主要演职员信息。
2.2 爬虫实现的三个关键步骤
这是整个项目里最容易翻车的地方,但也最值得分享。我整理成三步:
第一步,定位奖项列表页。通常这种页面会有一个表格,每一行代表一年的某个奖项,包含年份、获奖者、作品名称。先用浏览器开发者工具看 DOM 结构,确认表格的 class 或 id,不要瞎猜。
第二步,解析表格数据。BeautifulSoup 的选择器写起来很直观:
python复制import requests
from bs4 import BeautifulSoup
def fetch_winners(url):
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
resp.encoding = "utf-8"
soup = BeautifulSoup(resp.text, "html.parser")
rows = soup.select("table.winners tbody tr")
data = []
for row in rows:
cols = row.find_all("td")
if len(cols) < 4:
continue
data.append({
"year": cols[0].text.strip(),
"category": cols[1].text.strip(),
"name": cols[2].text.strip(),
"film": cols[3].text.strip(),
})
return data
实际页面里列的顺序可能不同,所以我会先用一个简单循环把所有表头打出来,确认列的索引再开始批量采集,这个习惯帮我省掉了很多脏数据清理工作。
第三步,控制采集节奏。在循环里加 time.sleep(1),一方面降低对目标站点的压力,另一方面也可以防止被封 IP。尤其是需要逐部电影采集演职员表时,请求量会成倍增加,不加延时的话很可能爬到一半就断了。
2.3 数据清洗:重名、字段错位和缺失值
数据抓下来之后,真正痛苦的环节才开始。我踩过最典型的几个坑:
一个是重名问题。影视圈有两个“Michael Smith”都很正常,如果只拿姓名做 ID,两个不同的人会被合并成同一个节点。我的解法是给 ID 加上出生年份,做成 name_1960 这种格式,但前提是数据源里能拿到出生年。拿不到时,就退而求其次,用“姓名 + 获奖作品”作为临时 ID,至少不会把同一届的两个人直接合并。
另一个是字段错位。有些页面会把导演和制片人放在同一列,或者干脆多一行赞助商信息,导致 len(cols) 不等于表头长度。我在清洗时统一用 pandas 做了严格的类型校验:
python复制import pandas as pd
df = pd.read_csv("oscar_winners.csv")
df["year"] = pd.to_numeric(df["year"], errors="coerce")
df["name"] = df["name"].astype(str).str.strip()
df = df.dropna(subset=["year", "name"])
df = df.drop_duplicates(subset=["person_id", "year", "category"])
还有编码问题。很多中文影视资料站的页面用的是 GBK 编码,如果用默认的 UTF-8 去解码,中文名会变成乱码。此时可以在 requests.get 之后手动检查 resp.apparent_encoding,再强制设置成正确的编码,或者在读取 CSV 时统一用 encoding="utf-8-sig",避免 BOM 头捣乱。
3. 关系建模与图数据准备
3.1 关系类型设计:不要只定义一种“合作”
拿到获奖者名单和作品列表后,最核心的问题来了:两个人之间到底算什么“有联系”?我尝试了三种关系定义,分别对应不同的网络语义。
第一种是“作品内合作”。只要两个获奖者共同出现在同一部影片的演职员名单里,就建立一条边。这是最主流、也最直观的定义,网络结构会很清晰。
第二种是“同届获奖”。同一届颁奖礼上获奖的人之间建立一条弱边。这种边不表示他们认识,但表示他们处在同一个历史时刻,适合做一些年代聚类分析。
第三种是“同系列作品”。比如两位获奖者分别出演过同一个系列电影的不同部集,虽然没有直接合作,但在作品谱系上是近邻。这种关系权重可以设低一些。
我在正式版本里主要用的是第一种,第二种作为辅助,第三种只在做细分洞察时才会启用。原因很简单:关系定义越严,边越少,图越干净;定义太宽,所有节点都会糊成一团,看不出任何结构。
3.2 边权重怎么算才合理
关于权重,我试过两个版本,效果差别很大。
第一个版本用的是“合作次数”,也就是两个人共同参演过几部电影。优点是好解释,缺点是容易被高产型演员刷高,边缘人物反而会被淹没。第二个版本加了一个“稀有度惩罚”,把权重视为:
text复制weight_ij = 合作次数 / (单独作品数_i + 单独作品数_j - 合作次数)
这个思路其实有点类似余弦相似度:如果 A 和 B 各自的作品都很少,但他们就是合作过,说明他们的关系非常紧密,权重应该被放大;如果 A 和 B 都是拍了五十部电影的劳模,合作三次在他们各自的履历里占比很小,权重要适当压低。实际跑下来,第二种权重得到的布局明显更合理,社区结构更清楚。
当然,如果你不想引入太复杂的计算,直接用合作次数也完全够用。先跑通整条链路,再优化权重,是更务实的路径。
3.3 用 NetworkX 构建关系图
NetworkX 是 Python 生态里最常用的图分析库,用起来很顺手。我大致是这么做的:
python复制import networkx as nx
G = nx.Graph()
for _, row in df.iterrows():
G.add_node(
row["person_id"],
name=row["name"],
award_year=row["year"],
category=row["category"],
film=row["film"]
)
for co_person_id, weight in cooccurrence_dict.items():
G.add_edge(row["person_id"], co_person_id, weight=weight)
建完图之后可以做几个快速检查:节点数、边数、连通分量数量、每个节点的度。我的数据清洗后大概有 900 多个节点和 2000 多条边,整体连通性还不错,最大的连通分量覆盖了绝大多数人,只有少数早期获奖者因为作品记录缺失成了孤立点。
这一步非常有价值。如果发现孤立点太多,说明作品关系数据没补全;如果某个节点的度高得离谱,多半是重名合并出了 bug。在进入可视化之前先检查这两个问题,能省下后面的大量调试时间。
4. 可视化呈现与交互设计
4.1 为什么选力导向图
关系网络的可视化方案有很多:矩阵热力图、桑基图、弧线图、力导向图。我最后选用的是力导向图(Force-directed Graph),原因是它最符合直觉:节点是获奖者,边是合作关系,相似的群体天然会聚集在一起,中心性和桥梁人物一眼就能看出来。
力导向图的代价是布局计算量大,但在几百个节点这个规模下完全没有性能压力。pyecharts 底层的渲染做了很多优化,网页缩放、拖拽、点击高亮都是现成的,不需要自己写交互逻辑。
4.2 用 pyecharts 出图的完整套路
pyecharts 的 Graph 组件上手难度很低,核心是准备 nodes 和 links 两个列表:
python复制from pyecharts import options as opts
from pyecharts.charts import Graph
nodes = [
{
"id": person_id,
"name": name,
"symbolSize": node_size,
"value": award_count,
"category": category
}
for person_id, name, node_size, award_count, category in node_data
]
links = [
{"source": src, "target": dst, "value": weight}
for src, dst, weight in edge_data
]
graph = (
Graph(init_opts=opts.InitOpts(width="1200px", height="800px"))
.add(
"",
nodes,
links,
repulsion=3000,
edge_length=[80, 200],
layout="force",
linestyle_opts=opts.LineStyleOpts(curve=0.2, opacity=0.6),
)
.set_global_opts(
title_opts=opts.TitleOpts(title="奥斯卡获奖者合作关系网络"),
legend_opts=opts.LegendOpts(orient="vertical", pos_right="2%"),
)
)
graph.render("oscar_network.html")
几个参数我调了很久。repulsion 是节点之间的斥力系数,数值越打布局越松散;edge_length 是最短边距和最长边距的区间。节点大小我用的是获奖次数的开根号,而不是获奖次数本身,这样不会让少数大节点把画面撑爆。边的粗细直接用权重映射,合作越频繁越粗。
4.3 进阶玩法:过滤、缩放和时间轴
基础图出来之后,真正让它“可用”的其实是两类增强功能。
第一类是过滤。完整网络图的节点太多,信息过载,反而不适合阅读。我会在数据层面先做剪枝:只保留获奖次数不少于 2 的人,或者只保留边权不低于 2 的关系。pyecharts 虽然不能直接在前端做复杂筛选,但你可以把不同的剪枝结果生成不同文件,或者用 select 组件切换数据集。
第二类是时间轴。把年份分成“1929-1959”、“1960-1989”、“1990-2023”三个阶段,分别渲染三个子图,然后用 Timeline 组件把它们串起来。这样做能看到很有意思的演变:早期的获奖者圈子很小、节点稀疏;中期的网络开始涌现明显的社区;近二十年的图则呈现高度连通又高度分化的复杂结构。时间轴不是为了炫技,而是让“联系”这个静态概念拥有历史纵深。
5. 常见问题与排查技巧实录
5.1 爬虫突然失效:页面结构变了怎么办
这类公开页面的 HTML 结构说改就改,今天能跑通的 soup.select,下周可能就返回空列表。一个很实用的应对技巧是:不要只写一个选择器,而是写一份“selector 候选列表”,比如优先尝试 table.winners,失败后再尝试 table.awards,同时打印日志告诉你用了哪个。这样即使一个选择器失效,程序也能自动切换。
更稳妥的做法是定期抓一份页面快照存到本地,用快照做开发调试,用真实页面做定时更新。如果快照解析正常但真实页面解析失败,几乎可以肯定对方改版了。
5.2 图太乱:连接线糊成一团怎么办
这是所有关系图项目绕不开的问题。连接线一多,视觉上就是一团蜘蛛网。我的处理策略按优先级排序:
- 边权剪枝,去掉合作次数只有 1 的弱连接;
- 节点剪枝,去掉孤立节点和度小于 2 的节点;
- 用社区发现算法给节点分组染色,让同一社区的节点颜色接近;
- 调整布局参数,适当增加斥力,减少边重叠。
如果你用 pyecharts 后发现布局还是不满意,可以把数据导入 Gephi 用它的布局算法算好坐标,再导出坐标文件回填到 Web 页面里。这个方法多一步工序,但布局质量确实高不少,尤其适合做最终汇报图。
5.3 图打开很卡:怎么优化前端性能
几百个节点的图一般不会卡,但如果你把边也全部渲染,有可能超过几千条,此时浏览器会明显掉帧。我用的方法有三个:一是优先渲染边权排名前 20% 的边;二是把节点大小和颜色统一简化,减少重绘开销;三是把 pyecharts 生成的 HTML 从开发模式改为发行模式,去掉不必要的调试信息。实测下来,从 8 秒加载降到 2 秒以内,内存占用也低了不少。
5.4 一些容易被忽略的细节
- 检查姓名里的空格。中英文混排时,全角空格和半角空格都会导致同一个名字被当成两个人。
- 检查作品名称规范化。“泰坦尼克号”和“Titanic”如果不做映射,同一个人名会被错误地连到两个作品实体上。
- 每次生成图之后,我都会随机抽 20 个节点,人工核对它们的一阶邻居是否符合常识。这个动作虽然麻烦,但能有效避免“因为一条脏数据导致整个网络结构偏移”的问题。
写在最后的个人体会
我自己的感受是:这个项目真正难的不是写爬虫,也不是画图,而是想清楚“什么才叫联系”。不同的关系定义会生成完全不同的网络结构,也会得出完全不同的结论。你定义得越窄,画面越干净,但可能漏掉重要连接;定义得越宽,信息越全,但干扰也越多。好的关系图不是把所有数据堆上去,而是让人一眼看懂一个道理,比如“原来这批人都是靠同一个制片人连接起来的”。如果你也想做类似的项目,建议先从最小数据量跑通全流程,再逐步补全数据。这个项目后续还可以扩展的方向很多:加入提名未获奖者建模“遗珠网络”,引入票房数据给边加权,或者做随时间变化的动态图。先把基础版做好,后面每一层扩展都会非常顺手。
