简介:这份资源是面向计算机相关专业学生与项目实战学习者的Python综合实践项目,围绕Boss直聘岗位数据的采集、清洗与可视化分析展开,可作为毕业设计、课程设计或期末大作业的完整参考方案。压缩包共38个文件,约246KB,以13个py脚本为核心,涵盖爬虫配置、数据管道、中间件与清洗逻辑,另含12个js、4个xml、1个html与1个css构成前端展示层,并附1个csv数据文件、1个md说明文档及配置文件,结构清晰、模块分明。项目经导师指导并获评审99分,代码完整可运行,对新手友好。已有162人学习关注,读者可据此掌握Scrapy采集流程、数据落盘与可视化呈现的完整链路,理解各模块职责与排错思路,快速复现并二次开发,省去从零搭建的时间成本。
1. 从一份 Python 期末大作业源码说起:岗位数据采集与分析到底能跑通什么
每年到了期末季,总有一批同学在找能直接跑通的 Python 大作业参考。这份「基于 Python 实现岗位数据采集及分析可视化」的项目源码,恰好卡在一个很实用的位置上:它不只是一个爬虫脚本,而是把采集、清洗、存储、分析、可视化串成了一条完整链路。你拿到手之后,能直接看到从请求构造到图表输出的全流程代码,而不是零散的片段。
它适合三类人:一是需要交期末大作业但不想从零搭框架的在校生;二是想快速了解招聘类数据采集套路的初级开发者;三是需要一份带文档说明的完整项目做二次开发参考的从业者。项目本身围绕岗位数据的字段抓取、结构化存储和可视化呈现展开,技术栈以 Python 为主,涉及请求库、解析库、数据处理库和图表库的配合使用。下面我按实际拆包的顺序,把这份资源里值得关注的技术点和落地路径讲清楚。
2. 拆解项目结构:从请求构造到图表输出的完整链路
2.1 采集层的核心逻辑与请求参数设计
这类岗位数据采集项目的采集层,通常不会一上来就写多线程或分布式,而是先用单线程把请求链路跑通。常见做法是构造一个带查询参数的列表页请求,拿到 HTML 或 JSON 响应后,再解析出岗位名称、公司、薪资、地点、学历要求等字段。项目源码里一般会有一个独立的采集模块,把请求头、参数拼接、翻页逻辑和异常重试分开处理。
以列表页采集为例,核心代码结构大致如下:
import requests import time import random # 请求头模拟浏览器,降低被识别为脚本的概率 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_page(keyword, page, city_code): """ keyword: 搜索关键词,如“Python” page: 页码,从 1 开始 city_code: 城市编码,用于限定地域 """ url = "https://example.com/jobs/list" params = { "query": keyword, "page": page, "city": city_code, } try: resp = requests.get(url, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f"第 {page} 页请求失败: {e}") return None # 翻页时加入随机间隔,避免请求过于密集 for p in range(1, 6): html = fetch_page("Python", p, "101010100") if html: # 后续解析逻辑 pass time.sleep(random.uniform(1.5, 3.5))这段代码里几个参数值得注意:timeout设成 10 秒是常见做法,太短容易误判超时,太长会拖慢整体节奏;time.sleep的随机区间建议不低于 1.5 秒,具体要根据目标站点的响应情况调整。项目文档里一般会说明哪些字段是必填、哪些是可选,拿到源码后先看配置项,不要直接改主逻辑。
2.2 解析与字段映射:把页面内容转成结构化数据
采集层拿到响应之后,解析层负责把非结构化的 HTML 或 JSON 转成字典或列表。如果响应是 JSON,直接用json.loads配合键值取值即可;如果是 HTML,常见方案是用 BeautifulSoup 或 lxml 配合 CSS 选择器。项目源码里通常会有一个字段映射表,把页面上的位置和最终存储的列名对应起来。
from bs4 import BeautifulSoup import json def parse_list(html): """从列表页 HTML 中提取岗位卡片信息""" soup = BeautifulSoup(html, "lxml") items = [] for card in soup.select(".job-card-wrapper"): item = { "job_name": card.select_one(".job-name").get_text(strip=True), "company": card.select_one(".company-name").get_text(strip=True), "salary": card.select_one(".salary").get_text(strip=True), "city": card.select_one(".city").get_text(strip=True), "education": card.select_one(".edu").get_text(strip=True) if card.select_one(".edu") else "不限", } items.append(item) return items这里的关键点是选择器的稳定性。项目文档里一般会标注选择器对应的页面区域,但页面结构一旦调整,选择器就会失效。我一般会建议在解析函数里加一层容错:对可能缺失的字段用if判断或try包裹,避免因为一个字段取不到导致整条数据丢弃。另外,薪资字段经常是「15-25K·13薪」这种格式,后续分析前需要做拆分和归一化,这部分在数据清洗章节会展开。
2.3 数据存储与清洗:从原始表到分析表的过渡
采集和解析完成之后,数据一般先落到 CSV 或 SQLite 里。项目源码里常见的是先写 CSV,再用 pandas 做清洗。原始数据里会有重复岗位、薪资格式不统一、城市名称带后缀等问题,清洗步骤通常包括去重、薪资拆解、城市归一化和缺失值处理。
import pandas as pd import re df = pd.read_csv("raw_jobs.csv") # 去重:按岗位名称+公司+城市组合去重 df.drop_duplicates(subset=["job_name", "company", "city"], inplace=True) # 薪资拆解:把“15-25K·13薪”拆成最低、最高和薪资月数 def parse_salary(s): if pd.isna(s): return pd.Series([None, None, None]) match = re.search(r"(\d+)-(\d+)K", s) months = re.search(r"·(\d+)薪", s) low = int(match.group(1)) if match else None high = int(match.group(2)) if match else None month_num = int(months.group(1)) if months else 12 return pd.Series([low, high, month_num]) df[["salary_low", "salary_high", "salary_months"]] = df["salary"].apply(parse_salary) # 城市归一化:去掉“·”后面的区域信息 df["city_clean"] = df["city"].str.split("·").str[0] df.to_csv("clean_jobs.csv", index=False)清洗完的数据才是分析层的输入。项目文档里一般会说明每个字段的含义和取值范围,拿到之后先跑一遍df.info()和df.describe(),看看有没有明显的异常值。这一步不做,后面的图表基本没法看。
3. 可视化与分析:用 pandas 和图表库把岗位数据讲清楚
3.1 岗位数量与薪资分布的基础图表
分析层的第一步通常是看整体分布:哪些城市岗位多、薪资集中在什么区间、学历要求占比如何。项目源码里一般会用 matplotlib 或 pyecharts 出图,前者适合静态报告,后者适合交互式展示。以薪资分布为例,用清洗后的salary_low和salary_high取中值,画一个直方图就能看出集中区间。
import matplotlib.pyplot as plt import pandas as pd df = pd.read_csv("clean_jobs.csv") df["salary_mid"] = (df["salary_low"] + df["salary_high"]) / 2 plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(1, 2, figsize=(12, 5)) # 薪资中值分布 axes[0].hist(df["salary_mid"].dropna(), bins=20, color="#4C72B0", edgecolor="white") axes[0].set_title("岗位薪资中值分布") axes[0].set_xlabel("薪资中值(K)") axes[0].set_ylabel("岗位数量") # 城市岗位数量 Top 10 city_counts = df["city_clean"].value_counts().head(10) axes[1].barh(city_counts.index[::-1], city_counts.values[::-1], color="#55A868") axes[1].set_title("城市岗位数量 Top 10") axes[1].set_xlabel("岗位数量") plt.tight_layout() plt.savefig("overview.png", dpi=150) plt.show()这段代码里,font.sans-serif设成 SimHei 是为了让中文正常显示,Linux 环境下可能需要换成其他中文字体。dpi=150是输出图片的清晰度参数,报告里用 150 到 200 之间比较合适。项目文档里一般会附带示例图,拿到源码后先跑一遍看输出是否一致,再改参数。
3.2 关键词提取与岗位要求词云
除了基础统计图,这类项目通常还会做一个岗位描述的关键词提取,用词云展示技能要求。常见做法是用 jieba 分词,去掉停用词后统计词频,再用 wordcloud 生成图片。项目源码里一般会有一个停用词表文件,放在data或config目录下。
import jieba from wordcloud import WordCloud from collections import Counter # 假设 df 里有一列 job_desc 存放岗位描述 texts = df["job_desc"].dropna().tolist() all_words = [] for text in texts: words = jieba.lcut(text) # 过滤掉长度小于 2 的词和纯数字 all_words.extend([w for w in words if len(w) >= 2 and not w.isdigit()]) # 加载停用词 with open("stopwords.txt", "r", encoding="utf-8") as f: stopwords = set(f.read().splitlines()) filtered = [w for w in all_words if w not in stopwords] counter = Counter(filtered) top_words = counter.most_common(100) wc = WordCloud( font_path="simhei.ttf", width=800, height=400, background_color="white", max_words=100, ) wc.generate_from_frequencies(dict(top_words)) wc.to_file("wordcloud.png")这里有几个参数需要根据实际情况调整:font_path必须指向一个支持中文的字体文件,否则词云会显示成方块;max_words控制显示多少个词,100 到 150 之间比较合适;停用词表的质量直接决定词云的可读性,项目自带的停用词表一般够用,但如果发现高频词里有大量无意义词汇,需要手动补充。
3.3 把分析结果串成一份可交付的报告
图表出来之后,项目通常还会有一个报告生成脚本,把关键指标和图表拼成一份 HTML 或 PDF。常见做法是用 jinja2 模板渲染 HTML,再用 pdfkit 或 weasyprint 转 PDF。项目文档里一般会说明报告模板的位置和可替换的变量。
from jinja2 import Template template = Template(""" <html> <head><meta charset="utf-8"><title>岗位数据分析报告</title></head> <body> <h1>岗位数据分析报告</h1> <p>样本量:{{ total }} 条</p> <p>平均薪资中值:{{ avg_salary }}K</p> <img src="overview.png" width="800"> <img src="wordcloud.png" width="800"> </body> </html> """) html = template.render( total=len(df), avg_salary=round(df["salary_mid"].mean(), 1), ) with open("report.html", "w", encoding="utf-8") as f: f.write(html)这一步的价值在于把零散的图表和指标收拢成一个可交付物。项目源码里如果带了报告模板,直接替换数据源即可;如果没有,按上面的结构补一个也不复杂。注意图片路径要用相对路径,否则换台机器打开报告时图片会丢失。
4. 避坑与排查:跑这份源码时最容易翻车的几个地方
4.1 请求被拒或返回空数据
现象:脚本运行后返回 403 或 200 但内容为空,解析出来零条数据。
原因:请求头缺失关键字段、请求频率过高触发限制、或者目标页面的参数结构已经调整。
解决:先检查HEADERS里是否带了User-Agent、Accept、Referer等常见字段;把翻页间隔调大到 3 秒以上;用浏览器开发者工具对比实际请求的参数名和值,确认query、page、city这些键名是否还一致。如果返回的是 JSON,先打印前 200 个字符看结构有没有变。
4.2 解析选择器失效导致字段为空
现象:数据能抓到,但某些列全是None或空字符串。
原因:页面 DOM 结构变化,原来的 CSS 选择器匹配不到元素。
解决:打开目标页面,用开发者工具重新定位字段对应的标签和类名,更新select_one里的选择器。建议在解析函数里对每个字段加if判断,缺失时给默认值而不是直接报错。项目文档里一般会标注选择器的编写时间,如果距离现在超过半年,大概率需要重新核对。
4.3 薪资字段格式不统一导致分析报错
现象:清洗脚本在parse_salary处抛出AttributeError,提示NoneType没有group方法。
原因:部分薪资字段是「面议」或「200-300元/天」这类非标准格式,正则匹配不到。
解决:在parse_salary里先判断是否匹配成功,匹配失败时返回None而不是直接取group。对于「元/天」这类格式,可以单独写一个分支转换成月薪再合并。清洗阶段宁可多写几个if,也不要让一条异常数据打断整个流程。
4.4 中文字体缺失导致图表乱码
现象:matplotlib 或 wordcloud 输出的图片里中文显示成方块。
原因:运行环境里没有安装中文字体,或者font_path指向的字体文件不存在。
解决:Windows 下一般用SimHei或Microsoft YaHei,Linux 下需要先确认系统里有哪些中文字体,用fc-list :lang=zh查看。wordcloud 的font_path必须指向具体的.ttf或.otf文件,不能只写字体名称。如果项目里自带了字体文件,确认路径是否正确。
4.5 数据量大了之后内存不够或跑得太慢
现象:采集几千条之后脚本变慢,或者 pandas 读取时内存占用飙升。
原因:一次性把所有数据读进内存,或者翻页时没有做分批存储。
解决:采集阶段每抓完一页就追加写入 CSV,不要全部攒在列表里最后一次性写。pandas 读取时用usecols只加载需要的列,dtype指定字段类型也能省不少内存。如果数据量确实大,考虑用 SQLite 做中间存储,分析时再按需查询。
5. 进阶用法:把这份源码改成可复用的分析模板
跑通基础流程之后,这份源码最大的价值在于它可以被改造成一个通用的岗位分析模板。我一般会做三件事:把采集层的 URL 和参数抽成配置文件,把清洗规则写成可插拔的函数,把图表输出改成命令行参数控制。这样换一个关键词或换一个城市,只需要改配置,不用动主逻辑。
import yaml # config.yaml # keyword: "Python" # city_code: "101010100" # max_pages: 10 # output_dir: "./output" def load_config(path="config.yaml"): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def run_pipeline(config): raw = collect(config["keyword"], config["city_code"], config["max_pages"]) clean = clean_data(raw) report(clean, config["output_dir"]) if __name__ == "__main__": cfg = load_config() run_pipeline(cfg)另一个实用的进阶方向是加一层简单的数据校验。比如在清洗完成后检查salary_low是否大于salary_high、城市名称是否在预设列表里、岗位名称长度是否异常。这些校验不需要多复杂,几行断言就能挡住大部分脏数据。
| 改造点 | 原始状态 | 建议改法 |
|---|---|---|
| 请求参数 | 硬编码在函数里 | 抽到 YAML 或 JSON 配置文件 |
| 清洗规则 | 写死在主流程 | 拆成独立函数,按需调用 |
| 图表输出 | 固定文件名 | 用时间戳或参数区分不同批次 |
| 异常处理 | 打印后继续 | 记录到日志文件,方便回溯 |
验证改造是否成功的方法很简单:换一个关键词跑一遍,看输出目录里是否生成了新的图表和报告,数据条数是否合理。如果换关键词后报错,说明还有硬编码没抽干净。
从那以后我每次拿到这类项目源码,都会先跑一遍原始流程确认能通,再按自己的习惯做配置化改造,最后才动分析逻辑。顺序反了的话,出了问题很难判断是源码本身的问题还是自己改出来的。希望这份拆解能帮到你,拿到资源后先跑通再改造,别一上来就大改。
本文还有配套的精品资源,点击获取