猫眼电影字体反爬破解与数据可视化全流程实践
2026/9/16 17:40:13 网站建设 项目流程

简介:面向高校计算机相关专业学生及毕业设计开发者,以猫眼电影为实战案例,完整覆盖数据爬取、反爬破解、数据清洗、存储分析与可视化全流程,适用于课程设计、毕业设计、项目初期演练,也可作为深度学习/数据分析实践的前置参考。资源共77个文件,压缩包4.79MB,核心包括33个Python脚本、Django项目配置、HTML可视化页面、爬虫所需字体与Cookie数据、SQLite数据库及报告文档等,目录按爬虫、分析、可视化、后台管理分模块组织,便于按需查阅。已有69人下载学习。内容除可直接运行的主程序外,还附带依赖清单、常见爬虫反破解思路及猫眼与豆瓣电影数据爬取与可视化报告,适合快速复现并二次扩展;若环境配置遇到问题,还可远程支持,对入门爬虫与数据分析方向的学生比较友好。

1. 猫眼字体反爬这个坎,值得拆一遍

做毕设或者练手项目时,很多人第一反应是爬豆瓣,但豆瓣的条目页结构简单、反爬也温和,真正能让人学到东西的是猫眼这种「看起来能爬、一提取全乱码」的站点。这套代码的切入点很有意思:猫眼用woff字体文件把票房、评分数值渲染成自定义字形,直接用 requests 抓到的是 HTML 里的实体字符,而真正展示给用户的是字体映射后的数字。换句话说,爬虫拿到的是「加密后的数字」,必须解析字体文件才能还原。

整个项目从爬虫到可视化是一条完整链路:requests + fake_useragent做请求,fontTools解析woff文件还原真实数字,SQLite 落库,再用 pyecharts 生成票房 TOP10、评分 TOP10 和产地饼状图,最后用 Django 把图表和数据分析结果搬到 Web 页面上。对想学 Python 数据爬取、数据分析与可视化的同学来说,这是一份能直接跑通全流程的参考实现。下面按「爬取 → 存储 → 可视化 → Web 展示」的顺序拆开讲。

2. 爬虫构建:UA伪装、woff字体解析与数字还原

2.1 请求侧的抗反爬:fake_useragent 与请求头

猫眼对异常 UA 的封禁比较直接:短时间高频请求会返回 403 或者验证页。这套代码里引入了fake_useragent.json,把常见浏览器的 UA 放在本地文件里,每次请求随机取一个。相比每次在线拉取fake-useragent库的默认数据,本地文件方式更适合离线环境,也避免因为库的在线更新接口不稳定导致爬虫启动失败。

from fake_useragent import UserAgent import requests ua = UserAgent(path="./fake_useragent.json") headers = { "User-Agent": ua.random, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3", "Connection": "keep-alive", } resp = requests.get(url, headers=headers, timeout=10)

参数说明:UserAgent(path=...)指定本地 JSON 文件路径,ua.random每次调用随机返回一个 Chrome、Firefox 或 Safari 的 UA 字符串;Accept-Language设为中文优先,避免服务端根据语言返回不同页面结构。timeout=10是硬性要求,猫眼部分接口偶尔会挂起,不设超时会导致线程卡死。实际抓取时建议在请求之间加time.sleep(random.uniform(1, 3)),这是最朴素但也最有效的频率控制手段。

2.2 字体反爬原理:woff 里的数字是怎么藏起来的

猫眼页面里的票房、评分、排名这些数字,源码中显示的是类似这样的私有区字符,浏览器加载maoyan.woff字体文件后,把这些字符映射成看起来正常的阿拉伯数字。每次请求页面时字体文件可能变化,也就是同一数字在不同请求里对应不同的字形编码。

文件作用
maoyan.woff当前页面使用的动态字体,含数字字形与编码映射
base.woff基准字体,用于比对当前字体与标准字形的差异
maoyan.py爬虫主脚本,包含字体下载与解析逻辑

破解思路是:下载当前页面引用的 woff 文件,解析它的cmap表,拿到「字符编码 → glyph 名称」的映射;再看字体内部 glyf 表里每个字形对应的轮廓,和基准字体做比对,找出当前字体里哪个字形对应数字几。这块是整套代码里最值得读的部分,也是 2018 猫眼电影数据爬取与可视化这个项目能拿高分的原因——它不是拿 Selenium 硬渲染,而是彻底拆了字体反爬。

2.3 字体还原:fontTools 解析与动态映射

常见做法是用fontTools.ttLib.TTFont读取字体文件,先获取getBestCmap()拿到字符映射,再对每个 glyph 名称做归一化对比。有些字体的 glyph 名直接带数字(如glyph00001),有些则无规律,需要借助字形轮廓的坐标来匹配。

from fontTools.ttLib import TTFont font = TTFont("maoyan.woff") cmap = font.getBestCmap() # cmap 的 key 是字符编码,value 是 glyph 名称 for code, name in list(cmap.items())[:10]: print(hex(code), name) glyph_order = font.getGlyphOrder() # 常见情况:glyph 名称里带有序号,可直接提取 for idx, name in enumerate(glyph_order[:10]): print(idx, name)

这段代码能快速确认字体结构。如果glyph_order里的名称是glyph00001这种规整格式,直接把序号作为映射值即可;如果是随机名,则需要遍历glyph表,提取每个字形的坐标点集合,和基准字体base.woff里的数字字形做相似度匹配。坐标比对时不需要精确相等,过滤掉轮廓的平移偏移后,比较端点数量和一阶差分即可,匹配成功后把字体内码映射成{"0": "0", "1": "1", ...}

提示:解码 HTML 字符串时,猫眼页面里是&#x开头的一串实体,用html.unescape()转成 Unicode 后再查映射表即可。

2.4 字体反爬的坑:缓存、字体过期、字段缺失

这套代码里值得注意的坑有三个。第一,woff 字体文件必须保持和当前请求页面一致,页面刷新后字体可能轮换,所以真实爬取时每次请求页面都要重新拉取字体文件,不能用本地缓存;第二,票房的「万」「亿」单位通常是 HTML 里的常规文本,不需要字体还原,还原的只是数字部分,拿到数字后要自己拼接单位;第三,部分字段存在缺失,比如有些电影没有开画日期,字段定位时要用try/except兜底,而不是直接下标取值。

def parse_font_mapping(woff_path): font = TTFont(woff_path) cmap = font.getBestCmap() mapping = {} for code, name in cmap.items(): char = chr(code) # 以 base.woff 为基准,建立字符到数字的对应 mapping[char] = base_mapping.get(name, char) return mapping

这里的base_mapping是预先从基准字体里提取的「glyph 名称 → 真实数字」字典,当前字体解析出的字符都要回查这个字典。相比每次从零识别字形,这种方式更稳定,也是这套代码能高效跑完 2018 全年数据的原因:只需在启动时解析一次基准字体,后续请求直接查内存映射,而不是每页都做坐标匹配。

3. 数据落库与去重:从 film_crawl.db 到 Django ORM 的衔接

3.1 为什么选 SQLite:单机爬虫的存储边界

项目里同时出现了film_crawl.dbdb.sqlite3dbs目录,前者是爬虫原始数据的落库位置,后者是 Django 项目的业务数据库。这种拆分是刻意的:爬虫写库和业务展示用不同的库,避免 Django 迁移时误操作覆盖掉爬取结果。SQLite 对毕设和中小型数据分析项目足够,文件即库,迁移方便,不需要单独部署 MySQL,这也是这个可视化项目能够一键复现的原因。

用 SQLite 的另一个理由是配合 pandas 做数据分析时非常顺滑——pd.read_sql_query()直接读库文件,不再需要维护数据库连接池。如果你的数据量级在百万行以内,SQLite 的读写性能完全够用,没必要引入额外组件。

3.2 库表设计与去重插入

爬虫解析出来的电影字段包括排名、片名、评分、票房、上映日期、产地等。防止重复数据是爬虫落库的第一要务,重跑脚本时一张表里全是重复记录,后面的分析和图表就直接失真了。

CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, rank INTEGER, title TEXT, score REAL, box_office TEXT, box_value REAL, release_date TEXT, origin TEXT, UNIQUE(title, release_date) );

插入用INSERT OR IGNORE,以「片名 + 上映日期」作为唯一键。这样同一部电影即使页面刷新后多次抓取,也只会保留第一次的记录。box_value是规范化后的票房数值(统一转为亿元),box_office保留原始字符串,例如「46.54亿」,这样图表和文档展示可以各取所需。

import sqlite3 conn = sqlite3.connect("film_crawl.db") cur = conn.cursor() cur.execute(""" INSERT OR IGNORE INTO movies (rank, title, score, box_office, box_value, release_date, origin) VALUES (?, ?, ?, ?, ?, ?, ?) """, (rank, title, score, box_office, box_value, release_date, origin)) conn.commit()

参数说明:INSERT OR IGNORE在违反唯一约束时不报错,而是跳过该行,适合批量插入;box_value在 Python 侧就把「46.54亿」标准化成46.54,避免在 SQL 里做字符串截取,效率和可读性都更好。

3.3 从 SQLite 到 Django 模型

Django 端通过 ORM 访问数据时,一种做法是直接配置多个数据库连接,在settings.py里添加film_crawl.db作为只读数据源;另一种更省事的方案是把爬虫数据导出成 JSON 或 CSV,再用 Django 的 migration 或 fixture 导入业务库。项目里走的是第二种思路,film_projectmodels.py与 SQLite 表结构一一对应。

from django.db import models class Movie(models.Model): rank = models.IntegerField(verbose_name="排名") title = models.CharField(max_length=100, verbose_name="片名") score = models.FloatField(verbose_name="评分") box_office = models.CharField(max_length=20, verbose_name="票房") box_value = models.FloatField(verbose_name="票房(亿)") release_date = models.CharField(max_length=20, verbose_name="上映日期") origin = models.CharField(max_length=50, verbose_name="产地", default="")

字段类型和爬虫建表基本一致,box_valueFloatField供图表排序使用,release_date保留字符串不做日期解析,因为猫眼页面里的日期格式存在「2018-02-16」和「2018年2月16日」混用的情况,统一字符串处理反而省事。

3.4 数据分析前的前置清洗

入库之后,真正做数据分析前还有一轮清洗逻辑,这也是views.pyanalysis.py里大量代码在做的事。我的建议是三步走:先过滤空值,score IS NOT NULLbox_value IS NOT NULL;再统一产地,猫眼页面的产地字段有的写「中国大陆」、有的简写「内地」,需要做一次字典映射;最后排序取 TOP10,用 SQL 的ORDER BY box_value DESC LIMIT 10完成,不要在 Python 里全量排序。

import pandas as pd df = pd.read_sql_query(""" SELECT title, score, box_value, origin FROM movies WHERE score IS NOT NULL AND box_value IS NOT NULL """, conn) df["origin"] = df["origin"].replace({"内地": "中国大陆", "美国/中国大陆": "合拍"}) top10 = df.nlargest(10, "box_value")

nlargest(10, "box_value")与 SQL 的ORDER BY + LIMIT等价,但在 pandas 里处理时可以直接复用后续可视化流程里的 DataFrame,少一次数据搬运,代码更紧凑。

4. pyecharts 可视化:评分 TOP10、票房 TOP10 与产地饼状图

4.1 图表选型与数据分析的关系

这个项目生成的四份图表各有用途:评分排行 TOP10 反映口碑维度,票房排行 TOP10 反映市场维度,产地状况饼状图反映 2018 年引进片与国产片的数量结构,而cloud_test.py是对影评关键词做词云分析,属于文本挖掘的补充。选 pyecharts 而不是 Matplotlib,原因是 pyecharts 生成的图表是交互式 HTML,鼠标悬停能看到具体数值,导出 PNG 也方便写进课程设计报告。

图表数据来自上一章清洗后的 DataFrame,核心是把 DataFrame 的列转化为 pyecharts 的add_xaxis/add_yaxis列表参数。

4.2 票房与评分 TOP10 柱状图配置

先看票房排行 TOP10 的实现,这段代码在maoyanVisual.py里,核心逻辑是把查询结果拆成 x 轴类目和 y 轴数值两个列表。

from pyecharts.charts import Bar from pyecharts import options as opts bar = ( Bar() .add_xaxis(top10["title"].tolist()) .add_yaxis( "票房(亿元)", top10["box_value"].round(2).tolist(), label_opts=opts.LabelOpts(position="top"), ) .set_global_opts( title_opts=opts.TitleOpts(title="2018猫眼电影票房排行TOP10"), yaxis_opts=opts.AxisOpts(name="票房(亿元)"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=15)), ) .render("2018猫眼电影票房排行TOP10.html") )

参数说明:add_yaxis里的label_opts=opts.LabelOpts(position="top")让数值标签显示在柱体顶部,不用鼠标悬停也能直读数值;xaxis_opts里的rotate=15是长片名最常用的参数,15 度倾斜能避免片名互相遮挡。set_global_opts里的title_opts标题会直接渲染在 HTML 右上角,导出的 PNG 同样保留。

评分排行 TOP10 的代码结构完全一样,只是数据列换成score,y 轴名称改「评分」,同时把LabelOptsformatter加上一位小数,让图表里显示 8.6 而不是 8.59999。

4.3 产地状况饼状图与数据聚合

产地状况用饼状图呈现,需要先按产地聚合统计电影数量,再传到Pie图表的data_pair参数。pyecharts 的Pie接受[(name, value), ...]格式,所以 pandas 端要做一次groupby

from pyecharts.charts import Pie origin_stats = df.groupby("origin").size().reset_index(name="count") data_pair = list(zip(origin_stats["origin"], origin_stats["count"])) pie = ( Pie() .add( series_name="产地分布", data_pair=data_pair, radius=["35%", "70%"], center=["50%", "55%"], label_opts=opts.LabelOpts(formatter="{b}: {c} 部"), ) .set_global_opts(title_opts=opts.TitleOpts(title="2018猫眼电影产地状况饼状图")) .render("2018猫眼电影产地状况饼状图.html") )

参数说明:radius=["35%", "70%"]表示环形图的内径和外径,做环图而不是实心饼图,视觉上更清爽;center控制饼图在画布中的位置;formatter="{b}: {c} 部"是饼状图最常用的标签模板,{b}是每块扇形名称,{c}是数值,显示效果是「中国大陆: 12 部」。

4.4 HTML 与 PNG 双输出

pyecharts 默认只生成 HTML 文件,写报告时需要静态图片。常见做法是调用pyechartsSnapshotSeleniumSnapshotPhantomJS渲染引擎,把 HTML 转成 PNG。项目里已经预置了生成的 PNG 文件,说明采用的是截图方案。

from pyecharts.render import make_snapshot from snapshot_selenium import snapshot make_snapshot(snapshot, "2018猫眼电影票房排行TOP10.html", "2018猫眼电影票房排行TOP10.png")

make_snapshot的延迟时间可以通过参数pixel_ratio=2控制输出分辨率,报告插图一般用 2 倍图,打印出来不糊。初次使用这个方案需要本地装 Chrome 和对应驱动,这也是不建议在服务器上跑完整可视化的原因,本地 Windows/macOS 环境一次就能配好。

5. Django 集成与本地验证:把分析结果跑成 Web 页面

5.1 项目结构与静态资源接入

film_project是 Django 工程,film_admin是业务应用,static目录下放的是图表 HTML、图片和字体文件。pyecharts 生成的 HTML 不能直接塞进 Django 模板的{% extends %}体系,最稳妥的做法是让 Django 直接返回这个静态文件,或者用FileResponse读取后返回。

from django.http import FileResponse import os def chart_view(request, chart_name): file_path = os.path.join(settings.BASE_DIR, "static", f"{chart_name}.html") return FileResponse(open(file_path, "rb"), content_type="text/html")

chart_view接收 URL 里的文件名参数,动态返回对应图表。映射到urls.py时,用<str:chart_name>捕获路径参数,避免为每个图表单独写一个视图函数。content_type="text/html"必须显式声明,否则浏览器可能直接下载文件而不是渲染页面。

5.2 启动验证与数据链路检查

本地验证时,先确认依赖安装完整。项目根目录的requirements.txt包含 django、requests、pyecharts、fake-useragent、fontTools、pandas 等库。

pip install -r requirements.txt python manage.py runserver 0.0.0.0:8000

0.0.0.0让服务监听所有网卡,这样局域网内其他设备也能访问,方便在手机上快速确认图表显示效果。启动后访问首页,检查三件事:图表是否正常加载、票房数值是不是真实数字(不是乱码方块)、切换页面是否报 500 错误。

5.3 数据更新链路:重跑爬虫后如何让图表同步

这是整套代码最实用的一条链路:爬虫入库后,图表如何同步刷新。常见做法是把「查询数据 → 生成图表 → 替换静态文件」封装成一个独立脚本,每次爬完手动执行一次。

python maoyan.py # 爬取数据,写入 film_crawl.db python maoyanVisual.py # 读取库表,重新生成 HTML 和 PNG

maoyanVisual.py结尾处直接调用 Django 的django.setup(),让自己变成可独立运行的脚本,不需要启动 Web 服务也能重新生成全部图表。注意如果 Django 版本在 3.2 以上,settings.py里的DEFAULT_AUTO_FIELD需要显式指定,否则执行makemigrations时会提示加AutoField配置。

提示:遇到中文乱码时,优先检查settings.py里是否设置了LANGUAGE_CODE = 'zh-hans'TIME_ZONE = 'Asia/Shanghai',以及models.py中是否在字段上声明了verbose_name,这会影响 Django 后台页面和模板中的中文显示。

5.4 高频报错与应对方案

本地跑这套代码最常见的报错有三个。第一个是FileNotFoundError: fake_useragent.json,脚本在相对路径下找不到 UA 文件,解决方式是改成os.path.join(os.path.dirname(__file__), "fake_useragent.json"),不依赖当前工作目录。第二个是TTFont解析报错ValueError: invalid glyph index,原因是字体文件下载不完整,需要在抓取时校验文件大小,低于阈值就重试。第三个是 Django 静态文件 404,检查settings.pySTATICFILES_DIRS是否包含static目录,以及urls.py是否在 DEBUG 模式下追加了static()路由。

if settings.DEBUG: from django.conf.urls.static import static urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])

这行代码解决了本地调试时静态图表不显示的问题,但生产环境不能依赖它,正确做法是交给 Nginx 处理静态文件。对毕设演示来说,本机跑 Django 开发服务器已经足够,需要分享给评委老师时,用python manage.py runserver 0.0.0.0:8000的方式演示即可,整套代码从爬虫到图表展示都不需要额外部署数据库或缓存组件。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询