豆瓣Top250爬虫到可视化:Requests+XPath+Flask+Echarts完整实践

发布时间:2026/10/18 17:13:32

豆瓣Top250爬虫到可视化:Requests+XPath+Flask+Echarts完整实践 简介一套面向计算机、数学、电子信息等专业课程设计与毕业设计的Python完整项目围绕豆瓣电影Top250的爬取、数据清洗分析与可视化展示展开应用Flask框架搭建Web界面借助Echarts与WordCloud生成交互图表和词云。压缩包共2000个文件以1800个Python脚本为核心涵盖爬虫、数据处理与Web应用各模块另含少量txt说明、json配置、html模板及md文档用于环境说明与使用指引整体大小约144.04MB。全部源码与项目说明均已包含在内下载后可直接运行适合已掌握Python基础、希望学习完整项目架构和FlaskEcharts技术栈的读者作为参考。目前已有371人学习项目中包含的代码注释与文件组织方式可为复现豆瓣榜单分析、扩展其他网站的爬取可视化提供清晰思路。1. 豆瓣Top250爬虫可视化一份源码看懂从数据到页面的最小闭环用Python做爬虫加数据可视化是许多课程设计和毕业设计的经典选题。这份基于豆瓣电影Top250的源码包从题目上看不算新但真正打开之后的价值在于它把数据采集、清洗、存储、Flask渲染、Echarts图表、WordCloud词云这几段完整串了起来不是给你一段孤立爬虫代码就完事。我把它拆过一遍整体能跑通适合想做“爬虫Web展示”一体的项目实例、又不想从零造轮子的人。你能从中看清一个数据项目从源头到页面的完整链路也能快速改成自己的毕设演示。下文按我自己复现时会走的顺序拆解。2. 爬虫层Requestsxpath抓取Top250分页参数与反爬边界的四个细节2.1 豆瓣Top250的分页规则start参数不是页码而是偏移量豆瓣Top250的列表地址是https://movie.douban.com/top250它不像很多网站那样用?page1表示页码而是用start作为偏移量。第一页是start0第二页是start25一直递增到start225共10页每页25条。这个规律是公开可见的不涉及任何接口破解。url https://movie.douban.com/top250 page_urls [] for i in range(10): params {start: i * 25, filter: } page_urls.append((url, params)) print(page_urls[:3])构造URL时filter参数保持为空字符串即可它不影响分页结果。注意start是整数偏移量不是页码这个搞错的话会重复抓同一页或者漏页。我一般会把page_urls做成列表方便后面循环时统一传入 requests。各参数含义见下面。参数名类型含义取值startint列表起始偏移量0、25、50……225filterstr页面筛选条件保持空字符串即可分页总数-固定10页range(0, 250, 25)生成这种分页方式在爬虫里属于最简单的类型不需要模拟点击不需要解析AJAX直接拼URL就能拿到完整列表。也正因为如此它特别适合课程设计评委问起“分页逻辑在哪”你可以直接指出start参数而不是甩出一套复杂的动态加载方案。2.2 xpath字段抽取从页面结构到结构化字典拿到HTML之后下一步是解析出电影名、评分、导演演员、年份、一句话评价等字段。这个项目用的是lxml的xpath不是BeautifulSoup。xpath的优点是路径选择精准抓列表页效率高缺点是一旦页面结构变化选择器会失效。import requests from lxml import html def parse_page(params): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } resp requests.get(https://movie.douban.com/top250, paramsparams, headersheaders, timeout10) resp.raise_for_status() doc html.fromstring(resp.content) rows [] for item in doc.xpath(//li[contains(class, grid_view)]): title item.xpath(.//span[classtitle]/text())[0] score item.xpath(.//span[classrating_num]/text())[0] bd item.xpath(.//div[classbd]/p[1]//text()) info .join(bd).strip() quote item.xpath(.//p[classquote]/span[classinq]/text()) rank item.xpath(.//div[classpic]/em/text())[0] rows.append({ rank: rank, title: title, score: score, info: info, quote: quote[0] if quote else , }) return rows这个解析逻辑里有几个值得强调的地方。bd这一段返回的是导演、主演、年份、地区、类型混在一起的文本行我用join拼起来再统一处理比直接取某个text()更稳。quote是短评字段有的电影没有所以取的时候要加一层空值判断否则会IndexError。rank取自div.pic里的em标签也就是左上角那个数字编号。解析时最重要的是先确认 xpath 路径对应的是哪个标签调试阶段可以先用print(doc.xpath(...))打印一遍确认有数据再批量跑。2.3 反爬兜底UA伪装、请求间隔与403处理豆瓣对爬虫的识别还算敏感但Top250这种列表页的压力测试并不高。常见做法是准备一个User-Agent列表每次随机取一个同时请求结束后随机sleep一段时间降低单位时间内的请求密度。import random import time from fake_useragent import UserAgent ua UserAgent() for idx, (url, params) in enumerate(page_urls): headers {User-Agent: ua.random} try: rows parse_page_with_headers(params, headers) save_to_csv(rows) except requests.HTTPError as e: print(f第{idx 1}页失败: {e}等待10秒重试) time.sleep(10) time.sleep(random.uniform(1.5, 3.5))这里我一般会用fake_useragent库生成随机UA而不是固定一个Chrome版本因为连续多次相同UA更容易被识别。parse_page_with_headers就是将之前的parse_page函数加上headers参数逻辑不变。请求间隔控制在1.5到3.5秒之间整套跑完接近半分钟体验上可以接受也基本不会触发封禁。如果返回403优先检查UA是否太老其次检查是否请求间隔太短最后再考虑被限IP而不是直接上代理或Cookie。3. 数据层pandas清洗和CSV落盘从脏数据到DataFrame的一步到位3.1 抓取结果里的典型脏数据空格、缺失、类型混杂爬下来的列表看着整齐但直接进可视化会出各种问题。rank字段带着小数点比如1.而不是1score是字符串类型做数值运算前必须先转floatinfo里的年份藏在导演、主演一大段文本里需要正则提取quote有的电影没有是空字符串。这些如果不处理后面画图时要么排序错乱要么显示N/A。3.2 数据清洗脚本pandas的replace、str.extract与类型转换import pandas as pd df pd.DataFrame(raw_rows) df[rank] df[rank].astype(str).str.replace(., , regexFalse).astype(int) df[score] df[score].astype(float) df[year] df[info].str.extract(r(\d{4})) df[director] df[info].str.split(主演).str[0].str.replace(导演:, ).str.strip() df[quote] df[quote].fillna() print(df.dtypes) print(df.head())清洗的核心逻辑是三件事。第一rank先转字符串把点去掉再转整数第二score直接用astype(float)因为抓取时已经是干净数字字符串第三用正则(\d{4})从info里把年份抠出来这部操作在pandas里是向量化的不需要写循环。director字段的做法是先用split(主演)切掉演员部分再去除导演:前缀最后strip()去掉两边空格。这里有个细节值得说info里的文本格式通常是导演: 某某 主演: 某某 / 某某 / 某某\n2019 / 中国大陆 / 犯罪所以年份提取后原始info还可以继续保留用来做地区或类型的后续切分。如果你只做Top250的评分分布和词云这个清洗程度已经足够。如果你还想按地区筛选可以再加一步df[area] df[info].str.split(\n).str[1].str.split( / ).str[1]这类扩展逻辑在项目说明里没有细讲但写起来不难。3.3 存储与读取CSV的中文编码陷阱清洗之后落盘首选CSV原因很简单表格结构清晰、后续pandas直接读、评审老师打开Excel也能看。但中文写入CSV有一个经典坑就是默认的utf-8编码在Excel里打开会乱码。解决方法是写入时用utf-8-sig也就是带BOM的UTF-8。df.to_csv(top250_cleaned.csv, indexFalse, encodingutf-8-sig) df_read pd.read_csv(top250_cleaned.csv, encodingutf-8-sig) print(df_read.shape)indexFalse的意思是不要把DataFrame自带的行索引写进去否则CSV里会多出一列无意义的Unnamed: 0。读取时同样指定utf-8-sig保证字段里的中文按原样还原。这一步做完后端的可视化就有了一个干净、可复现的数据源。如果之后想在Jupyter里先跑一遍分析直接read_csv就能继续往下走不需要重新爬。4. 可视化层FlaskEchartsWordCloud把静态CSV变成可交互的Web页面4.1 Flask路由设计一个app.py承载数据接口与页面渲染可视化部分用的是Flask框架。整个项目可以拆成两个角色Flask负责提供HTTP接口前端页面负责发请求拿数据、渲染图表。最简实现里app.py只有两个路由。from flask import Flask, jsonify, render_template, request import pandas as pd app Flask(__name__) df pd.read_csv(top250_cleaned.csv, encodingutf-8-sig) app.json.ensure_ascii False app.route(/) def index(): return render_template(index.html) app.route(/data) def data(): records df.to_dict(orientrecords) return jsonify({code: 0, data: records})to_dict(orientrecords)会把DataFrame转成[{列名: 值}, ...]的形式正好是前端JS友好处理的JSON结构。app.json.ensure_ascii False让Flask返回的中文保持原样不加\u转义调试时一眼能看清内容。运行方式很简单终端里python app.py浏览器访问http://127.0.0.1:5000。这里的实现刻意保持精简没有用蓝图也没有拆模块因为课程设计答辩时代码越直白越好解释。你完全可以把app.py当成一个数据服务入口后续想加接口就在下面继续加路由。4.2 Echarts图表渲染评分分布与年份趋势的配置范式前端页面引入Echarts的CDN然后通过fetch向后端要数据。以评分分布为例前端拿到电影列表后先按0.5分一个区间统计数量再交给Echarts画柱状图。fetch(/data) .then(res res.json()) .then(json { const raw json.data; const bins {}; raw.forEach(item { const key item.score.toFixed(1); bins[key] (bins[key] || 0) 1; }); const categories Object.keys(bins).sort((a, b) a - b); const values categories.map(k bins[k]); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: categories }, yAxis: { type: value }, series: [{ type: bar, data: values, name: 电影数量 }] }); });这个写法把统计逻辑放在了前端优点是Flask后端不用做二次聚合接口保持通用。item.score.toFixed(1)会把9.2、9.3这类分数归入对应区间bins对象用来计数。需要注意Object.keys(bins)返回的是字符串数组排序时必须显式写成(a, b) a - b否则按字符串字典序排9.5会排在9.2前面。Echarts的series.type设为bar就是柱状图改成line就是折线图年份趋势图同样可以复用这个配置模式。4.3 WordCloud中文词云jieba分词与字体路径词云部分用WordCloud库配合jieba分词。中文词云和英文词云有本质区别英文按空格切就行中文必须先分词否则生成的全是一个个字。而且WordCloud默认字体不支持中文不指定font_path的话产出的图片里全是方块。import jieba from wordcloud import WordCloud import matplotlib.pyplot as plt comment_text .join(df[quote].dropna().tolist()) words .join(jieba.cut(comment_text)) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, background_colorwhite, max_words100 ).generate(words) wc.to_file(static/wordcloud.jpg)font_path在Windows下指向黑体字体文件Linux/Mac环境下需要换成各自系统的中文字体路径比如/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc。max_words100限制最多显示100个词避免画面过于拥挤。生成结果存到static/wordcloud.jpg页面里直接img src/static/wordcloud.jpg就能展示。如果词云图内容很稀疏多半是quote字段本身内容就短可以考虑把标题也拼进文本源效果会更丰富。5. 避坑与排查四个真实翻车现场从打包残留到中文乱码的修复记录5.1 压缩包里混入C源码文件numpy的编译残留别当成项目源码现象是解压后能看到fortranobject.c、cpu_avx512_icl.c这类C语言源码文件第一反应是项目需要C编译环境吓得不敢动。原因是打包时把Python环境里site-packages目录下numpy的C编译辅助文件一并打进了压缩包这部分和项目本身没有关系。解决方式是直接忽略这些文件项目的核心代码在.py文件和templates目录里。运行时也不需要C编译器只要pip install requests lxml pandas flask jieba wordcloud装好依赖就能跑。附带说一句打包时这几个.c文件其实可以删掉不影响任何功能。5.2 Flask页面打开后图表空白接口路径和渲染时机不对现象是http://127.0.0.1:5000能打开页面但图表区域空荡荡浏览器控制台报404或数据未定义。原因有两类最常见的是前端fetch(/data)拼错路径比如写成了fetch(data)相对路径会多一层目录其次是渲染时机问题图表初始化代码写在数据加载之前数据还没返回就调了setOption。解决方法是先用浏览器直接访问/data确认返回的是JSON数组再在JS里把fetch和setOption串成链式调用等数据到达后再初始化图表。经验是Flask调式阶段永远先验证接口再怀疑前端。5.3 词云图片全是方块字体和编码的双重失误现象是词云能生成但图片里全是黑色方框一个汉字都认不出来。原因是WordCloud默认使用英文DroidSansMono字体完全不含中文字形另一个连带问题是文本里如果混入未正确解码的字符分词结果也会连带异常。解决方法是必填font_path参数加载系统中文字体同时把文本源先做一次strip()和空值过滤。Mac系统推荐用系统自带的PingFang.ttcLinux下安装fonts-noto-cjk后指定相应路径。5.4 连续抓取后返回403UA一致与请求过密现象是前几页抓得好好的到第4、5页突然403 Forbidden再往下sleep再久也没用。原因是同一IP的请求频率过高且每次请求的UA完全相同特征过于明显。解决方法是维护一个UA列表随机切换把请求间隔从固定值改成随机区间如果仍然403可以手动打开页面复制Cookie到headers里再试。单线程跑Top250就已经足够完全不需要ThreadPoolExecutor并发只会加速被限。依赖库用途安装命令requests发送HTTP请求pip install requestslxml解析HTML结构pip install lxmlpandas数据清洗与分析pip install pandasflaskWeb服务pip install flaskjieba中文分词pip install jiebawordcloud生成词云图片pip install wordcloud6. 进阶技巧给页面加年份筛选用Ajax让图表局部刷新基础版的页面是打开即展示全部数据交互偏静态。既然Flask接口已经存在完全可以加一个年份下拉框让用户选择某一年的电影图表只展示对应年份的评分分布。后端改动很小给/data接口加一个year查询参数有值就过滤没值就返回全量。前端下拉框的change事件触发新的fetch拿到数据后重新拼装图表配置不需要刷新整个页面。const select document.getElementById(yearFilter); function refreshChart() { const year select.value; const query year ? ?year${year} : ; fetch(/data${query}) .then(res res.json()) .then(json { const bins {}; json.data.forEach(item { const key item.score.toFixed(1); bins[key] (bins[key] || 0) 1; }); chart.setOption({ xAxis: { data: Object.keys(bins).sort((a, b) a - b) }, series: [{ type: bar, data: Object.keys(bins).sort((a, b) a - b).map(k bins[k]) }] }); }); } select.addEventListener(change, refreshChart); refreshChart();对应的Flask接口过滤逻辑建议写这样一段app.route(/data) def data(): year request.args.get(year, ) records df[df[year] year] if year else df return jsonify({code: 0, data: records.to_dict(orientrecords)})这里有个坑从arg拿到的year永远是字符串而DataFrame里的year列是整数直接比较会类型不匹配。我一般会在清洗阶段把年份列转成字符串或者在后端int(year)一转避免这种隐晦的“类型不对但不报错”的玄学问题。一个更稳妥的习惯是统一成字符串因为下拉框值本身就是字符串。从那以后我每次做这类筛选交互都会强制走一遍检查清单接口先直接带参数访问、前端每次change确认发起新请求、最后看Network面板里返回的数据量是否变化。三步都过完再谈样式能省掉大半调试时间。这份源码包本身就具备往上扩展的基础能把你从“会爬数据”推进到“能做一个完整数据应用”的状态希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/18 17:13:32

Octop:Python依赖图确定性验证工具原理与实践

1. 这不是另一个Python包管理器——Octop到底在解决什么真问题?最近在PyPI上刷到一个叫Octop的新项目,GitHub star数涨得挺快,MIT许可证,Ruff风格的代码规范,文档里反复强调“no setuptools, no pip install -e, no py…

2026/10/18 17:13:32

Octop:轻量级 Python 环境生命周期管理工具

1. 项目概述:Octop 是什么,它解决的不是“安装 Python”而是“管理 Python 生态的隐性成本”你搜“Python 安装教程”“vscode 配置 python”“python 第三方库”,页面刷出来全是手把手教你怎么下载安装包、改环境变量、pip install -r requi…

2026/10/18 17:13:31

Android多网络环境下bindProcessToNetwork流量绑定实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 20:28:51

JPA List字段转String存储:@Convert与AttributeConverter实战

在使用 JPA 时,把一个 List 字段转成 String 存入数据库,最简单直接的方案就是 Convert 注解配合一个 AttributeConverter 实现。这个场景在业务里实在太常见了:用户标签、兴趣分组、图片地址列表、功能开关编号……这些数据说重要也重要&…

2026/10/18 20:28:51

appweb 8.2.1源码编译与嵌入式集成实战指南

简介:AppWeb 8.2.1 是 Embedthis 公司开源的嵌入式 Web 服务器源码包,面向资源受限的嵌入式设备与 IoT 应用场景,以轻量、高效、模块化为设计目标。它采用事件驱动架构,支持多线程与异步 I/O,内部集成 MbedTLS 加密库&…

2026/10/18 20:23:51

Claude Code 完全配置指南:从环境准备到团队协作与排错

写配置教程前,得先交代几句大实话。Claude Code 这段时间的讨论度确实高,但多数人的痛点不在“装不上”,而在“装完不知道怎么用得像样”。我见过太多人装完之后,第一句就是“怎么让它写代码”,第二句是“怎么让它别乱…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑