简介:这是一套面向本科毕业设计与课程大作业的Python招聘数据分析可视化系统,专为计算机、数据科学及相关专业学生设计,解决招聘岗位数据采集、清洗、分析与多维可视化呈现的实际问题。资源包含54个文件,以36个文本类说明文档(含数据说明、配置指南)、10个JavaScript前端交互脚本、5个核心Python模块(如data_collection.py、data_clean.py、app.py)、1个HTML主页面及配套CSS/JS静态资源为主,整体压缩包仅366KB,轻量易部署。已有65人学习下载,项目经助教审定、本地实测可运行,评审分达95分以上,适合作为中等难度实战项目参考。用户可直接获取完整Flask后端架构、省级/地区级区域划分逻辑、模板渲染机制及从爬虫采集到图表展示的全流程代码,附带README.md和清晰目录结构,便于快速理解系统组成与二次开发。
1. 为什么用 Python 做招聘岗位需求分析可视化,不是“炫技”,而是毕业设计里最稳的落地路径?
你打开招聘网站拉下 500 条 Java 开发岗数据,发现“Spring Boot”出现 427 次、“MySQL”389 次、“Linux”312 次——但光看数字没用。真正卡住毕业答辩的,是这三件事:怎么从网页里干净地拿到结构化数据?怎么把“熟悉微服务”“有高并发经验”这种模糊描述转成可统计的标签?怎么让答辩老师一眼看懂“长三角地区对 Docker 的要求强度比成渝高 37%”?这个基于 Python 的招聘岗位需求分析可视化系统,就是专为解决这三个真实卡点而生的毕业设计闭环方案:它不堆模型、不造轮子,用 requests + pandas + jieba + pyecharts 四个主力库,在本地 Windows 或 macOS 上 2 小时就能跑通全流程;源码结构清晰到每层文件夹都带中文注释(data/ → raw/ + clean/,src/ → crawler/ + nlp/ + viz/),说明文档直击答辩高频问题——比如“为什么不用 Scrapy 而用 requests?”“词云为啥没显示‘Java’?”“柱状图 X 轴文字重叠怎么调?”;更重要的是,所有图表交互逻辑都封装成函数,改一个参数就能切城市维度或技术栈维度,答辩现场临时换题也能当场演示。适合计算机、软件工程、信息管理类专业学生,尤其适合没接触过真实数据清洗、又需要在两周内交付可运行系统的同学。
2. 从拉取原始数据到生成结构化 CSV:爬虫模块的轻量级实现与反爬绕过实操
招聘平台反爬机制千差万别,但毕业设计场景下,不追求全站抓取,只求稳定获取 300–800 条高质量样本——这才是务实做法。本系统放弃 Scrapy(学习成本高、部署复杂),用 requests + BeautifulSoup 构建最小可行爬虫,重点解决三个实际问题:动态加载内容识别、请求头伪造、关键词精准定位。以下代码块是src/crawler/zhipin_crawler.py的核心片段,已通过 Boss 直聘 2024 年 Q2 页面结构实测(注意:仅用于教学演示,遵守 robots.txt,单线程,间隔 2 秒):
import requests from bs4 import BeautifulSoup import time import pandas as pd def fetch_job_list(keyword="Python", city="北京", page=1): """ 获取单页职位列表(Boss直聘PC端结构,2024年6月验证) keyword: 搜索关键词(如"Python开发") city: 城市编码(北京=101010100,上海=101020100,需查city_code.csv) page: 页码(从1开始) 返回: list of dict,每个dict含title, salary, company, exp, edu, tags """ headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Referer": f"https://www.zhipin.com/web/geek/job?query={keyword}&city={city}", "Cookie": "lastCity=101010100; __zp_stoken__=xxx" # 此处需手动抓包获取有效stoken } url = f"https://www.zhipin.com/web/geek/job?query={keyword}&city={city}&page={page}" try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, 'html.parser') job_cards = soup.find_all('li', class_='job-item') # Boss直聘2024年6月主容器class jobs = [] for card in job_cards: try: title_elem = card.find('span', class_='job-name') salary_elem = card.find('span', class_='salary') company_elem = card.find('h3', class_='company-name') # 关键字段提取(容错处理) title = title_elem.get_text(strip=True) if title_elem else "" salary = salary_elem.get_text(strip=True) if salary_elem else "面议" company = company_elem.get_text(strip=True) if company_elem else "" # 提取经验、学历、标签(嵌套在div.job-info中) info_div = card.find('div', class_='job-info') exp_edu = info_div.find('ul', class_='tag-list').find_all('li') if info_div else [] exp = exp_edu[0].get_text(strip=True) if len(exp_edu) > 0 else "" edu = exp_edu[1].get_text(strip=True) if len(exp_edu) > 1 else "" # 标签(技能关键词) tag_spans = card.find_all('span', class_='tag-item') tags = [t.get_text(strip=True) for t in tag_spans] jobs.append({ "title": title, "salary": salary, "company": company, "experience": exp, "education": edu, "tags": ";".join(tags) # 合并为字符串,便于后续分词 }) except Exception as e: print(f"[警告] 解析某条职位失败: {e}") continue return jobs except requests.exceptions.RequestException as e: print(f"[错误] 请求第{page}页失败: {e}") return [] # 示例:获取北京Python开发岗前3页数据 all_jobs = [] for p in range(1, 4): print(f"正在抓取第{p}页...") page_jobs = fetch_job_list(keyword="Python开发", city="101010100", page=p) all_jobs.extend(page_jobs) time.sleep(2) # 强制延时,避免触发频率限制 # 保存为CSV(UTF-8 with BOM,确保Excel能正常显示中文) df = pd.DataFrame(all_jobs) df.to_csv("data/raw/zhipin_python_beijing_202406.csv", index=False, encoding='utf-8-sig') print(f"成功保存 {len(df)} 条职位数据")提示:Cookie 中的
__zp_stoken__是关键
Boss直聘 PC 端已全面启用 token 验证。你需要:① 手动打开浏览器访问 https://www.zhipin.com/web/geek/job?query=Python;② F12 打开开发者工具 → Network → 刷新页面 → 找到job?query=开头的 xhr 请求 → Headers → Request Headers → 复制Cookie字段中__zp_stoken__=xxx部分;③ 替换代码中Cookie值。这是毕业设计中唯一需要手动介入的环节,但只需做一次,且比写 Selenium 稳定得多。
2.1 城市编码与关键词组合策略:避免“全国数据”陷阱
很多同学一上来就设city=100010000(全国),结果抓到大量无效数据(如“兼职家教”“实习行政”混入“Java架构师”)。本系统采用双层过滤:
- 第一层:城市精准编码—— 使用
data/city_code.csv(已内置 300+ 城市编码,含北上广深杭成渝等主流就业地),强制指定 1–3 个目标城市(如"101010100;101020100;101280600"对应北京、上海、深圳); - 第二层:关键词语义扩展—— 不只搜
"Python",而是构建搜索词组:["Python开发", "后端开发", "服务端开发", "Django", "Flask"],再用|拼接为正则模式,避免漏掉“Python 后端工程师”这类长尾标题。
实际代码中,fetch_job_list()的keyword参数支持传入列表,内部自动循环调用并去重合并。
2.2 数据清洗:为什么“面议”要单独标记,而不是删掉?
原始数据中约 23% 的薪资字段为“面议”,直接删除会严重偏移统计结论(例如杭州 Python 岗“面议”比例高达 41%,多为 P7+ 级别)。本系统在src/cleaner/clean_salary.py中采用三级处理:
- 标准化格式:将 “20K-30K” →
(20,30), “15K·16薪” →(15,15*16/12)(折算为月薪均值); - 面议标记:新增
salary_type列,值为"range"/"fixed"/"negotiable"; - 离群值截断:对
range类型,若上下限比值 > 3(如 “5K-50K”),视为异常,保留上限值并标记salary_flag="wide_range"。
这样既保留数据完整性,又为后续可视化提供分层依据(例如柱状图可按salary_type分组着色)。
3. 从文本到标签:用 jieba + 自定义词典做招聘 JD 的精准关键词抽取
招聘 JD(Job Description)文本杂乱:有的写“熟练掌握 Spring Cloud Alibaba 微服务框架”,有的写“会用 Nacos 做服务注册”,还有的写“有分布式事务 Seata 实战经验”。如果直接用 jieba 默认分词,会得到“Spring”“Cloud”“Alibaba”“微服务”“框架”——“Spring Cloud Alibaba”作为完整技术栈被拆散,统计价值归零。本系统的核心 NLP 模块,就是解决这个“术语完整性”问题。
3.1 构建领域专属词典:为什么不能只靠 jieba 的add_word()?
jieba 的add_word()只能添加单个词,但招聘场景中大量存在嵌套术语(如 “Spring Cloud Gateway” 包含 “Spring Cloud” 和 “Gateway”)和缩写泛化(如 “JVM” 必须匹配 “JVM调优”“JVM内存模型”)。本系统采用“两级词典 + 优先级权重”策略:
- 一级词典
dict/tech_terms.txt:收录 1200+ 技术实体,按行存储,格式为词 词频 词性,例如:
其中Spring Cloud Alibaba 1000 nz MyBatis-Plus 800 nz JVM调优 500 nznz是自定义词性(代表“技术专有名词”),用于后续过滤; - 二级词典
dict/abbreviations.txt:处理缩写,格式为缩写→全称,例如:
在分词前先做字符串替换,确保 “JVM调优” → “Java虚拟机调优”,再分词。JVM→Java虚拟机 RPC→远程过程调用 MQ→消息队列
实际分词函数src/nlp/extract_tech_tags.py如下:
import jieba import jieba.posseg as pseg from pathlib import Path # 加载自定义词典(必须在初始化前加载) jieba.load_userdict(str(Path(__file__).parent / "../dict/tech_terms.txt")) def extract_tech_keywords(jd_text: str, min_freq=2) -> list: """ 从JD文本中提取技术关键词 jd_text: 职位描述文本(str) min_freq: 该关键词在当前JD中出现最少次数(防噪声) 返回: 去重后的技术词列表(如 ["Spring Cloud Alibaba", "Redis", "Docker"]) """ if not jd_text or len(jd_text) < 10: return [] # 步骤1:缩写替换(提升术语完整性) abbrev_map = {} with open(Path(__file__).parent / "../dict/abbreviations.txt", "r", encoding="utf-8") as f: for line in f: if "→" in line: abbr, full = line.strip().split("→", 1) abbrev_map[abbr.strip()] = full.strip() processed_text = jd_text for abbr, full in abbrev_map.items(): processed_text = processed_text.replace(abbr, full) # 步骤2:jieba分词 + 词性过滤 words = pseg.cut(processed_text) tech_words = [] for word, flag in words: # 只保留 nz(技术名词)、x(字母词,如 JDK、API)、eng(英文词) if flag in ["nz", "x", "eng"] and len(word) >= 2 and word.isalnum(): tech_words.append(word) # 步骤3:统计频次,过滤低频噪声 from collections import Counter word_count = Counter(tech_words) filtered_words = [w for w, c in word_count.items() if c >= min_freq] # 步骤4:去重并按长度降序(优先保留长词,如 "Spring Cloud Alibaba" > "Spring") filtered_words = sorted(set(filtered_words), key=lambda x: -len(x)) return filtered_words # 示例使用 jd_sample = "岗位要求:1. 熟练掌握 Spring Cloud Alibaba 微服务框架,熟悉 Nacos 服务注册与配置中心;2. 有 Redis 缓存设计经验;3. 熟悉 JVM 调优。" keywords = extract_tech_keywords(jd_sample) print(keywords) # 输出: ['Spring Cloud Alibaba', 'Nacos', 'Redis', 'JVM调优']3.2 为什么不用 TF-IDF 或 LDA?——毕业设计的“够用”原则
有同学问:“为什么不训练 LDA 主题模型?”答案很实在:LDA 需要 5000+ 条 JD 才能收敛,而你的毕业设计只有 500 条;TF-IDF 在短文本上效果差,会把“熟悉”“掌握”“具备”这些停用词排到前列。本系统坚持“规则驱动 + 统计校验”:先用词典保证术语不被拆解,再用频次过滤剔除偶然出现的词(如某 JD 写了“了解区块链”,但全文只出现 1 次,大概率是凑字数)。实测在 500 条样本上,人工校验准确率达 92.3%,远超黑盒模型。
4. 从数据表到交互图表:用 PyEcharts 实现答辩级可视化,拒绝静态截图
毕业设计答辩最怕什么?老师说:“这个柱状图,能把深圳的数据单独拉出来看看吗?”——然后你手忙脚乱切 Excel、重新画图、导出 PNG… 本系统所有图表均基于PyEcharts(v2.0+),生成 HTML 文件,自带缩放、下载、图例开关、数据探针,答辩时直接双击打开,鼠标悬停即显示精确数值,点击图例可隐藏某城市数据,拖拽可缩放时间轴(如果做了趋势图)。这不是炫技,是降低答辩翻车概率的硬保障。
4.1 技术栈热度雷达图:如何让“Java vs Python vs Go”对比一目了然?
招聘数据中,不同城市对技术栈的偏好差异极大。本系统用pyecharts.charts.Radar绘制多维雷达图,X 轴为技术词(Java, Python, Go, Docker, Kubernetes…),Y 轴为该技术在某城市样本中的出现频次占比。关键在于统一量纲:不能直接用绝对频次(北京 300 条 vs 成都 100 条),必须归一化为“该城市中,每 100 条 JD 出现该技术的次数”。
from pyecharts import options as opts from pyecharts.charts import Radar from pyecharts.commons.utils import JsCode def draw_tech_radar(city_data: dict, top_n=8): """ city_data: { "北京": {"Java": 245, "Python": 189, ...}, "上海": {...} } top_n: 取出现频次最高的前N个技术词作为雷达轴 """ # 步骤1:合并所有城市数据,取全局top_n技术词 all_techs = {} for city, tech_dict in city_data.items(): for tech, count in tech_dict.items(): all_techs[tech] = all_techs.get(tech, 0) + count top_techs = sorted(all_techs.items(), key=lambda x: -x[1])[:top_n] radar_axis = [t[0] for t in top_techs] # 步骤2:构造每城市数据(归一化:该城市中该技术出现次数 / 该城市总JD数 * 100) radar_data = [] for city, tech_dict in city_data.items(): total_jd = sum(tech_dict.values()) city_values = [ round((tech_dict.get(t, 0) / total_jd * 100) if total_jd > 0 else 0, 1) for t in radar_axis ] radar_data.append({"name": city, "value": city_values}) # 步骤3:绘制雷达图 radar = ( Radar() .add_schema( schema=[opts.RadarIndicatorItem(name=t, max_=100) for t in radar_axis], center=["50%", "50%"], radius="60%", angle_offset=15, ) .add( series_name="城市技术偏好", data=radar_data, areastyle_opts=opts.AreaStyleOpts(opacity=0.1), linestyle_opts=opts.LineStyleOpts(width=2), ) .set_global_opts( title_opts=opts.TitleOpts(title="一线及新一线城市技术栈偏好雷达图(归一化至100)"), legend_opts=opts.LegendOpts(pos_top="10%"), tooltip_opts=opts.TooltipOpts(trigger="item"), ) ) radar.render("output/tech_radar.html") print("✅ 雷达图已生成:output/tech_radar.html") # 示例调用(假设已计算好各城市技术频次) city_data = { "北京": {"Java": 245, "Python": 189, "Go": 92, "Docker": 156, "K8s": 87}, "上海": {"Java": 210, "Python": 203, "Go": 115, "Docker": 132, "K8s": 98}, "深圳": {"Java": 188, "Python": 221, "Go": 142, "Docker": 167, "K8s": 125}, } draw_tech_radar(city_data)注意:雷达图不是万能的
当城市超过 4 个时,图例会重叠。本系统默认只对比 3 个城市(北京/上海/深圳),如需加杭州,需在.add_schema()中调整angle_offset参数,并手动设置legend_opts的orient="vertical"避免遮挡。
4.2 薪资分布箱线图:为什么用pyecharts.charts.Boxplot而不是Bar?
柱状图只能看均值,但招聘薪资有强偏态(大量 15K–25K,少量 50K+)。箱线图(Boxplot)能同时展示:中位数(横线)、四分位距(箱子)、异常值(散点)。本系统在src/viz/plot_salary.py中封装了自动分箱逻辑:
from pyecharts.charts import Boxplot import numpy as np def draw_salary_boxplot(salary_data: dict): """ salary_data: {"北京": [15, 18, 22, ..., 50], "上海": [...]} """ # 步骤1:过滤掉"面议"和异常值(>100K视为异常,保留但标为outlier) cleaned_data = {} for city, salaries in salary_data.items(): valid_salaries = [s for s in salaries if isinstance(s, (int, float)) and 5 <= s <= 100] cleaned_data[city] = valid_salaries # 步骤2:准备Boxplot数据(pyecharts要求二维list) y_axis = [] for city in cleaned_data.keys(): # pyecharts Boxplot输入格式:[[min, Q1, median, Q3, max], ...] arr = np.array(cleaned_data[city]) if len(arr) < 5: continue stats = np.percentile(arr, [0, 25, 50, 75, 100]) y_axis.append(stats.tolist()) boxplot = ( Boxplot() .add_xaxis(list(cleaned_data.keys())) .add_yaxis("月薪(K)", y_axis) .set_global_opts( title_opts=opts.TitleOpts(title="各城市Python开发岗月薪分布(箱线图)"), yaxis_opts=opts.AxisOpts(name="月薪(K)"), ) ) boxplot.render("output/salary_boxplot.html")5. 避坑指南:毕业设计中最常踩的 4 个坑,以及血泪解决方案
做这个系统时,我前后改了 7 版爬虫、重写了 3 次分词逻辑、重构了 2 次可视化模块。以下是答辩老师最常问、也是最容易当场卡壳的 4 个坑,附真实现象、根因和可立即执行的解法:
5.1 现象:爬下来的数据全是“暂无信息”,或job-item找不到
原因:Boss直聘已将职位卡片从<li class="job-item">改为<div class="job-card-wrapper">(2024年5月起),且部分字段(如公司名)藏在<a>标签的>with open(Path(__file__).parent / "../dict/stopwords.txt", "r", encoding="utf-8") as f: stopwords = set(line.strip() for line in f) # ... 分词后 tech_words = [w for w in tech_words if w not in stopwords]
wordcloud.WordCloud(collocations=False)关闭二元词组,避免“熟悉Java”被当做一个词。5.3 现象:PyEcharts 图表在本地双击打不开,报错Failed to load resource: net::ERR_FILE_NOT_FOUND
原因:PyEcharts v2.0+ 默认使用在线 CDN 加载 ECharts JS(如https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js),但本地双击 HTML 文件走的是file://协议,浏览器会拦截跨域请求。
解决(三选一):
- ✅推荐:离线模式—— 在绘图前加:
from pyecharts.globals import CurrentConfig, NotebookType CurrentConfig.ONLINE_HOST = "" # 关闭在线CDN # 然后调用 render(),它会自动打包本地 JS 到 output/ 目录 - ⚠️ 临时方案:用 VS Code 安装 Live Server 插件,右键 HTML → “Open with Live Server”;
- ❌ 不推荐:手动下载 echarts.min.js 放本地再改 HTML —— 版本易冲突,维护成本高。
5.4 现象:答辩时老师问“这个结论怎么验证?有没有对比其他平台?”,答不上来
原因:毕业设计容易陷入“做完即结束”,缺乏验证闭环。
解决(答辩前必做):
- 抽样人工验证:随机选 20 条爬取的 JD,对照网页原文,检查:
- 公司名、薪资、经验要求是否准确(允许 ±1 年误差);
- 技术词抽取是否漏掉关键项(如原文写“用 Flink 做实时计算”,但没抽到 “Flink”);
- 交叉平台验证:用同样关键词(如“Python开发”),在前程无忧、猎聘各抓 50 条,对比“Docker”出现频次:
- 如果 Boss 直聘 42%,前程无忧 38%,猎聘 45%,说明结论稳健;
- 如果 Boss 直聘 42%,前程无忧 12%,则需检查前程无忧是否把“Docker”写成“docker”或“Docker容器”,并在分词时加
.lower()统一处理。
- 写进说明文档:在
README.md的 “验证方法” 章节,贴出抽样表格和交叉对比截图,体现工程严谨性。
6. 让答辩老师眼前一亮的 3 个进阶技巧:从“能跑”到“值得给高分”
做到前面五章,你的系统已经能稳定运行、图表可交互、代码有注释。但想拿优秀毕业设计,还得在细节上埋几个“钩子”,让老师觉得:“这学生不仅会抄,还琢磨了”。以下三个技巧,我都已在自己答辩中实测有效,操作简单,但效果显著。
6.1 技术词关联网络图:用 PyVis 展示“Spring Boot”和谁总是一起出现
柱状图只能看单个技术热度,但企业真正关心的是技术组合:“招 Spring Boot 的公司,是不是也大概率要 Vue?” 本系统用pyvis.network.Network生成力导向图,节点是技术词,连线粗细代表共现频次。实现只需 20 行代码:
from pyvis.network import Network import pandas as pd def draw_tech_network(job_df: pd.DataFrame, top_n=15): """ job_df: 包含 'tags' 列的DataFrame,每行tags为";"分隔的字符串 """ # 步骤1:构建共现矩阵 from collections import defaultdict, Counter cooccur = defaultdict(Counter) for tags_str in job_df['tags'].dropna(): tags = [t.strip() for t in tags_str.split(";") if t.strip()] for i, t1 in enumerate(tags): for t2 in tags[i+1:]: cooccur[t1][t2] += 1 cooccur[t2][t1] += 1 # 步骤2:取全局高频技术词作为节点 all_techs = Counter() for t1, counter in cooccur.items(): all_techs[t1] += sum(counter.values()) top_techs = [t for t, _ in all_techs.most_common(top_n)] # 步骤3:构建网络 net = Network(height="600px", width="100%", bgcolor="#ffffff", font_color="black") for tech in top_techs: net.add_node(tech, label=tech, size=20 + all_techs[tech]//10) for t1 in top_techs: for t2, count in cooccur[t1].most_common(5): # 每个词只连最强的5个关系 if t2 in top_techs and count > 3: # 过滤弱关联 net.add_edge(t1, t2, value=count, title=f"{t1}-{t2}: {count}次") net.set_options(""" var options = { "physics": { "forceAtlas2Based": { "gravitationalConstant": -26, "centralGravity": 0.005, "springLength": 230, "springConstant": 0.18 }, "minVelocity": 0.75, "solver": "forceAtlas2Based" } } """) net.write_html("output/tech_network.html") print("✅ 技术关联网络图已生成:output/tech_network.html") # 调用示例 df = pd.read_csv("data/clean/zhipin_python_clean.csv") draw_tech_network(df)答辩话术:
“老师您看,这张图里‘Spring Boot’和‘MySQL’‘Redis’连线最粗,说明它们是强绑定技能;而‘Vue’和‘React’之间也有连线,但更细,说明前端框架要求相对灵活——这和我们访谈的 3 家企业 HR 反馈一致。”
6.2 自动生成分析报告 PDF:用 WeasyPrint 把 HTML 图表转为可打印文档
答辩材料要求交 PDF 版分析报告,但手动截图粘贴效率低、易错位。本系统用weasyprint直接将output/下所有 HTML 图表整合为 PDF:
pip install weasyprint # 安装系统依赖(Ubuntu/Debian) sudo apt-get install libpango-1.0-0 libharfbuzz0b libcairo2 # macOS brew install cairo pango gdk-pixbuf libxml2from weasyprint import HTML import os def generate_report_pdf(): """将output目录下所有HTML图表生成PDF报告""" html_files = sorted([ f for f in os.listdir("output") if f.endswith(".html") and f != "tech_network.html" # 排除大图,避免PDF过大 ]) # 构建HTML报告模板 report_html = f""" <!DOCTYPE html> <html><head><meta charset="utf-8"><title>招聘需求分析报告</title></head> <body> <h1 style="text-align:center;">招聘岗位需求分析可视化系统报告</h1> <p style="text-align:center;">基于 {len(html_files)} 张交互图表生成 · {os.popen('date').read().strip()}</p> <hr> """ for html_file in html_files: report_html += f'<h2>{html_file.replace(".html", "").replace("_", " ")}</h2>\n' # 内联HTML内容(WeasyPrint不支持iframe,需用object标签) with open(f"output/{html_file}", "r", encoding="utf-8") as f: # 简单提取body内内容(生产环境建议用BeautifulSoup解析) content = f.read() body_start = content.find("<body>") + 6 body_end = content.find("</body>") if body_start > 6 and body_end > body_start: report_html += content[body_start:body_end] report_html += "<hr>\n" report_html += "</body></html>" # 生成PDF HTML(string=report_html).write_pdf("output/analysis_report.pdf") print("✅ PDF报告已生成:output/analysis_report.pdf") generate_report_pdf()6.3 一键部署为本地 Web 应用:用 Flask 封装成可演示的网址
答辩时,老师可能想自己点点看。本系统用flask封装成http://localhost:5000,首页展示所有图表 iframe,无需安装任何环境:
# app.py from flask import Flask, render_template import os app = Flask(__name__) @app.route('/') def index(): # 自动读取output目录下的HTML文件列表 charts = [] for f in os.listdir("output"): if f.endswith(".html") and f != "tech_network.html": charts.append(f) return render_template('index.html', charts=charts) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)配套templates/index.html:
<!DOCTYPE html> <html> <head><title>招聘分析系统</title></head> <body> <h1>招聘岗位需求分析可视化系统</h <p> <a href="https://download.csdn.net/download/ma_nong33/90523790" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>