
1. 项目概述与核心价值最近在带几个刚入行的朋友做数据分析项目发现一个挺有意思的现象很多人学Python、学爬虫、学数据分析但一到实际应用场景就有点懵不知道如何把这些技术点串联起来形成一个能解决实际问题的完整项目。正好前段时间我为了分析某个特定技术岗位的市场需求用Scrapy爬取了拉勾网的招聘数据并完成了从数据采集、清洗到分析可视化的全流程。这个项目虽然听起来像是“爬虫数据分析”的经典组合但实际操作中尤其是在应对拉勾网这类有一定反爬机制的网站以及后续将非结构化数据转化为有价值的商业洞察时有很多细节和坑需要特别注意。今天我就把这个项目的核心部分特别是第四、第五关——也就是数据清洗、存储与深度分析——的完整思路和实操代码分享出来。无论你是想了解如何用Scrapy应对复杂的Ajax请求和反爬还是想知道如何将爬取到的JSON数据清洗成规整的DataFrame亦或是想学习如何从海量招聘信息中挖掘出薪资分布、技能需求等关键信息这篇文章都能给你一份可以直接“抄作业”的实战指南。2. 项目整体架构与技术选型解析2.1 为什么是Scrapy 拉勾网在开始动手之前我们先聊聊为什么选这个技术栈和这个目标网站。拉勾网作为国内知名的互联网招聘平台其数据具有几个显著特点一是数据结构相对规整职位、公司、薪资、地点、要求等字段明确便于后续分析二是它大量使用了Ajax动态加载技术对于初学者来说用简单的requestsBeautifulSoup组合可能会无从下手而这正是Scrapy框架结合其内置的FormRequest或处理JSON API能力大显身手的地方三是它具备一定的反爬措施如请求头校验、频率限制等这迫使我们必须以更规范、更接近真实浏览器的方式去编写爬虫这个过程本身就是一个极佳的学习场景。技术选型上Scrapy是Python生态中工业级的爬虫框架它提供了完整的项目结构、异步处理、中间件、管道等组件非常适合构建需要维护和扩展的爬虫项目。相比自己从零搭建Scrapy能帮你处理好并发、去重、异常处理等繁琐问题让你更专注于核心的解析逻辑。数据分析部分Pandas和Matplotlib/Seaborn的组合是黄金搭档。Pandas的DataFrame是处理表格数据的利器其强大的数据清洗、分组、聚合功能能让我们轻松地将原始、杂乱的爬虫数据变成干净的分析数据集。可视化部分则用Matplotlib绘制基础图表Seaborn提供更美观的统计图形。2.2 核心爬虫设计思路拉勾网的职位列表页和详情页通常通过不同的接口获取。我们的爬虫核心思路是模拟浏览器行为入口与翻页首先分析列表页的请求。通过浏览器开发者工具的“网络Network”选项卡筛选XHR/Fetch请求找到返回职位列表数据的真实API接口。观察其请求方式通常是POST、请求参数如city、positionName、pn页码等和返回格式JSON。解析列表获取详情链接从列表接口的JSON响应中解析出每个职位的唯一标识如positionId或详情页的URL构造规则。请求详情数据同样详情页数据很可能也是通过另一个API接口返回。我们需要构造请求获取包含职位描述、技能要求、公司详情等完整信息的JSON数据。应对反爬关键在于设置合理的请求头User-Agent,Referer等并可能需要在Scrapy的Downloader Middlewares中处理Cookie、设置下载延迟DOWNLOAD_DELAY或使用IP代理池来避免被封。注意在编写和运行爬虫时务必遵守网站的robots.txt协议并设置合理的爬取频率例如在Scrapy的settings.py中设置DOWNLOAD_DELAY 2避免对目标网站服务器造成过大压力。这是基本的网络礼仪和合规要求。3. 第四关数据清洗与结构化存储实战爬虫成功运行后我们得到的是一个包含大量JSON对象的列表。每个对象对应一个职位但字段可能深浅不一、格式混乱这就是数据清洗要解决的问题。3.1 原始数据问题诊断拉勾网返回的原始数据常见问题包括嵌套字段如company字段本身又是一个包含companyName、companySize、financeStage等子字段的字典。字段缺失某些职位可能没有salary薪资或workYear经验要求信息。格式不统一薪资字段可能是“8k-16k”也可能是“8-16k”甚至“面议”。城市字段可能混有“北京”、“北京-海淀区”。冗余信息职位描述jobDetail是包含HTML标签的长文本需要提取纯文本和关键信息。3.2 使用Pandas进行数据清洗假设我们已经将爬取到的JSON数据列表保存为raw_jobs.json。清洗的核心步骤如下import pandas as pd import numpy as np import re import json # 1. 加载原始数据 with open(raw_jobs.json, r, encodingutf-8) as f: raw_data json.load(f) df_raw pd.DataFrame(raw_data) print(f原始数据量: {len(df_raw)}) print(df_raw.head()) # 2. 扁平化嵌套字典字段 # 假设原始数据中company是一个字典 df pd.json_normalize(raw_data) # 这个函数可以自动展开嵌套字典 # 3. 列名重命名可选为了更清晰 df.rename(columns{ positionName: 职位名称, company.fullName: 公司名称, company.size.name: 公司规模, salary: 薪资, city: 城市, workYear: 经验要求, education: 学历要求, jobDetail: 职位描述, createTime: 发布时间 }, inplaceTrue) # 4. 处理薪资字段提取最低、最高和平均薪资单位千/月 def parse_salary(salary_str): if pd.isna(salary_str) or 面议 in salary_str: return np.nan, np.nan, np.nan # 使用正则表达式匹配数字处理“8k-16k”、“8-16k”等格式 nums re.findall(r(\d(?:\.\d)?), salary_str) if len(nums) 2: low, high float(nums[0]), float(nums[1]) # 判断单位是千还是万拉勾通常为k if 万 in salary_str: low, high low * 10, high * 10 # 转换为千/月 avg (low high) / 2 return low, high, avg else: return np.nan, np.nan, np.nan salary_df df[薪资].apply(lambda x: pd.Series(parse_salary(x), index[最低薪资(千),最高薪资(千),平均薪资(千)])) df pd.concat([df, salary_df], axis1) # 5. 处理城市字段提取一级城市如“北京-海淀区” - “北京” df[城市_清洗] df[城市].apply(lambda x: str(x).split(-)[0] if pd.notna(x) else x) # 6. 处理经验要求统一格式如“1-3年” - “1-3” def parse_experience(exp): if pd.isna(exp): return np.nan # 提取数字部分 nums re.findall(r(\d), exp) if len(nums) 2: return f{nums[0]}-{nums[1]} elif len(nums) 1: return f{nums[0]} else: return 不限 df[经验要求_清洗] df[经验要求].apply(parse_experience) # 7. 清洗职位描述去除HTML标签 def clean_html(raw_html): if pd.isna(raw_html): return cleanr re.compile(.*?) cleantext re.sub(cleanr, , raw_html) return cleantext.strip() df[职位描述_文本] df[职位描述].apply(clean_html) # 8. 处理缺失值 # 对于数值型列可以用中位数填充对于类别型列可以用众数或“未知”填充 df[平均薪资(千)].fillna(df[平均薪资(千)].median(), inplaceTrue) df[城市_清洗].fillna(未知, inplaceTrue) df[经验要求_清洗].fillna(不限, inplaceTrue) # 9. 删除完全无用或重复的列 columns_to_drop [薪资, 城市, 经验要求, 职位描述] # 保留清洗后的版本 df_clean df.drop(columnscolumns_to_drop, errorsignore) print(\n清洗后数据预览) print(df_clean[[职位名称, 公司名称, 平均薪资(千), 城市_清洗, 经验要求_清洗]].head()) print(f\n清洗后数据形状: {df_clean.shape})3.3 数据存储策略清洗后的数据需要持久化方便后续分析。这里推荐两种方式CSV文件通用性强易于用Excel打开查看。使用df_clean.to_csv(lagou_jobs_cleaned.csv, indexFalse, encodingutf-8-sig)保存。SQLite数据库适合数据量较大或需要复杂查询的场景。使用Pandas的to_sql方法或直接使用sqlite3库。import sqlite3 # 使用Pandas保存到SQLite conn sqlite3.connect(lagou_data.db) df_clean.to_sql(jobs, conn, if_existsreplace, indexFalse) conn.close()实操心得在数据清洗阶段最花时间的往往不是写代码而是理解原始数据的“脏法”。务必先用df_raw.head().to_dict()或直接查看几个原始JSON样本把字段结构摸清。正则表达式re模块是处理不规则文本字段的利器但编写模式时要多测试边界情况。另外填充缺失值时要结合业务逻辑。例如薪资“面议”填充为行业中位数可能比直接删除更有分析价值。4. 第五关多维数据分析与可视化洞察数据清洗完毕我们手里就有了一个规整的DataFrame。接下来就是最有趣的部分——从数据中发现问题、总结规律。4.1 分析维度规划我们可以从以下几个核心维度切入薪资分析各城市的平均薪资对比、不同经验要求的薪资分布、热门职位的薪资水平。需求分析哪些城市招聘需求最旺盛哪些技能关键词在职位描述中出现频率最高公司分析不同融资阶段的公司给出的薪资有何差异公司规模与薪资的关系。职位要求分析学历要求分布、经验要求分布。4.2 具体分析与可视化实现我们使用matplotlib和seaborn进行可视化。首先确保安装pip install matplotlib seaborn。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans] # 解决中文显示问题 plt.rcParams[axes.unicode_minus] False # 解决负号显示问题 sns.set_style(whitegrid) # 分析1: 各城市平均薪资TOP10 city_salary df_clean.groupby(城市_清洗)[平均薪资(千)].agg([mean, count]).round(1) city_salary_top10 city_salary.sort_values(mean, ascendingFalse).head(10) fig, ax1 plt.subplots(figsize(12, 6)) ax1.bar(city_salary_top10.index, city_salary_top10[mean], colorskyblue, label平均薪资) ax1.set_ylabel(平均薪资 (千/月), colorskyblue) ax1.tick_params(axisy, labelcolorskyblue) ax1.set_title(各城市平均薪资TOP10与职位数量, fontsize15) ax2 ax1.twinx() ax2.plot(city_salary_top10.index, city_salary_top10[count], colorcoral, markero, linewidth2, label职位数量) ax2.set_ylabel(职位数量, colorcoral) ax2.tick_params(axisy, labelcolorcoral) fig.tight_layout() plt.show() # 分析2: 经验要求与薪资的关系 exp_order [不限, 1以下, 1-3, 3-5, 5-10, 10] # 自定义顺序 # 确保我们的数据分类在这个顺序里 df_clean[经验要求_清洗] pd.Categorical(df_clean[经验要求_清洗], categoriesexp_order, orderedTrue) exp_salary df_clean.groupby(经验要求_清洗, observedFalse)[平均薪资(千)].agg([mean, median, count]).round(1) plt.figure(figsize(10, 6)) sns.boxplot(x经验要求_清洗, y平均薪资(千), datadf_clean, orderexp_order) plt.title(不同经验要求的薪资分布箱线图) plt.xlabel(经验要求) plt.ylabel(平均薪资 (千/月)) plt.show() # 分析3: 热门技能词频分析 (从职位描述中提取) from collections import Counter import jieba # 中文分词库需安装: pip install jieba # 将所有职位描述文本合并 all_text .join(df_clean[职位描述_文本].dropna().astype(str).tolist()) # 使用jieba分词并过滤停用词和短词 words [word for word in jieba.lcut(all_text) if len(word) 1 and word not in [职位, 描述, 要求, 负责, 工作]] word_freq Counter(words).most_common(20) # 绘制词云 (需要安装wordcloud: pip install wordcloud) from wordcloud import WordCloud wordcloud WordCloud(font_pathsimhei.ttf, width800, height400, background_colorwhite).generate_from_frequencies(dict(word_freq)) plt.figure(figsize(12, 6)) plt.imshow(wordcloud, interpolationbilinear) plt.axis(off) plt.title(职位描述高频技能词云) plt.show() # 分析4: 公司规模与平均薪资的关系 if 公司规模 in df_clean.columns: size_order [少于15人, 15-50人, 50-150人, 150-500人, 500-2000人, 2000人以上] # 同样处理为有序类别 df_clean[公司规模] pd.Categorical(df_clean[公司规模], categoriessize_order, orderedTrue) size_salary df_clean.groupby(公司规模, observedFalse)[平均薪资(千)].mean().round(1) plt.figure(figsize(10, 6)) size_salary.plot(kindbar, colorlightgreen) plt.title(不同公司规模的平均薪资) plt.xlabel(公司规模) plt.ylabel(平均薪资 (千/月)) plt.xticks(rotation45) plt.tight_layout() plt.show()4.3 生成分析报告除了图表我们还可以用Pandas的describe函数和分组聚合生成数据摘要# 生成关键指标的描述性统计 print(薪资关键统计信息) print(df_clean[平均薪资(千)].describe()) print(\n--- 按城市分组的详细统计 ---) report df_clean.groupby(城市_清洗).agg({ 平均薪资(千): [mean, median, std, count], 职位名称: lambda x: x.mode()[0] if not x.mode().empty else N/A # 该城市最常见职位 }).round(2) print(report.head(15))5. 项目复盘与进阶思考走完爬取、清洗、分析、可视化的全流程一个完整的招聘市场数据分析项目就成型了。回顾整个过程有几个关键点值得再次强调第一逆向工程是关键。爬虫的核心难点往往在于找到并理解数据接口。熟练使用浏览器开发者工具F12特别是“网络Network”面板学会筛选XHR/JS请求查看请求载荷Payload和预览Preview响应这个技能价值千金。拉勾网这类网站其接口参数可能包含加密或动态令牌这就需要更深入的JS逆向分析这通常是中级爬虫工程师和初级工程师的分水岭。第二数据质量决定分析上限。清洗步骤看似枯燥却直接决定了后续所有分析的可靠性。对薪资字段的解析是否考虑了“13薪”、“14薪”的表述对“城市”字段的拆分是否考虑了“远程办公”这种特殊情况对“职位描述”的分词是否加入了领域特定的停用词表如“优先考虑”、“具备...能力”这些细节处理得越好你的分析结论就越精准。第三分析要服务于问题。可视化不是为了画图而画图。在开始画图之前先问自己我想回答什么问题是“Python开发工程师在北京和上海的薪资差距有多大”还是“对于3-5年经验的候选人哪些技能最稀缺”。带着问题去分析你的图表才会有灵魂结论才会有洞察力。这个项目还可以从多个方向进行扩展时序分析定期爬取数据分析某个职位薪资随时间的变化趋势或是招聘需求的季节性波动。技能图谱构建利用更复杂的NLP技术如TF-IDF、LDA主题模型从职位描述中提取技能实体并构建技能之间的关联关系。薪资预测模型以城市、经验、学历、技能关键词等作为特征尝试构建一个简单的薪资回归预测模型。部署自动化使用Scrapyd或Docker将爬虫部署到服务器结合Cron定时任务实现数据的自动采集、清洗和报表生成。最后技术是工具思维是灵魂。这个项目练手的意义不仅在于掌握了Scrapy、Pandas这几个库的用法更在于培养了一种“数据驱动”的思维方式如何从公开信息中提取有价值的数据如何将杂乱的数据转化为清晰的信息最终又如何从信息中提炼出指导行动的洞察。这才是数据分析师或数据工程师的核心竞争力。