各位读者朋友好。
这段时间在梳理数据分析方向的学习路径,很多同学都会拿招聘平台的岗位信息来练手。原因很简单:招聘数据是真实业务场景,字段比较规整,包含岗位、城市、薪资、经验要求、学历要求、公司规模等信息,非常适合做清洗、入库、分析和可视化这条完整链路。本文就围绕 Boss 直聘岗位数据,给出一个基于 Python + MySQL 的岗位可视化分析项目教程。
这个项目适合三类读者:一是正在学 Python 数据分析,想找一个完整实战项目练手的人;二是准备做毕业设计或课程设计,需要“爬取—存储—分析—可视化”全流程代码的人;三是对招聘行业薪资分布、岗位需求趋势感兴趣,想用数据辅助职业选择的人。
学完本文,你能掌握:requests 或 Selenium 抓取网页数据的基本思路,pandas 做数据清洗的方法,pymysql 操作 MySQL 数据库的完整流程,以及 pyecharts 生成交互式可视化图表的套路。文章末尾还整理了常见报错和排查思路,方便你复制代码后快速跑通。
需要提前说明一点:任何爬虫项目都必须遵守法律和网站协议,控制请求频率,不得影响目标网站正常服务。本文代码主要用于学习数据分析流程,请结合实际情况合规使用。
1. 项目背景与整体设计
1.1 项目解决什么问题
Boss 直聘上有大量真实岗位信息,这些数据可以告诉我们很多信息:当前市场上哪个岗位需求量最大,一线城市和新一线城市的平均薪资差异有多大,Java、Python、前端这些岗位的薪资中位数分别处于什么水平,不同规模的公司对学历的要求是怎样的。
如果我们手动去浏览这些数据,效率太低,而且无法形成全局视角。通过 Python 采集数据、MySQL 存储数据、pyecharts 可视化展示,就可以把零散的信息变成一张张直观的图表。整个项目跑完后,你能得到类似这样的产出:岗位数量 Top10 城市柱状图、各岗位平均薪资对比图、学历要求分布饼图、工作经验要求条形图等。
1.2 技术栈选型
项目使用的主要技术如下:
| 模块 | 技术选型 | 作用 |
|---|---|---|
| 数据采集 | Python + requests + parsel | 请求网页并解析 HTML 数据 |
| 数据存储 | MySQL 8.0 | 以结构化表存储岗位数据 |
| 数据库驱动 | pymysql | Python 连接和操作 MySQL |
| 数据处理 | pandas | 清洗、去重、类型转换 |
| 可视化 | pyecharts | 生成交互式 HTML 图表 |
选择这些技术主要是考虑到初学者上手难度低,生态成熟,资料多,遇到问题很容易搜到解决方案。MySQL 在招聘岗位中应用极广,学会它的基本使用对后续找工作也有帮助。
1.3 项目流程拆解
整个项目可以拆成四个大步骤:
- 数据采集:请求招聘平台接口或页面,拿到岗位列表数据。
- 数据入库:将解析好的数据清洗后写入 MySQL 表。
- 数据分析:从 MySQL 读取数据,用 pandas 做聚合统计。
- 可视化展示:使用 pyecharts 生成图表,并组合成一个可视化报告页面。
为了方便你理解,我画一个简单的流程示意:
采集岗位数据
↓
解析 JSON / HTML
↓
数据清洗(去空、去重、转换薪资字段)
↓
写入 MySQL
↓
pandas 读取 + 聚合分析
↓
pyecharts 图表渲染
↓
生成可视化 HTML 报告
2. 环境准备与版本说明
2.1 安装 Python
项目使用 Python 3.8 以上版本。如果你电脑上还没有安装 Python,可以到 Python 官网下载对应系统的安装包。安装时记得勾选“Add Python to PATH”,否则命令行里可能无法直接使用 python 命令。
安装完成后,在终端执行:
python --version
正常情况下会输出类似
Python 3.10.11
的版本号。
2.2 安装 MySQL
本文示例以 MySQL 8.0 为例。如果你使用 MySQL 5.7,大部分 SQL 语句可以通用,但连接配置和字符集相关细节需要留意。
安装并启动 MySQL 后,建议先创建一个单独的数据库,避免和本机其他业务数据混在一起。登录 MySQL 后执行:
CREATE DATABASE boss_jobs DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
这里使用
utf8mb4
字符集,是因为岗位名称、公司名称中可能包含中文和特殊符号,
utf8mb4
对中文和 emoji 支持更好。
2.3 安装 Python 依赖库
建议先创建一个虚拟环境,然后再安装依赖。在项目根目录执行:
pip install requests parsel pandas pymysql pyecharts
如果下载速度较慢,可以换成国内镜像源:
pip install requests parsel pandas pymysql pyecharts -i https://pypi.tuna.tsinghua.edu.cn/simple
本文使用的核心库版本思路如下,实际以你安装到的最新稳定版本为准:
- requests:2.x 版本。
- parsel:1.6 以上版本。
- pandas:1.x 或 2.x 版本。
- pymysql:1.x 版本。
- pyecharts:2.x 版本。
注意 pyecharts 1.x 和 2.x 的 API 差异比较大,本文示例基于 pyecharts 2.x 的写法。如果你安装的是旧版本,建议先升级到最新版。
2.4 项目目录结构
建议按照下面的结构来组织文件:
boss_jobs_analysis/
├── main.py # 主程序入口
├── collect_data.py # 数据采集模块
├── clean_data.py # 数据清洗模块
├── mysql_helper.py # MySQL 连接与操作封装
├── analysis.py # 数据分析模块
├── visualize.py # 可视化图表模块
├── data/
│ └── jobs.json # 采集到的原始 JSON 数据
├── output/
│ └── 岗位可视化报告.html # 最终生成的报告
└── requirements.txt
这里先把模块拆开,是为了让每个文件的职责更清晰。实际写代码时,你可以根据需要合并,比如把采集和清洗放在同一个脚本里。
3. 数据库表设计与连接封装
3.1 岗位信息表字段设计
在 MySQL 中创建岗位信息表,核心字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 主键自增 | 主键 |
| job_name | VARCHAR(100) | 岗位名称 |
| salary_min | INT | 月薪下限,单位千元 |
| salary_max | INT | 月薪上限,单位千元 |
| city | VARCHAR(50) | 工作城市 |
| experience | VARCHAR(50) | 经验要求 |
| education | VARCHAR(50) | 学历要求 |
| company_name | VARCHAR(200) | 公司名称 |
| company_size | VARCHAR(100) | 公司规模 |
| industry | VARCHAR(100) | 所属行业 |
| create_time | TIMESTAMP | 入库时间 |
建表语句如下:
CREATE TABLE job_info (
id INT PRIMARY KEY AUTO_INCREMENT,
job_name VARCHAR(100) NOT NULL,
salary_min INT DEFAULT 0,
salary_max INT DEFAULT 0,
city VARCHAR(50),
experience VARCHAR(50),
education VARCHAR(50),
company_name VARCHAR(200),
company_size VARCHAR(100),
industry VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里把薪资拆成
salary_min
和
salary_max
两个字段,是为了方便后续做数值计算。如果只存一个“15K-20K”的字符串,后面统计平均薪资时还要再做字符串解析,比较麻烦。
3.2 pymysql 连接封装
新建
mysql_helper.py
,封装一个简单的数据库操作类:
# 文件路径:mysql_helper.py
import pymysql
class MySQLHelper:
def __init__(self, host="localhost", port=3306, user="root",
password="123456", database="boss_jobs"):
self.conn = pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=database,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor
)
def insert_job(self, job: dict):
sql = """
INSERT INTO job_info(job_name, salary_min, salary_max, city,
experience, education, company_name,
company_size, industry)
VALUES(%(job_name)s, %(salary_min)s, %(salary_max)s, %(city)s,
%(experience)s, %(education)s, %(company_name)s,
%(company_size)s, %(industry)s)
"""
with self.conn.cursor() as cursor:
cursor.execute(sql, job)
self.conn.commit()
def batch_insert_jobs(self, jobs: list):
sql = """
INSERT INTO job_info(job_name, salary_min, salary_max, city,
experience, education, company_name,
company_size, industry)
VALUES(%(job_name)s, %(salary_min)s, %(salary_max)s, %(city)s,
%(experience)s, %(education)s, %(company_name)s,
%(company_size)s, %(industry)s)
"""
with self.conn.cursor() as cursor:
cursor.executemany(sql, jobs)
self.conn.commit()
def close(self):
if self.conn:
self.conn.close()
这里有几个细节需要解释。
cursorclass=pymysql.cursors.DictCursor
的作用是让查询结果以字典形式返回,这样后续通过字段名取值会更直观。
executemany
方法可以批量插入多条数据,比一条条 insert 性能好很多。如果采集了几千条数据,批量插入的优势非常明显。
每天晚上或者每次跑批之后,记得调用
close()
方法关闭连接。养成好习惯可以避免连接数过多导致数据库报错。
4. 数据采集模块
4.1 合法合规说明
在编写采集代码之前,先强调合规问题。采集第三方网站数据前,你应该确认:
- 目标网站是否有 robots 协议,明确禁止爬取的内容不要碰。
- 请求频率要合理,建议在请求之间加入随机延时,避免给服务器造成压力。
- 采集数据仅用于学习研究,不要用于商业盈利,更不要转卖数据。
- 不得绕过网站登录、验证码等安全机制。
Python 爬虫本身不是违法行为,非法的是爬取未授权数据、破坏对方服务器、将数据用于违法用途。下面代码只演示技术思路,实际运行时你需要结合目标网站的接口规范自行调整。
4.2 使用 requests 获取接口数据
很多招聘平台的职位数据是通过 JSON 接口返回的,比解析 HTML 方便很多。我们可以先尝试直接请求接口地址,拿到结构化 JSON 数据。
一个简单的示例逻辑如下:
# 文件路径:collect_data.py
import time
import random
import json
import requests
def fetch_jobs(keyword="Python", city="全国", pages=5):
"""
请求招聘平台接口,获取岗位数据。
注意:这里仅展示请求思路,实际接口参数以目标网站为准。
"""
all_jobs = []
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://www.zhipin.com/",
}
for page in range(1, pages + 1):
params = {
"query": keyword,
"city": city,
"page": page,
}
try:
resp = requests.get(
"https://www.zhipin.com/wapi/zpgeek/search/joblist.json",
params=params,
headers=headers,
timeout=10
)
resp.raise_for_status()
data = resp.json()
# 根据实际返回结构解析职位列表
job_list = data.get("zpData", {}).get("jobList", [])
for item in job_list:
job = {
"job_name": item.get("jobName"),
"salary_min": extract_salary_min(item.get("salaryDesc")),
"salary_max": extract_salary_max(item.get("salaryDesc")),
"city": item.get("cityName"),
"experience": item.get("jobExperience"),
"education": item.get("jobDegree"),
"company_name": item.get("brandName"),
"company_size": item.get("brandScaleName"),
"industry": item.get("brandIndustry"),
}
all_jobs.append(job)
print(f"第 {page} 页抓取完成,共 {len(job_list)} 条数据")
except Exception as e:
print(f"第 {page} 页抓取失败:{e}")
# 随机延时,降低请求频率
time.sleep(random.uniform(1, 3))
return all_jobs
def extract_salary_min(salary_desc: str) -> int:
"""从类似 '15K-25K' 的字符串中解析薪资下限(单位:千元)"""
if not salary_desc:
return 0
parts = salary_desc.split("-")
if not parts:
return 0
return int(parts[0].replace("K", "").replace("k", ""))
def extract_salary_max(salary_desc: str) -> int:
"""从类似 '15K-25K' 的字符串中解析薪资上限(单位:千元)"""
if not salary_desc:
return 0
parts = salary_desc.split("-")
if len(parts) < 2:
return 0
return int(parts[1].replace("K", "").replace("k", ""))
这里的接口地址和字段名是基于公开资料整理的示例,实际使用时需要根据目标网站当前的接口结构调整。如果你的请求被拒绝,常见的处理方式是换一个 User-Agent、降低请求频率,或者使用 Selenium 模拟浏览器操作。
4.3 使用 Selenium 兜底
当接口请求拿不到数据时,可以用 Selenium 模拟浏览器行为。Selenium 通过驱动浏览器加载页面,能够执行 JavaScript,适合抓取动态渲染的网页内容。
安装 Selenium 并准备浏览器驱动后,核心思路如下:
# 示例思路:使用 Selenium 获取页面数据
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
options = webdriver.ChromeOptions()
options.add_argument("--headless") # 无头模式
driver = webdriver.Chrome(options=options)
driver.get("https://www.zhipin.com/s?query=Python&city=100010000")
time.sleep(3)
job_cards = driver.find_elements(By.CLASS_NAME, "job-card-wrapper")
for card in job_cards[:10]:
job_name = card.find_element(By.CLASS_NAME, "job-name").text
print(job_name)
driver.quit()
注意:Selenium 需要下载对应浏览器版本的驱动,比如 ChromeDriver 需要匹配 Chrome 版本。如果浏览器版本和驱动不匹配,启动时会直接报错。
从工程角度来说,优先使用接口采集,接口失效再考虑页面解析,最后才考虑 Selenium。因为 Selenium 速度慢、资源占用高、维护成本大,不适合大规模采集。
4.4 数据保存为 JSON 文件
采集结果建议先以 JSON 文件落盘,再做清洗和入库。这样即使中途程序崩溃,原始数据也不会丢失。
# 在 collect_data.py 追加写入逻辑
def save_to_json(jobs: list, filepath="data/jobs.json"):
with open(filepath, "w", encoding="utf-8") as f:
json.dump(jobs, f, ensure_ascii=False, indent=2)
print(f"数据已保存至 {filepath}")
5. 数据清洗与入库
5.1 读取原始数据并清洗
采集到的原始数据往往存在几个问题:
- 薪资字段可能是字符串,比如“15K-25K”,需要拆成数值。
- 某些字段为空,比如城市为空、学历为空。
- 同一岗位可能重复采集,需要去重。
- 岗位名称含有特殊字符或前后空格。
清洗的核心理念是: 先让数据变成统一的、可计算的格式,再入库。
新建
clean_data.py
:
# 文件路径:clean_data.py
import json
import pandas as pd
def load_json(filepath="data/jobs.json"):
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
def clean_jobs(jobs: list) -> pd.DataFrame:
df = pd.DataFrame(jobs)
# 去掉完全空的行
df = df.dropna(how="all")
# 去掉关键字段为空的行
df = df.dropna(subset=["job_name", "city", "salary_min", "salary_max"])
# 去除重复数据,以后续所有字段作为去重依据
df = df.drop_duplicates()
# 薪资字段转数值
df["salary_min"] = pd.to_numeric(df["salary_min"], errors="coerce")
df["salary_max"] = pd.to_numeric(df["salary_max"], errors="coerce")
# 过滤非法薪资:上限小于下限的数据视为异常
df = df[df["salary_max"] >= df["salary_min"]]
# 重置索引
df = df.reset_index(drop=True)
return df
清洗为什么要单独做?因为如果直接把爬到的脏数据写入 MySQL,后面做聚合统计时会频繁遇到类型报错、空值问题、重复数据干扰结果等问题。数据清洗虽然看起来枯燥,却是数据分析项目中最重要的环节之一。
5.2 将清洗后的数据写入 MySQL
在
main.py
中串联整个流程:
# 文件路径:main.py
from collect_data import fetch_jobs, save_to_json
from clean_data import load_json, clean_jobs
from mysql_helper import MySQLHelper
def main():
# 1. 采集数据
print("开始采集数据...")
jobs = fetch_jobs(keyword="Python", city="全国", pages=3)
save_to_json(jobs, "data/jobs.json")
# 2. 清洗数据
print("开始清洗数据...")
raw_jobs = load_json("data/jobs.json")
df = clean_jobs(raw_jobs)
print(f"清洗后剩余 {len(df)} 条有效数据")
# 3. 写入 MySQL
print("开始写入 MySQL...")
helper = MySQLHelper()
records = df.to_dict(orient="records")
helper.batch_insert_jobs(records)
helper.close()
print("数据入库完成")
if __name__ == "__main__":
main()
其中
df.to_dict(orient="records")
会把 DataFrame 转成列表嵌套字典的结构,每一行对应一条记录,正好符合
executemany
的传参要求。
5.3 验证入库结果
进入 MySQL 查询:
SELECT COUNT(*) FROM job_info;
SELECT city, COUNT(*) AS cnt
FROM job_info
GROUP BY city
ORDER BY cnt DESC
LIMIT 10;
如果能够看到统计结果,说明数据已经成功入库。到这里,“数据采集—清洗—入库”链路就已经打通了。
6. 数据分析模块
6.1 从 MySQL 读取数据
新建
analysis.py
,使用 pandas 读取 MySQL 中的数据。
# 文件路径:analysis.py
import pandas as pd
import pymysql
def load_data_from_mysql():
conn = pymysql.connect(
host="localhost",
port=3306,
user="root",
password="123456",
database="boss_jobs",
charset="utf8mb4"
)
sql = "SELECT * FROM job_info"
df = pd.read_sql(sql, conn)
conn.close()
return df
def analyze_city_distribution(df: pd.DataFrame):
"""岗位数量城市分布"""
result = df["city"].value_counts().reset_index()
result.columns = ["city", "count"]
return result
def analyze_salary_by_job(df: pd.DataFrame):
"""不同岗位平均薪资"""
df["avg_salary"] = (df["salary_min"] + df["salary_max"]) / 2
result = df.groupby("job_name")["avg_salary"].mean().reset_index()
result = result.sort_values("avg_salary", ascending=False)
return result
def analyze_education_distribution(df: pd.DataFrame):
"""学历要求分布"""
return df["education"].value_counts().reset_index()
def analyze_experience_distribution(df: pd.DataFrame):
"""经验要求分布"""
return df["experience"].value_counts().reset_index()
值得说明的是,
pd.read_sql
能在读取数据库结果时直接生成 DataFrame,节省了手动逐行转换的步骤。这是 pandas 在数据分析场景中非常实用的功能。
6.2 计算平均薪资的注意事项
平均薪资的计算方式可以根据业务需求调整:
-
简单平均:
(salary_min + salary_max) / 2。 - 加权平均:根据岗位数量进行加权。
- 中位数:用中位数代替平均值,可以避免极端高薪数据扭曲整体结果。
在实际分析中,中位数往往比平均值更有参考价值。比如一个岗位样本里有 5 个月薪 5K 的数据和 1 个月薪 50K 的数据,平均值是 12.5K,但中位数更能反映大多数人的薪资水平。后续可视化时,可以同时输出平均值和中位数。
6.3 将分析结果导出为 JSON
为了让可视化模块和数据分析模块解耦,可以把聚合结果导出为中间文件:
def export_analysis_to_json():
df = load_data_from_mysql()
city_dist = analyze_city_distribution(df)
salary_by_job = analyze_salary_by_job(df)
edu_dist = analyze_education_distribution(df)
exp_dist = analyze_experience_distribution(df)
payload = {
"city_dist": city_dist.to_dict(orient="records"),
"salary_by_job": salary_by_job.to_dict(orient="records"),
"edu_dist": edu_dist.to_dict(orient="records"),
"exp_dist": exp_dist.to_dict(orient="records"),
}
with open("data/analysis_result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
7. 可视化图表实现
7.1 pyecharts 基础用法
pyecharts 是基于 ECharts 封装的 Python 可视化库,可以生成交互式 HTML 图表。它的核心用法是先创建一个图表对象,然后添加数据,最后渲染到 HTML 文件。
# 文件路径:visualize.py
from pyecharts.charts import Bar, Pie, Line
from pyecharts import options as opts
import json
def load_analysis_result(filepath="data/analysis_result.json"):
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
7.2 岗位数量 Top10 城市柱状图
def draw_city_bar(data, output="output/city_bar.html"):
"""
绘制城市岗位数量分布柱状图
"""
data = sorted(data, key=lambda x: x["count"], reverse=True)[:10]
cities = [d["city"] for d in data]
counts = [d["count"] for d in data]
bar = (
Bar()
.add_xaxis(cities)
.add_yaxis("岗位数量", counts)
.set_global_opts(
title_opts=opts.TitleOpts(title="岗位数量 Top10 城市"),
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=45)),
yaxis_opts=opts.AxisOpts(name="数量"),
)
)
bar.render(output)
print(f"图表已保存:{output}")
这里设置了
axislabel_opts=opts.LabelOpts(rotate=45)
,目的是让城市名称旋转 45 度,避免文字重叠。真实项目中,城市名较长时这个设置非常管用。
7.3 平均薪资横向条形图
当岗位名称较长时,横向条形图比纵向柱状图更美观。因为横向条形图的 Y 轴是分类项,可以容纳更长的名称。
def draw_salary_bar(data, output="output/salary_bar.html"):
"""
绘制平均薪资 Top15 岗位横向条形图
"""
data = sorted(data, key=lambda x: x["avg_salary"], reverse=True)[:15]
jobs = [d["job_name"] for d in data]
salary = [round(d["avg_salary"], 1) for d in data]
bar = (
Bar()
.add_xaxis(jobs)
.add_yaxis("平均薪资(K)", salary)
.reversal_axis()
.set_global_opts(
title_opts=opts.TitleOpts(title="不同岗位平均薪资 Top15"),
yaxis_opts=opts.AxisOpts(name="岗位"),
xaxis_opts=opts.AxisOpts(name="平均薪资(K)"),
)
)
bar.render(output)
print(f"图表已保存:{output}")
reversal_axis()
就是横向翻转的核心方法。调用后原来的 X 轴和 Y 轴互换,适合分类项比较多、名称比较长的场景。
7.4 学历要求饼图
def draw_education_pie(data, output="output/education_pie.html"):
"""
绘制学历要求分布饼图
"""
edu_pairs = [(d["education"], d["count"]) for d in data]
pie = (
Pie()
.add(series_name="学历要求", data_pair=edu_pairs)
.set_global_opts(
title_opts=opts.TitleOpts(title="学历要求分布"),
legend_opts=opts.LegendOpts(orient="vertical", pos_left="5%")
)
.set_series_opts(
label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)")
)
)
pie.render(output)
print(f"图表已保存:{output}")
formatter="{b}: {c} ({d}%)"
是 ECharts 的模板语法,
{b}
表示名称,
{c}
表示数值,
{d}
表示占比。这样设置之后,饼图上会直接显示“本科: 45 (42.5%)”这样的标签,信息更完整。
7.5 组合生成报告页面
如果只想单独查看某一类图表,逐个生成 HTML 文件是可以的。但如果想做一个完整的数据报告,可以尝试用
Page
把多个图表组合在同一个页面中。
from pyecharts.charts import Page
def generate_report():
result = load_analysis_result()
city_bar = draw_city_bar(result["city_dist"])
salary_bar = draw_salary_bar(result["salary_by_job"])
edu_pie = draw_education_pie(result["edu_dist"])
exp_bar = draw_experience_bar(result["exp_dist"])
page = Page(layouter=Page.SimplePageLayout())
page.add(city_bar, salary_bar, edu_pie, exp_bar)
page.render("output/岗位可视化报告.html")
print("报告生成完成")
需要说明的是,上面示例中我在每个绘图函数里都调用了
render()
,如果把这些函数结果加入
Page
,会分别生成单图文件。更合理的做法是让绘图函数只返回图表对象,再由
Page
统一渲染。你可以按这个思路自行调整:
def create_city_bar(data):
bar = Bar()
# 配置图表
return bar
def create_salary_bar(data):
bar = Bar()
# 配置图表
return bar
# 在 generate_report 中统一渲染
page.add(create_city_bar(result["city_dist"]), create_salary_bar(result["salary_by_job"]))
8. 完整项目代码整合
为了方便你直接跑通整个流程,下面给出一个简化版的主程序。这个版本不依赖采集接口,而是模拟生成一份样例数据,重点演示“清洗—入库—分析—可视化”链路。等你确认流程没问题,再把采集模块替换成真实数据即可。
# 文件路径:demo_main.py
import random
import pymysql
import pandas as pd
def generate_demo_data():
"""生成一份模拟岗位数据,用于流程演示"""
cities = ["北京", "上海", "深圳", "广州", "杭州", "成都", "武汉", "南京", "西安", "长沙"]
jobs = ["Java开发", "Python开发", "前端开发", "测试工程师", "数据分析师", "算法工程师"]
education_list = ["本科", "大专", "硕士", "不限"]
experience_list = ["1-3年", "3-5年", "5-10年", "在校/应届"]
data = []
for _ in range(500):
item = {
"job_name": random.choice(jobs),
"salary_min": random.randint(8, 20),
"salary_max": random.randint(20, 50),
"city": random.choice(cities),
"experience": random.choice(experience_list),
"education": random.choice(education_list),
"company_name": "示例科技有限公司",
"company_size": "100-499人",
"industry": "互联网",
}
data.append(item)
return data
def create_table():
conn = pymysql.connect(
host="localhost", port=3306, user="root",
password="123456", database="boss_jobs", charset="utf8mb4"
)
sql = """
CREATE TABLE IF NOT EXISTS job_info (
id INT PRIMARY KEY AUTO_INCREMENT,
job_name VARCHAR(100) NOT NULL,
salary_min INT DEFAULT 0,
salary_max INT DEFAULT 0,
city VARCHAR(50),
experience VARCHAR(50),
education VARCHAR(50),
company_name VARCHAR(200),
company_size VARCHAR(100),
industry VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
with conn.cursor() as cursor:
cursor.execute(sql)
conn.commit()
conn.close()
def insert_demo_data(data):
conn = pymysql.connect(
host="localhost", port=3306, user="root",
password="123456", database="boss_jobs", charset="utf8mb4"
)
sql = """
INSERT INTO job_info(job_name, salary_min, salary_max, city,
experience, education, company_name,
company_size, industry)
VALUES(%(job_name)s, %(salary_min)s, %(salary_max)s, %(city)s,
%(experience)s, %(education)s, %(company_name)s,
%(company_size)s, %(industry)s)
"""
with conn.cursor() as cursor:
cursor.executemany(sql, data)
conn.commit()
conn.close()
def read_and_analysis():
conn = pymysql.connect(
host="localhost", port=3306, user="root",
password="123456", database="boss_jobs", charset="utf8mb4"
)
df = pd.read_sql("SELECT * FROM job_info", conn)
conn.close()
print("城市岗位数量分布:")
print(df["city"].value_counts().head(10))
df["avg_salary"] = (df["salary_min"] + df["salary_max"]) / 2
print("\n岗位平均薪资:")
print(df.groupby("job_name")["avg_salary"].mean().sort_values(ascending=False))
if __name__ == "__main__":
# 如果你只需要演示流程,运行前请确保 MySQL 中存在 boss_jobs 数据库
create_table()
demo_data = generate_demo_data()
insert_demo_data(demo_data)
read_and_analysis()
运行这个脚本之前,请先确认 MySQL 中已经创建了
boss_jobs
数据库,并且账号密码与脚本一致。运行命令:
python demo_main.py
预期输出类似这样:
城市岗位数量分布:
杭州 63
北京 58
上海 57
深圳 54
广州 52
成都 50
...
岗位平均薪资:
算法工程师 36.20
数据分析师 31.35
Java开发 29.88
Python开发 28.74
前端开发 26.92
测试工程师 23.56
因为示例数据是随机生成的,所以每次运行结果会略有不同,但整体流程是一致的。
9. 运行效果与结果解读
9.1 图表文件说明
运行可视化模块后,
output
目录下会生成以下文件:
| 文件 | 内容 |
|---|---|
| city_bar.html | 岗位数量 Top10 城市柱状图 |
| salary_bar.html | 平均薪资 Top15 岗位横向条形图 |
| education_pie.html | 学历要求分布饼图 |
| experience_bar.html | 经验要求分布条形图 |
| 岗位可视化报告.html | 整合以上图表的单页报告 |
直接用浏览器双击 HTML 文件即可打开,图表支持鼠标悬停查看详细数据。
9.2 实际业务解读思路
拿到图表后,可以从以下几个角度做解读:
- 城市维度:岗位数量越多的城市,通常意味着该领域的产业集聚效应越明显。
- 薪资维度:平均薪资最高的岗位往往需要较高的技术门槛,比如算法、大数据相关岗位。
- 学历维度:如果“本科”和“不限”占比很高,说明该岗位对学历的硬性门槛不是特别高,更多看技术和项目经验。
- 经验维度:如果“3-5年”要求占比最高,说明该岗位更看重熟手,新人进入可能有一定门槛。
这些解读可以用于求职决策,比如判断自己该往哪个城市投递简历,该重点补充哪方面技能,以及评估当前市场的薪资区间。
10. 常见问题与排查思路
10.1 MySQL 连接报错
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Access denied for user 'root'@'localhost' | 账号密码错误 | 检查 MySQL 账号密码 |
| Unknown database 'boss_jobs' | 数据库未创建 |
执行
CREATE DATABASE boss_jobs
|
| Access denied for user 'root'@'localhost' (using password: NO) | 密码为空但未传入 | 检查连接参数中的 password |
| /tmp/mysql.sock 报错 | MySQL 服务未启动 | 启动 MySQL 服务 |
这里重点说下第三条。有些本机环境 MySQL 的 root 用户密码为空,你直接写
password=""
是可以连上的,但如果你写了
password=None
,pymysql 可能把它当成无密码处理,也容易产生理解偏差。更推荐的做法是显式指定一个非空密码,在本地测试环境里也保持和线上环境一致的习惯。
10.2 pymysql 插入数据时报错 Unknown column
这种问题通常是因为 SQL 中的字段名和表结构不一致,比如代码里写了
job_name
,但表里的字段实际叫
jobName
。排查方法:
DESC job_info;
先查看表的真实字段名,再和 INSERT 语句中的字段一一对比。
10.3 pandas 读取 MySQL 时报错
如果
pd.read_sql
报错,先检查 SQL 语句能否在 Navicat 或 MySQL 命令行中正常执行。如果 SQL 本身能跑通,再检查 pymysql 是否安装。注意 MySQL 8.0 默认使用 caching_sha2_password 认证方式,如果 pymysql 版本过旧,可能会出现认证错误,建议升级 pymysql 到最新版。
10.4 pyecharts 图表不显示
HTML 图表打开后空白,常见原因有两种:
- 图表数据为空,比如传入的列表是空的。
- 文件路径有中文或空格,导致浏览器加载本地资源异常。
可以在代码里加一行
print(data)
检查数据。如果是路径问题,把输出文件名改成纯英文,比如
output/city_bar.html
。
10.5 爬虫请求被拒绝
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 403 Forbidden | 服务器拒绝请求 | 检查 User-Agent、Referer 等请求头 |
| 302 重定向到登录页 | 需要登录才能访问 | 确认是否需要在请求中携带 Cookie |
| 请求频率过高被限制 | 触发反爬机制 | 降低请求频率,增加随机延时 |
这里要尤其注意,如果目标网站明确要求登录,或者页面存在验证码,请不要尝试绕过。这不仅是技术问题,也涉及合规风险。学习阶段,建议使用模拟数据或网站提供的公开示例数据来练习整个流程。
11. 最佳实践与工程建议
11.1 数据表设计的可扩展性
岗位数据表可以根据业务需求扩展更多字段,比如:
- 岗位标签(如“五险一金”“弹性工作”)。
- 职位描述详情。
- 工作地址经纬度,便于后续做地图可视化。
- 数据来源渠道,便于做多渠道数据对比分析。
字段设计时遵循一个原则:能拆开的字段尽量拆开,不要塞进一个字符串里。比如“15K-25K·14薪”这样的字符串,在入表前就应该拆成
salary_min
、
salary_max
、
bonus_months
。否则后续每次分析都要做字符串解析,既麻烦又容易出错。
11.2 配置管理
项目中的数据库信息、接口地址、请求头都属于配置信息,不应该硬编码在代码里。建议在项目根目录创建
config.py
或
.env
文件集中管理。
示例
config.py
:
# 文件路径:config.py
DB_CONFIG = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "123456",
"database": "boss_jobs",
"charset": "utf8mb4",
}
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
}
11.3 代码分层
即使是一个练手项目,也建议保持基本的分层意识。数据采集、数据清洗、数据入库、数据分析、可视化分别放在独立模块中,这样以后更换数据源或者更换可视化方案时,不需要改整盘代码。
如果一家公司后来说可以直连数据库推数,不再需要爬虫,你只需要把采集模块替换成从公司数据仓库读文件就可以了,MySQL 连接模块、分析模块、可视化模块都不用动。
11.4 数据质量校验
入库前进行数据质量校验,是数据分析项目的基本功。建议检查以下内容:
- 必填字段是否为空。
- 数值字段是否越界,比如薪资为负。
- 是否有重复记录。
- 字段长度是否超出数据库定义的长度。
如果发现某批数据异常,应该先定位问题原因,再进行清洗或丢弃,而不是直接把脏数据写入数据库。
11.5 异常处理与日志
真实项目中,采集任务可能因为网络波动、对方改版、接口参数变化而失败。建议在代码中加入日志输出,方便事后追踪。
一个简单的日志示例:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("project.log", encoding="utf-8"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("开始采集第 1 页数据")
11.6 生产环境注意事项
如果这个项目需要进入生产环境定时运行,有几个点需要额外注意:
- 数据库账号使用最小权限账号,不要用 root 连接业务库。
- 采集任务和数据库写入任务分开部署,避免数据库写入阻塞时影响采集。
- 监控任务运行状态,失败后自动重试并发送告警。
- 数据库表定期备份,尤其是已经积累了大量历史数据时。
12. 总结与学习路线
本文从零完成了一个“Boss 直聘岗位可视化分析”项目的主体流程,覆盖了数据采集思路、MySQL 表结构设计、pymysql 数据写入、pandas 聚合分析、pyecharts 可视化图表生成,以及常见报错排查。代码整体可以直接复制运行,但由于真实网站的接口和字段会随着时间变化,你实际动手时需要根据目标网站的返回结构做调整。
如果你希望在这个项目基础上继续深入,可以按下面的路线学习:
- 扩充数据源:除了岗位数据,再采集一些公司信息、面试经验、薪资爆料数据,做多表联合分析。
- 引入定时调度:使用 cron 或者 APScheduler 定时采集,形成一定时间跨度的岗位趋势数据。
- 增加机器学习分析:基于岗位描述文本做关键词提取,分析不同岗位的技能需求分布。
- 前端展示优化:把 pyecharts 生成的 HTML 报告嵌入 Flask 或 FastAPI 服务,做成一个可视化看板系统。
- 自动化报告推送:定时生成趋势图,通过企业微信或者邮件发送给团队。
回到项目本身,我最想提醒你的一点是:不要只把代码跑通就结束了。数据清洗时想一想为什么要去重,为什么要把薪资拆成下限和上限;可视化时想一想为什么横向条形图更适合长分类名;分析时想一想平均薪资和中位数薪资的区别。这些细节才是数据分析能力提升的关键。
如果本文对你有帮助,可以收藏备用。下一篇可以聊聊如何把这份岗位分析做成一个实时看板系统,欢迎关注后续更新。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_29056701/article/details/166496354



