Python3 爬虫实战 — 前程无忧招聘信息爬取 + 数据可视化


  • 爬取时间:2020-07-11(2020年10月测试,增加了反爬,此代码已失效!!!)
  • 实现目标:根据用户输入的关键字爬取相关职位信息存入 MongoDB,读取数据进行可视化展示。
  • 涉及知识:请求库 requests、Xpath 语法、数据库 MongoDB、数据处理 Numpy、Pandas、数据可视化 Matplotlib。
  • 完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/SpiderDataVisualization/51job
  • 其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
  • 爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278

文章目录

    • 【1x00】获取数据 get_51job_data.py
      • 【01x01】构建请求地址
      • 【01x02】获取总页数
      • 【01x03】提取详情页 URL
      • 【01x04】提取职位信息
      • 【01x05】保存数据到 MongoDB
    • 【2x00】数据可视化 draw_bar_chart.py
      • 【02x01】数据初处理
      • 【02x02】数据清洗
      • 【02x03】绘制经验与平均薪资关系图
      • 【02x04】绘制学历与平均薪资关系图
    • 【3x00】数据截图
    • 【4x00】完整代码


【1x00】获取数据 get_51job_data.py

【01x01】构建请求地址

以 Python 职位为例,请求地址如下:

第一页:https://search.51job.com/list/000000,000000,0000,00,9,99,python,2,1.html

第二页:https://search.51job.com/list/000000,000000,0000,00,9,99,python,2,2.html

第三页:https://search.51job.com/list/000000,000000,0000,00,9,99,python,2,3.html

初始化函数:

    def __init__(self):self.base_url = 'https://search.51job.com/list/000000,000000,0000,00,9,99,%s,2,%s.html'self.headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.13 Safari/537.36'}self.keyword = input('请输入关键字:')

【01x02】获取总页数

在页面的下方给出了该职位一共有多少页,使用 Xpath 和正则表达式提取里面的数字,方便后面翻页爬取使用,注意页面编码为 gbk

01

    def tatal_url(self):url = self.base_url % (self.keyword, str(1))response = requests.get(url=url, headers=self.headers)tree = etree.HTML(response.content.decode('gbk'))# 提取一共有多少页text = tree.xpath("//div[@class='p_in']/span[1]/text()")[0]number = re.findall('[0-9]', text)number = int(''.join(number))print('%s职位共有%d页' % (self.keyword, number))return number

【01x03】提取详情页 URL

定义一个 detail_url() 方法,传入总页数,循环提取每一页职位详情页的 URL,将每一个详情页 URL 传递给 parse_data() 方法,用于解析详情页内的具体职位信息。

提取详情页时有以下几种特殊情况:

特殊情况一:如果有前程无忧自己公司的职位招聘信息掺杂在里面,他的详情页结构和普通的不一样,页面编码也有差别。

页面示例:https://51rz.51job.com/job.html?jobid=115980776

页面真实数据请求地址类似于:https://coapi.51job.com/job_detail.php?jsoncallback=&key=&sign=params={“jobid”:""}

请求地址中的各参数值通过 js 加密:https://js.51jobcdn.com/in/js/2018/coapi/coapi.min.js

02
03

特殊情况二:部分公司有自己的专属页面,此类页面的结构也不同于普通页面

页面示例:http://dali.51ideal.com/jobdetail.html?jobid=121746338

04
05

为了规范化,本次爬取将去掉这部分特殊页面,仅爬取 URL 带有 jobs.51job.com 的数据

    def detail_url(self, number):for num in range(1, number+1):url = self.base_url % (self.keyword, str(num))response = requests.get(url=url, headers=self.headers)tree = etree.HTML(response.content.decode('gbk'))detail_url1 = tree.xpath("//div[@class='dw_table']/div[@class='el']/p/span/a/@href")"""深拷贝一个 url 列表,如果有连续的不满足要求的链接,若直接在原列表里面删除,则会漏掉一些链接,因为每次删除后的索引已改变,因此在原列表中提取不符合元素后,在深拷贝的列表里面进行删除。最后深拷贝的列表里面的元素均符合要求。"""detail_url2 = copy.deepcopy(detail_url1)for url in detail_url1:if 'jobs.51job.com' not in url:detail_url2.remove(url)self.parse_data(detail_url2)print('第%d页数据爬取完毕!' % num)time.sleep(2)print('所有数据爬取完毕!')

【01x04】提取职位信息

解析详情页时页面编码是 gbk,但是某些页面在解析时仍然会报编码错误,因此使用 try-except 语句捕捉编码错误(UnicodeDecodeError),如果该页面有编码错误则直接 return 结束函数。

    def parse_data(self, urls):"""position:            职位wages:               工资region:              地区experience:          经验education:           学历need_people:         招聘人数publish_date:        发布时间english:             英语要求welfare_tags:        福利标签job_information:     职位信息work_address:        上班地址company_name:        公司名称company_nature:      公司性质company_scale:       公司规模company_industry:    公司行业company_information: 公司信息"""for url in urls:response = requests.get(url=url, headers=self.headers)try:text = response.content.decode('gbk')except UnicodeDecodeError:returntree = etree.HTML(text)"""提取内容时使用 join 方法将列表转为字符串,而不是直接使用索引取值,这样做的好处是遇到某些没有的信息直接留空而不会报错"""position = ''.join(tree.xpath("//div[@class='cn']/h1/text()"))wages = ''.join(tree.xpath("//div[@class='cn']/strong/text()"))# 经验、学历、招聘人数、发布时间等信息都在一个标签里面,逐一使用列表解析式提取content = tree.xpath("//div[@class='cn']/p[2]/text()")content = [i.strip() for i in content]if content:region = content[0]else:region = ''experience = ''.join([i for i in content if '经验' in i])education = ''.join([i for i in content if i in '本科大专应届生在校生硕士'])need_people = ''.join([i for i in content if '招' in i])publish_date = ''.join([i for i in content if '发布' in i])english = ''.join([i for i in content if '英语' in i])welfare_tags = ','.join(tree.xpath("//div[@class='jtag']/div//text()")[1:-2])job_information = ''.join(tree.xpath("//div[@class='bmsg job_msg inbox']/p//text()")).replace(' ', '')work_address = ''.join(tree.xpath("//div[@class='bmsg inbox']/p//text()"))company_name = ''.join(tree.xpath("//div[@class='tCompany_sidebar']/div[1]/div[1]/a/p/text()"))company_nature = ''.join(tree.xpath("//div[@class='tCompany_sidebar']/div[1]/div[2]/p[1]//text()"))company_scale = ''.join(tree.xpath("//div[@class='tCompany_sidebar']/div[1]/div[2]/p[2]//text()"))company_industry = ''.join(tree.xpath("//div[@class='tCompany_sidebar']/div[1]/div[2]/p[3]/@title"))company_information = ''.join(tree.xpath("//div[@class='tmsg inbox']/text()"))job_data = [position, wages, region, experience, education, need_people, publish_date,english, welfare_tags, job_information, work_address, company_name,company_nature, company_scale, company_industry, company_information]save_mongodb(job_data)

【01x05】保存数据到 MongoDB

指定一个名为 job51_spider 的数据库和一个名为 data 的集合,依次将信息保存至 MongoDB。

def save_mongodb(data):client = pymongo.MongoClient(host='localhost', port=27017)db = client.job51_spidercollection = db.datasave_data = {'职位': data[0],'工资': data[1],'地区': data[2],'经验': data[3],'学历': data[4],'招聘人数': data[5],'发布时间': data[6],'英语要求': data[7],'福利标签': data[8],'职位信息': data[9],'上班地址': data[10],'公司名称': data[11],'公司性质': data[12],'公司规模': data[13],'公司行业': data[14],'公司信息': data[15]}collection.insert_one(save_data)

【2x00】数据可视化 draw_bar_chart.py

【02x01】数据初处理

从 MongoDB 里面读取数据为 DataFrame 对象,本次可视化只分析工资与经验、学历的关系,所以只取这三项,由于获取的数据有些是空白值,因此使用 replace 方法将空白值替换成缺失值(NaN),然后使用 DataFrame 对象的 dropna() 方法删除带有缺失值(NaN)的行。将工资使用 apply 方法,将每个值应用于 wish_data 方法,即对每个值进行清洗。

def processing_data():# 连接数据库,从数据库读取数据(也可以导出后从文件中读取)client = pymongo.MongoClient(host='localhost', port=27017)db = client.job51_spidercollection = db.data# 读取数据并转换为 DataFrame 对象data = pd.DataFrame(list(collection.find()))data = data[['工资', '经验', '学历']]# 使用正则表达式选择空白的字段并填充为缺失值,然后删除带有缺失值的所有行data.replace(to_replace=r'^\s*$', value=np.nan, regex=True, inplace=True)data = data.dropna()# 对工资数据进行清洗,处理后的工作单位:元/月data['工资'] = data['工资'].apply(wish_data)return data

【02x02】数据清洗

def wish_data(wages_old):"""数据清洗规则:分为元/天,千(以上/下)/月,万(以上/下)/月,万(以上/下)/年若数据是一个区间的,则求其平均值,最后的值统一单位为元/月"""if '元/天' in wages_old:if '-' in wages_old.split('元')[0]:wages1 = wages_old.split('元')[0].split('-')[0]wages2 = wages_old.split('元')[0].split('-')[1]wages_new = (float(wages2) + float(wages1)) / 2 * 30else:wages_new = float(wages_old.split('元')[0]) * 30return wages_newelif '千/月' in wages_old or '千以下/月' in wages_old or '千以上/月' in wages_old:if '-' in wages_old.split('千')[0]:wages1 = wages_old.split('千')[0].split('-')[0]wages2 = wages_old.split('千')[0].split('-')[1]wages_new = (float(wages2) + float(wages1)) / 2 * 1000else:wages_new = float(wages_old.split('千')[0]) * 1000return wages_newelif '万/月' in wages_old or '万以下/月' in wages_old or '万以上/月' in wages_old:if '-' in wages_old.split('万')[0]:wages1 = wages_old.split('万')[0].split('-')[0]wages2 = wages_old.split('万')[0].split('-')[1]wages_new = (float(wages2) + float(wages1)) / 2 * 10000else:wages_new = float(wages_old.split('万')[0]) * 10000return wages_newelif '万/年' in wages_old or '万以下/年' in wages_old or '万以上/年' in wages_old:if '-' in wages_old.split('万')[0]:wages1 = wages_old.split('万')[0].split('-')[0]wages2 = wages_old.split('万')[0].split('-')[1]wages_new = (float(wages2) + float(wages1)) / 2 * 10000 / 12else:wages_new = float(wages_old.split('万')[0]) * 10000 / 12return wages_new

【02x03】绘制经验与平均薪资关系图

def wages_experience_chart(data):# 根据经验分类,求不同经验对应的平均薪资wages_experience = data.groupby('经验').mean()# 获取经验和薪资的值,将其作为画图的 x 和 y 数据w = wages_experience['工资'].index.valuese = wages_experience['工资'].values# 按照经验对数据重新进行排序,薪资转为 int 类型(也可以直接在前面对 DataFrame 按照薪资大小排序)wages = [w[6], w[1], w[2], w[3], w[4], w[5], w[0]]experience = [int(e[6]), int(e[1]), int(e[2]), int(e[3]), int(e[4]), int(e[5]), int(e[0])]# 绘制柱状图plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']plt.figure(figsize=(9, 6))x = wagesy = experiencecolor = ['#E41A1C', '#377EB8', '#4DAF4A', '#984EA3', '#FF7F00', '#FFFF33', '#A65628']plt.bar(x, y, color=color)for a, b in zip(x, y):plt.text(a, b, b, ha='center', va='bottom')plt.title('Python 相关职位经验与平均薪资关系', fontsize=13)plt.xlabel('经验', fontsize=13)plt.ylabel('平均薪资(元 / 月)', fontsize=13)plt.savefig('wages_experience_chart.png')plt.show()

09

【02x04】绘制学历与平均薪资关系图

def wages_education_chart(data):# 根据学历分类,求不同学历对应的平均薪资wages_education = data.groupby('学历').mean()# 获取学历和薪资的值,将其作为画图的 x 和 y 数据wages = wages_education['工资'].index.valueseducation = [int(i) for i in wages_education['工资'].values]# 绘制柱状图plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']plt.figure(figsize=(9, 6))x = wagesy = educationcolor = ['#E41A1C', '#377EB8', '#4DAF4A']plt.bar(x, y, color=color)for a, b in zip(x, y):plt.text(a, b, b, ha='center', va='bottom')plt.title('Python 相关职位学历与平均薪资关系', fontsize=13)plt.xlabel('学历', fontsize=13)plt.ylabel('平均薪资(元 / 月)', fontsize=13)plt.savefig('wages_education_chart.png')plt.show()

10

【3x00】数据截图

一共有 34009 条数据,完整数据已放在 github,可自行下载。

MongoDB:

06

CSV 文件:

07

JSON 文件:

08

【4x00】完整代码

完整代码地址(点亮 star 有 buff 加成):https://github.com/TRHX/Python3-Spider-Practice/tree/master/SpiderDataVisualization/51job

其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice

爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/437668.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【转】Postman系列三:Postman中post接口实战(上传文件、json请求)

一:接口测试过程中GET请求与POST请求的主要区别 从开发角度我们看get与post的主要区别是: 1.Get是用来从服务器上获得数据,而Post是用来向服务器上传递数据; 2.Get安全性比Post低:Get将表单中数据的按照keyvalue的形式…

Hadoop datanode正常启动,但是jps差不多datanode进程,而且Live nodes中却缺少节点

启动时可以看到启动成功,但是在chun2,jps的时候却没有了datanode进程,而且web端Live nodes也缺少了 百度搜索之后查到是因为hdfs.site.xml配置文件里dfs.data.dir配置的路径重复,就是多个节点存放data数据的目录路径相同了&#x…

【转】Postman系列四:Postman接口请求设置环境变量和全局变量、测试沙箱和测试断言、测试集运行与导入数据文件

一:Postman中接口请求设置环境变量和全局变量 全局变量和环境变量可以通过Pre-request Script和Tests设置,会在下面测试沙箱和测试断言中讲到。 全局变量的设置:官网参考https://learning.getpostman.com/docs/postman/environments_and_glob…

Python 算法之递归与尾递归,斐波那契数列以及汉诺塔的实现

文章目录递归概念递归要素递归与迭代的区别示例一:阶乘示例二:斐波那契数列示例三:汉诺塔问题尾递归Python 中尾递归的解决方案递归概念 递归:程序调用自身的编程技巧称为递归( recursion)。用一种通俗的话…

【转】Postman系列五:Postman中电商网站cookie、token检验与参数传递实战

一:Postman中电商网站cookie实战 Postman接口请求使用cookie两种方式: 1.直接在header(头域)中添加cookie,适用于已知请求cookie头域的情况 2.使用Postman的cookie管理机制,即可以手动添加,同时…

Python 数据结构之栈的实现

文章目录栈的概念栈的特点栈的操作Python 实现栈栈的简单应用:括号匹配问题栈的简单应用:倒序输出一组元素栈的概念 栈(stack)又名堆栈,栈是一种线性数据结构,用先进后出或者是后进先出的方式存储数据&…

CSDN 2020 博客之星实时数据排名(Python 爬虫 + PyEcharts)

CSDN 2020 博客之星实时数据排名:csdn.itrhx.com CSDN 一年一度的博客之星评选开始了,官网地址:https://bss.csdn.net/m/topic/blog_star2020 ,由于官网是按照随机编号排序的,没有按照票数多少排序,为了方便…

【转】注册Azure AD 应用程序

作者:陈希章 发表于2017年3月22日 在此前的文章中,我给大家介绍了分别用Graph 浏览器以及第三方工具(POSTMAN)快速体验Microsoft Graph的功能,其中有一个重要的环节就是,开发人员需要访问Microsoft Graph的…

Python + GitHub Actions 实现 CSDN 自动签到与抽奖(非 selenium 版本)

文章目录【1x00】技术栈【2x00】代码实现签到与抽奖【3x00】签到结果通知【03x01】Server 酱【03x02】企业微信【03x03】钉钉【4x00】自动签到【5x00】完整代码【6x00】如何使用【06x01】方法一:直接 Fork 代码(推荐)【06x01】方法二&#xf…

Spark安装配置

Scala基础语法学习的差不多了,先把spark安装上 首先官网下载解压安装后 进入到conf目录下修改文件名 修改spark-env.sh(配置jdk路径) export JAVA_HOME/usr/local/java/jdk1.8.0_221修改slaves(添加子节点名) chun…

【转】注册Azure AD 2.0 应用程序

作者:陈希章 发表于 2017年3月22日 上一篇 介绍了Microsoft Graph应用程序的一些概念,以及目前还比较普遍的Azure AD 1.0应用程序的注册方式。但正如我多次提到的那样,虽然目前功能还在不断完善,但Azure AD 2.0会逐渐成为主流&…

Python 采集 Facebook 评论插件、留言外挂程序

实现时间:2021-05-30实现难度:★★★☆☆☆实现目标:采集 Facebook 评论插件、留言外挂程序的所有评论。完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/CommentPlugin/facebook-comments其他爬虫实战代码…

写第一个spark程序(wordcount)

首先启动集群与spark 其次把spark目录下的README.md上传到hdfs 进入spark下的bin目录,运行spark-shell ./spark-shell运行 val textFile sc.textFile("hdfs://chun1:9000/spark/README.md")val wordCounts textFile.flatMap(line>line.split("…

【中国版Office 365 应用程序注册】

中国版Office 365是由世纪互联进行运营的一个云服务,单纯从技术角度来看的话,它基本保持了与国际版的同步。但是由于两个版本本质上是完全独立的,其中最关键的就是账号系统是分开的,所以从使用角度来看,不管是直接用户…

Python 中如何解决 asyncio 文件描述符最大数量限制问题

文章目录问题复现问题分析事件循环 EventLoopI/O 多路复用select 的缺点解决方法1.更换事件循环选择器2.限制并发量3.修改最大文件描述符限制WindowsLinux总结WindowsLinux问题复现 Windows 平台下,Python 版本 3.5,使用异步框架 asyncio,有…

【转】掀起Azure AD的盖头来——深入理解Microsoft Graph应用程序和服务权限声明

引子 这是一篇计划外的文章。我们都知道要进行Microsoft Graph的开发的话,需要进行应用程序注册。这个在此前我已经有专门的文章写过了。但这里存在一个小的问题:国内版的Office 365在申请好之后,并没有像国际版那样,有一个对应的…

Python3 学习系列 丨 博客目录索引

整个博客有关 Python 学习目录索引,方便快捷定位查询基础学习篇 Python3 基础学习笔记 C01【变量和简单数据类型】Python3 基础学习笔记 C02【列表】Python3 基础学习笔记 C03【操作列表】Python3 基础学习笔记 C04【if 语句】Python3 基础学习笔记 C05【字典】Pyt…

【转】日邮物流:实现智慧物流,这个云上对了!

和阳光、空气、水、网络一样,「物流」早已成为当代企业、个人赖以生存的必要条件。2020第一季度全球物流受疫情影响面临挑战,业内普遍预计全球物流及供应链将重新优化布局。借此时机,物流业纷纷将目光投向“数字化智慧物流”方向,…

Python 实现十大经典排序算法

目录排序算法分类一、冒泡排序(Bubble Sort)1、原理2、步骤3、动画演示4、代码实现5、具体示例二、选择排序(Selection Sort)1、原理2、步骤3、动画演示4、代码实现5、具体示例三、插入排序(Insertion Sort&#xff09…

【转】Microsoft Graph 桌面应用程序

桌面应用程序,在我这篇文章的语境中,我是特指在Windows桌面上面直接运行的.NET应用程序,包括Console Application,WPF Application,Windows Forms Application, UWP Application,并且限于篇幅,我…