
简介面向计算机相关专业学生尤其适合需要完成数据分析可视化期末大作业、课程设计或入门实战的学习者。项目以前程无忧网站的Python岗位信息采集为完整案例覆盖了从请求网页、解析数据、清洗字段、存入数据库到多维度可视化分析的全流程源码经本地编译与严格调试可直接运行并附有导师认可的高分项目报告。zip压缩包共17个文件、约14.94MB主要包含4个爬虫与处理脚本、3个表格数据文件、2个数据库脚本、5张可视化分析图以及1份项目说明文档结构清晰便于按模块查阅。可视化部分围绕福利待遇关键词词云、不同学历要求下的岗位数量、各地区相对真实月薪箱型图、各城市岗位数量分布、不同工作经验的薪资差异等主题展开能直观呈现岗位市场规律适合答辩展示。目前已有283人学习或下载可作为课程设计参考、毕业设计延伸或数据采集与分析入门的练手素材方便在此基础上扩展爬虫策略或完善分析维度。1. python期末数据分析可视化大作业用前程无忧岗位数据把爬虫到可视化整条链路跑通期末大作业最怕的不是不会写代码而是不知道“到底要做到什么程度才算完”。如果你选的是「python期末数据分析可视化大作业-前程无忧python岗位信息爬取和分析源码文档报告」这个题目恭喜你它其实是一条标准的“爬虫 数据分析 可视化”完整链路先从前程无忧把Python岗位的招聘信息抓下来再对薪资、经验要求、学历门槛、技能关键词做清洗和统计最后用图表把结论呈现出来配上文档报告就能交差。同样是做这个题目有人用requests加BeautifulSoup半小时抓完数据有人卡在翻页和反爬上一整天有人用pandas十分钟做完清洗有人被薪资字段里的“千/月”和“万/年”搞到怀疑人生有人用pyecharts画出漂亮的动态图有人还在纠结matplotlib中文乱码。这篇笔记按完整作业的推进顺序把每一步怎么做、参数怎么调、坑在哪写清楚新手能照着重现熟手也能查漏补缺。2. 抓取前先搞清楚三件事爬什么、怎么存、会不会被封2.1 前程无忧的页面结构和岗位ID是突破口在做爬虫之前先花十分钟打开前程无忧的搜索页面按F12看网络请求。Python岗位的搜索结果页URL是这样的https://we.51job.com/api/job/search-pc?api_key51jobtimestampxxxkeywordpythonsearchType2function0industry0jobArea000000pageNum1pageSize50注意这个URL它不是传统的服务端渲染页面而是前端通过AJAX请求拿JSON数据再渲染的。这意味着你可以直接请求这个接口拿结构化数据比解析HTML省事得多。响应是JSON格式里面有jobName岗位名称、provideSalary薪资、workArea工作地点、companyName公司名称、jobType岗位类型、jobTags岗位标签、issueDate发布日期、jobId岗位唯一ID等字段。岗位ID是关键。前程无忧的岗位详情页URL是https://jobs.51job.com/职位ID.html你在列表接口拿到的jobId可以直接拼出详情页地址。如果作业要求里写了“分析岗位描述”你就需要再写一个parse_detail函数去抓详情页的岗位JD文本做词频统计和词云图。import requests import json import time # 模拟浏览器请求头少了这个很容易被识别为爬虫 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://we.51job.com/, Accept: application/json, text/plain, */* } # 搜索关键词这里以python为例 keyword python url fhttps://we.51job.com/api/job/search-pc?api_key51jobtimestamp{int(time.time()*1000)}keyword{keyword}searchType2function0industry0jobArea000000pageNum1pageSize50 resp requests.get(url, headersheaders, timeout10) data resp.json() print(data[data][result]) # 里面就是当前页的岗位列表参数说明searchType2表示按关键词搜索jobArea000000表示不限工作地点pageNum从1开始翻页pageSize最大可以到100但建议用50timestamp是当前毫秒级时间戳。有些时候接口会校验timestamp和signature的关联关系如果请求返回401或者{code: 403}说明签名算法变了这时候回到页面搜索几次把请求头里的signature参数格式抓下来对比一下。2.2 翻页循环和限速策略少给自己惹麻烦确认第一页能拿到数据后把pageNum改成循环变量抓取前几页数据。前程无忧默认按相关度排序翻页深度超过10页后数据重复率开始上升所以做课程作业抓5到10页就够了每页50条合计250到500条岗位数据足以支撑后续分析。all_jobs [] max_page 8 # 抓8页共约400条数据够做分析展示 for page in range(1, max_page 1): url fhttps://we.51job.com/api/job/search-pc?api_key51jobtimestamp{int(time.time()*1000)}keyword{keyword}searchType2function0industry0jobArea000000pageNum{page}pageSize50 resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f第{page}页请求失败状态码: {resp.status_code}) continue data resp.json() result data.get(data, {}).get(result, []) if not result: print(f第{page}页没有数据提前结束) break for item in result: job_info { job_id: item.get(jobId), job_name: item.get(jobName), company: item.get(companyName), salary: item.get(provideSalary), area: item.get(workArea), edu_level: item.get(degree), experience: item.get(workYear), tags: |.join(item.get(jobTags, [])) if item.get(jobTags) else , publish_date: item.get(issueDate) } all_jobs.append(job_info) print(f第{page}页完成累计{len(all_jobs)}条) time.sleep(1) # 每页间隔1秒别给人家服务器添堵代码逻辑说明每页请求结束后休息1秒这是一个「够用又不招人烦」的节奏。课程作业不建议用高并发或代理池一是没那个必要二是提高了被反爬识别的概率。代码里对jobTags用|拼接是为了后续存入DataFrame时避免列表类型带来的麻烦jobArea字段虽然传参时是000000但返回结果里是中文地名直接存原始值即可。2.3 把数据落成CSV现在就定好字段格式后面少改代码爬到的数据先存CSV方便后面pandas做分析。存CSV时有个关键问题如果直接在循环里一行行追加写入中途断了会导致文件头重复应该先汇总到内存列表最后一次性写入。import pandas as pd df pd.DataFrame(all_jobs) # 对job_id去重防止翻页过程中出现重复记录 df df.drop_duplicates(subsetjob_id, keepfirst) # 保存为UTF-8编码注意用utf-8-sig这样Excel打开csv时中文不乱码 df.to_csv(python_jobs.csv, indexFalse, encodingutf-8-sig) print(f数据已保存共{len(df)}条记录字段如下) print(df.columns.tolist())参数说明subsetjob_id指定按岗位ID去重keepfirst保留第一条出现的记录。编码选择utf-8-sig而不是utf-8是因为Excel在打开UTF-8编码的CSV时会把中文识别成乱码加BOM头就不会。如果作业要求用SQLite数据库存储df.to_sql(jobs, conn, if_existsreplace, indexFalse)一行就能搞定。3. 从CSV到DataFrame薪资解析和字段清洗是这个题目的真正难点3.1 把“1.5-2.5万/月”变成数值统一单位才能算平均数爬下来的薪资字段是字符串比如“1.5-2.5万/月”“2-3.5万/年”“300-500元/天”——这些没法直接画图、算均值。课程设计报告里最常见的翻车点就是拿字符串做统计画出来的图全是分类柱状图根本看不出薪资分布规律。我的做法是写一个解析函数把薪资字符串统一成“元/月”为单位的数值型数据。def parse_salary(s): 将薪资字符串解析为统一的月薪数值单位元/月 支持格式万/月、万/年、千/月、元/天 解析失败返回None后续做缺失值处理 if not isinstance(s, str): return None s s.strip().replace( , ) if 万/月 in s: nums re.findall(r(\d(?:\.\d)?), s) if len(nums) 1: return float(nums[0]) * 10000 elif len(nums) 2: return (float(nums[0]) float(nums[1])) / 2 * 10000 # 取区间中值 elif 万/年 in s: nums re.findall(r(\d(?:\.\d)?), s) if len(nums) 1: return float(nums[0]) * 10000 / 12 elif len(nums) 2: return (float(nums[0]) float(nums[1])) / 2 * 10000 / 12 elif 千/月 in s: nums re.findall(r(\d(?:\.\d)?), s) if len(nums) 1: return float(nums[0]) * 1000 elif len(nums) 2: return (float(nums[0]) float(nums[1])) / 2 * 1000 elif 元/天 in s: nums re.findall(r(\d(?:\.\d)?), s) if len(nums) 1: return float(nums[0]) * 21 # 按月工作21天折算 elif len(nums) 2: return (float(nums[0]) float(nums[1])) / 2 * 21 return None import re df[salary_monthly] df[salary].apply(parse_salary)逻辑说明先判断字符串里的单位关键词再用正则提取所有数字取区间中值作为该岗位的月薪。正则r(\d(?:\.\d)?)能同时匹配整数和小数比如“1.5-2.5万/月”中的1.5和2.5。元/天按21个工作日折算成月薪这是行业内的通用做法。解析失败的记录比如“面议”或空值返回None后面统一处理。3.2 用apply把“在校生/应届生”“3-4年经验”等文本字段做成分箱经验字段的原始值是“在校生/应届生”“1年经验”“3-4年经验”“5-7年经验”“无需经验”等字符串。做可视化时需要压缩成“应届/1-3年/3-5年/5年以上”几个档位。学历字段也是同样的思路“本科”“大专”“硕士”“博士”直接映射成数值等级。def map_experience(exp): if not isinstance(exp, str): return 未知 exp exp.strip() if exp in [在校生/应届生, 无需经验]: return 应届/无经验 elif 1年 in exp: return 1年 elif 2年 in exp or 3年 in exp or 4年 in exp: return 1-3年 elif 5年 in exp or 6年 in exp or 7年 in exp: return 3-5年 elif 8年 in exp or 9年 in exp or 10年 in exp: return 5-10年 else: return 未知 def map_edu(edu): if not isinstance(edu, str): return 未知 if 初中 in edu or 高中 in edu or 中技 in edu: return 高中及以下 elif 大专 in edu: return 大专 elif 本科 in edu: return 本科 elif 硕士 in edu: return 硕士 elif 博士 in edu: return 博士 else: return 未知 df[exp_bin] df[experience].apply(map_experience) df[edu_bin] df[edu_level].apply(map_edu) # 看一下分箱后的分布 print(df[exp_bin].value_counts()) print(df[edu_bin].value_counts())这里特别说一下exp_bin映射里为什么把2年到4年归到“1-3年”——因为前程无忧的经验字段写的是区间比如“3-4年经验”实际上是1到3年的进阶档直接按原始字符串切分会导致分箱过多、每箱样本量不足。这种映射需要你结合实际数据分布来调整如果某个档位样本量少于20条就考虑合并到相邻档位。3.3 确定性的数据清洗流程先看缺失率再决定是删是填清洗阶段要做的事按顺序来第一步df.info()看每列的非空数量第二步df.isnull().sum()统计缺失值第三步根据重要性决定处理方式。薪资解析失败的记录、岗位名称缺失的记录直接删掉因为后续分析薪资分布和职位分布都依赖这两列公司名缺失的可以保留画图时不涉及地区缺失的可以填“未知”。# 缺失率统计 missing_rate df.isnull().sum() / len(df) * 100 print(missing_rate) # 删除薪资解析失败和岗位名称为空的记录 df_clean df.dropna(subset[salary_monthly, job_name]) # 其余列的缺失值填未知 df_clean df_clean.fillna(未知) # 删除薪资为0或不合理的记录比如解析错误产生的极端值 df_clean df_clean[df_clean[salary_monthly] 0] df_clean df_clean[df_clean[salary_monthly] 200000] # 月薪20万以上视为异常删除 # 保存清洗后的数据后面可视化全部用这个 df_clean.to_csv(python_jobs_clean.csv, indexFalse, encodingutf-8-sig) print(f清洗后剩余{len(df_clean)}条清洗前{len(df)}条)注意dropna(subset...)的写法它只检查指定列是否非空不会因为其他列的缺失值把整行干掉。薪资上限200000是按常识设的月薪20万以上的Python岗位极少出现了大概率是“面议”被解析成了异常数值。清洗完成后如果发现删除比例超过20%回去检查parse_salary函数是不是对某个格式漏解析了。4. 把数据变成图表画一张能直接放进课程设计报告的图4.1 先做静态分析matplotlib画薪资分布和学历要求直方图matplotlib是基础虽然样子朴素但胜在可控性强。中文乱码问题是每个用Pythone做可视化的人都会遇到的头号坑解决办法是画图前统一设置字体。import matplotlib.pyplot as plt # 配置中文字体Windows用SimHeiMac用Arial Unicode MS plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块的问题 fig, axes plt.subplots(1, 2, figsize(12, 5)) # 子图1薪资分布直方图 axes[0].hist(df_clean[salary_monthly], bins20, color#4C72B0, edgecolorwhite, alpha0.8) axes[0].set_xlabel(月薪元) axes[0].set_ylabel(岗位数量) axes[0].set_title(Python岗位薪资分布) # 子图2学历要求柱状图 edu_counts df_clean[edu_bin].value_counts() edu_counts edu_counts.reindex([高中及以下, 大专, 本科, 硕士, 博士]) # 固定顺序 axes[1].bar(edu_counts.index, edu_counts.values, color#55A868) axes[1].set_ylabel(岗位数量) axes[1].set_title(学历要求分布) plt.tight_layout() plt.savefig(analysis_basic.png, dpi150, bbox_inchestight) plt.show()参数说明bins20把月薪数据切分成20个区间如果最右边的区间聚集了特别多的数据说明高薪值把横轴拉长了考虑用plt.xlim(0, 50000)截断reindex方法确保柱子按你指定的顺序排列而不是按value_counts排序dpi150保证图片放进Word文档时足够清晰bbox_inchestight自动裁掉多余白边。先跑通这张图再往下加箱线图、饼图。4.2 用pyecharts做交互式图表让报告看起来更像回事课程设计报告如果想加分建议用pyecharts做2到3张交互式图表它能生成HTML文件用浏览器打开可以悬停看数值、缩放、点击图例筛选。很多教程直接让装最新版但版本差异导致API变化很大建议固定版本pip install pyecharts2.0.5注意pyecharts2.x的API和1.x完全不同网上的旧教程调用pyecharts.xxx的写法在2.x里跑不通。以下以2.0.5为例写一个薪资和地区的关系图from pyecharts.charts import Bar, Pie from pyecharts import options as opts # 准备数据各地区平均月薪 area_salary df_clean.groupby(area)[salary_monthly].mean().sort_values(ascendingFalse).head(10) bar ( Bar() .add_xaxis(area_salary.index.tolist()) .add_yaxis( 平均月薪元, [round(v) for v in area_salary.values], color#2F4554 ) .set_global_opts( title_optsopts.TitleOpts(titlePython岗位各地区平均月薪Top10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate30)), yaxis_optsopts.AxisOpts(name月薪元), ) ) bar.render(bar_area_salary.html) print(图表已生成用浏览器打开 bar_area_salary.html 查看)参数说明groupby(area)[salary_monthly].mean()计算每个地区的平均月薪head(10)取最高的10个地区避免X轴标签挤成一团rotate30让X轴文字斜着显示解决长地名重叠问题。render方法生成独立HTML文件交作业时把这个HTML文件一起打包或者截图插入Word文档。4.3 词云图分析技能关键词从岗位描述里挖出“企业真正要什么”如果你的作业要求分析岗位描述可以继续爬详情页拿JD文本然后做词云。这一步能直接回答“企业招聘Python岗位时最看重哪些技能”这个问题。用wordcloud和jieba配合from wordcloud import WordCloud import jieba # 假设你有一个列表 jd_texts 存放所有岗位描述的纯文本 jd_texts [精通Python语言熟悉Django框架了解MySQL数据库...] # 实际是你爬到的详情 # 用jieba分词过滤掉单个字和常见无意义词 text .join(jd_texts) words jieba.lcut(text) filtered_words [w for w in words if len(w) 1 and w not in [可以, 相关, 进行, 以及, 工作]] filtered_text .join(filtered_words) # 生成词云图 wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, # Windows中文字体路径 width1200, height600, background_colorwhite, max_words100, ) wc.generate(filtered_text) wc.to_file(skills_wordcloud.png) print(词云图已保存)逻辑说明jieba.lcut先把中文句子切成词再用长度过滤和停用词表去掉“的”“了”“可以”这类干扰词。font_path是wordcloud中文显示的关键不设置就全是方框。max_words100控制词云里显示的词数量词越多越乱100个足够看出主要技术栈。如果报告里需要技术关键词频次表用Counter(filtered_words).most_common(20)导出前20个词及频次做一个小表格放进文档。5. 避坑集合爬虫、清洗、可视化最常见的五个拦路虎5.1 第一页能爬到翻页就403现象第一页请求正常返回数据pageNum2开始返回{code: 403}或者直接被重定向到验证码页面。原因前程无忧的接口对相同请求头、相同参数的连续请求做了频次限制第一页能过是因为没有历史请求记录第二页触发风控。解决把time.sleep(1)改成time.sleep(random.uniform(2, 4))给每页之间的间隔增加随机性模拟真人操作。请求头里的User-Agent换成完整的浏览器UA并在headers里补上Accept-Language: zh-CN,zh;q0.9。如果还是403用Session对象管理请求并先GET访问一次前程无忧首页获取Cookie再用这个Session请求接口成功率高很多。5.2 Excel打开CSV中文乱码现象用pandas写入CSV后用Excel打开中文全是乱码用记事本打开正常。原因df.to_csv默认用utf-8编码写入Excel对无BOM的UTF-8识别不友好。解决df.to_csv(file.csv, indexFalse, encodingutf-8-sig)带上UTF-8 BOM头Excel就能正确识别。这个坑特别低端但每年课程设计里都有人栽进去务必一次写对。5.3 “面议”薪资处理成缺失值后饼图/直方图缺了一大块现象薪资分布直方图里“面议”的岗位大量存在导致有效数据只有总数据量的60%看起来分析结论不完整。原因前程无忧上大约有三成岗位不写薪资写成“面议”。如果直接把parse_salary返回None的记录删掉会损失大量样本。解决把“面议”当作一类单独分析统计“面议岗位占比”并单独画一张饼图计算平均薪资时剔除“面议”但在报告里注明“本分析排除面议岗位占总数XX%”。更进阶的做法是尝试用同一地区、相近经验要求的已公布薪资岗位做均值填充但课程作业不建议这么做容易在答辩时被问住。5.4 matplotlib画不出图或者直接闪退现象在PyCharm里运行代码plt.show()之后什么都没有或者在Jupyter里图只显示一行文字。原因PyCharm中pandas或matplotlib的交互后端配置问题plt.show()默认不阻塞导致窗口一闪而过。解决用plt.savefig(output.png, dpi150, bbox_inchestight)替代plt.show()直接把图保存为文件再看。如果一定要弹窗预览加plt.show(blockTrue)参数。在Jupyter里在代码开头加%matplotlib inline确保图内嵌显示。5.5 pyecharts按教程写了却报错module pyecharts has no attribute Bar现象照着网上教程写from pyecharts import Bar第5版跑通了换第10版就报错。原因pyecharts在v1.x之后做了彻底重构老代码全部不兼容。解决强制安装兼容版本用pip install pyecharts2.0.5。再用from pyecharts.charts import Bar导入。同时注意在pip install pyecharts之后让pip list看已安装版本某些一键安装脚本会把pyecharts升级到最新版导致历史代码全部失效。6. 把作业从“能跑”做到“能答辩”最后加三天工作量分数拉开一个档6.1 增量爬取设计让报告里多一句“数据是持续更新的”很多同学交上去的脚本只能手动运行一次数据是死的。如果你把爬虫写成“增量模式”在文档报告里能多写一段“本系统支持每日增量更新新岗位数据自动追加至数据库”这个卖相在答辩时很加分。实现方案不复杂爬取下一批数据时先读取CSV里已有的job_id列表新爬到的数据如果job_id已存在就跳过不存在才追加。关键代码就一行existing_ids set(pd.read_csv(python_jobs.csv)[job_id].tolist()) new_jobs [job for job in all_jobs if job[job_id] not in existing_ids]在文档报告里把这解释成“增量爬取模块”列一个流程图说清楚“完整爬取”和“增量爬取”的触发条件比只贴一段完整爬虫代码更显功力。实现时注意set的in判断是O(1)复杂度几千条数据毫无压力不需要特意优化。6.2 多维度交叉分析薪资、学历、经验三个字段互相验证单一的柱状图、饼图只能算“展示数据”交叉分析才能体现“分析能力”。强烈建议在报告中加一张pivot_table热力图行是学历等级列是经验区间值是该组合的平均薪资。这张图能同时回答“本科1-3年经验能拿多少钱”和“硕士3-5年的薪资溢价到底有多少”两个问题。pivot_data df_clean.pivot_table( valuessalary_monthly, indexedu_bin, columnsexp_bin, aggfuncmean ) pivot_data pivot_data[[应届/无经验, 1年, 1-3年, 3-5年, 5-10年]] pivot_data pivot_data.loc[[大专, 本科, 硕士]] print(pivot_data.round(0))参数说明values指定要聚合的数值列index是行索引columns是列索引aggfuncmean表示取均值。跑出来后你会发现一个比较反直觉的规律在很多公司本科学历加3-5年经验的平均薪资实际上高于硕士学历加1-3年经验的平均薪资这个结论放进报告的“分析发现”部分比罗列图表更有说服力。6.3 用reportlab或Word模板生成PDF报告最后一公里的加分套路课程设计文档报告一般要求Word或PDF格式。手工截图粘图又慢又容易乱常见做法是用python-docx自动生成带图表的Word文档pip install python-docxfrom docx import Document from docx.shared import Inches doc Document() doc.add_heading(Python岗位数据分析报告, level1) doc.add_heading(一、数据来源与采集方法, level2) doc.add_paragraph(本次分析从前程无忧采集Python岗位招聘信息共获取有效数据XXX条。) doc.add_picture(analysis_basic.png, widthInches(6)) doc.save(分析报告.docx) print(报告已生成)这段代码只演示了最核心的画面插入逻辑实际写的时候把每一章的标题和数据结论都写成变量最后一次性拼装。我的习惯是图表用savefig输出到figures/目录写报告时统一引用这样即使图表重跑报告里的图片路径也不会乱。课程作业做到这个程度源码加文档报告加可交互HTML图表足以覆盖这门课的全部考核点了。最后说一个我自己的教训做这类大作业爬虫写得花哨不如下游清洗做得扎实重要。真正的分数差距在“数据分析结论是否可信”和“文档报告是否有逻辑”而不是你用了多酷的库。程序能跑是及格线能解释每一步为什么这么做才是优秀线。希望这篇笔记能帮你把从爬数据到出报告的整条链路理顺少走我当年走过的弯路。本文还有配套的精品资源点击获取