基于Scrapy的拉勾网招聘数据爬取与Python数据分析实战

📅 2026/7/31 8:31:17 👁️ 阅读次数 📝 编程学习
基于Scrapy的拉勾网招聘数据爬取与Python数据分析实战

1. 项目缘起:从招聘数据中洞察市场脉搏

最近在帮一个做技术猎头的朋友分析市场趋势,他经常问我:“现在哪个技术栈最火?”“Python后端和Java后端,哪个岗位需求更大,薪资更高?” 这类问题,如果只凭感觉或者零星的招聘信息来回答,显然不够客观。于是,我决定用最直接的方式——从招聘网站抓取数据,用数据说话。拉勾网作为国内知名的互联网招聘平台,其数据具有很高的参考价值。这个项目,就是用Python的Scrapy框架,对拉勾网的招聘信息进行定向爬取,并对获取的数据进行清洗、分析和可视化,最终形成一份可量化的市场洞察报告。

这不仅仅是一个爬虫练习,更是一个完整的数据分析项目闭环。它涵盖了从数据采集(Scrapy爬虫)、数据清洗(Pandas)、到数据分析与可视化(Matplotlib/Seaborn)的全流程。对于想学习Python数据抓取、或者希望用数据驱动决策的朋友来说,这是一个非常典型的实战案例。无论你是想了解某个岗位的薪资分布、技能要求,还是想分析不同城市、不同规模公司的招聘偏好,这套方法都能为你提供扎实的数据支撑。

2. Scrapy框架选型与环境搭建:为什么是它?

在Python的爬虫生态里,我们有Requests+BeautifulSoup这样的“手动挡”组合,也有Selenium这样的“自动化”工具,那为什么这个项目我坚定地选择了Scrapy?这背后有几个核心考量。

首先,Scrapy是一个为大规模、结构化数据抓取而生的异步框架。拉勾网的招聘列表页、详情页结构清晰,我们需要爬取的是成千上万条具有相同字段(职位名、公司、薪资、地点、要求等)的记录。Scrapy内置的Item和Pipeline机制,天生就是为了高效、规范地处理这类结构化数据流。相比之下,用Requests需要自己管理请求队列、处理重试、解析和存储,代码会显得冗长且不易维护。

其次,性能与抗封禁能力。Scrapy基于Twisted异步网络库,可以轻松实现并发请求,极大地提高了数据抓取速度。同时,它内置了中间件(Middleware)机制,我们可以很方便地集成随机User-Agent、代理IP池、请求延迟等反反爬策略。拉勾网这类网站通常会有一定的反爬措施,Scrapy的这套架构让我们应对起来更加从容。

最后,项目的可扩展性与工程化。这个项目不是一次性的脚本。我们可能今天分析Python,明天分析Java,后天想增加对BOSS直聘的数据抓取。Scrapy的项目结构(spiders, items, pipelines, settings)非常清晰,不同站点的爬虫可以独立开发,共享通用的数据处理和存储逻辑,便于后续维护和扩展。

2.1 环境准备与依赖安装

接下来,我们开始搭建环境。我个人的习惯是使用condavenv创建独立的Python虚拟环境,避免包冲突。这里以venv为例。

# 1. 创建并激活虚拟环境(假设项目目录为 lagou_analysis) python -m venv lagou_venv # Windows lagou_venv\Scripts\activate # Linux/Mac source lagou_venv/bin/activate # 2. 安装核心依赖 pip install scrapy pandas matplotlib seaborn jupyter

这里解释一下每个包的作用:

  • scrapy: 爬虫框架本体。
  • pandas: 数据分析的核心库,用于数据清洗、转换和分析。
  • matplotlib&seaborn: 数据可视化库,seaborn基于matplotlib,能绘制更美观的统计图表。
  • jupyter: 可选,但强烈推荐。在数据分析阶段,使用Jupyter Notebook进行交互式的探索和可视化会非常方便。

安装完成后,我们可以初始化Scrapy项目。

scrapy startproject lagou_project cd lagou_project

这个命令会生成一个标准的Scrapy项目目录结构,其中最重要的几个文件/目录是:

  • lagou_project/spiders/: 存放爬虫文件,每个爬虫对应一个网站或一个抓取逻辑。
  • lagou_project/items.py: 定义我们要抓取的数据结构。
  • lagou_project/pipelines.py: 定义数据处理管道,如数据清洗、去重、存储到数据库或文件。
  • lagou_project/middlewares.py: 定义请求和响应的中间件,用于处理User-Agent、代理等。
  • lagou_project/settings.py: 项目的全局设置,如并发数、下载延迟、启用的组件等。

2.2 关键配置调优

在编写爬虫前,我们需要先调整settings.py中的一些关键配置,这直接关系到爬虫的稳定性和道德合规性。

# lagou_project/settings.py # 1. 遵守Robots协议并设置基本礼貌 ROBOTSTXT_OBEY = False # 拉勾网可能有robots.txt限制,根据实际情况调整,但务必谨慎 DOWNLOAD_DELAY = 3 # 每个请求之间的延迟(秒),这是最重要的反爬措施之一,给服务器喘息时间 CONCURRENT_REQUESTS_PER_DOMAIN = 2 # 对单个域名的并发请求数,不宜过高 # 2. 启用并配置中间件 DOWNLOADER_MIDDLEWARES = { 'lagou_project.middlewares.RandomUserAgentMiddleware': 543, # 自定义随机UA中间件 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 禁用默认的 } # 3. 配置Item Pipeline和Feed Export(用于存储) ITEM_PIPELINES = { 'lagou_project.pipelines.LagouDataPipeline': 300, } FEED_EXPORT_ENCODING = 'utf-8' # 导出文件编码

注意ROBOTSTXT_OBEY = False是一个需要慎重考虑的设置。在动手之前,最好查看一下目标网站的robots.txt文件(通常在网站根目录,如https://www.lagou.com/robots.txt),了解网站是否允许爬虫抓取招聘相关页面。即使设置为False,我们也必须通过DOWNLOAD_DELAY等手段,将请求频率控制在极低的水平,避免对目标网站造成负载压力。这是编写爬虫的基本职业道德。

3. 爬虫核心:解析拉勾网的数据结构

一切准备就绪,现在进入核心环节——编写爬虫。首先,我们需要分析拉勾网招聘列表页和详情页的网页结构。通过浏览器开发者工具(F12)进行网络抓包和元素审查,是这一步的标准操作。

3.1 列表页分析与请求模拟

拉勾网的搜索列表页(例如搜索“Python”)URL通常形如:https://www.lagou.com/jobs/list_Python?px=default&city=全国。但更重要的是,我们发现其真实的数据是通过一个POST请求的API接口返回的JSON数据,而不是直接渲染在HTML里。这是一个非常常见的前后端分离架构。

通过抓包,我们找到这个接口:https://www.lagou.com/jobs/positionAjax.json。它需要携带一些必要的参数和请求头。

# lagou_project/spiders/lagou_spider.py import scrapy import json from lagou_project.items import LagouJobItem class LagouSpider(scrapy.Spider): name = 'lagou' allowed_domains = ['lagou.com'] # 起始URL并不是列表页,而是直接调用API def start_requests(self): # 基础URL url = 'https://www.lagou.com/jobs/positionAjax.json' # 可以搜索不同的关键词和城市 keywords = ['Python', 'Java', '前端'] cities = ['北京', '上海', '深圳', '杭州', '广州'] for city in cities: for keyword in keywords: # 构造表单数据 form_data = { 'first': 'true', 'pn': '1', # 页码 'kd': keyword, 'city': city, } # 重要的请求头,特别是Referer headers = { 'Referer': f'https://www.lagou.com/jobs/list_{keyword}?px=default&city={city}', 'User-Agent': 'Mozilla/5.0...', # 会在中间件中动态设置 'X-Requested-With': 'XMLHttpRequest', # 表明是Ajax请求 } # 使用FormRequest发送POST请求 yield scrapy.FormRequest( url=url, formdata=form_data, headers=headers, callback=self.parse_list, meta={'keyword': keyword, 'city': city, 'pn': 1} # 传递额外参数给回调函数 ) def parse_list(self, response): """解析列表页API返回的JSON数据""" data = json.loads(response.text) # 拉勾网的数据结构:data -> content -> positionResult -> result jobs = data.get('content', {}).get('positionResult', {}).get('result', []) keyword = response.meta['keyword'] city = response.meta['city'] current_page = response.meta['pn'] if jobs: for job in jobs: # 提取列表页已有的关键信息 item = LagouJobItem() item['job_name'] = job.get('positionName') item['company'] = job.get('companyFullName') item['salary'] = job.get('salary') # 如 “15k-30k” item['city'] = job.get('city') item['work_year'] = job.get('workYear') item['education'] = job.get('education') item['company_size'] = job.get('companySize') item['finance_stage'] = job.get('financeStage') item['position_id'] = job.get('positionId') # 用于构造详情页链接 item['keyword'] = keyword item['source_city'] = city # 构造详情页URL,详情页通常有独立页面 detail_url = f"https://www.lagou.com/jobs/{item['position_id']}.html" # 将item通过meta传递,在详情页回调中补充数据 yield scrapy.Request( url=detail_url, callback=self.parse_detail, meta={'item': item}, headers={'Referer': response.url} # 详情页请求也需要Referer ) # 翻页逻辑:判断是否还有下一页 total_count = data.get('content', {}).get('positionResult', {}).get('totalCount', 0) page_size = 15 # 拉勾网每页默认显示15条 if current_page * page_size < total_count and current_page < 10: # 限制爬取页数,避免请求过多 next_page = current_page + 1 form_data = { 'first': 'false', # 翻页时first为false 'pn': str(next_page), 'kd': keyword, 'city': city, } yield scrapy.FormRequest( url=response.url, formdata=form_data, callback=self.parse_list, meta={'keyword': keyword, 'city': city, 'pn': next_page}, headers={'Referer': f'https://www.lagou.com/jobs/list_{keyword}?px=default&city={city}&pn={next_page}'} )

这里有几个关键点:

  1. API请求模拟:直接请求数据接口比解析HTML更高效、稳定。需要正确构造formdataheaders,特别是RefererX-Requested-With,缺少这些很可能被服务器拒绝。
  2. 翻页逻辑:通过totalCountpageSize计算总页数。务必添加页码限制(如current_page < 10,这是控制爬取规模、避免过度请求的关键。
  3. 详情页抓取:列表页信息有限,职位描述(JD)等详细信息在详情页。我们通过positionId构造详情页链接,并采用Requestmeta参数将初步构建的item传递下去。

3.2 详情页解析与数据补全

详情页是传统的HTML页面,我们需要用XPath或CSS选择器来提取信息。

def parse_detail(self, response): """解析职位详情页,补充职位描述、地址等信息""" # 从meta中取出之前初步构建的item item = response.meta['item'] # 使用XPath提取详情页数据 # 注意:拉勾网的页面结构可能变动,以下路径需要根据实际情况调整 job_description = response.xpath('//div[@class="job-detail"]//text()').getall() item['job_description'] = ''.join(job_description).strip() if job_description else '' # 提取工作地址 job_address = response.xpath('//div[@class="work_addr"]//text()').getall() item['job_address'] = ''.join([text.strip() for text in job_address if text.strip()]).replace('查看地图', '') if job_address else '' # 提取职位标签(技能关键词) job_tags = response.xpath('//ul[@class="position-label clearfix"]/li/text()').getall() item['job_tags'] = ', '.join([tag.strip() for tag in job_tags]) # 数据提取完成,交给Pipeline处理 yield item

实操心得:详情页的HTML结构不如API稳定,容易改版。因此,XPath路径不要写得太死板,尽量选择具有稳定idclass的父级容器,再向下查找。编写完爬虫后,务必先用scrapy shell <详情页URL>命令测试你的XPath是否能准确提取到数据。

3.3 定义数据模型与存储

在抓取之前,我们需要在items.py中定义好数据的“蓝图”。

# lagou_project/items.py import scrapy class LagouJobItem(scrapy.Item): # 从列表页获取 position_id = scrapy.Field() # 职位ID job_name = scrapy.Field() # 职位名称 company = scrapy.Field() # 公司全名 salary = scrapy.Field() # 薪资范围 city = scrapy.Field() # 工作城市 work_year = scrapy.Field() # 工作经验要求 education = scrapy.Field() # 学历要求 company_size = scrapy.Field() # 公司规模 finance_stage = scrapy.Field() # 融资阶段 keyword = scrapy.Field() # 搜索关键词 source_city = scrapy.Field() # 搜索城市 # 从详情页补充 job_description = scrapy.Field() # 职位描述 job_address = scrapy.Field() # 工作地址 job_tags = scrapy.Field() # 职位标签/技能关键词 # 系统字段 crawl_time = scrapy.Field() # 爬取时间

接下来,在pipelines.py中编写数据处理管道。这里我们选择将数据存储为CSV文件,方便后续用Pandas分析。

# lagou_project/pipelines.py import csv import os from datetime import datetime from itemadapter import ItemAdapter class LagouDataPipeline: def open_spider(self, spider): # 爬虫启动时,创建或打开CSV文件,并写入表头 file_name = f'lagou_jobs_{datetime.now().strftime("%Y%m%d_%H%M")}.csv' self.file = open(file_name, 'w', newline='', encoding='utf-8-sig') # utf-8-sig解决Excel打开乱码 fieldnames = [ 'position_id', 'job_name', 'company', 'salary', 'city', 'work_year', 'education', 'company_size', 'finance_stage', 'keyword', 'source_city', 'job_description', 'job_address', 'job_tags', 'crawl_time' ] self.writer = csv.DictWriter(self.file, fieldnames=fieldnames) self.writer.writeheader() def process_item(self, item, spider): # 为每条数据添加爬取时间 adapter = ItemAdapter(item) adapter['crawl_time'] = datetime.now().isoformat() # 写入CSV文件 self.writer.writerow(adapter.asdict()) return item def close_spider(self, spider): # 爬虫关闭时,关闭文件 self.file.close() print(f"数据已保存至 {self.file.name}")

提示:在实际项目中,如果数据量很大,建议使用数据库(如MySQL, MongoDB, SQLite)。Scrapy支持通过Pipeline轻松接入数据库。CSV文件适合中小规模数据集的快速分析和共享。

4. 反爬策略与道德爬虫实践

运行上面的爬虫,你很可能会很快遇到403 Forbidden错误,或者收到“请求过于频繁”的提示。这就是反爬机制在起作用。我们必须实施一些策略来绕过这些限制,同时恪守道德底线。

4.1 动态User-Agent中间件

固定的User-Agent是爬虫最明显的特征之一。我们需要一个中间件,在每次请求时随机从一组常见的浏览器UA中选取一个。

# lagou_project/middlewares.py import random class RandomUserAgentMiddleware: """随机User-Agent中间件""" def __init__(self): self.user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ...', # 可以准备几十个常见的UA ] def process_request(self, request, spider): request.headers['User-Agent'] = random.choice(self.user_agents)

4.2 请求延迟与并发控制

这是最有效也最必要的礼貌性措施。在settings.py中我们已经设置了DOWNLOAD_DELAY = 3和较低的CONCURRENT_REQUESTS_PER_DOMAIN。这意味着爬虫每发出一个请求后,会等待至少3秒再发下一个,并且同时对lagou.com的并发请求不超过2个。这能显著降低对目标服务器的压力。

一个重要的技巧:Scrapy的延迟是针对每个域名的。如果你的爬虫只爬拉勾网,这个设置是全局生效的。但如果你设置了RANDOMIZE_DOWNLOAD_DELAY = True(默认为True),实际的延迟会在0.5 * DOWNLOAD_DELAY1.5 * DOWNLOAD_DELAY之间随机取值,使得请求间隔更像人类行为。

4.3 处理Cookie与Session

拉勾网需要维持一个会话(Session)。Scrapy的Request/FormRequest会自动处理Cookie。我们只需要确保在初始请求(start_requests)中,像正常浏览器一样访问一下列表页(我们设置了Referer),Scrapy的CookieJar就会为我们管理会话状态。在后续的请求中,Cookie会自动携带。

4.4 应对验证码与IP封锁

如果采取了以上措施仍然被封锁,可能触发了更高级别的防御。

  1. 代理IP池:这是应对IP封锁的终极方案。可以购买或搭建代理IP服务,然后在中间件中随机为请求分配代理。Scrapy中可以通过设置request.meta['proxy']来实现。
  2. 验证码识别:如果遇到验证码,项目复杂度会急剧上升。可以考虑:
    • 降低请求频率:这是首选,很多时候就是因为请求太快了。
    • 使用打码平台:将验证码图片发送到第三方平台进行识别(需要付费)。
    • 自动化测试工具:对于复杂情况,可以短暂切换到Selenium等工具模拟真人操作,但效率极低,仅作为最后手段。

我的经验是:对于拉勾网这类公开招聘网站,只要将DOWNLOAD_DELAY设置得足够长(比如5-10秒),并发数设为1,并配合随机UA,通常就能稳定抓取数百条数据。我们的目的是分析,不是扒站,控制好爬取规模(比如每个城市每个关键词只抓前10页)是双赢的做法。

5. 数据清洗:从原始文本到结构化洞察

假设我们已经成功爬取了几千条数据,保存为lagou_jobs_20231027_1430.csv。原始数据是混乱的,直接分析没有意义。接下来,我们使用Pandas进行数据清洗。

# analysis.ipynb (Jupyter Notebook) import pandas as pd import numpy as np import re # 1. 加载数据 df = pd.read_csv('lagou_jobs_20231027_1430.csv') print(f"原始数据量: {df.shape[0]} 条") print(df.head()) print(df.info())

首先查看数据概览,检查缺失值、重复项。

# 2. 基础清洗 # 去除完全重复的行(根据position_id) df.drop_duplicates(subset=['position_id'], inplace=True) print(f"去重后数据量: {df.shape[0]} 条") # 处理缺失值 # 对于关键字段如salary, city缺失的,可以考虑删除 df = df.dropna(subset=['salary', 'city', 'job_name']) # 对于描述、地址等字段,缺失可以填充为空字符串 df['job_description'].fillna('', inplace=True) df['job_address'].fillna('', inplace=True) # 3. 薪资字段解析与标准化 # 原始薪资格式:“15k-30k”、“20k以上”、“面议” def parse_salary(salary_str): """将薪资字符串解析为最低、最高和平均薪资(单位:千/月)""" if pd.isna(salary_str) or '面议' in salary_str: return np.nan, np.nan, np.nan # 使用正则表达式提取数字 pattern = r'(\d+(?:\.\d+)?)' numbers = re.findall(pattern, salary_str) numbers = [float(num) for num in numbers] if len(numbers) == 2: low, high = numbers avg = (low + high) / 2 # 判断单位是“k”还是“万” if '万' in salary_str: low, high, avg = low * 10, high * 10, avg * 10 # 万/月 -> k/月 return low, high, avg elif len(numbers) == 1 and '以上' in salary_str: low = numbers[0] high = np.nan # 没有上限 avg = low # 保守估计取最低值 if '万' in salary_str: low, avg = low * 10, avg * 10 return low, high, avg else: return np.nan, np.nan, np.nan # 应用解析函数 salary_parsed = df['salary'].apply(parse_salary) df[['salary_low_k', 'salary_high_k', 'salary_avg_k']] = pd.DataFrame( salary_parsed.tolist(), index=df.index ) # 4. 工作经验字段标准化 # 原始格式:“经验不限”、“1年以内”、“1-3年”、“3-5年”、“5-10年”、“10年以上” def parse_work_year(year_str): if pd.isna(year_str): return np.nan if '不限' in year_str or '应届' in year_str: return 0 pattern = r'(\d+(?:\.\d+)?)' numbers = re.findall(pattern, year_str) numbers = [float(num) for num in numbers] if len(numbers) == 1 and '以内' in year_str: return numbers[0] * 0.5 # 取中值估计,如“1年以内”估为0.5年 elif len(numbers) == 1 and '以上' in year_str: return numbers[0] + 3 # 如“10年以上”估为13年 elif len(numbers) == 2: return (numbers[0] + numbers[1]) / 2 # 取范围中值 else: return np.nan df['work_year_mid'] = df['work_year'].apply(parse_work_year) # 5. 从职位描述中提取关键技能 # 这是一个更高级的文本分析,这里先做一个简单的关键词匹配示例 skill_keywords = ['Python', 'Java', 'MySQL', 'Redis', 'Docker', 'Kubernetes', 'Linux', 'Spring', 'Django', 'Flask', 'Vue', 'React'] def extract_skills(desc): desc = str(desc).lower() found_skills = [] for skill in skill_keywords: if skill.lower() in desc: found_skills.append(skill) return ', '.join(found_skills) if found_skills else '未识别' df['extracted_skills'] = df['job_description'].apply(extract_skills) # 6. 公司规模分类 def categorize_company_size(size_str): if pd.isna(size_str): return '未知' if '少于' in size_str or '15人' in size_str: return '小型 (0-15人)' elif '15-50' in size_str: return '中小型 (15-50人)' elif '50-150' in size_str: return '中型 (50-150人)' elif '150-500' in size_str: return '中大型 (150-500人)' elif '500-2000' in size_str: return '大型 (500-2000人)' elif '2000' in size_str: return '超大型 (2000人以上)' else: return '其他' df['company_size_category'] = df['company_size'].apply(categorize_company_size) print("数据清洗完成!") print(df[['job_name', 'salary', 'salary_avg_k', 'city', 'work_year_mid', 'company_size_category']].head(10))

经过以上步骤,我们得到了一个干净、结构化的DataFrame,包含了可用于分析的数值型字段(如平均薪资、工作经验中值)和分类字段(如公司规模类别、提取的技能)。

6. 数据分析与可视化:挖掘数据背后的故事

数据清洗完毕,现在是最有意思的部分——从数据中发现规律。我们使用Pandas进行统计分析,并用Matplotlib和Seaborn进行可视化。

6.1 薪资分布分析

import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 sns.set_style("whitegrid") # 1. 整体薪资分布(直方图) plt.figure(figsize=(12, 6)) # 过滤掉无效薪资数据 salary_valid = df['salary_avg_k'].dropna() plt.subplot(1, 2, 1) plt.hist(salary_valid, bins=30, edgecolor='black', alpha=0.7) plt.xlabel('平均月薪 (k)') plt.ylabel('职位数量') plt.title('所有职位平均月薪分布') # 2. 箱线图查看薪资离群点与分位数 plt.subplot(1, 2, 2) sns.boxplot(y=salary_valid) plt.ylabel('平均月薪 (k)') plt.title('薪资箱线图') plt.tight_layout() plt.show() # 打印关键统计量 print(f"平均薪资: {salary_valid.mean():.2f}k") print(f"薪资中位数: {salary_valid.median():.2f}k") print(f"薪资标准差: {salary_valid.std():.2f}k") print(f"25%分位数: {salary_valid.quantile(0.25):.2f}k") print(f"75%分位数: {salary_valid.quantile(0.75):.2f}k")

6.2 城市与薪资、需求量的关系

# 按城市分组分析 city_stats = df.groupby('city').agg({ 'salary_avg_k': ['mean', 'median', 'count'], # 平均薪资、中位数、职位数量 'position_id': 'count' }).round(2) city_stats.columns = ['平均薪资(k)', '薪资中位数(k)', '职位数量'] city_stats = city_stats.sort_values('职位数量', ascending=False) print("各城市招聘情况统计:") print(city_stats.head(10)) # 可视化:城市-平均薪资-职位数量气泡图 plt.figure(figsize=(14, 8)) top_cities = city_stats.head(15) # 取职位数量最多的15个城市 scatter = plt.scatter( top_cities.index, top_cities['平均薪资(k)'], s=top_cities['职位数量']/10, # 气泡大小代表职位数量 alpha=0.6, c=top_cities['薪资中位数(k)'], cmap='viridis' ) plt.xticks(rotation=45) plt.xlabel('城市') plt.ylabel('平均月薪 (k)') plt.title('主要城市薪资水平与职位需求量关系') plt.colorbar(scatter, label='薪资中位数 (k)') # 为每个气泡添加数量标签 for i, row in top_cities.iterrows(): plt.annotate(f"{int(row['职位数量'])}", (i, row['平均薪资(k)']), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8) plt.tight_layout() plt.show()

6.3 技能要求分析

# 分析技能关键词的出现频率 from collections import Counter all_skills = [] for skills in df['extracted_skills']: if skills != '未识别': all_skills.extend([s.strip() for s in skills.split(',')]) skill_counter = Counter(all_skills) # 取出现频率最高的20个技能 top_skills = skill_counter.most_common(20) skills, counts = zip(*top_skills) plt.figure(figsize=(12, 8)) bars = plt.barh(skills, counts) plt.xlabel('出现频次') plt.title('职位描述中最常要求的技能TOP20') # 在条形末端添加频次数字 for bar, count in zip(bars, counts): plt.text(bar.get_width() + 5, bar.get_y() + bar.get_height()/2, f'{count}', va='center') plt.gca().invert_yaxis() # 让最高的在最上面 plt.tight_layout() plt.show()

6.4 公司规模与薪资、经验的关系

# 多维度分析:公司规模 vs 平均薪资 vs 平均经验要求 size_salary_exp = df.groupby('company_size_category').agg({ 'salary_avg_k': 'mean', 'work_year_mid': 'mean', 'position_id': 'count' }).round(2).sort_values('position_id', ascending=False) size_salary_exp.columns = ['平均薪资(k)', '平均经验要求(年)', '职位数量'] print("不同规模公司招聘情况:") print(size_salary_exp) # 绘制分组柱状图 fig, ax1 = plt.subplots(figsize=(12, 6)) x = np.arange(len(size_salary_exp)) width = 0.35 bars1 = ax1.bar(x - width/2, size_salary_exp['平均薪资(k)'], width, label='平均薪资(k)', color='skyblue') ax1.set_xlabel('公司规模类别') ax1.set_ylabel('平均月薪 (k)', color='skyblue') ax1.tick_params(axis='y', labelcolor='skyblue') ax1.set_xticks(x) ax1.set_xticklabels(size_salary_exp.index, rotation=45) ax2 = ax1.twinx() bars2 = ax2.bar(x + width/2, size_salary_exp['平均经验要求(年)'], width, label='平均经验要求(年)', color='lightcoral') ax2.set_ylabel('平均经验要求 (年)', color='lightcoral') ax2.tick_params(axis='y', labelcolor='lightcoral') # 添加图例 fig.legend(loc='upper left', bbox_to_anchor=(0.1, 0.9)) plt.title('不同规模公司的薪资与经验要求对比') fig.tight_layout() plt.show()

7. 高级分析与报告生成

基础分析之后,我们可以进行一些更深入的交叉分析,并生成一份简单的数据报告。

7.1 薪资与工作经验的相关性分析

# 计算薪资与工作经验的相关系数 # 首先过滤掉无效数据 valid_df = df[['salary_avg_k', 'work_year_mid']].dropna() correlation = valid_df['salary_avg_k'].corr(valid_df['work_year_mid']) print(f"平均月薪与工作经验中值的相关系数: {correlation:.3f}") # 绘制散点图与回归线 plt.figure(figsize=(10, 6)) sns.regplot(x='work_year_mid', y='salary_avg_k', data=valid_df, scatter_kws={'alpha':0.5}, line_kws={'color': 'red'}) plt.xlabel('工作经验 (年)') plt.ylabel('平均月薪 (k)') plt.title(f'薪资与工作经验关系 (相关系数: {correlation:.3f})') plt.show()

7.2 生成简易分析报告

我们可以将关键发现汇总到一个文本文件中。

report_lines = [] report_lines.append("="*60) report_lines.append("拉勾网招聘数据分析报告") report_lines.append(f"分析时间: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}") report_lines.append(f"有效数据量: {len(df)} 条") report_lines.append("="*60) report_lines.append("\n【核心发现】") report_lines.append(f"1. 整体市场平均月薪: {df['salary_avg_k'].mean():.1f}k,中位数: {df['salary_avg_k'].median():.1f}k。") top_city = city_stats.iloc[0] report_lines.append(f"2. 招聘需求最旺盛的城市是【{top_city.name}】,发布了 {int(top_city['职位数量'])} 个职位,平均薪资 {top_city['平均薪资(k)']}k。") top_skill = top_skills[0] report_lines.append(f"3. 最热门的技能要求是【{top_skill[0]}】,在 {top_skill[1]} 个职位描述中被提及。") report_lines.append(f"4. 薪资与工作经验呈正相关(相关系数 {correlation:.3f}),但相关性并非极强,说明技能、城市、公司等因素影响很大。") report_lines.append(f"5. 从公司规模看,【{size_salary_exp.iloc[0].name}】类公司发布的职位最多。") report_lines.append("\n【给求职者的建议】") report_lines.append("1. 聚焦高需求城市(如北上深杭),但需综合考虑生活成本。") report_lines.append(f"2. 重点掌握如 {top_skills[0][0]}, {top_skills[1][0]}, {top_skills[2][0]} 等高频技能,提升竞争力。") report_lines.append("3. 关注中型及以上规模公司,其在薪资和经验要求上可能提供更平衡的选择。") report_lines.append("\n【给招聘方的启示】") report_lines.append("1. 在热门城市招聘竞争激烈,薪资需具备竞争力。") report_lines.append("2. 职位描述中清晰列出核心技能关键词,有助于吸引目标人才。") report_lines.append("3. 合理设置经验与薪资的匹配度,避免要求过高而薪资不匹配。") report_lines.append("="*60) report_text = '\n'.join(report_lines) print(report_text) # 保存报告到文件 with open('lagou_analysis_report.txt', 'w', encoding='utf-8') as f: f.write(report_text)

运行整个分析流程后,你不仅得到了一系列直观的图表,还获得了一份包含核心洞察的数据报告。这个从抓取到分析的全流程,为你回答最初的业务问题(如“Python和Java哪个需求大?”“哪个城市薪资高?”)提供了坚实的数据基础。

这个项目的价值在于其可复现性和可扩展性。你可以修改爬虫的关键词和城市,分析任何你感兴趣的职位;也可以深化分析维度,比如加入融资阶段对薪资的影响、分析职位描述的情感倾向等。数据驱动的决策,就从这样一个完整的项目开始。