☰
Python招聘数据爬取与可视化分析实战:从网页到交互图表
2026/10/3 3:21:19 网站建设 项目流程

简介:本资源是一套完整的Python招聘数据分析与可视化毕业设计项目,面向计算机、数据科学及相关专业本科生,解决求职市场数据采集、清洗、分析与交互式呈现等典型实践问题。压缩包共54个文件,含14个核心Python脚本(涵盖爬虫spider_main.py、数据分析analyze_data.py、图表生成create_chart.py及Web服务server.py)、10个HTML前端页面(含show.html、index.html等交互视图)、14张图表图片(PNG/JPG),以及配置文件(ini)、样式脚本(CSS/JS)和测试数据(CSV),整体8.59MB,结构清晰,模块职责分明。已有1547人学习下载,资源附带完整文档说明与README.md,提供从环境配置(install_package.bat)、数据获取、分析逻辑到本地部署的全流程支持,特别适合毕业设计选题参考、课程设计复现及数据分析可视化能力进阶训练。

1. 这不是又一个“爬完拉倒”的毕业设计:它真能把智联/前程无忧原始招聘页转成可交互的岗位热力图、薪资分布直方图和技能词云,且所有分析逻辑全在本地跑通——适合想用真实数据练手、但被“假数据+静态图表”毕设模板折磨到失眠的同学

你见过多少个标着“Python招聘数据分析”的毕设项目?点开一看,是用 Excel 手动填了 200 条虚构岗位信息,调matplotlib画三张饼图,再套个flask框架把 HTML 塞进去,最后在答辩 PPT 上写“实现可视化大屏”。这不是毕设,这是行为艺术。而这个源码包——WorkAggregation-master——从spider_main.py开始就拒绝表演:它默认配置直连前程无忧(conf.ini中source = qiancheng),用requests + BeautifulSoup稳定抓取带真实公司名、薪资范围、学历要求、工作经验、技能标签的结构化数据;analyze_data.py里不做“平均值”这种安全牌,而是按城市+行业双维度聚合计算中位数薪资、技能共现频次、JD 文本 TF-IDF 向量;create_chart.py不只输出 PNG,而是生成带hover交互的 ECharts 图表,嵌入templates/show.html后能直接拖拽缩放热力图、点击筛选某类岗位。它不是教你怎么“假装分析”,而是给你一套能在答辩现场当场改参数、换城市、重跑分析链路的真实工作流。如果你正卡在“数据哪来→怎么清洗→如何归因→怎样讲清楚结论”这四步闭环上,这个包就是你缺的那块拼图——它不教你 Python 语法,但它告诉你:当pandas的groupby遇到空值、当jieba分词撞上“Java开发工程师”和“Java架构师”这种同源异构词、当pyecharts渲染失败却只报KeyError: 'series'时,该翻哪行代码、改哪个参数、查哪份日志。


2. 从 raw HTML 到可交互图表:完整数据流水线拆解与关键模块实操

2.1 爬虫模块:为什么不用 Scrapy 而选 requests + BeautifulSoup?三个硬约束下的务实选择

这个项目没上 Scrapy,不是不会,是不敢。毕设场景下有三个现实约束:第一,目标网站反爬强度中等(前程无忧、智联对未登录用户限制 IP 频率但不校验 JS 渲染);第二,学生机性能有限(4GB 内存跑 Scrapy 多进程常 OOM);第三,答辩演示需快速验证(Scrapy 项目启动慢,scrapy crawl jobspider要等 3 秒才出 log)。所以作者用requests控制 UA 和 session,用BeautifulSoup解析 DOM,把复杂度压到最低。

核心逻辑在spider/spider_main.py:

# spider/spider_main.py 关键片段 def fetch_page(url, headers, timeout=10): try: response = requests.get(url, headers=headers, timeout=timeout) response.raise_for_status() return response.text except requests.exceptions.RequestException as e: logging.error(f"请求失败 {url}: {e}") return None def parse_qiancheng(html): soup = BeautifulSoup(html, 'lxml') jobs = [] for item in soup.find_all('div', class_='job-primary'): title = item.find('div', class_='job-title').get_text(strip=True) if item.find('div', class_='job-title') else '' salary = item.find('span', class_='red').get_text(strip=True) if item.find('span', class_='red') else '' # 注意:这里没用 .text 而用 get_text(strip=True),避免换行符污染后续正则提取 company = item.find('div', class_='company-text').find('a').get_text(strip=True) if item.find('div', class_='company-text') else '' # 提取技能标签:前程无忧的技能是 <p class="tags"> 下的多个 <span> tags = [tag.get_text(strip=True) for tag in item.find('p', class_='tags').find_all('span')] if item.find('p', class_='tags') else [] jobs.append({ 'title': title, 'salary': salary, 'company': company, 'tags': ';'.join(tags) # 统一用分号分隔,便于后续 pandas.str.split }) return jobs

提示:parse_qiancheng函数里tags字段用';'.join(tags)而非列表,是因为后续analysis/analyze_data.py中pandas.read_csv默认将字符串列读为 object,若存为 list 会触发TypeError: unhashable type: 'list',导致value_counts()报错。这是新手最容易栽的坑——以为“存结构化数据就要用 list”,结果在聚合阶段翻车。

conf.ini是控制入口:

[source] # 可选值:qiancheng(前程无忧)、zhilian(智联招聘)、liepin(猎聘) source = qiancheng base_url = https://search.51job.com/list/ # 前程无忧搜索页 URL 模板,{city} 和 {keyword} 由脚本替换 search_url_template = https://search.51job.com/list/{city},000000,0000,00,9,99,{keyword},2,1.html [spider] # 每页抓取条数(前程无忧每页最多 40 条) page_size = 40 # 最大抓取页数(避免答辩时跑太久) max_pages = 3 # 请求间隔(秒),防被封 delay = 2

install_package.bat是 Windows 友好型依赖安装器,内容极简:

@echo off pip install -r requirements.txt --trusted-host pypi.tuna.tsinghua.edu.cn echo 依赖安装完成,请检查是否出现红色错误信息 pause

它强制走清华镜像源,解决国内学生机pip install卡死问题。requirements.txt里pandas==1.3.5而非pandas>=1.3.0,因为高版本pandas对空字符串''的str.extract()行为变更,会导致process_data.py中薪资解析失败——这点在第 4 章避坑部分详述。

2.2 数据清洗与结构化:process_data.py如何把“15K-25K/月”变成数值列并处理缺失

原始爬取数据最大的痛点是“薪资字段非结构化”。前程无忧返回的是"15K-25K/月",智联可能是"15000-25000元/月",猎聘甚至有"面议"。process_data.py用正则+条件分支做标准化,而非简单str.replace:

# analysis/process_data.py 关键函数 def extract_salary_range(salary_str): """ 从薪资字符串中提取最小值和最大值(单位:元/月) 支持格式:'15K-25K/月', '15000-25000元/月', '面议', '8K以上' 返回 tuple: (min_salary, max_salary),单位均为整数 """ if not isinstance(salary_str, str) or '面议' in salary_str: return (None, None) # 统一替换 K 为 000,去掉单位 salary_clean = re.sub(r'[Kk]', '000', salary_str) salary_clean = re.sub(r'[元/月]', '', salary_clean) # 匹配数字范围:如 '15000-25000' range_match = re.search(r'(\d+)-(\d+)', salary_clean) if range_match: min_val = int(range_match.group(1)) max_val = int(range_match.group(2)) return (min_val, max_val) # 匹配单值+修饰词:如 '8K以上' -> (8000, None) single_match = re.search(r'(\d+)(?:K|k|元)?[以\s]*上', salary_clean) if single_match: val = int(single_match.group(1)) * 1000 if 'K' in salary_str else int(single_match.group(1)) return (val, None) # 兜底:尝试提取单个数字(如 '20K') single_num = re.search(r'(\d+)(?:K|k)', salary_clean) if single_num: return (int(single_num.group(1)) * 1000, int(single_num.group(1)) * 1000) return (None, None) # 在主流程中调用 df['salary_min'], df['salary_max'] = zip(*df['salary'].apply(extract_salary_range)) # 生成中间列:用 min_salary 填充缺失的 max_salary(保守估计) df['salary_estimated'] = df['salary_min'].fillna(df['salary_max']).fillna(8000) # 设默认值 8K

这段代码的价值在于:它把模糊的业务规则(“面议”算 8K、“以上”取下限、“K”要转千)编码成可复现的逻辑,而不是靠人工在 Excel 里 Ctrl+H。df['salary_estimated']是后续所有统计(如城市薪资中位数)的基准列,它的生成过程决定了分析结论的可信度。

process_data.py还处理技能标签的标准化:

# 技能去重与合并:把 "Java", "JAVA", "java开发" 统一为 "Java" def normalize_skill(skill): skill = skill.strip().lower() # 映射常见别名 alias_map = { 'java': ['java', 'java开发', 'j2ee', 'springboot'], 'python': ['python', 'python开发', 'django', 'flask'], 'sql': ['sql', 'mysql', 'oracle', 'postgresql'] } for standard, variants in alias_map.items(): if skill in variants: return standard.title() # 首字母大写 return skill.title() # 应用到 tags 列(之前用 ';' 分隔) df['skills_normalized'] = df['tags'].str.split(';').apply( lambda x: [normalize_skill(s) for s in x if s.strip()] )

这样做的好处是:analyze_data.py中skills_normalized列可直接explode()展开,再value_counts()得到真实技能热度,避免“Java”和“JAVA”被算作两个技能。

2.3 分析引擎:analyze_data.py如何用 groupby 实现“城市×行业”交叉分析

毕设答辩最怕被问:“你这个平均薪资,是所有岗位的平均,还是某类岗位的平均?”——这暴露了分析维度缺失。analyze_data.py用pandas.DataFrame.groupby构建多维透视,核心是agg()的灵活组合:

# analysis/analyze_data.py 关键分析函数 def analyze_by_city_industry(df): """ 按城市和行业(title 中提取)分组,计算: - 岗位数 count - 估算薪资中位数 median - 技能词频 top5(需先 explode skills_normalized) """ # 步骤1:提取行业关键词(简单规则:title 含 '开发'→IT,'会计'→财务,'销售'→销售) def extract_industry(title): if not isinstance(title, str): return '其他' title_lower = title.lower() if any(kw in title_lower for kw in ['开发', '工程师', '程序员', '架构']): return 'IT技术' elif any(kw in title_lower for kw in ['会计', '财务', '审计']): return '财务' elif any(kw in title_lower for kw in ['销售', '商务', '市场']): return '销售/市场' else: return '其他' df['industry'] = df['title'].apply(extract_industry) # 步骤2:按 city 和 industry 分组 grouped = df.groupby(['city', 'industry']) # 步骤3:聚合指标(注意:median 必须用 numeric_only=False,否则含 NaN 列报错) result = grouped.agg( job_count=('title', 'count'), salary_median=('salary_estimated', 'median'), salary_mean=('salary_estimated', 'mean'), # 技能词频需单独处理:先 explode,再 groupby 计数 ).reset_index() # 步骤4:为每个分组计算 top5 技能(较耗时,仅演示逻辑) skill_top5_list = [] for (city, industry), group_df in grouped: # 展开技能列表 skills_exploded = group_df.explode('skills_normalized') # 过滤空值并计数 skill_counts = skills_exploded['skills_normalized'].value_counts().head(5) skill_top5_list.append({ 'city': city, 'industry': industry, 'top_skills': skill_counts.index.tolist(), 'skill_counts': skill_counts.values.tolist() }) skill_df = pd.DataFrame(skill_top5_list) return result.merge(skill_df, on=['city', 'industry'], how='left') # 调用示例 analysis_result = analyze_by_city_industry(df) print(analysis_result[['city', 'industry', 'job_count', 'salary_median', 'top_skills']])

这个函数输出的analysis_resultDataFrame,就是create_chart.py的输入源。它把“北京 IT 技术岗平均薪资 22K”这种笼统结论,拆解成“北京 IT 技术岗中位数 20.5K,其中 Java 出现 187 次、Python 142 次、MySQL 96 次”——这才是答辩时能展开讲的细节。


3. 可视化落地:ECharts 图表生成与 Flask 服务部署的轻量级实现

3.1create_chart.py:如何用 PyEcharts 生成可交互 HTML 而非静态 PNG

很多毕设可视化止步于plt.savefig('salary.png'),但这无法满足“答辩现场点击切换城市”的需求。本项目用pyecharts生成带 JavaScript 交互的 HTML 文件,核心是Chart.render()和Chart.overlap():

# analysis/create_chart.py 关键函数 from pyecharts import options as opts from pyecharts.charts import Bar, Line, HeatMap, WordCloud from pyecharts.commons.utils import JsCode def generate_salary_bar_chart(df_city_salary, output_path='static/charts/salary_bar.html'): """ 生成城市薪资柱状图(带 hover 显示中位数和均值) df_city_salary: 包含 'city', 'salary_median', 'salary_mean' 的 DataFrame """ bar = Bar(init_opts=opts.InitOpts(width="1000px", height="600px")) # X轴:城市名 cities = df_city_salary['city'].tolist() # Y轴:中位数薪资(主系列) medians = df_city_salary['salary_median'].round(1).tolist() # 均值作为辅助系列(用 line 叠加) means = df_city_salary['salary_mean'].round(1).tolist() bar.add_xaxis(cities) bar.add_yaxis("薪资中位数(元/月)", medians, label_opts=opts.LabelOpts(is_show=True, position="top")) # 叠加均值折线 line = Line() line.add_xaxis(cities) line.add_yaxis("薪资均值(元/月)", means, linestyle_opts=opts.LineStyleOpts(type_="dashed"), label_opts=opts.LabelOpts(is_show=True, position="bottom")) # 合并图表 bar.overlap(line) # 配置全局选项 bar.set_global_opts( title_opts=opts.TitleOpts(title="各城市岗位薪资中位数与均值对比"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-45)), # 城市名旋转防重叠 yaxis_opts=opts.AxisOpts(name="月薪(元)"), tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="shadow"), # 悬停显示 datazoom_opts=[opts.DataZoomOpts()], # 横向滚动条,应对城市过多 ) bar.render(output_path) print(f"薪资柱状图已生成:{output_path}") # 调用示例 generate_salary_bar_chart(analysis_result[['city', 'salary_median', 'salary_mean']])

生成的salary_bar.html直接嵌入templates/show.html的<iframe>或通过{{ url_for('static', filename='charts/salary_bar.html') }}引入。pyecharts会自动注入echarts.min.js和渲染逻辑,无需额外配置 CDN。

注意:pyecharts版本必须为1.9.1(见requirements.txt),因为 2.x 版本 API 全面重构,add_yaxis()参数名变更,会导致create_chart.py报TypeError: add_yaxis() got an unexpected keyword argument 'label_opts'。

3.2 Flask Web 服务:server.py如何用最少代码支撑多页面路由

毕设答辩需要“打开浏览器就能看”,server.py就是那个入口。它没用 Flask-Admin 或大型框架,只用原生@app.route:

# server.py from flask import Flask, render_template, send_from_directory import os app = Flask(__name__, template_folder='templates', static_folder='static') # 主页路由 @app.route('/') def index(): return render_template('index.html') # 数据展示页 @app.route('/show') def show(): return render_template('show.html') # 交互式分析页(支持前端传参) @app.route('/interaction') def interaction(): return render_template('interaction.html') # 静态文件路由(图表 HTML、JS、CSS) @app.route('/static/<path:filename>') def static_files(filename): return send_from_directory('static', filename) # 启动服务(仅开发用,生产环境用 gunicorn) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) # debug=True 方便查错

templates/下的 HTML 文件用 Jinja2 模板语法嵌入动态数据:

<!-- templates/show.html --> <!DOCTYPE html> <html> <head> <title>招聘数据分析可视化</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <h1>招聘数据分析大屏</h1> <!-- 嵌入生成的图表 --> <iframe src="{{ url_for('static', filename='charts/salary_bar.html') }}" width="100%" height="600px" frameborder="0"></iframe> <!-- 技能词云(用 canvas 实现,非 pyecharts) --> <div id="wordcloud" style="width:100%;height:400px;"></div> <script src="{{ url_for('static', filename='js/wordcloud.js') }}"></script> </body> </html>

static/js/wordcloud.js是独立的 D3.js 词云实现,与pyecharts解耦——这样即使pyecharts渲染失败,词云仍可用。这种“多技术栈兜底”思维,是工程化意识的体现。

3.3templates/目录结构:为什么us.html和interaction.html是答辩加分项

templates/不只是放index.html,它包含三个关键页面:

  • index.html:项目介绍页,含技术栈图标(Python、Pandas、ECharts)、功能列表、作者信息;
  • show.html:数据大屏页,嵌入所有create_chart.py生成的图表(柱状图、热力图、词云);
  • interaction.html:交互分析页,含下拉框选择城市、行业、技能,前端通过fetch调用后端/api/analyze接口(该接口在server.py中需自行补充,源码未提供,但留了interaction.py占位)。

us.html(User Story 页面)是隐藏彩蛋:它用 Markdown 渲染一份README.md的精简版,描述“用户如何用这个系统发现北京 Java 岗位薪资高于深圳但技能要求更侧重 Spring Cloud”,把技术实现升维到业务价值。答辩时老师问“这有什么用?”,你就打开us.html,指着“某求职者通过热力图发现杭州电商运营岗竞争小、薪资涨得快”那段话——比讲一百行代码更有说服力。


4. 避坑指南:那些让答辩前夜崩溃的 4 个真实 Bug 与血泪修复方案

4.1 现象:spider_main.py运行后data/jobs.csv是空的,日志只显示INFO:root:开始抓取第1页就卡住

原因:前程无忧更新了反爬策略,conf.ini中base_url已失效,新搜索页 URL 结构变为https://we.51job.com/pc/search?keyword=Python&area=000000&pageno=1,而原代码仍用旧域名search.51job.com。requests.get()返回 403 或空 HTML,BeautifulSoup解析后找不到job-primaryclass。
解决:

  1. 修改conf.ini的base_url为https://we.51job.com/pc/search?;
  2. 在spider_main.py中重写build_url()函数:
def build_url(keyword, city, page): # 新 URL 格式:https://we.51job.com/pc/search?keyword=Python&area=000000&pageno=1 params = { 'keyword': keyword, 'area': city_code_map.get(city, '000000'), # 需维护 city_code_map 字典 'pageno': page } return base_url + urllib.parse.urlencode(params)
  1. 补充city_code_map = {'北京': '010000', '上海': '020000', '深圳': '030000'}—— 这些编码可在前程无忧网页源码中找到。

4.2 现象:analyze_data.py报错KeyError: 'salary_estimated',但df.columns明明有这一列

原因:process_data.py中extract_salary_range()函数对salary_str类型判断不严。当某行salary字段为NaN(pandas 读 CSV 时的空值),isinstance(salary_str, str)返回False,函数返回(None, None),但zip(*df['salary'].apply(...))会把None写入salary_min列,导致df['salary_estimated'] = df['salary_min'].fillna(...)时salary_min全为None,fillna()无效,最终salary_estimated列全NaN。
解决:在process_data.py开头加类型强转:

# 在 apply 前确保 salary 列为 string df['salary'] = df['salary'].astype(str) # 再执行 extract_salary_range df['salary_min'], df['salary_max'] = zip(*df['salary'].apply(extract_salary_range))

4.3 现象:create_chart.py生成的salary_bar.html打开后图表空白,浏览器控制台报Uncaught ReferenceError: echarts is not defined

原因:pyecharts1.9.1 默认生成的 HTML 使用本地echarts.min.js,但static/目录下没有该文件。源码包中static/js/只有wordcloud.js,缺少echarts.min.js。
解决:

  1. 下载echarts.min.js(v5.4.3,与pyecharts1.9.1 兼容)到static/js/;
  2. 修改create_chart.py的render()调用,强制指定 JS CDN:
bar.render_embed() # 改为 render_embed(),它会内联 JS 代码,不依赖外部文件 # 或者在 render() 前设置全局 CDN from pyecharts.globals import CurrentConfig CurrentConfig.ONLINE_HOST = "https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/"

4.4 现象:server.py运行后访问http://localhost:5000/show报 404,但index.html能打开

原因:Flask 的template_folder路径错误。源码包解压后目录结构是WorkAggregation-master/templates/,但server.py中写的是template_folder='templates',如果用户把整个 zip 解压到D:\project\,则实际路径是D:\project\WorkAggregation-master\templates\,而server.py在D:\project\WorkAggregation-master\下运行,'templates'相对路径正确。但如果用户错误地把templates/文件夹剪切到父目录,路径就断了。
解决:用绝对路径保险:

import os app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), 'templates'), static_folder=os.path.join(os.path.dirname(__file__), 'static'))

5. 进阶技巧:用analyze_spark.py替换 Pandas 实现万级数据加速,以及答辩现场应急方案

5.1 当数据量突破 5000 条:为什么analyze_spark.py是预留的性能逃生通道

源码包里analysis/analyze_spark.py是个半成品——它存在,但analysis_main.py并未调用。这是因为 Spark 在毕设场景属于“超纲项”,但作者留了接口,只为应对一种情况:答辩前两天,导师突然说“你这数据太少了,去抓 10000 条看看”。此时重跑pandas链路可能卡死(内存不足、groupby耗时过长),而analyze_spark.py就是后悔药。

它基于pyspark,核心是把pandas.DataFrame转为SparkDataFrame:

# analysis/analyze_spark.py(补全版) from pyspark.sql import SparkSession from pyspark.sql.functions import col, when, regexp_extract, explode, split from pyspark.sql.types import StructType, StructField, StringType, DoubleType, IntegerType def init_spark(): # 本地模式启动 Spark,无需集群 spark = SparkSession.builder \ .appName("JobAnalysis") \ .master("local[*]") \ .config("spark.driver.memory", "4g") \ .getOrCreate() return spark def spark_analyze_large_data(spark, csv_path): # 定义 schema,避免 inferSchema 的性能损耗 schema = StructType([ StructField("title", StringType(), True), StructField("salary", StringType(), True), StructField("city", StringType(), True), StructField("tags", StringType(), True) ]) df = spark.read.csv(csv_path, header=True, schema=schema) # 步骤1:解析薪资(用 Spark SQL 内置函数,比 UDF 快) df = df.withColumn("salary_min", when(col("salary").contains("面议"), None) .otherwise(regexp_extract(col("salary"), r'(\d+)K', 1).cast("int") * 1000) ) # 步骤2:爆炸技能标签(比 pandas explode 快 3 倍) df_skills = df.withColumn("skill", explode(split(col("tags"), ";"))) # 步骤3:聚合(Spark 的 groupBy 天然分布式) result = df_skills.groupBy("city", "skill") \ .count() \ .orderBy("city", col("count").desc()) \ .limit(10) return result # 调用示例(需先 pip install pyspark) if __name__ == "__main__": spark = init_spark() result_df = spark_analyze_large_data(spark, "data/jobs.csv") result_df.show() # 或 result_df.toPandas() 转回 pandas 供 create_chart.py 用

提示:pyspark安装只需pip install pyspark==3.3.2(与 Hadoop 3.3 兼容),无需配置 Hadoop 环境变量。local[*]表示用本机所有 CPU 核心,4GB 内存足够处理 5 万行数据。

5.2 答辩现场 5 分钟应急方案:当图表不加载、爬虫挂掉、Flask 报错时,如何体面救场

我带过 12 届毕设,最惨的一次是学生答辩时pyecharts渲染失败,大屏一片空白。他没慌,打开data/jobs.csv,用 Excel 快速做了三件事:

  1. 用数据透视表:行=城市,列=行业,值=岗位数 → 得到热力图雏形;
  2. 用条件格式:对“薪资中位数”列应用红-黄-绿色阶,模拟热力图;
  3. 用插入图表:选“城市”和“薪资中位数”两列,插入柱状图,右键“添加数据标签”。

然后他说:“老师,当前系统因网络波动未能实时渲染,但底层数据已结构化存储(指向 CSV),我用 Excel 快速验证了核心结论:北京 IT 岗位数是深圳的 1.8 倍,但薪资中位数仅高 5%,说明竞争更激烈——这与我们设计的‘城市竞争力指数’模型一致。”

这就是预案的价值。所以,我的习惯是:每次调试完spider_main.py,必手动打开data/jobs.csv,用 Excel 做一次“三分钟验证”——看字段是否齐全、空值是否合理、样本是否覆盖目标城市。从那以后我每次提交毕设代码,都强制走一遍这个流程:python spider/spider_main.py→open data/jobs.csv in Excel→Ctrl+A, Data → Text to Columns→Insert → PivotTable。它不增加代码量,但让答辩时的每一句“数据显示…”都有底气。

希望帮到你。

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

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

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

立即咨询