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

发布时间:2026/7/31 8:31:30
基于Scrapy的拉勾网招聘数据爬取与Python数据分析实战 1. 项目缘起从招聘数据中洞察市场脉搏最近在帮一个做技术猎头的朋友分析市场趋势他经常问我“现在哪个技术栈最火”“Python后端和Java后端哪个岗位需求更大薪资更高” 这类问题如果只凭感觉或者零星的招聘信息来回答显然不够客观。于是我决定用最直接的方式——从招聘网站抓取数据用数据说话。拉勾网作为国内知名的互联网招聘平台其数据具有很高的参考价值。这个项目就是用Python的Scrapy框架对拉勾网的招聘信息进行定向爬取并对获取的数据进行清洗、分析和可视化最终形成一份可量化的市场洞察报告。这不仅仅是一个爬虫练习更是一个完整的数据分析项目闭环。它涵盖了从数据采集Scrapy爬虫、数据清洗Pandas、到数据分析与可视化Matplotlib/Seaborn的全流程。对于想学习Python数据抓取、或者希望用数据驱动决策的朋友来说这是一个非常典型的实战案例。无论你是想了解某个岗位的薪资分布、技能要求还是想分析不同城市、不同规模公司的招聘偏好这套方法都能为你提供扎实的数据支撑。2. Scrapy框架选型与环境搭建为什么是它在Python的爬虫生态里我们有RequestsBeautifulSoup这样的“手动挡”组合也有Selenium这样的“自动化”工具那为什么这个项目我坚定地选择了Scrapy这背后有几个核心考量。首先Scrapy是一个为大规模、结构化数据抓取而生的异步框架。拉勾网的招聘列表页、详情页结构清晰我们需要爬取的是成千上万条具有相同字段职位名、公司、薪资、地点、要求等的记录。Scrapy内置的Item和Pipeline机制天生就是为了高效、规范地处理这类结构化数据流。相比之下用Requests需要自己管理请求队列、处理重试、解析和存储代码会显得冗长且不易维护。其次性能与抗封禁能力。Scrapy基于Twisted异步网络库可以轻松实现并发请求极大地提高了数据抓取速度。同时它内置了中间件Middleware机制我们可以很方便地集成随机User-Agent、代理IP池、请求延迟等反反爬策略。拉勾网这类网站通常会有一定的反爬措施Scrapy的这套架构让我们应对起来更加从容。最后项目的可扩展性与工程化。这个项目不是一次性的脚本。我们可能今天分析Python明天分析Java后天想增加对BOSS直聘的数据抓取。Scrapy的项目结构spiders, items, pipelines, settings非常清晰不同站点的爬虫可以独立开发共享通用的数据处理和存储逻辑便于后续维护和扩展。2.1 环境准备与依赖安装接下来我们开始搭建环境。我个人的习惯是使用conda或venv创建独立的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: 数据分析的核心库用于数据清洗、转换和分析。matplotlibseaborn: 数据可视化库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?pxdefaultcity全国。但更重要的是我们发现其真实的数据是通过一个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: fhttps://www.lagou.com/jobs/list_{keyword}?pxdefaultcity{city}, User-Agent: Mozilla/5.0..., # 会在中间件中动态设置 X-Requested-With: XMLHttpRequest, # 表明是Ajax请求 } # 使用FormRequest发送POST请求 yield scrapy.FormRequest( urlurl, formdataform_data, headersheaders, callbackself.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 fhttps://www.lagou.com/jobs/{item[position_id]}.html # 将item通过meta传递在详情页回调中补充数据 yield scrapy.Request( urldetail_url, callbackself.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( urlresponse.url, formdataform_data, callbackself.parse_list, meta{keyword: keyword, city: city, pn: next_page}, headers{Referer: fhttps://www.lagou.com/jobs/list_{keyword}?pxdefaultcity{city}pn{next_page}} )这里有几个关键点API请求模拟直接请求数据接口比解析HTML更高效、稳定。需要正确构造formdata和headers特别是Referer和X-Requested-With缺少这些很可能被服务器拒绝。翻页逻辑通过totalCount和pageSize计算总页数。务必添加页码限制如current_page 10这是控制爬取规模、避免过度请求的关键。详情页抓取列表页信息有限职位描述JD等详细信息在详情页。我们通过positionId构造详情页链接并采用Request的meta参数将初步构建的item传递下去。3.2 详情页解析与数据补全详情页是传统的HTML页面我们需要用XPath或CSS选择器来提取信息。def parse_detail(self, response): 解析职位详情页补充职位描述、地址等信息 # 从meta中取出之前初步构建的item item response.meta[item] # 使用XPath提取详情页数据 # 注意拉勾网的页面结构可能变动以下路径需要根据实际情况调整 job_description response.xpath(//div[classjob-detail]//text()).getall() item[job_description] .join(job_description).strip() if job_description else # 提取工作地址 job_address response.xpath(//div[classwork_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[classposition-label clearfix]/li/text()).getall() item[job_tags] , .join([tag.strip() for tag in job_tags]) # 数据提取完成交给Pipeline处理 yield item实操心得详情页的HTML结构不如API稳定容易改版。因此XPath路径不要写得太死板尽量选择具有稳定id或class的父级容器再向下查找。编写完爬虫后务必先用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 flagou_jobs_{datetime.now().strftime(%Y%m%d_%H%M)}.csv self.file open(file_name, w, newline, encodingutf-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, fieldnamesfieldnames) 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_DELAY和1.5 * DOWNLOAD_DELAY之间随机取值使得请求间隔更像人类行为。4.3 处理Cookie与Session拉勾网需要维持一个会话Session。Scrapy的Request/FormRequest会自动处理Cookie。我们只需要确保在初始请求start_requests中像正常浏览器一样访问一下列表页我们设置了RefererScrapy的CookieJar就会为我们管理会话状态。在后续的请求中Cookie会自动携带。4.4 应对验证码与IP封锁如果采取了以上措施仍然被封锁可能触发了更高级别的防御。代理IP池这是应对IP封锁的终极方案。可以购买或搭建代理IP服务然后在中间件中随机为请求分配代理。Scrapy中可以通过设置request.meta[proxy]来实现。验证码识别如果遇到验证码项目复杂度会急剧上升。可以考虑降低请求频率这是首选很多时候就是因为请求太快了。使用打码平台将验证码图片发送到第三方平台进行识别需要付费。自动化测试工具对于复杂情况可以短暂切换到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], inplaceTrue) print(f去重后数据量: {df.shape[0]} 条) # 处理缺失值 # 对于关键字段如salary, city缺失的可以考虑删除 df df.dropna(subset[salary, city, job_name]) # 对于描述、地址等字段缺失可以填充为空字符串 df[job_description].fillna(, inplaceTrue) df[job_address].fillna(, inplaceTrue) # 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(), indexdf.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, bins30, edgecolorblack, alpha0.7) plt.xlabel(平均月薪 (k)) plt.ylabel(职位数量) plt.title(所有职位平均月薪分布) # 2. 箱线图查看薪资离群点与分位数 plt.subplot(1, 2, 2) sns.boxplot(ysalary_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(f25%分位数: {salary_valid.quantile(0.25):.2f}k) print(f75%分位数: {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(职位数量, ascendingFalse) 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)], stop_cities[职位数量]/10, # 气泡大小代表职位数量 alpha0.6, ctop_cities[薪资中位数(k)], cmapviridis ) plt.xticks(rotation45) 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)]), textcoordsoffset points, xytext(0,5), hacenter, fontsize8) 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}, vacenter) 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, ascendingFalse) 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), colorskyblue) ax1.set_xlabel(公司规模类别) ax1.set_ylabel(平均月薪 (k), colorskyblue) ax1.tick_params(axisy, labelcolorskyblue) ax1.set_xticks(x) ax1.set_xticklabels(size_salary_exp.index, rotation45) ax2 ax1.twinx() bars2 ax2.bar(x width/2, size_salary_exp[平均经验要求(年)], width, label平均经验要求(年), colorlightcoral) ax2.set_ylabel(平均经验要求 (年), colorlightcoral) ax2.tick_params(axisy, labelcolorlightcoral) # 添加图例 fig.legend(locupper 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(xwork_year_mid, ysalary_avg_k, datavalid_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(f1. 整体市场平均月薪: {df[salary_avg_k].mean():.1f}k中位数: {df[salary_avg_k].median():.1f}k。) top_city city_stats.iloc[0] report_lines.append(f2. 招聘需求最旺盛的城市是【{top_city.name}】发布了 {int(top_city[职位数量])} 个职位平均薪资 {top_city[平均薪资(k)]}k。) top_skill top_skills[0] report_lines.append(f3. 最热门的技能要求是【{top_skill[0]}】在 {top_skill[1]} 个职位描述中被提及。) report_lines.append(f4. 薪资与工作经验呈正相关相关系数 {correlation:.3f}但相关性并非极强说明技能、城市、公司等因素影响很大。) report_lines.append(f5. 从公司规模看【{size_salary_exp.iloc[0].name}】类公司发布的职位最多。) report_lines.append(\n【给求职者的建议】) report_lines.append(1. 聚焦高需求城市如北上深杭但需综合考虑生活成本。) report_lines.append(f2. 重点掌握如 {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, encodingutf-8) as f: f.write(report_text)运行整个分析流程后你不仅得到了一系列直观的图表还获得了一份包含核心洞察的数据报告。这个从抓取到分析的全流程为你回答最初的业务问题如“Python和Java哪个需求大”“哪个城市薪资高”提供了坚实的数据基础。这个项目的价值在于其可复现性和可扩展性。你可以修改爬虫的关键词和城市分析任何你感兴趣的职位也可以深化分析维度比如加入融资阶段对薪资的影响、分析职位描述的情感倾向等。数据驱动的决策就从这样一个完整的项目开始。