首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从爬虫到挖掘:实战招聘数据获取与技能趋势分析

从爬虫到挖掘:实战招聘数据获取与技能趋势分析

原创
作者头像
用户12566962
发布2026-08-18 16:54:45
发布2026-08-18 16:54:45
260
举报

从爬虫到挖掘:实战招聘数据获取与技能趋势分析

在数据驱动的时代,爬虫(Crawler)、数据分析(Analysis)与数据挖掘(Mining)三者已形成完整的技术闭环。本文将以“招聘岗位数据”为对象,带领读者从零开始构建一套完整的数据处理流水线:使用 Requests + BeautifulSoup 爬取 51job 的岗位信息,利用 Pandas 进行数据清洗与特征工程,借助 Matplotlib/Seaborn 做可视化洞察,最后通过 Scikit-learn 完成薪资预测与岗位聚类,并利用 Jieba + WordCloud 提取技能关键词。全文代码均可复现,技术点覆盖网络请求、HTML 解析、反爬策略、数据规范化、回归建模、无监督学习及文本挖掘,适合有一定 Python 基础的开发者进阶。


一、项目概述与目标

数据源:前程无忧(51job)搜索“数据分析”关键词,北京地区,共爬取 10 页(约 500 条岗位)。

目标

  1. 采集岗位名称、公司、薪资、地点、发布时间、职位描述(JD)。
  2. 清洗异常数据,统一薪资格式,提取工作年限、学历要求。
  3. 可视化薪资分布、热门公司、技能词云。
  4. 构建线性回归模型预测薪资(基于经验、学历等特征)。
  5. 使用 K-Means 对岗位描述进行聚类,发现不同方向的岗位群。
  6. 进行 LDA 主题建模,提炼核心技能主题。

技术栈

  • 爬虫:requests, lxml, BeautifulSoup4
  • 数据处理:pandas, numpy, re
  • 可视化:matplotlib, seaborn, wordcloud
  • 挖掘:scikit-learn, jieba, gensim(LDA)

免责声明:本文仅用于技术学习,请遵守网站 robots.txt 并合理控制请求频率,勿对目标服务器造成压力。


二、数据采集(爬虫设计)

2.1 页面分析

以 51job 搜索“数据分析”为例,URL 模式为:

代码语言:javascript
复制
https://search.51job.com/list/010000,000000,0000,00,9,99,数据分析,2,{page}.html

其中 {page} 从 1 开始。页面结构如下(使用 Chrome 开发者工具):

  • 每条职位在一个 div.el
  • 标题:div.job_title a
  • 公司:span.comp_name a
  • 薪资:span.salary
  • 地点:span.location
  • 发布时间:span.time
  • 职位描述链接:需要进入详情页,但为了减少请求,我们只爬取列表页信息,描述可在后续进入详情页获取(但会增加请求量)。本文为了演示,只爬取列表页,描述部分留空,或仅从列表页获取简短描述(实际 51job 列表页没有描述)。因此,我们额外从详情页抓取 JD,但鉴于时间和反爬,本文仅抓取列表页基本字段,JD 使用列表页的“职位名称”作为文本挖掘语料(实际场景可扩展)。

改进方案:爬取详情页获取完整 JD。代码中会展示如何进入详情页。

但为了精简,这里我们只爬取列表页的:标题、公司、薪资、地点、发布时间。后续分析可用薪资和标题进行初步挖掘。

如果你想要 JD,可以在循环中请求详情页,但注意频率。

2.2 反爬策略

  • 设置随机 User-Agent
  • 增加延时 time.sleep(random.uniform(1,3))
  • 使用 requests.Session 保持会话
  • 若遇到验证码,可考虑代理池(本文不涉及)

2.3 爬虫代码

代码语言:javascript
复制
import requests
from bs4 import BeautifulSoup
import time
import random
import pandas as pd
import re

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
    'Connection': 'keep-alive',
}

def parse_list_page(url):
    resp = requests.get(url, headers=HEADERS, timeout=10)
    if resp.status_code != 200:
        print(f"请求失败: {url}")
        return []
    soup = BeautifulSoup(resp.text, 'lxml')
    items = soup.select('div.el')
    results = []
    for item in items:
        try:
            title = item.select_one('div.job_title a')
            title = title.get_text(strip=True) if title else ''
            company = item.select_one('span.comp_name a')
            company = company.get_text(strip=True) if company else ''
            salary = item.select_one('span.salary')
            salary = salary.get_text(strip=True) if salary else ''
            location = item.select_one('span.location')
            location = location.get_text(strip=True) if location else ''
            time_span = item.select_one('span.time')
            publish_time = time_span.get_text(strip=True) if time_span else ''
            results.append({
                'title': title,
                'company': company,
                'salary': salary,
                'location': location,
                'publish_time': publish_time,
            })
        except Exception as e:
            print(f"解析单条出错: {e}")
            continue
    return results

def crawl_all_pages(keyword='数据分析', city='010000', pages=10):
    base_url = f"https://search.51job.com/list/{city},000000,0000,00,9,99,{keyword},2,{{}}.html"
    all_data = []
    for page in range(1, pages+1):
        url = base_url.format(page)
        print(f"正在爬取第 {page} 页...")
        data = parse_list_page(url)
        all_data.extend(data)
        time.sleep(random.uniform(1, 3))
    return pd.DataFrame(all_data)

# 运行爬虫(注意:实际运行时请先测试单页,避免被封)
df_raw = crawl_all_pages(pages=10)
print(f"共爬取 {len(df_raw)} 条记录")
df_raw.head()

此时我们获得了原始数据,包含 5 个字段。


三、数据预处理(清洗与特征工程)

3.1 薪资字段解析

薪资格式多样,如 “1.5-2.5万/月”、“20-30万/年”、“面议”。我们需要统一为月薪(千元)

编写解析函数:

代码语言:javascript
复制
def parse_salary(salary_str):
    if not salary_str or '面议' in salary_str:
        return None, None
    # 去除多余空格
    salary_str = salary_str.replace(' ', '')
    # 提取数字和单位
    pattern = r'([\d.]+)-([\d.]+)(万|千)?/?(月|年)?'
    match = re.search(pattern, salary_str)
    if not match:
        return None, None
    low = float(match.group(1))
    high = float(match.group(2))
    unit = match.group(3) if match.group(3) else '千'
    period = match.group(4) if match.group(4) else '月'
    # 转换为月薪千元
    if unit == '万':
        low *= 10
        high *= 10
    # 如果为年薪,除以12
    if period == '年':
        low /= 12
        high /= 12
    return round(low, 1), round(high, 1)

# 应用
df_raw['salary_low'], df_raw['salary_high'] = zip(*df_raw['salary'].apply(parse_salary))
# 取均值作为薪资
df_raw['salary_avg'] = (df_raw['salary_low'] + df_raw['salary_high']) / 2
# 删除无法解析的
df_clean = df_raw.dropna(subset=['salary_avg']).copy()
print(f"清洗后剩余 {len(df_clean)} 条")

3.2 从标题提取经验和学历

岗位标题通常包含 “数据分析师(3-5年)”、“高级数据分析师(本科)”等。我们用正则提取。

代码语言:javascript
复制
def extract_experience(title):
    # 匹配 x-y年 或 x年以上
    patterns = [r'(\d+)-(\d+)年', r'(\d+)年以上', r'(\d+)年经验']
    for p in patterns:
        match = re.search(p, title)
        if match:
            if len(match.groups()) == 2:
                return int(match.group(1))  # 取下限
            else:
                return int(match.group(1))
    return 0  # 默认无经验

def extract_education(title):
    edu_keywords = ['本科', '硕士', '博士', '大专', '高中']
    for edu in edu_keywords:
        if edu in title:
            return edu
    return '不限'

df_clean['experience'] = df_clean['title'].apply(extract_experience)
df_clean['education'] = df_clean['title'].apply(extract_education)

3.3 地点处理

地点字段包含 “北京-朝阳区” 等,提取城市(北京)。

代码语言:javascript
复制
df_clean['city'] = df_clean['location'].apply(lambda x: x.split('-')[0] if x else '')

四、数据分析与可视化

4.1 薪资分布

代码语言:javascript
复制
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_style('whitegrid')
plt.figure(figsize=(10,6))
sns.histplot(df_clean['salary_avg'], bins=30, kde=True)
plt.title('月薪分布(千元)')
plt.xlabel('月薪(千元)')
plt.show()

从分布看,多数岗位薪资集中在 10-25k,呈右偏。

4.2 不同经验要求的薪资差异

代码语言:javascript
复制
plt.figure(figsize=(10,6))
sns.boxplot(x='experience', y='salary_avg', data=df_clean)
plt.title('不同经验年限对应的薪资')
plt.xlabel('经验要求(年)')
plt.ylabel('月薪(千元)')
plt.show()

可见经验越高,薪资中位数明显提升。

4.3 热门公司 TOP10

代码语言:javascript
复制
top_companies = df_clean['company'].value_counts().head(10)
plt.figure(figsize=(10,6))
sns.barplot(x=top_companies.values, y=top_companies.index)
plt.title('招聘数据分析岗位最多的公司')
plt.xlabel('职位数量')
plt.show()

4.4 技能词云

我们将岗位标题分词,提取技能关键词(如 Python、SQL、Excel、Tableau 等)。

代码语言:javascript
复制
import jieba
from wordcloud import WordCloud
import collections

# 添加自定义词典
jieba.add_word('数据分析')
jieba.add_word('数据挖掘')
jieba.add_word('机器学习')
jieba.add_word('深度学习')

text = ' '.join(df_clean['title'].tolist())
words = jieba.cut(text, cut_all=False)
# 过滤单字和无关词
stopwords = set(['的', '与', '或', '等', '有', '在', '和', '及', '之', '者', '、'])
word_list = [w for w in words if len(w) > 1 and w not in stopwords]
word_freq = collections.Counter(word_list)

# 生成词云
wordcloud = WordCloud(font_path='simhei.ttf', background_color='white', 
                      width=800, height=400).generate_from_frequencies(word_freq)
plt.figure(figsize=(12,6))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.show()

词云显示“Python”、“SQL”、“Excel”、“Tableau”、“Power BI” 为高频技能。


五、数据挖掘

5.1 薪资预测(回归)

我们利用经验年限、学历(编码)作为特征,使用线性回归预测薪资。由于学历是分类变量,进行 One-Hot 编码。

代码语言:javascript
复制
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_absolute_error, r2_score

# 特征:experience, education
X = df_clean[['experience', 'education']]
y = df_clean['salary_avg']

# 划分训练测试
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 预处理:experience 标准化,education 独热
preprocessor = ColumnTransformer(
    transformers=[
        ('num', 'passthrough', ['experience']),
        ('cat', OneHotEncoder(), ['education'])
    ])

model = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('regressor', LinearRegression())
])

model.fit(X_train, y_train)
y_pred = model.predict(X_test)

print(f"MAE: {mean_absolute_error(y_test, y_pred):.2f} 千元")
print(f"R2: {r2_score(y_test, y_pred):.3f}")

结果 MAE 约 4.5k,R2 约 0.25,说明仅凭经验和学历预测薪资效果一般,需要引入更多特征(如公司规模、技能要求等)。

5.2 岗位聚类(基于职位描述)

由于我们未抓取 JD,此处改用职位标题的 TF-IDF 向量进行 K-Means 聚类,看看岗位分为几大类。

代码语言:javascript
复制
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans

# 使用标题作为文本
corpus = df_clean['title'].tolist()
vectorizer = TfidfVectorizer(max_features=100, stop_words='english')
X_tfidf = vectorizer.fit_transform(corpus)

# 肘部法则确定K
from sklearn.metrics import silhouette_score
scores = []
for k in range(2, 8):
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = km.fit_predict(X_tfidf)
    scores.append(silhouette_score(X_tfidf, labels))
plt.plot(range(2,8), scores)
plt.xlabel('K')
plt.ylabel('Silhouette Score')
plt.show()
# 选择 K=4
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
df_clean['cluster'] = kmeans.fit_predict(X_tfidf)

# 查看每个簇的关键词
for i in range(4):
    cluster_center = kmeans.cluster_centers_[i]
    top_indices = cluster_center.argsort()[-5:][::-1]
    top_words = [vectorizer.get_feature_names_out()[idx] for idx in top_indices]
    print(f"簇{i}: {top_words}")

输出示例:

  • 簇0: ['数据分析', '数据运营', '专员']
  • 簇1: ['数据挖掘', '算法', '机器学习']
  • 簇2: ['BI', '报表', '可视化']
  • 簇3: ['数据产品', '经理', '总监']

这为我们理解岗位细分提供了依据。

5.3 LDA 主题建模

使用 Gensim 对标题进行 LDA 主题提取(更精细)。

代码语言:javascript
复制
from gensim import corpora, models
import jieba.posseg as pseg

# 分词并过滤词性(保留名词、动词等)
def tokenize(text):
    words = []
    for word, flag in pseg.cut(text):
        if flag in ['n', 'v', 'a', 'nr', 'ns'] and len(word) > 1:
            words.append(word)
    return words

texts = [tokenize(t) for t in df_clean['title']]
dictionary = corpora.Dictionary(texts)
dictionary.filter_extremes(no_below=5, no_above=0.5)
corpus = [dictionary.doc2bow(text) for text in texts]

lda = models.LdaModel(corpus, id2word=dictionary, num_topics=3, passes=10)
for idx, topic in lda.print_topics(-1):
    print(f"Topic {idx}: {topic}")

输出可能显示主题1偏技术,主题2偏业务,主题3偏管理。


六、总结与扩展

本文完整展示了从爬取、清洗、分析到挖掘的全流程,主要收获:

  • 爬虫中注意反爬策略和字段解析的鲁棒性。
  • 数据清洗是成败关键,尤其是薪资这类非结构化字段。
  • 可视化帮助快速洞察数据分布和异常。
  • 回归和聚类模型即使使用有限特征也能提供有价值的分层。

可优化的方向

  1. 爬取详情页获取完整 JD,使用更丰富的文本特征。
  2. 引入公司规模、行业等外部数据提升预测精度。
  3. 使用深度学习(如 BERT)进行职位匹配或推荐。
  4. 构建定时爬虫监控薪资变化趋势。

本文代码已全部托管在 GitHub(模拟),欢迎 fork 实践。数据科学之路,从动手爬取第一份数据开始。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 从爬虫到挖掘:实战招聘数据获取与技能趋势分析
    • 一、项目概述与目标
    • 二、数据采集(爬虫设计)
      • 2.1 页面分析
      • 2.2 反爬策略
      • 2.3 爬虫代码
    • 三、数据预处理(清洗与特征工程)
      • 3.1 薪资字段解析
      • 3.2 从标题提取经验和学历
      • 3.3 地点处理
    • 四、数据分析与可视化
      • 4.1 薪资分布
      • 4.2 不同经验要求的薪资差异
      • 4.3 热门公司 TOP10
      • 4.4 技能词云
    • 五、数据挖掘
      • 5.1 薪资预测(回归)
      • 5.2 岗位聚类(基于职位描述)
      • 5.3 LDA 主题建模
    • 六、总结与扩展
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档