2026/10/12 9:19:05

IMDB完整数据集导入SQLite实战:下载、建表与查询避坑指南

IMDB完整数据集导入SQLite实战:下载、建表与查询避坑指南 简介IMDb互联网电影资料库是全球影视领域使用最广泛的公开数据源之一这份资源将原始数据整理为可直接使用的完整数据集适合自然语言处理方向的初学者、研究人员及高校师生用于情感分析、文本分类、词嵌入等经典任务免去自行爬取、清洗与格式转换的繁琐流程。压缩包共包含2个文件整体大小仅17.26MB其中imdb.npz以numpy数组格式存储了预处理后的影评文本特征与标签数据加载后即可喂入深度学习框架进行模型训练无需额外编写解析脚本imdb_word_index.json则提供单词与索引的映射字典便于完成文本序列化、词向量查询与语料统计等预处理环节。资源结构简洁清晰文件格式通用兼顾学术研究与工程实践需求目前已有4714人浏览学习适合需要快速获取标准IMDb数据集开展实验的中高级开发者使用。1. IMDB完整数据集是什么影视分析里那个“原厂快照”做影视项目的分析和推荐系统最绕不开的问题是数据从哪里来。手动写爬虫去抓影视站要么被反爬限制要么字段不全补齐演职员关系的时候更难受。所谓 IMDB完整数据集指的是官方按固定格式导出的 TSV 快照文件电影、剧集、演员、评分、别名、剧集关系全拆成若干张表定期更新、可直接下载。我第一次拿它跑演员共演网络从下载到查询只花了一个下午。下面要讲的就是这条路径怎么下载、怎么建表、哪些地方必须注意照着做基本不会跑偏。适合想用干净原始数据做分析、又不想自己维护爬虫的从业者。2. IMDB完整数据集的文件构成与字段模型先弄明白七张TSV表2.1 七个官方TSV快照分别存了什么东西官方快照一共七个压缩包全部是制表符分隔的 TSV 文件压缩格式为 gzip。下载之前先把文件构成摸清楚比直接上手跑脚本重要得多。文件名记录粒度关键字段title.basics.tsv.gz每行一个片目电影、剧集、单集都算tconst、titleType、primaryTitle、originalTitle、isAdult、startYear、endYear、runtimeMinutes、genrestitle.ratings.tsv.gz每行一个片目的评分聚合tconst、averageRating、numVotestitle.akas.tsv.gz每行一个片目的一条别名titleId、ordering、title、region、language、types、attributes、isOriginalTitletitle.crew.tsv.gz每行一个片目的导演/编剧ID列表tconst、directors、writerstitle.principals.tsv.gz每行一个片目中的一个演职人员tconst、ordering、nconst、category、job、characterstitle.episode.tsv.gz每行一集记录它的父级剧集tconst、parentTconst、seasonNumber、episodeNumbername.basics.tsv.gz每行一个人名nconst、primaryName、birthYear、deathYear、primaryProfession、knownForTitles理解这套数据关键在“片目”和“人”是两个ID体系。tconst 代表一部片或一集nconst 代表一个真实的人。title.basics 只存片目自己的属性导演、编剧、演员关系都放在 title.principals 和 title.crew 里靠ID关联回去。title.episode 的存在尤其重要它让单集也拥有 tconst并通过 parentTconst 找到所属剧集。如果你想统计“剧集有多少季”得从 episodes 表聚合而不是在 basics 里数 titleTypetvSeries 的行数。还有一个常见误区title.basics 里的 titleType 不只是 movie 和 tvSeries还包括 tvMovie、tvShort、video、videoGame、short、tvEpisode 等。所以“完整数据集”不等于“全是电影”。统计电影数量时一定要先用 titleType 做一次分布检查否则你得到的数字会完全对不上预期。title.akas 里一条别名对应 titleId 加 ordering 才是唯一键这会在后面避坑部分展开讲。2.2 从字段设计看关联成本为什么官方TSV适合做关联分析这套数据的字段设计是典型的“窄表”思路每个文件只存一种关系不做冗余宽表。比如 title.principals 里面没有演员姓名只有 nconst你想知道演员名字就得 join name.basics。这样设计的好处是单张表体积可控、更新时互相干扰小代价是几乎所有分析都离不开 join。我自己的体会是先花十分钟理解这七张表之间的关系后面写查询能少走很多弯路。用图来比喻tconst 和 nconst 是节点principals 是边表akas 是别名ratings 是属性。另一个细节是 title.crew 的 directors 和 writers 字段它们是逗号分隔的多个 nconst 字符串不是一行一个。如果你习惯关系型数据库范式会觉得这个设计很别扭。常见做法是导入后单独拆成子表一行一个导演ID。name.basics 的 knownForTitles 也是同样结构逗号分隔多个 tconst。这两个字段在初次查询时不用管做人物作品年表时才需要拆。需要提前说明的是“完整”不代表“你想查什么都有”。这份数据里有评分、票数、片目类型、年份、演职员、别名但不会有票房、预算、海报图、发行商信息、设备列表。如果你做票房回测或宣发分析得另外接商业数据源。IMDB完整数据集更适合的是推荐系统、影视知识图谱、演职员关系网络、评分分布研究这些方向。另外官方文件里用\N表示空值导入阶段直接用 TEXT 类型最省事别一开始就声明 INTEGER。这样 join 之前统一清洗避免\N混进数字列。具体洗法见下一章。3. 把IMDB完整数据集导入SQLite下载、建表、空值清洗一次走完3.1 用脚本批量下载并做压缩完整性检查把七个文件拉下来官方URL的文件名是固定的脚本本身没有难度。我一般会加三个保险断点续传、重试、解压自检。#!/bin/bash BASE_URLhttps://datasets.imdbws.com FILES(title.basics title.ratings title.crew title.principals title.episode name.basics title.akas) mkdir -p imdb_raw cd imdb_raw for f in ${FILES[]}; do if [ -f $f.tsv.gz ]; then echo $f 已存在跳过下载 else curl -L -o $f.tsv.gz $BASE_URL/$f.tsv.gz \ --retry 3 --connect-timeout 30 -C - fi gzip -t $f.tsv.gz 2/dev/null echo $f 完整 || echo $f 损坏请删掉重新下载 done脚本逻辑分成两步先判断文件是否已存在避免重复下载再对已经存在的文件做 gzip 完整性校验。curl -C -表示断点续传网络中断后重跑脚本会从断开位置继续。gzip -t只做解压测试不改动文件如果某次下载被截断这一步会直接报错。七个文件都通过校验后再进入下一步不要跳过这个检查否则后面导入造出来的脏数据很难定位。注意 title.principals.tsv.gz 解压后通常比其余文件大一个数量级这是影视数据集的常态因为每个主要演员和配角都占一行。如果磁盘紧张可以先只下载 title.basics、name.basics、title.principals 跑通流程其余表晚点再补。3.2 按“先宽后严”建表导入阶段全部用TEXT为什么建表时一律 TEXT官方空值写成\N而\N不是数字如果字段类型声明成 INTEGER导入时可能出现 0、空字符串、存储类型混乱的问题。先按原文导入清洗阶段再转类型最不折腾。PRAGMA journal_modeOFF; PRAGMA synchronousOFF; PRAGMA cache_size-20000; PRAGMA temp_storeMEMORY; CREATE TABLE title_basics_raw ( tconst TEXT, titleType TEXT, primaryTitle TEXT, originalTitle TEXT, isAdult TEXT, startYear TEXT, endYear TEXT, runtimeMinutes TEXT, genres TEXT ); CREATE TABLE title_ratings_raw ( tconst TEXT, averageRating TEXT, numVotes TEXT ); CREATE TABLE title_akas_raw ( titleId TEXT, ordering TEXT, title TEXT, region TEXT, language TEXT, types TEXT, attributes TEXT, isOriginalTitle TEXT ); CREATE TABLE title_crew_raw ( tconst TEXT, directors TEXT, writers TEXT ); CREATE TABLE title_principals_raw ( tconst TEXT, ordering TEXT, nconst TEXT, category TEXT, job TEXT, characters TEXT ); CREATE TABLE title_episode_raw ( tconst TEXT, parentTconst TEXT, seasonNumber TEXT, episodeNumber TEXT ); CREATE TABLE name_basics_raw ( nconst TEXT, primaryName TEXT, birthYear TEXT, deathYear TEXT, primaryProfession TEXT, knownForTitles TEXT );前四条 PRAGMA 在导入阶段作用很大。journal_modeOFF关掉事务日志导入速度能快一大截synchronousOFF让 SQLite 不再主动把数据刷到物理磁盘正常情况下我不会给长期使用的业务库开它但一次性导入几个 GB 的历史数据时很值得cache_size-20000表示预留 20MB 页缓存内存充足可以改成-40000。表名带_raw后缀是让这张表只做缓冲清洗后我会生成正式表。保留 raw 表还有个额外好处哪次清洗逻辑写错了可以回滚重来不用重新解压原始文件。3.3 用import命令批量灌数据注意去表头# 去掉第一行表头再交给 sqlite3 for f in title.basics title.ratings title.crew title.episode name.basics title.akas; do gzip -dc $f.tsv.gz | tail -n 2 $f.tsv sqlite3 ../imdb.db .mode tabs .import $f.tsv ${f//./_}_raw done # 大文件单独处理便于观察进度 gzip -dc title.principals.tsv.gz | tail -n 2 title.principals.tsv sqlite3 ../imdb.db .mode tabs .import title.principals.tsv title_principals_raw.mode tabs是这次导入最容易出问题的地方必须用 tabs 而不要用 csv。TSV 文件里没有引号转义机制标题原文就是原样文本用 csv 模式会把含有逗号的标题解析碎。tail -n 2把第一行的字段名去掉因为.import默认把每一行都当数据如果不去掉表里会混入一条tconst\tprimaryTitle...的假数据后面统计时经常莫名多出一行。每张表导入完成后建议立刻执行SELECT COUNT(*) FROM 表名_raw;看行数是否合理如果数量级不对就提前排查别等七张表全导完再说。大文件单独处理的原因是它耗时最长期间你可以做别的事。导入过程中终端长时间没有输出属于正常现象不要误以为是死循环提前杀掉。3.4 导入后的空值清洗与类型转换-- 把 \N 替换成真正的 NULL只处理可能为空的字段 UPDATE title_basics_raw SET startYear CASE WHEN startYear \N THEN NULL ELSE startYear END, endYear CASE WHEN endYear \N THEN NULL ELSE endYear END, runtimeMinutes CASE WHEN runtimeMinutes \N THEN NULL ELSE runtimeMinutes END, genres CASE WHEN genres \N THEN NULL ELSE genres END; UPDATE name_basics_raw SET birthYear CASE WHEN birthYear \N THEN NULL ELSE birthYear END, deathYear CASE WHEN deathYear \N THEN NULL ELSE deathYear END; UPDATE title_ratings_raw SET averageRating CASE WHEN averageRating \N THEN NULL ELSE averageRating END, numVotes CASE WHEN numVotes \N THEN NULL ELSE numVotes END;清洗后建正式表用 CAST 转类型顺便把 isAdult 转成整数CREATE TABLE title_basics AS SELECT tconst, titleType, primaryTitle, originalTitle, CASE WHEN isAdult 1 THEN 1 ELSE 0 END AS isAdult, CAST(startYear AS INTEGER) AS startYear, CAST(endYear AS INTEGER) AS endYear, CAST(runtimeMinutes AS INTEGER) AS runtimeMinutes, genres FROM title_basics_raw; CREATE TABLE title_ratings AS SELECT tconst, CAST(averageRating AS REAL) AS averageRating, CAST(numVotes AS INTEGER) AS numVotes FROM title_ratings_raw;正式表建好后再加索引不要提前加在 raw 表上CREATE INDEX idx_ratings_votes ON title_ratings(numVotes DESC); CREATE INDEX idx_basics_type_year ON title_basics(titleType, startYear); CREATE INDEX idx_principals_nconst ON title_principals_raw(nconst); CREATE INDEX idx_principals_tconst ON title_principals_raw(tconst);索引不要在导入期间建批量写入时索引会造成明显的性能下降。我通常的节奏是raw 表全部导入成功 → 清洗 → 建正式表 → 建索引 → 删掉 raw 表。title_principals_raw 的 nconst 和 tconst 索引务必建因为后面所有演员相关查询都会走它没有索引时一次 join 就是全表扫描SQLite 在千万行表上会非常吃力。4. IMDB完整数据集避坑指南5个最常遇到的翻车现场4.1 现象年份查出来一堆 0条件过滤失效对刚导入的 title_basics 执行WHERE startYear2020结果只有几千条换成BETWEEN 2010 AND 2020更是明显偏少。再查SELECT startYear, COUNT(*) ... GROUP BY startYear发现多了一个 0 值行数还不少。原因就是\N。官方把未知年份写成反斜杠加大写 N如果你建表时把 startYear 声明成 INTEGER导入过程会把\N转成 0或者存成文本。无论哪种都能让你的查询逻辑瞬间失效。解决回到第3章的做法导入阶段一律 TEXT清洗时用CASE WHEN startYear\N THEN NULL ELSE CAST(startYear AS INTEGER) END。如果已经导成 0先执行UPDATE title_basics SET startYearNULL WHERE startYear0再对整列做类型转换。我第一次遇到时排查了很久最后才发现是空值表示方式的问题。现在我把这个检查直接写进建表模板每次导入前先看一遍。4.2 现象把 tconst 当主键导入直接报 UNIQUE constraint failed我起步时很自然地以为“主键就是 tconst”给 title_principals 表加了tconst PRIMARY KEY导入到一半就报 UNIQUE 约束失败。原因title_principals 是按演职人员行的同一部电影里有十个演员就有十行tconst 必然重复。它的唯一逻辑是(tconst, ordering)其中 ordering 是官方的人员排列顺序号从 1 开始。title.akas 也不是 titleId 唯一而是(titleId, ordering)。title.episode 的 tconst 是单集 IDparentTconst 是剧集 IDtconst 做主键安全但 parentTconst 上要单独建索引。解决建 raw 表时不声明任何主键全部导入完后再对需要判断唯一性的记录建CREATE UNIQUE INDEX idx_tp ON title_principals_raw(tconst, ordering);。做分析时单行主键其实不是必须的为了节约导入时间甚至可以跳过唯一索引只建普通查询索引。4.3 现象用CSV模式导入同一份文件电影标题被拆碎导入完 title_basics查WHERE titleTypemovie发现 primaryTitle 被拆成两段例如带逗号的标题分成了两个字段整表行数也多出一截。有些标题本身带引号也被剥掉或移位。原因很直白.mode csv按逗号与引号规则解析但官方给的是 TSV字段内容没有按 CSV 规则做转义。影视标题天然包含大量逗号、括号、引号csv 模式必碎。解决只使用.mode tabs。这行代码看起来简单但每次都能拦下一批翻车。导入后如果想自检可以跑SELECT COUNT(*) FROM title_basics_raw WHERE titleType NOT IN (movie,short,tvSeries,tvEpisode,tvMovie,video,videoGame,tvShort,tvSpecial)看看有没有被拆出来的错乱行。如果发现字段数量不是预期的 9 列基本可以认定分隔模式选错了。4.4 现象is_adult 过滤把短片、剧集全误伤统计数量比预期少我想统计“全部电影”写了WHERE isAdult0 AND titleTypemovie但结果数量始终不对后来才发现 titleType 里还有 short、videoGame、tvEpisode 这些条目。另一个朋友更直接他把 isAdult 当成了“是否面向成年人”想在分析中保留正常成人向内容顺手排除了“成人内容”结果把一批相对小众但完全正常的短片和剧集一起删掉了。原因官方字段定义里 isAdult 只表示是否涉及成人内容不是年龄段分级titleType 才是区分电影、剧集、短片、游戏的关键。统计电影时必须同时考虑 titleType而且“电影”在官方定义里分成 movie、tvMovie、short、tvShort 这几类只写 movie 会漏掉电视电影和短片。解决先跑一条分布查询把 titleType 的家底摸清楚SELECT titleType, COUNT(*) AS cnt FROM title_basics_raw GROUP BY titleType ORDER BY cnt DESC;看到每种类型的占比后再决定过滤条件。长片通常用titleType IN (movie,tvMovie)“所有影视条目”用titleType NOT IN (videoGame,short,tvShort)。在一个含义不清楚的字段上直接叠加排他条件很容易把数据过滤没了。先看分布再过滤这是做数据分析的基本习惯。4.5 现象principals 导入一小时还没完其余表早就结束了在导入七个文件的循环里其他表几分钟就完成了卡在 title.principals 上半天没动静。终端没有任何输出我一度以为脚本死循环最后发现是在正常写入。原因principals 是最大的一张表行数比其他表多一到两个数量级每条记录还包含演员、职业、角色、排序等多个字段。如果导入时提前建了索引或者没有关掉 journal慢上加慢。解决导入前执行第3章的四个 PRAGMA导入时不建索引把大文件拆成多个小块顺序导入还能看到阶段性进度gzip -dc title.principals.tsv.gz | tail -n 2 | split -l 1000000 - split_principals_ for p in split_principals_*; do echo 导入 $p sqlite3 imdb.db .mode tabs .import $p title_principals_raw done rm -f split_principals_*这个拆分思路对大文件很实用每导入 100 万行就能看到进度中途出问题也知道断点位置。拆分后每个小块都没有表头追加到同一张 raw 表不会重复导入头部。如果你追求极致速度可以在一个连接里用 BEGIN/COMMIT 包住全部拆分文件但中途崩溃时回滚更麻烦我一般不会为了省那几分钟去冒回滚的风险。5. 三个值得跑的IMDB完整数据集分析查询冷门高分、演员周期、共演关系5.1 冷门高分片推荐榜评分高且投票数不多不少数据导入以后我最常跑的第一类查询是“冷门高分”式榜单。IMDb 的评分样本集中在流行片上纯按 averageRating 排序会得到一堆只有个位数票数、评分接近满分的异常条目。要找到“有一定人看过但不算热门”的电影需要在投票数上开一个窗口。SELECT tb.primaryTitle, tb.startYear, tr.averageRating, tr.numVotes FROM title_basics tb JOIN title_ratings tr ON tb.tconst tr.tconst WHERE tb.titleType movie AND tb.isAdult 0 AND tr.numVotes BETWEEN 1000 AND 30000 AND tr.averageRating 8.0 ORDER BY tr.averageRating DESC, tr.numVotes DESC LIMIT 50;numVotes 的下限 1000 是为了排除评分样本太少导致的高分噪音上限 30000 是把大众热片挡在门外这两个值可以根据目标调整。如果做的是给推荐池召回用上限放到 10 万也没问题如果做小众榜单上限 3000 更有味道。条件里的 isAdult0 先把成人向内容排除避免冷门榜里混进奇怪结果。5.2 演员活跃周期统计join两张表算出“黄金年份”想知道一位演员作品最多的年份区间按 startYear 聚合即可。由于官方对剧集取的是首播年这一查询算的是“作品首播年份”不是拍摄年份出结果时记得标注口径。SELECT nb.primaryName, tn.startYear, COUNT(DISTINCT tn.tconst) AS title_cnt FROM name_basics nb JOIN title_principals tp ON nb.nconst tp.nconst JOIN title_basics tn ON tp.tconst tn.tconst WHERE tp.category IN (actor, actress) AND tn.titleType movie AND tn.isAdult 0 GROUP BY nb.nconst, nb.primaryName, tn.startYear HAVING COUNT(DISTINCT tn.tconst) 5 ORDER BY title_cnt DESC LIMIT 30;这条查询的重点在tp.category IN (actor,actress)因为 title.principals 里除了演员还有导演、编剧、摄影、剪接等各种职务。不过滤的话统计出来的“作品数”会把演职员表里几十个角色全算进去结果非常水。另外COUNT(DISTINCT tn.tconst)比COUNT(*)稳妥同一部片可能在 principals 里因多行重复出现虽然标准源按 tconstordering 不重复但多一层去重总归保险。5.3 共演关系建图给“六度关系”准备边表演员关系网络的原始材料就是 title_principals 自身两个 nconst 出现在同一个 tconst 上就是一条共演边。CREATE TABLE co_star_edges AS SELECT pa.nconst AS actor_a, pb.nconst AS actor_b, pa.tconst AS title, tb.primaryTitle AS title_name, tb.startYear FROM title_principals pa JOIN title_principals pb ON pa.tconst pb.tconst AND pa.nconst pb.nconst JOIN title_basics tb ON pa.tconst tb.tconst WHERE pa.category IN (actor, actress) AND pb.category IN (actor, actress) AND tb.titleType movie AND tb.isAdult 0;pa.nconst pb.nconst用来保证 A-B 和 B-A 不重复因为 nconst 是统一前缀加数字的编码字符串比较可以稳定分出先后。这张图的规模取决于你保留的 titleType只算电影边表相对可控连剧集一起算一部电视剧几十个演员会连出很大的边数查询和存储都会吃紧。建完后跑SELECT COUNT(*) FROM co_star_edges;看规模再决定要不要限制只在 principals 的 ordering 5 范围内建边。这个表建好后可以继续做连通分量、最短路径、中心度计算往图数据库方向走也很顺。6. 数据每天在变用 MD5 校验识别快照更新避免全量重导IMDb 的官方快照几乎天天更新。如果每次做分析前都把七个文件重新下载、解压、清洗一遍绝大部分时间都在重复劳动。我现在的习惯是把“全量重导”从每天行动改成每周或每月行动日常只盯着变化最频繁的 title.ratings用 MD5 判断文件是否真的变了。#!/bin/bash BASE_FILEhttps://datasets.imdbws.com/title.ratings.tsv.gz LOCAL_FILEtitle.ratings.tsv.gz curl -L -s -o $LOCAL_FILE $BASE_FILE NEW_MD5$(md5sum $LOCAL_FILE | awk {print $1}) OLD_MD5$(cat .md5_title_ratings 2/dev/null || echo ) if [ $NEW_MD5 ! $OLD_MD5 ]; then echo ratings 有更新导入 gzip -dc $LOCAL_FILE | tail -n 2 title.ratings.tsv sqlite3 ../imdb.db .mode tabs .import title.ratings.tsv title_ratings_raw # 导入后记得补第3.4节的空值清洗和类型转换 echo $NEW_MD5 .md5_title_ratings else echo ratings 没有变化 fi脚本先下载到临时文件计算 MD5 后和上次记录比对。文件没变就直接跳过导入变了才进库避免每天重复跑同一份数据的写入开销。title.ratings 是更新最勤快的表而 title.basics、name.basics 这类大表的变化频率低很多用同样的思路对它们做“每周一次全量”即可。做评分趋势、每日推荐榜类应用时只更新 ratings 表就够用了。如果是刚接触这套数据我建议不要一上来就追求“每天自动跟进”。先把全量导入做一遍把第4章的坑都过一遍再用上面的脚本做增量。我最初为了图省事每周都把七个文件无脑重导一遍后来才发现大多数文件根本没变化白白烧了很多等待时间。MD5 判断是成本最低的取舍它不是严格版本对比但对快照型数据已经足够可靠。希望这些细节能帮到你少走我当年走过的弯路。本文还有配套的精品资源点击获取