资讯详情 豆瓣电影数据集清洗与推荐系统实战:从CSV到SQLite全流程
📅 2026/10/17 21:56:57
简介这份豆瓣电影数据集以SQLite数据库与CSV表格形式沉淀约10万条电影相关记录面向数据分析初学者、推荐系统开发者及影视市场研究人员可用于用户观影偏好挖掘、电影属性分析与影评情感洞察。包内共4个文件两个csv分别承载用户评分历史、昵称注册信息与电影片名、导演、类型、上映年份等基础资料db文件收录观影记录及影评互动内容py脚本则为豆瓣数据爬虫源码可作为requests与BeautifulSoup库的实战示例。借助用户行为与电影属性可实践推荐算法与观影偏好分析影评互动内容则支持情感挖掘与社交网络研究。数据覆盖从采集、清洗到结构化存储的完整链路便于学习将网页信息整理为可分析表格与数据库的方法。整个压缩包约40.02MB已有3442人下载学习适合期望以真实数据提升数据处理与爬虫实战能力的读者。1. 为什么要折腾一份豆瓣电影数据集从冷启动到推荐系统的练手样本拿到《豆瓣电影数据集10万.zip》这种资源第一反应通常是“又是一份压缩包”但真正打开后你会发现这 10 万条数据其实就是一份结构化的电影行为记录字段覆盖电影基本信息、评分、评论人数、类型标签和上映时间维度基本能支撑从 Pandas 清洗到推荐系统召回评估的完整链路。我当时是在做一个模拟项目X的推荐系统冷启动环节需要一份包含用户评分和电影元数据的样本集找了一圈公开数据要么是英文的 MovieLens要么是爬虫抓来的半结构化 JSON处理成本反而比这份 zip 更高。豆瓣数据集的优点在于字段语义清晰中文标签完整评分分布贴近真实用户行为适合用来验证协同过滤算法在稀疏矩阵下的表现。整份 zip 适合几类人想练特征工程的初学者、需要做离线评估的推荐系统开发者以及做电影数据分析报告的学生。它不能直接当生产数据用但作为研究样本价值足够实。2. 解压之后的数据结构先弄清这 10 万条到底存了什么打开 zip 之前我先说明一个经验电影类数据集最常见的翻车点不是文件损坏而是你假设的字段顺序和实际完全对不上。解压后我的做法是先别急着写分析代码而是用命令行工具快速扫一遍文件格式和编码再用脚本打印前几行确认结构。这个项目的压缩包内部包含一个核心 CSV 文件每一行对应一部电影字段基本覆盖电影名称、导演、编剧、主演、类型、制片国家/地区、语言、上映日期、片长、又名、IMDb 链接、豆瓣评分、评价人数、标签top250 标记、剧情简介和短评片段。不同来源的版本字段排列会略有差异所以第一步一定是确认列名而不是直接按记忆中的索引去读。2.1 文件格式与编码用 file 命令和 Python 双重确认拿到 zip 后我习惯先做一次“体检”用unzip -l查看压缩包内文件列表确认是否只有一个 CSV 还是有多个分片文件然后解压到独立目录避免和项目代码混在一起。接着用file命令检测编码因为 CSV 的编码决定了后面 Pandas 读取时的参数选择这一步翻车最常见很多人直接默认 UTF-8结果读出来中文全是乱码其实文件是 GBK 或 GB18030 编码。下面是我常用的检查流程unzip -l 豆瓣电影数据集10万.zip mkdir -p douban_data unzip 豆瓣电影数据集10万.zip -d douban_data/ file douban_data/*.csv逻辑说明unzip -l先列出压缩包内部结构不实际解压用于确认是否包含多个文件mkdir -p创建独立目录防止文件散落file命令输出文件的字符编码检测结果比如UTF-8 Unicode text或ISO-8859 text如果是后者说明文件实际是 GBK 系编码后面读取时要指定encodinggbk。参数方面没有额外的坑唯一需要注意的是file对中文编码的检测有时不准它会输出ISO-8859这其实是 GBK 被误判了所以还得靠 Python 来做最终确认。然后用 Python 做二次确认这一步的核心是打印前几行原始字节观察中文是否能被正确解码import chardet with open(douban_data/douban_movie.csv, rb) as f: raw f.read(10000) result chardet.detect(raw) print(result) print(raw[:500])逻辑说明chardet.detect会返回一个包含编码和置信度的字典比如{encoding: GB2312, confidence: 0.99}这个信息直接决定 Pandas 的encoding参数怎么写。打印原始字节能肉眼观察文件开头是否有 UTF-8 的 BOM 标记\xef\xbb\xbf以及中文是否存在。注意chardet不是标准库如果没有安装可以用pip install chardet先装上或者退一步用encodingutf-8和encodinggbk轮换尝试。2.2 字段全解每一列是什么、能用来做什么确认完编码后最该做的一件事是把字段语义弄清楚。我一般会用 Python 读取数据后逐列打印非空值数量和样例值这一步能过滤掉很多后面分析时的玄学问题。下面是一段快速概览代码import pandas as pd df pd.read_csv(douban_data/douban_movie.csv, encodingutf-8) print(df.shape) print(df.columns.tolist()) print(df.dtypes) print(df.isna().sum()) print(df.head(3).T)逻辑说明read_csv的encoding参数改成第一步确认的编码.shape输出行数和列数确认 10 万条是否真实.columns.tolist()输出所有列名方便后续按名称操作.dtypes查看每一列的数据类型识别哪些列被误读成了字符串.isna().sum()统计每列缺失值数量这个数字直接暴露数据质量.head(3).T转置打印前 3 行所有字段一眼看全每列的内容形态。参数说明方面read_csv还需要注意两个常用参数low_memoryFalse可以避免混合类型时出现的 DtypeWarningon_bad_linesskip可以在某些行字段数量不一致时跳过而不是直接报错。不过我不建议一开始就用skip而是先让它报错观察错误行数再决定是手工修复还是跳过因为跳过的行可能包含有效信息这种处理方式能让后续分析更干净。这份数据集的字段中最有价值的几个是douban_score评分、comment_num评论人数、movie_type类型标签、release_date上映日期、director和actors。评分和评论人数可以做回归预测或评分分布统计类型标签可以做多标签分类上映日期可以提取年份做时间趋势分析导演和演员可以做协作过滤的冷启动特征。剧情简介虽然是文本但长度参差不齐适合用来做 NLP 分类的附加特征不过清洗成本较高后面我会单独说。3. 数据清洗实战从 10 万条原始数据到结构化分析表原始数据不能直接喂给算法这一步没必要太急优先解决三件事重复值、缺失值和类型不统一。豆瓣电影数据集虽然整体质量尚可但因为是爬虫聚合的结果会出现同一个电影名在不同行有细微差异、评分列混入“暂无评分”这类文本、日期格式不统一等问题。我在这份数据上花的时间占比大约是清洗 60%分析 30%建模 10%这比例不算夸张真实项目里清洗永远是大头。3.1 去除重复项与处理缺失值先拿掉脏数据重复项在电影数据里很容易被忽略因为同一部电影可能有多个版本比如大陆版和港版片名完全一致但上映年份不同。清洗时应该先基于“电影名 导演 上映年份”做组合去重而不是只按电影名去重否则会把不同版本错误合并。下面是我的处理逻辑# 先看完全重复的行有多少 print(df.duplicated().sum()) # 基于业务主键去重电影名 导演 年份 df[year] pd.to_datetime(df[release_date], errorscoerce).dt.year df_dedup df.drop_duplicates(subset[movie_name, director, year], keepfirst) print(f去重前: {len(df)}, 去重后: {len(df_dedup)})逻辑说明pd.to_datetime把字符串日期统一转成 datetime 类型errorscoerce的用法是把无法解析的日期变成NaTNot a Time这样不会让整个转换报错.dt.year提取年份字段drop_duplicates的subset参数指定列子集作为判断重复的依据keepfirst保留第一次出现的行。print 两种行数对比能直观看到重复比例的严重程度。参数说明errorscoerce是这里的关键参数如果不加只要有一行日期格式异常整个to_datetime就会抛异常subset参数不写的话则是全列去重对于电影数据来说几乎找不到完全相同的行起不到去重效果。在实际项目里我还会把director列做一次标准化去掉首尾空格、统一全角半角否则“张艺谋”和“张艺谋 ”会被当成两个不同导演这一步能被很多人忽略但非常关键。缺失值处理策略取决于分析目标。如果是做评分预测评分缺失行直接删掉是最安全的如果是做类型分布统计类型字段缺失可以填未知而actors和plot这种文本字段缺失了也不影响结构化分析没必要强行填充。我一般不会用fillna一把梭而是分列处理因为不同列的缺失含义完全不同。3.2 评分与评论数目的数据类型转换这个数据集里最容易翻车的字段就是评分和评论人数。很多行看起来是数字实际在 CSV 里被存成了字符串比如8.9、12,345、暂无评分。直接用astype(float)会当场报错。我的做法是先做一次强制转换然后统计转换失败的行再单独处理这些异常值# 先转成字符串去掉千分位逗号 df[comment_num] df[comment_num].astype(str).str.replace(,, , regexFalse) # 把无法转成数字的值变成 NaN df[comment_num] pd.to_numeric(df[comment_num], errorscoerce) # 评分列同样处理并转换为 float df[douban_score] pd.to_numeric(df[douban_score], errorscoerce) print(df[[douban_score, comment_num]].dtypes) print(df[[douban_score, comment_num]].isna().sum())逻辑说明astype(str)先确保列内所有值都是字符串否则.str.replace会报错regexFalse表示把逗号当作普通字符替换不需要正则解析pd.to_numeric配合errorscoerce会把所有不能转成数字的值变成 NaN包括空字符串、暂无评分、未知 这类脏值。最后打印类型和缺失值数量就知道有多少脏数据混进来了。这里的一个关键点处理完后需要重新决定这些 NaN 行是删还是留。我的经验是如果一行的评分和评论人数都是 NaN那这部电影对大部分分析场景都没价值建议直接删掉如果只是评论人数缺失而评分正常这行还能用于评分分布分析。所以清洗代码要按列决定行去留而不是一刀切。3.3 导演、演员和类型的多值拆分电影数据集中导演、演员和类型这几个字段通常是用斜杠/、逗号或空格分隔的多个值。比如剧情/爱情/历史莱昂纳多·迪卡普里奥/汤姆·哈迪。做分析时你可能需要把类型拆开统计而不是把整个字符串当作一个标签。下面是一段常见的拆分处理def split_field(value): if pd.isna(value): return [] return [x.strip() for x in str(value).replace( / , /).split(/) if x.strip()] df[type_list] df[movie_type].apply(split_field) df[actor_list] df[actors].apply(split_field) # 统计出现最多的10个类型 from collections import Counter type_counter Counter([t for lst in df[type_list] for t in lst]) print(type_counter.most_common(10))逻辑说明split_field函数首先判断是否为空值然后把/两侧的空格去掉再按/分割最后过滤掉空字符串apply将函数应用到每一行生成新的列表列。Counter统计所有类型标签的频次most_common(10)输出前 10 个高频标签便于快速了解数据集的类型分布。注意replace( / , /)这个步骤不能省因为原始数据中分隔符有的是 / 斜杠两边带空格有的是/如果不统一按/split 后会出现带首尾空格的标签比如剧情 和剧情这是两个不同的标签统计时会各算各的导致结果看起来很奇怪这种情况我见过很多次。另一种写法是直接在split后用 strip 清理效果一样但写进函数里更通用。4. 数据入库与查询把 10 万条电影数据装进 SQLite 分析清洗完数据后我建议把它写入 SQLite而不是一直留在 Pandas DataFrame 里。原因有三一是 Pandas 对内存的占用会随着数据量线性上升10 万行虽然不算大但加上类型列表和演员列表的 Python 对象开销实际占用会明显超出预期二是 SQL 查询适合做分组聚合并能让结果可复现比反复写 Pandas 链式操作更清晰三是后续做推荐系统离线评估时SQLite 的随机读取能力比 CSV 快得多。SQLite 是 Python 标准库自带支持的数据库不需要安装任何额外的服务非常适合本地数据和单机分析。4.1 设计表结构与批量写入把清洗后的 DataFrame 写入 SQLite表结构设计要尽量贴合查询需求。我的做法是拆成两张表一张movies电影主表一张movie_types类型关联表。拆表能避免类型多值存储在一个字段里导致无法高效查询。import sqlite3 conn sqlite3.connect(douban_movies.db) cursor conn.cursor() # 创建电影主表 cursor.execute( CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_name TEXT, director TEXT, year INTEGER, douban_score REAL, comment_num INTEGER, release_date TEXT, actors TEXT ) ) # 创建类型关联表 cursor.execute( CREATE TABLE IF NOT EXISTS movie_types ( movie_id INTEGER, type_name TEXT, FOREIGN KEY (movie_id) REFERENCES movies(id) ) ) # 批量写入电影主表 df_clean[[movie_name, director, year, douban_score, comment_num, release_date, actors]].to_sql( movies, conn, if_existsappend, indexFalse ) conn.commit()逻辑说明CREATE TABLE IF NOT EXISTS确保重复执行脚本不会报错movies表用自增主键id标识每部电影movie_types表通过movie_id外键关联主表每行存一个类型标签。to_sql是 Pandas 的便捷方法if_existsappend表示如果表已存在则追加数据indexFalse表示不把 DataFrame 的索引写入数据库。参数方面的关键点to_sql在写入大量数据时默认是逐行 INSERT速度还可以但不够快想要更快可以把methodmulti参数加上生成批量 INSERT 语句10 万行数据从慢的 10 秒级优化到 1 秒级。但methodmulti需要较新的 Pandas 和 SQLite 版本支持如果报 OFFSET 相关的错误说明 SQLite 版本太老需要升级或改用默认方式。type 关联表的写入必须在 movies 表写入之后因为需要拿到movies表的自增id来关联类型一种做法是写入 movies 后用cursor.lastrowid获取当前插入的 ID再逐行插入类型。4.2 用 SQL 做探索性分析评分分布和类型热度数据入库后分析起来就顺手多了。我分享两个高频查询一个是按年份看平均评分趋势另一个是按类型统计电影数量和平均评分这两条 SQL 能快速给出一份数据报告的骨架。-- 按年份统计电影数量、平均评分、平均评论数 SELECT year, COUNT(*) AS movie_cnt, ROUND(AVG(douban_score), 2) AS avg_score, ROUND(AVG(comment_num), 1) AS avg_comment FROM movies WHERE year BETWEEN 1990 AND 2024 AND douban_score IS NOT NULL GROUP BY year ORDER BY year;逻辑说明WHERE year BETWEEN 1990 AND 2024过滤掉异常年份比如 0 或 1895 这样的脏数据AND douban_score IS NOT NULL确保平均值不被 NaN 污染ROUND(AVG(...), 2)控制小数位数让结果更易读。GROUP BY year按年份分组ORDER BY year让结果按时间排序方便后续画趋势图。注意comment_num如果之前清洗时保留了一些为 0 的值AVG会把它们算进去如果统计的是评论热度可能需要额外加一个comment_num 0条件。第二个查询类型热度可以这样写-- 类型热度按类型统计电影数、平均评分 SELECT t.type_name, COUNT(*) AS movie_cnt, ROUND(AVG(m.douban_score), 2) AS avg_score FROM movie_types t JOIN movies m ON t.movie_id m.id WHERE m.douban_score IS NOT NULL GROUP BY t.type_name HAVING COUNT(*) 10 ORDER BY movie_cnt DESC LIMIT 20;逻辑说明JOIN movies m ON t.movie_id m.id把类型表和主表关联起来HAVING COUNT(*) 10是个好习惯它会过滤掉只出现几次的冷门类型只统计样本量足够的类型避免一两部高分电影把平均分拉得很高。ORDER BY movie_cnt DESC LIMIT 20输出出现次数最多的 20 个类型。这个查询的结果我会导出成 CSV 再画成柱状图观察数据集的类型分布是否偏斜。豆瓣电影数据集中常见的情况是“剧情”和“爱情”类型数量明显偏多“纪录片”类型相对较少这种不均衡直接影响了后续推荐系统的评估方式如果直接拿全量数据做训练模型会对头部类型过拟合后面离线评估的指标就会被“剧情片”主导。5. 避坑指南豆瓣电影数据集常见的五个坑及对应解法这一章全是实操中真实的踩坑记录每一条我都亲自趟过耗费的时间短则十分钟长则一下午写下来给后来者省点时间。踩坑一读取 CSV 时中文乱码或直接报 UnicodeDecodeError现象pd.read_csv报错utf-8 codec cant decode byte 0xd5 in position 10或者读出来中文变成一堆乱码。原因这份数据集的编码不是 UTF-8而是 GBK 或 GB18030尤其是在 Windows 环境下导出 CSV 的 Python 脚本自带 GBK 编码。解决先用chardet.detect()检测实际编码然后用对应编码读取如果检测结果是ISO-8859或置信度很低直接试用encodinggb18030这个编码比gbk覆盖更广能兼容更多生僻字。从那以后我每次拿到 CSV第一步就是跑一次chardet不在这上面赌运气。踩坑二用默认参数读 CSV 时出现 DtypeWarning现象读取 10 万行数据后控制台输出DtypeWarning: Columns have mixed types。原因某些字段比如comment_num在不同行分别是整数和字符串比如暂无Pandas 自动推断类型失败。解决在read_csv时手动指定各列的类型或者使用low_memoryFalse。更稳的做法是先读一次获取列名再手动指定可疑列的类型为str统一走一遍清洗流程最后再转成正确的数值类型这样 DtypeWarning 就不会出现了。踩坑三年份列出现 0 或异常值导致统计结果失控现象SELECT year, AVG(...)出来的数据里出现 1895 年这种老掉牙年份甚至 year 为 0计算结果明显异常。原因原始数据中release_date可能有缺失、空字符串或格式不标准的日期比如2000-00-00pd.to_datetime解析成 2000 年或者解析失败返回 NaT再提取年份时得到 NaN有些小白会在后面填 0。解决在提取年份后加一个过滤条件只保留 1900 到当前年份之间的值范围内外的直接赋为 NaN。我后来写了个通用函数年份不在合理区间的一律置空宁缺毋滥。踩坑四类型字段的标签被空格和分隔符污染统计结果分裂现象Counter统计出的 Top 10 类型里同时出现剧情和剧情 两个标签的原始含义相同但被统计成两类。原因原始数据里剧情 / 爱情和剧情/爱情混用按/分割后带空格的标签没有被清理。解决split之后一定对每个元素执行strip()或者先做一次全局替换把 / 统一为/。这个坑是我在前 1000 行数据上观察到的前 1000 行恰好都是统一的 / 格式让我误以为所有数据格式一致直到看完整数据集才发现这个问题所以建议清洗后用Counter统计一遍标签的分布检查是否有相似标签。踩坑五写入 SQLite 时外键关联出错类型表数据对不上现象movie_types表中的movie_id和movies表的id对不上JOIN 出来结果为空或数据错位。原因写入 movies 表后cursor.lastrowid拿到的 ID 并不是当前 DataFrame 行对应的 ID尤其是在to_sql批量写入或自增 ID 因为之前删除操作而跳号的情况下用行序号去推断 ID 完全是靠玄学。解决不要依赖lastrowid而是写入 movies 表后通过查询把 DataFrame 的movie_name director year与表内记录匹配得到真实 ID 再插入类型表。这一步慢一些但绝对可靠10 万行的连接查询在 SQLite 上耗时也就是几秒完全能接受。6. 进阶用法从静态数据集到轻量级特征工程与相似度检索前面的步骤如果做完了你已经拥有了一份干净的、存储在 SQLite 中的豆瓣电影结构化数据。最后一章我想把用法再推进一步如何用这份数据做电影相似度检索。这个需求在实际项目中很常见比如推荐系统详情页里的“喜欢这部电影的人也喜欢”就是一个典型的基于内容特征的 TopN 相似度检索场景适合构建基线用的原型验证代码。特征工程方面我会把电影的类型标签做成 One-Hot 特征或多热编码特征然后使用余弦相似度计算电影之间的相似性。这里用 TF-IDF 加余弦相似度来处理类型标签而不是直接用 One-Hot因为类型标签本身是稀疏的直接用 One-Hot 会让所有电影之间的相似度普遍偏低而且毫无区分度。下面是一段可运行的代码from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # 把类型列表重新拼回字符串用空格分隔 df_clean[type_str] df_clean[type_list].apply(lambda x: .join(x)) # 使用 TF-IDF 向量化类型标签 vectorizer TfidfVectorizer(token_patternr\S) tfidf_matrix vectorizer.fit_transform(df_clean[type_str]) # 计算余弦相似度矩阵 similarity_matrix cosine_similarity(tfidf_matrix, tfidf_matrix) def recommend_by_id(movie_id, top_n10): sim_scores list(enumerate(similarity_matrix[movie_id])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) sim_scores sim_scores[1:top_n 1] sim_indices [i[0] for i in sim_scores] return df_clean.iloc[sim_indices][[movie_name, douban_score]].values.tolist() # 测试一部电影的推荐结果 print(recommend_by_id(0, top_n10))逻辑说明type_str把类型列表合并成空格分隔的字符串因为TfidfVectorizer的默认 token 模式是按空格分词token_patternr\S暗示按非空白符分 token避免中文被默认的正则拆成单字这是一个很实用的调整cosine_similarity返回 N×N 的相似度矩阵similarity_matrix[movie_id]取某一行的相似度向量enumerate拿到电影索引和相似度分数排序后跳过自己[1:top_n1]返回 Top N 结果。这样做出来的推荐虽然只用了类型一个特征但对于原型验证完全够用。如果想要更好的效果可以把导演和演员也加进特征向量里比如把director和actors拼到type_str后面用 TF-IDF 一起向量化。不过要注意演员列拼接后特征空间会变大10 万行数据对应的词表可能有数万个词计算相似度矩阵的时间会从秒级涨到分钟级做实验前要有预期。如果只做 TopN 检索而不是计算全量相似度矩阵可以考虑用pairwise_distances的n_jobs-1参数做并行计算或者改用sklearn.neighbors.NearestNeighbors的metriccosine来避免显式构建 N×N 矩阵这个在数据量到 50 万行以上时是必须做的优化。如果要做推荐系统的离线评估我的习惯是先把数据按 8:2 划分训练集和测试集在训练集上计算相似度矩阵然后对测试集中的每一部电影用它最相似的若干电影来预测评分再计算 RMSE 或 PrecisionN。这套流程 10 万条数据跑下来速度完全可以接受比你在网上找的一大堆 Jupyter Notebook 示例更贴合真实开发环境。最后补一句我在这个数据集上翻过最大的一个教训拿到数据集后一定不要急着建模先把字段语义、编码、缺失值、重复项这四个基础问题全部清干净再做探索性分析最后才是模型。那一次我直接在未清洗的数据上跑出了很高的精度指标觉得很兴奋后来发现是因为测试集里混了训练集的重复样本属于严重的数据泄漏结果毫无意义。从那以后我每次拿到数据都强制走一遍清洗流程哪怕看起来再干净的数据也当脏数据对待希望这套流程能帮你在做自己的项目时少走一点我走过的弯路。本文还有配套的精品资源点击获取