ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

爬取前程无忧Python岗位数据:清洗、薪资分析与可视化实战

爬取前程无忧Python岗位数据:清洗、薪资分析与可视化实战 简介面向Python期末大作业与数据分析可视化实战的完整项目资料以“前程无忧Python岗位信息爬取与分析”为主题适合正在完成期末作业、毕业设计或需要项目练习的计算机相关专业学生。资源包共17个文件大小约14.94MB内含4个Python源码脚本、3个CSV数据集、2个SQL数据库脚本、5张可视化分析截图、1份项目文档及配套字体文件覆盖数据采集、清洗、入库、分析与可视化全流程。源码均经本地调试可运行项目在导师指导下完成并获评审98分文档报告可帮助理解整体设计与实现思路。目前已有283人学习下载对希望参考完整项目结构、系统掌握爬虫与数据分析可视化方法的学习者具有较高参考价值。1. 期末大作业选“爬前程无忧”挖Python岗位数据这门课最难交付的是什么很多计算机相关专业的期末大作业题目都长成这样某个具体平台 某种数据类型 爬取和分析 可视化。前程无忧的Python岗位信息爬取就是这类题目里最经典的一个版本因为51job的岗位数据结构完整、字段多、更新频繁天然适合做数据分析和可视化展示。但真正把这份作业交上去拿到高分的人并不多问题通常不在“爬不下来”而在“爬下来的数据太脏、图表太丑、报告不会写”。这门作业的隐性考察点是你能不能从零搭出一条数据流水线——请求网页、解析字段、清洗数据、多维分析、输出可视化并且把过程讲清楚。文章的定位就是帮你把这五步全部落地直接对着抄。读者如果是要交期末大作业的学生这篇文可以当一份带注释的实战参考如果是想练手爬虫和数据分析的工程师后半段的薪资解析和反爬排查也有可迁移的地方。2. 项目架构与技术选型requests还是scrapy数据存哪里最省事2.1 先拆需求一条数据流水线到底要经过哪几个环节这类大作业看上去是“爬虫题”实际上评分老师看的是整条链路完整度。一个能拿得出手的项目至少要包含四个阶段数据采集、数据清洗、数据分析和可视化呈现。数据采集阶段要解决“从哪些页面拿什么字段”前程无忧的岗位信息分布在两个层级——搜索结果列表页和职位详情页。列表页能拿到职位名称、公司名称、薪资区间、工作地点、经验要求但福利标签、职位描述、公司规模这些字段要进详情页才能拿到。数据清洗阶段要处理的是薪资文本不规范、城市字段带区县、发布时间格式不统一这类真实脏数据。数据分析阶段决定报告里能讲出什么结论可视化的价值则在于让结论一眼可见。2.2 技术选型轻量爬虫组合是最稳妥的方案对期末作业来说Scrapy框架不是最优选择它的学习曲线和调试成本会让作业周期拖得很长。常规做法是使用requests BeautifulSoup的组合requests负责发HTTP请求拿HTMLBeautifulSoup负责解析页面结构这套组合代码直观、出错容易排查。如果目标网站是纯前端渲染页面数据要等JavaScript执行后才出现则需要兜底方案selenium。数据分析和可视化用pandas加pyechartspandas负责DataFrame清洗和聚合pyecharts负责输出带交互效果的HTML图表答辩时可以现场打开浏览器展示比静态matplotlib图更有说服力。存储上不需要引入MySQL一个CSV文件足够。期末作业的数据量一般在几百条到几千条CSV的好处是直观、可以用Excel打开检查中间结果、也可以在pandas里一键读入。但要注意编码问题写入时使用utf-8-sig而不是utf-8这个坑后面避坑章节会详细说。2.3 开发环境与依赖安装给一份能直接复制的命令清单开发环境建议用Python 3.8以上版本Windows、macOS均可。依赖安装顺序无所谓但建议一次性装齐避免写代码过程中频繁补包。下面这段命令在终端执行即可。pip install requests beautifulsoup4 pandas pyecharts jieba如果你的环境里有多个Python版本先确认pip指向哪个环境用pip --version查看。如果终端提示pip不是内部命令说明Python安装时没有勾选“Add Python to PATH”重新运行Python安装包勾选后再试。安装完成后用下面两行命令验证关键依赖是否可用。python -c import requests; print(requests.__version__) python -c import pandas; print(pandas.__version__)第一行验证requests是否装上第二行验证pandas。这里有个常见的翻车点pandas装好后在依赖列表里能看到但导入时却报ModuleNotFoundError。这多半是当前终端激活的Python环境和pip安装时用的不是同一个解释器在PyCharm里要检查项目的Interpreter设置是否指向了正确的虚拟环境。3. 爬虫实现从列表页到详情页把51job的岗位数据抓下来3.1 列表页请求requests直接抓HTML还是找接口前程无忧的搜索结果页有一个特点直接requests请求网页URL拿到的是经过服务端拼接的HTML里面确实包含职位数据。但它的反爬机制比较敏感不带请求头直接抓大概率返回一个空壳页面或者302重定向到验证码页。所以第一件事是把请求头伪装成浏览器的正常访问。常见做法是构造一个完整的请求头至少包含User-Agent和Referer。下面是抓取列表页的完整函数URL中的python是搜索关键词最后的1是页码。实际搜索时从浏览器地址栏复制URL即可不要手拼。import requests from bs4 import BeautifulSoup 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://search.51job.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def fetch_list_page(keyword: str, page: int) - str: 抓取搜索结果列表页返回HTML文本 url fhttps://search.51job.com/list/010000,000000,0000,00,9,99,{keyword},2,{page}.html resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() # 状态码不是200时抛异常 resp.encoding utf-8 return resp.text这段代码的逻辑是先用f-string拼出带关键词和页码的搜索URL然后发起GET请求。010000是地区编码表示北京如果要限定其他城市去网站上搜索一次把地址栏里第一段数字替换掉就行。timeout10很关键没有超时设置的请求可能因为网络阻塞而卡死整个爬虫。拿到响应后手动指定编码为utf-8因为51job的页面在部分情况下自动检测编码会出错导致中文乱码。3.2 解析列表页用BeautifulSoup定位职位卡片拿到HTML之后下一步是解析。建议先在浏览器里打开搜索结果页按F12查看职位卡片的DOM结构。每个职位卡片是一个div里面包含职位名、公司名、薪资、地点等字段这些字段都有独立的CSS类名。解析函数如下。def parse_list_html(html: str) - list: 从列表页HTML中提取职位基本信息 soup BeautifulSoup(html, html.parser) jobs [] for card in soup.select(div.joblist div.e): # 每一张职位卡片对应一个dict title_tag card.select_one(span.jname) company_tag card.select_one(a.cname) salary_tag card.select_one(span.salary) location_tag card.select_one(span.add) date_tag card.select_one(span.time) if title_tag is None: continue jobs.append({ title: title_tag.get_text(stripTrue), company: company_tag.get_text(stripTrue) if company_tag else , salary: salary_tag.get_text(stripTrue) if salary_tag else , location: location_tag.get_text(stripTrue) if location_tag else , publish_date: date_tag.get_text(stripTrue) if date_tag else , detail_url: card.get(href, ), }) return jobs这里select(div.joblist div.e)是核心选择逻辑div.joblist是职位列表容器div.e是单个职位卡片。.get_text(stripTrue)会把标签内所有文本取出并去掉首尾空白。注意if title_tag is None: continue这一行——列表页偶尔会在中间夹带广告卡片没有职位名直接跳过比在后面清洗时再费劲处理要麻利。每个职位卡片还带一个detail_url这是跳转到详情页的链接后面补全字段时要用。3.3 详情页补全字段福利标签、职位描述、公司规模都在这列表页拿到的字段只有五六个对数据分析来说远远不够。职位描述是词云分析的原料福利标签是判断岗位质量的一个参考维度公司规模则可以跟薪资做交叉分析。详情页是另一个URL结构同样用requests请求解析时提取的字段更细。def fetch_detail(url: str) - dict: 抓取职位详情页并提取附加字段 resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) desc_tag soup.select_one(div.bmsg.job_msg) welfare_tag soup.select_one(div.tCompany_tag) info_tag soup.select_one(div.tCompany_main) desc desc_tag.get_text( , stripTrue) if desc_tag else welfare welfare_tag.get_text( , stripTrue) if welfare_tag else info info_tag.get_text( , stripTrue) if info_tag else return { description: desc[:500], # 截断超长描述避免数据冗余 welfare: welfare, company_info: info, }desc[:500]是个人习惯职位描述动辄一两千字词云分析用前500字足够也可以节省存储空间。company_info里包含公司规模、所属行业、融资阶段等信息格式通常是“1000-5000人 | 互联网/电子商务 | 已上市”这类拼接文本后面清洗时再拆开。详情页的请求要比列表页慢很多因为它携带的数据量大爬全量数据时建议加上随机睡眠频率控制的内容放到避坑章节讲。3.4 主流程与落盘控制爬取规模写CSV时用utf-8-sig主流程就是把上面的函数串起来先翻列表页收集所有职位卡片再遍历详情页补全字段最后统一写入CSV。控制爬取规模是期末作业里必须学会的一件事爬300条和爬3000条对结论的稳定性影响不大但爬取时间从两分钟变成二十分钟且触发反爬的概率直线上升。建议爬前5页列表大约有250到300条职位足够支撑分析。import time import csv import random def main(keyword: str, max_pages: int 5): results [] for page in range(1, max_pages 1): html fetch_list_page(keyword, page) jobs parse_list_html(html) print(f第{page}页抓到{len(jobs)}条职位) for job in jobs: detail fetch_detail(job[detail_url]) job.update(detail) results.append(job) time.sleep(random.uniform(1, 2)) # 随机睡眠降低请求密度 return results if __name__ __main__: data main(python) with open(python_jobs.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameslist(data[0].keys())) writer.writeheader() writer.writerows(data)time.sleep(random.uniform(1, 2))这一段是整段代码里最容易被人忽略却最重要的参数。固定间隔请求会被服务器识别为脚本行为随机间隔虽然也不能完全规避反爬但至少能让请求频率逼近人工操作的范围。encodingutf-8-sig是第二个细节普通utf-8写入的CSV用Excel打开会乱码必须加-sig后缀写入BOM头。fieldnameslist(data[0].keys())要求data非空所以在main()里先打印每页抓到的条数就是一种快速检查手段——如果某页返回0条就要停下来翻看是不是被反爬了。4. 数据清洗与薪资解析把脏数据变成能进图表的数据表4.1 薪资文本的六种写法与正则归一爬虫落盘后的数据不能直接分析因为“1-1.5万/月”“6-8千/月”“30-50万/年”这种文本是无法参与计算的。要做薪资分析必须先把它们解析成数值型的月薪上下限。先把可能出现的格式列全避免正则写了又漏。import re def parse_salary(text: str): 解析51job薪资文本返回(月薪下限, 月薪上限)单位元/月。 支持的格式 6-8千/月 1-1.5万/月 30-50万/年 200-300元/天 if not text or text in (面议, 薪资面议): return (None, None) m re.search(r([\d.])-([\d.])(万|千)(/月|/年)?, text) if m: low, high, unit, period m.groups() low float(low) * (10000 if unit 万 else 1000) high float(high) * (10000 if unit 万 else 1000) if period /年: low, high low / 12, high / 12 return (int(low), int(high)) # 按天计薪的实习岗 m re.search(r([\d.])([\d.])?(元)?/天, text) if m: low float(m.group(1)) high float(m.group(2)) if m.group(2) else low return int(low * 21.75), int(high * 21.75) # 按每月21.75个工作日折算 return (None, None)这段解析函数的逻辑分三层第一层用re.search匹配“数字-数字单位周期”的标准格式第二层单独处理按天计薪的实习岗按每月21.75个工作日折算成月薪第三层兜底返回空值。4000-8000元/月这种不带“万/千”的写法在51job上也存在上面的正则会漏掉建议在调用前先用replace(元, )把单位剥掉或者把正则扩展成([\d.])-([\d.])(万|千|元)?(/月|/年)?。4.2 城市口径统一与发布时间清洗城市字段里存的是“北京-海淀区”或者“上海-浦东新区”这种带区县的字符串做城市维度分析时要只保留城市一级。用pandas处理这类字段非常方便。import pandas as pd df pd.read_csv(python_jobs.csv) # 城市只保留第一个“-”前的内容 df[city] df[location].str.split(-).str[0] # 发布时间统一格式化51job上常见为“03-22发布”或“2025-03-22发布” def clean_date(x): x str(x).replace(发布, ).strip() if len(x) 5: # 形如 03-22补年份 return 2025- x return x df[publish_date] df[publish_date].apply(clean_date) # 薪资文本解析成数值列 df[[salary_min, salary_max]] df[salary].apply( lambda s: pd.Series(parse_salary(s)) ) # 计算薪资中位数 df[salary_mid] (df[salary_min] df[salary_max]) / 2 print(df.shape) print(df[[city, salary_min, salary_max, salary_mid]].head())str.split(-).str[0]是pandas向量化字符串操作的写法比写循环快得多一行代码解决城市归一。salary.apply(lambda s: pd.Series(parse_salary(s)))有两层含义先对每行调用parse_salary拿到二元组再用pd.Series把它拆成两列这样才能赋给df[[“salary_min”, “salary_max”]]。薪资解析完成之后salary_mid列就为后续的均值比较、直方图分布提供了数值基础。4.3 数据质量检查跑描述性统计前先做三件事拿到清洗后的DataFrame不要急着画图先做三件事看缺失值、看唯一值、看样本量。这三件事决定后续分析的可靠性。# 1. 缺哪些字段缺多少 print(df.isnull().sum()) # 2. 城市和学历的取值分布 print(df[city].value_counts().head(10)) print(df[experience].value_counts() if experience in df.columns else 无经验字段) # 3. 数值列的统计描述 print(df[[salary_min, salary_max, salary_mid]].describe())如果salary_min和salary_max的缺失值加起来超过三成说明薪资解析的覆盖度不够优先回去检查是否有新的薪资格式没有被正则匹配到。city字段如果出现“异地”或者空字符串说明部分职位不提供固定工作地点可以直接过滤掉。describe()输出里count会告诉你有效样本量如果有效样本不足150条后面做城市Top榜和薪资分布图时结论会单薄这时应该回头加爬几页数据而不是硬着头皮出图。5. 避坑爬取与分析中最常遇到的五个翻车现场5.1 列表页HTML里没有职位数据现象调用fetch_list_page()后parse_list_html()返回空列表检查HTML文本发现页面里只有搜索框和底部的脚本标签没有职位卡片。原因51job的搜索列表页在某些情况下会返回一个JS动态渲染的空壳页面职位数据是前端通过异步请求加载的直接解析静态HTML拿不到内容。也可能是请求头不完整被识别为爬虫后返回了一个降级页面。解决先加完整请求头重试一次仍然失败就改用selenium。用selenium打开同一个搜索URL等待2到3秒让页面渲染完成后再拿page_source把这个HTML传给parse_list_html()即可。注意selenium方案不要和requests方案的代码混在一个模块里单独写一个spider_selenium.py因为selenium启动浏览器非常耗时只在确认requests方案失效时才启用。5.2 请求频率稍高就弹验证码现象连续翻页爬取到第三页或第四页时页面被重定向到验证码页页面内容变成“安全验证”相关字样。原因请求频率太高IP被服务端限流。51job对同一个IP的短时间请求次数有阈值超过阈值就触发验证码机制。解决把请求间隔从固定值改成随机值比如random.uniform(2, 4)并且在每个页面请求失败时退避重试。退避的逻辑是第一次失败等5秒第二次等15秒第三次直接放弃这一页。如果数据量需求不大更省事的办法是用time.sleep把每页间隔拉到10秒以上爬取总时长多一两分钟但基本不会触发验证码。5.3 详情页返回404职位已下线现象爬取过程中某条职位的detail_url请求返回404状态码整个程序因为raise_for_status()抛出异常中断。原因列表页上有少量职位已经过了有效期但搜索结果里仍然保留展示点击进入详情页就是404。解决对fetch_detail()做异常捕获404时返回空字典而不是让程序崩溃。改动很小但很关键——没有这个保护爬虫会在中途翻车前面爬的数据全部丢在内存里没有落盘。def fetch_detail_safe(url: str) - dict: try: return fetch_detail(url) except requests.HTTPError as e: if e.response.status_code 404: print(f职位已下线: {url}) return {} raise5.4 薪资解析把“千/月”当成“万/月”算错现象清洗后salary_mid的中位数达到3万以上和肉眼观察的岗位薪资水平明显不符。原因parse_salary()在匹配“1-1.5万/月”之后又去匹配了一段薪资文本“6-8千/月”如果正则里漏掉单位分支就会把6千误解析成6万或者把“千”和“万”的逻辑写反。解决在正则匹配中显式区分单位和周期匹配到万乘10000匹配到千乘1000。解析完成后立刻做一个频次检查统计salary_mid大于50000的比例如果超过10%大概率是解析逻辑有bug。用df[df[salary_mid] 50000][salary].head()打印出原始薪资文本对照检查这是最快定位的方式。5.5 CSV用Excel打开全是乱码现象数据写入python_jobs.csv后用Excel打开中文全部显示为乱码但用记事本打开正常。原因Python的open()默认编码是utf-8utf-8没有BOM头Excel默认按ANSI编码解析中文自然乱码。解决写入时显式指定encodingutf-8-sig。如果CSV已经写坏了用pandas重新读入再以utf-8-sig覆盖保存即可。这个坑在所有爬虫项目里都会遇到养成写入CSV就带utf-8-sig的习惯比事后补救省心得多。6. 数据分析与可视化从清洗好的DataFrame到答辩能直接展示的成果6.1 分析维度怎么选四张图讲清一个岗位的市场画像数据分析和可视化要回答四个问题Python岗位集中在哪里、薪资分布呈现什么形态、学历和经验门槛有多高、岗位描述里出现的高频词是什么。对应四张图城市岗位数Top20柱状图、薪资中位数直方图、学历要求饼图、职位描述词云图。这四张图从地域、待遇、门槛、技能四个角度完整覆盖了“Python就业市场”这个主题答辩时每一张图都能讲出独立结论。图表生成使用pyecharts它输出的HTML文件可以直接双击打开交互效果包括鼠标悬停显示数值、拖拽缩放等比静态图片更能体现工作量。生成前先确认pyecharts的版本1.x版本的API和0.5.x版本完全不兼容下面代码按1.x版本编写。6.2 用pyecharts生成四张图的完整代码from pyecharts.charts import Bar, Pie, WordCloud from pyecharts import options as opts import jieba # 1. 城市岗位数Top20柱状图 city_count df[city].value_counts().head(20) bar ( Bar() .add_xaxis(city_count.index.tolist()) .add_yaxis(岗位数, city_count.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(titlePython岗位城市分布Top20), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)), ) ) bar.render(城市分布.html) # 2. 薪资中位数直方图 salaries df[salary_mid].dropna() hist ( Bar() .add_xaxis([0-10k, 10-20k, 20-30k, 30-40k, 40k以上]) .add_yaxis(岗位数, [ int(((salaries 0) (salaries 10000)).sum()), int(((salaries 10000) (salaries 20000)).sum()), int(((salaries 20000) (salaries 30000)).sum()), int(((salaries 30000) (salaries 40000)).sum()), int((salaries 40000).sum()), ]) .set_global_opts(title_optsopts.TitleOpts(titlePython岗位薪资分布)) ) hist.render(薪资分布.html) # 3. 学历要求饼图 edu_count df[education].value_counts() pie ( Pie() .add(, [list(t) for t in edu_count.items()]) .set_global_opts(title_optsopts.TitleOpts(title学历要求分布)) ) pie.render(学历分布.html) # 4. 职位描述词云图 words jieba.cut( .join(df[description].dropna().tolist())) word_list [w for w in words if len(w) 1 and w not in (我们, 公司, 相关, 工作)] word_freq pd.Series(word_list).value_counts().head(60) wc ( WordCloud() .add(, [list(t) for t in word_freq.items()], word_size_range[20, 100]) .set_global_opts(title_optsopts.TitleOpts(title职位描述高频词)) ) wc.render(词云.html)xaxis_opts里的rotate45解决城市名称过长导致标签重叠的问题这是柱状图最常被忽略的参数。薪资分箱用了五次布尔运算统计各区间数量逻辑直观但代码重复可以用pd.cut一行替换。词云部分用jieba.cut分词后过滤掉单字和通用词剩下的高频词能直接反映岗位技能要求。四个render()会生成四个HTML文件文件名建议用英文或拼音中文字符在部分浏览器地址栏里会转义虽然不影响打开但答辩现场演示时容易让老师觉得文件混乱。6.3 文档报告的组织与答辩演示技巧文档报告是源码之外最重要的交付物很多学生代码写得不错但报告只有几张图加几段话拿不到高分。一份能拿优的报告至少要包含五个部分项目背景与目标、技术方案与架构图、数据采集流程说明、数据分析结论、问题与改进方向。摘要写在最前面300字以内重点突出“用什么工具、爬了什么数据、得出了什么结论”。每一张可视化图表旁边都要配一段“图表解读”先描述现象再给解释比如“北京以120个岗位位居第一占样本总量的40%说明北京Python就业机会最为集中”。答辩演示时有一个小技巧不要从代码开始讲先从图表开始。打开四个HTML页面让老师看到结果再切回代码讲关键实现最后主动提一句“在爬取过程中遇到了验证码、职位下线等问题分别采用了随机延迟和异常捕获来处理”。这会让老师觉得项目是真实跑过的而不是拼接的代码。我自己的习惯是保留一份原始数据CSV、一份清洗后CSV答辩时老师问到数据量或者某个字段的处理方式直接打开Excel展示比口头解释有说服力得多。最后说一个血泪经验项目文件夹里一定要区分spider/、data/、analysis/、output/四个目录图表和CSV按时间命名。我见过太多人把爬虫代码、中间数据、图表全堆在桌面改了一版代码之后自己都分不清哪份数据是最新的。这个项目从爬虫到出图不算复杂但把过程组织得清晰本身就是期末作业的隐藏加分项。希望帮到你。本文还有配套的精品资源点击获取
返回列表