李傲天头像
关注

Python+MySQL实战:Boss直聘岗位数据可视化分析全流程教程

各位读者朋友好。

这段时间在梳理数据分析方向的学习路径,很多同学都会拿招聘平台的岗位信息来练手。原因很简单:招聘数据是真实业务场景,字段比较规整,包含岗位、城市、薪资、经验要求、学历要求、公司规模等信息,非常适合做清洗、入库、分析和可视化这条完整链路。本文就围绕 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 项目流程拆解

整个项目可以拆成四个大步骤:

  1. 数据采集:请求招聘平台接口或页面,拿到岗位列表数据。
  2. 数据入库:将解析好的数据清洗后写入 MySQL 表。
  3. 数据分析:从 MySQL 读取数据,用 pandas 做聚合统计。
  4. 可视化展示:使用 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 合法合规说明

在编写采集代码之前,先强调合规问题。采集第三方网站数据前,你应该确认:

  1. 目标网站是否有 robots 协议,明确禁止爬取的内容不要碰。
  2. 请求频率要合理,建议在请求之间加入随机延时,避免给服务器造成压力。
  3. 采集数据仅用于学习研究,不要用于商业盈利,更不要转卖数据。
  4. 不得绕过网站登录、验证码等安全机制。

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 读取原始数据并清洗

采集到的原始数据往往存在几个问题:

  1. 薪资字段可能是字符串,比如“15K-25K”,需要拆成数值。
  2. 某些字段为空,比如城市为空、学历为空。
  3. 同一岗位可能重复采集,需要去重。
  4. 岗位名称含有特殊字符或前后空格。

清洗的核心理念是: 先让数据变成统一的、可计算的格式,再入库。

新建 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 实际业务解读思路

拿到图表后,可以从以下几个角度做解读:

  1. 城市维度:岗位数量越多的城市,通常意味着该领域的产业集聚效应越明显。
  2. 薪资维度:平均薪资最高的岗位往往需要较高的技术门槛,比如算法、大数据相关岗位。
  3. 学历维度:如果“本科”和“不限”占比很高,说明该岗位对学历的硬性门槛不是特别高,更多看技术和项目经验。
  4. 经验维度:如果“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 图表打开后空白,常见原因有两种:

  1. 图表数据为空,比如传入的列表是空的。
  2. 文件路径有中文或空格,导致浏览器加载本地资源异常。

可以在代码里加一行 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 数据质量校验

入库前进行数据质量校验,是数据分析项目的基本功。建议检查以下内容:

  1. 必填字段是否为空。
  2. 数值字段是否越界,比如薪资为负。
  3. 是否有重复记录。
  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 生产环境注意事项

如果这个项目需要进入生产环境定时运行,有几个点需要额外注意:

  1. 数据库账号使用最小权限账号,不要用 root 连接业务库。
  2. 采集任务和数据库写入任务分开部署,避免数据库写入阻塞时影响采集。
  3. 监控任务运行状态,失败后自动重试并发送告警。
  4. 数据库表定期备份,尤其是已经积累了大量历史数据时。

12. 总结与学习路线

本文从零完成了一个“Boss 直聘岗位可视化分析”项目的主体流程,覆盖了数据采集思路、MySQL 表结构设计、pymysql 数据写入、pandas 聚合分析、pyecharts 可视化图表生成,以及常见报错排查。代码整体可以直接复制运行,但由于真实网站的接口和字段会随着时间变化,你实际动手时需要根据目标网站的返回结构做调整。

如果你希望在这个项目基础上继续深入,可以按下面的路线学习:

  1. 扩充数据源:除了岗位数据,再采集一些公司信息、面试经验、薪资爆料数据,做多表联合分析。
  2. 引入定时调度:使用 cron 或者 APScheduler 定时采集,形成一定时间跨度的岗位趋势数据。
  3. 增加机器学习分析:基于岗位描述文本做关键词提取,分析不同岗位的技能需求分布。
  4. 前端展示优化:把 pyecharts 生成的 HTML 报告嵌入 Flask 或 FastAPI 服务,做成一个可视化看板系统。
  5. 自动化报告推送:定时生成趋势图,通过企业微信或者邮件发送给团队。

回到项目本身,我最想提醒你的一点是:不要只把代码跑通就结束了。数据清洗时想一想为什么要去重,为什么要把薪资拆成下限和上限;可视化时想一想为什么横向条形图更适合长分类名;分析时想一想平均薪资和中位数薪资的区别。这些细节才是数据分析能力提升的关键。

如果本文对你有帮助,可以收藏备用。下一篇可以聊聊如何把这份岗位分析做成一个实时看板系统,欢迎关注后续更新。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_29056701/article/details/166496354

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--