用python爬虫招聘信息_Python爬虫实战之二 - 基于Scrapy框架抓取Boss直聘的招聘信息...

Python爬虫实战之三 - 基于Scrapy框架抓取Boss直聘的招聘信息

---------------readme---------------

简介:本人产品汪一枚,Python自学数月,对于小白,本文会是一篇比较容易上手的经验贴。当然毕竟是新手,欢迎大牛拍砖、狂喷~

致谢:

本着了解招聘行情,以备不时之需,之所以选择转战Boss,是因为爬完拉钩网之后,发现招聘质量有待商榷;同时也感谢Boss的权威招聘信息,也使2018年的十一假期有一段不错的学习经历收获。

爬取Boss官网(www.zhipin.com)期间,若对Boss造成或小或大的影响,本人深感歉意。本文只为获取招聘信息和交流学习,并无恶意,再次鸣谢。

---------------正文分隔符---------------

开发环境

MacBook Air (13-inch, Early 2015)

macOS High Sierra 10.13.6

1.6GHZ Inter Core i5

Python:V 3.7.0

一、兵马未动,粮草先行

(1)、安装Scrapy

使用Mac自带终端安装,使用pip辅助安装。

在bash中输入命令:pip3 install scrapy

如果pip3安装没有问题,安装scrapy理论上应该很顺利,因为坑都在pip3的安装过程里填完了...

ps:一个scrapy包里面竟然有如此之多的包,也难称作scarpy框架...

(2)、创建项目

安装完成Scrapy的安装,可以使用命令行创建新项目。

输入命令行:scrapy startproject www_zhipin_com

ps:我是参考经验贴中的教程创建的项目名称,项目的名称(www_zhipin_com)可按需求自定义。

创建项目完成后,在python3中打开该项目。

打开流程如下:python3->file->open,选择刚创建的项目。

导入项目后,可以看到在执行创建项目命令后,Scrapy会为我们自动创建spider所需的标准文件目录。

ps:开始一点点的体会到scrapy的强大,另外,也验证scrapy的安装已经没有问题了。

创建的项目,其中的文件作用如下:

spiders(Python Package):相当于主程序main的package,后续会在该package中创建相应spider的python file目录

item(Python File):spider项目的item文件,用于设置本次需从页面爬取的要素信息,如:职位名称、薪资水平、平台简称...

middlewares:spider项目的middlewares文件,主要用于设置防反爬虫的相关策略,例如配置user_agent、代理IP等方法的设定。因本教程是小批量爬取,可以暂时不涉及。

pipelines:spider项目的pipelines文件,用于编写已爬取的item数据的处理和存储的文件,例如,需将数据去重清洗后,保存至数据库中,则需要在该文件中定义规则和方法

settings:spider项目的settings文件,顾名思义为配置文件,所有相关的配置信息均在此文件中定义

scrapy.cfg:spider项目部署的相关文件,因本次不涉及更改其配置,知晓作用即可

看下入门教程给的解释,无力吐槽...

中文夹杂着英文的Scrapy官网地址:Scrapy入门教程,不过依然感激有中文解释...

(3)、创建spider代码文件

在spiders的Python Package文件中创建本次的爬取的Python File文件。

文件名:zhipin_spider (ps:这个命名可随意设置,你高兴就好,给你一个机智的眼神...)

二、备马囤粮,攻城略地

(1)、大战在即,先谋而后动

以产品经理的为例,以列表第一为参考,Boss的页面布局图如下:

ps:VIPKID给的薪水很诱人啊...咳、咳...打完这一仗,回家可以整理下简历了...

我们依次从:微观->中观->宏观,三个视角分析下Boss的html页面布局。

1)、微观视角

单条招聘信息的Html布局如下:

页面的主要信息如下(按出场顺序):

招聘职位:产品经理

职位薪水:25k-50k

公司地址:北京 西城区 鼓楼

工作年限:3-5年

教育背景:本科

平台简称:VIPKID

所在行业:移动互联网

融资规模:D轮及以上

公司规模:10000人以上

发布时间:发布于03月28日

如此看来,我们的item相应的要素信息基本有了,详情见Item模块。

2)、中观视角

单页招聘信息的Html布局如下:

一个ul中包含30个li,对应页面的30个招聘信息,工工整整的码着,就像等待被翻牌一样...呃,脑中瞬间闪过雍正爷和乾隆爷的伟岸形象...

ps:看着这JS代码,想起了今年前半年自学JS的那段时光,一晃18年已过大半。

3)、宏观视角

单此检索查询,Boss只提供了10页的检索信息,这也就意味着,单次一个关键词检索,我们只能获取到300个职位。

此处有个问题需要思考下:针对这样的宏观视角,我们的spider策略要如何制定?

我是这样思考的,贴出来大家可以讨论下:

首先,从效率的角度,如果关键词限定的范围越宽泛,则单次检索到所需的信息越少,例如,我需要查询的是产品经理及以上职位招聘信息,如果仅输入”产品“这个关键词,检索结果就会充斥着产品专员、产品运营等无效信息,所以从效率来讲,检索精度需越精准越好;

其次,从边界的角度,如果定位精准,就会出现边界限制的困境,很难通过我输入的精准关键词查询到关联的招聘职位,搜索范围就只限于当前的职级限制。

所以,基于此,本次作战的方针如下:单个关键词精准定位,不同领域多职级的轮循。

ps:方针确定,战略布局也就清晰了...

(2)、战略目标(Item)

既然要攻城略地,就需要确认下,哪些城哪片地可以入我等法眼。

在此,就不得不提一下产品的职业病:用户视角。

如果我是应聘者,我会需要以怎样的信息去快速筛选有意向的职位呢?答:首先是职位、薪水、哪个公司,其次是地址、要求、公司规模等。

其实,在微观的Html中已经标明Boss页面中展示的招聘信息,这些信息也是我们本次行动需要斩获的首要目标。

所以,items.py中的要素信息配置如下:

1 #-*- coding: utf-8 -*-

2

3 #Define here the models for your scraped items

4 #5 #See documentation in:

6 #https://doc.scrapy.org/en/latest/topics/items.html

7

8 importscrapy9

10

11 classWwwZhipinComItem(scrapy.Item):12 #对应页面的data-jid

13 jid =scrapy.Field()14 #对应页面的data-jobid

15 pid =scrapy.Field()16 #为招聘职位的名称,对应页面的job-title

17 positionName =scrapy.Field()18 #工作年限

19 workYear =scrapy.Field()20 #薪水

21 salary =scrapy.Field()22 #公司所在地

23 city =scrapy.Field()24 #教育背景

25 education =scrapy.Field()26 #公司平台简称

27 companyShortName =scrapy.Field()28 #所属行业

29 industryField =scrapy.Field()30 #融资阶段

31 financeStage =scrapy.Field()32 #公司规模

33 companySize =scrapy.Field()34 #发布时间

35 time =scrapy.Field()36 #爬取时间

37 spider_at =scrapy.Field()38 pass

item.py

官方入门教程给的Item教程如下:Scrapy官方入门教程

(3)、战略实施

指导方针和战略目标已经搞定,此时便可剑指城池,策马扬鞭,走起~

首先,看下Scrapy官方入门教程是怎么写的。Scrapy官方入门教程

有模板,一切就好办了,按照模版需定义相关name、start_urls和prase等信息,不多说,先粘代码,然后一步一步分解。

1 #-*- coding: utf-8 -*-

2 importscrapy3 importrandom4 importtime5 importdatetime6 from www_zhipin_com.items importWwwZhipinComItem7

8

9 classZhipinSpider(scrapy.Spider):10

11 name = 'zhipin'

12 allowed_domains = ['www.zhipin.com']13 start_urls = ['https://www.zhipin.com/']14 #目标是:北京、上海、杭州、广州、深圳、天津,先以北京去验证代码

15 scity = ['c101010100/h_101010100']16 positions = ['产品经理']17

18 #爬取的需求:一个scity下,所有positions的10页招聘信息

19 curPage = 1 #当前spider的页码

20 curScityIndex = 0 #当前spider的城市索引值

21 curPositionIndex = 0 #当前spider的岗位索引值

22

23 headers ={24 'Accept': 'application/json, text/javascript, */*; q=0.01',25 'Accept-Encoding': 'gzip, deflate, br',26 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',27 'Cookie': 'lastCity=101010100; JSESSIONID=""; __g=-; Hm_lvt_194df3105ad7148dcf2b98a91b5e727a=1538397559; __c=1538397566; __l=r=https%3A%2F%2Fwww.zhipin.com%2F&l=%2Fwww.zhipin.com%2Fjob_detail%2F%3Fquery%3D%25E4%25BA%25A7%25E5%2593%2581%25E7%25BB%258F%25E7%2590%2586%26scity%3D101010100%26industry%3D%26position%3D; t=jPFEjDvhnhIeAV4s; wt=jPFEjDvhnhIeAV4s; __a=7566280.1538397547.1538397547.1538397566.16.2.15.16; Hm_lpvt_194df3105ad7148dcf2b98a91b5e727a=1538457781',28 'token': 'OPX6QDsGzqpLwns',29 'Host': 'www.zhipin.com',30 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',31 'x-requested-with': 'XMLHttpRequest',32 'Referer': 'https://www.zhipin.com/'

33 }34

35 #因url需要scity和position进行多次拼装,且start_requests其必须返回一个可迭代Itearable对象,所以调用url_link进行url拼装,在start_requests中进行可迭代

36 defstart_requests(self):37 return[self.url_link()]38

39 #负责处理response并返回处理的数据以及(/或)跟进的URL

40 defparse(self, response):41 print("request ->" +response.url)42 job_list = response.css('div.job-list > ul > li') #提取job-list的ul中的li

43 for job injob_list:44 item =WwwZhipinComItem()45 job_primary = job.css('div.job-primary')46 item['jid'] =job.css(47 'div.info-primary > h3 > a::attr(data-jid)').extract_first().strip()48 item['pid'] =job.css(49 'div.info-primary > h3 > a::attr(data-jobid)').extract_first().strip()50 item["positionName"] =job_primary.css(51 'div.info-primary > h3 > a > div::text').extract_first().strip()52

53 #直接将salary的格式Xk-Xk,改写成high、low、avg

54 salary =job_primary.css(55 'div.info-primary > h3 > a > span::text').extract_first().strip()56 salary_list = salary.replace('k', '000').split('-')57

58 #将salary进行格式转换之后,我们就可以按照期望salary条件更精准的筛选

59 #假设期望的最低标注是不低于20000,同时可以有的上限不低于25000,不符合条件的招聘直接过滤掉

60 if int(salary_list[0]) < 20000 or int(salary_list[1]) < 25000:61 continue

62 else:63 item["salary"] ={64 'low': int(salary_list[0]),65 'high': int(salary_list[1]),66 'avg': int((int(salary_list[0]) + int(salary_list[1])) / 2)67 }68

69 info_primary =job_primary.css(70 'div.info-primary > p::text').extract()71 item['city'] =info_primary[0].strip()72 item['workYear'] = info_primary[1].strip()73 item['education'] = info_primary[2].strip()74 item['companyShortName'] =job_primary.css(75 'div.info-company > div.company-text > h3 > a::text'

76 ).extract_first().strip()77 company_infos =job_primary.css(78 'div.info-company > div.company-text > p::text').extract()79 if len(company_infos) == 3:80 item['industryField'] =company_infos[0].strip()81 item['financeStage'] = company_infos[1].strip()82 item['companySize'] = company_infos[2].strip()83

84 #将发布时间的格式调整成年月日,目前有三种形式:'发布于03月31日','发布于03月31日','发布于11:31'

85 item_time = job.css('div.info-publis > p::text').extract_first().strip()86 item_time = item_time.replace("发布于", "2018-")87 item_time = item_time.replace("月", "-")88 item_time = item_time.replace("日", "")89 if item_time.find("昨天"):90 item_time = str(datetime.date.today() - datetime.timedelta(days=1))91 elif item_time.find(":"):92 item_time =str(datetime.date.today())93 item['time'] =item_time94

95 item['spider_at'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())96 yielditem97

98 #实现城市->职位->页面的层级轮循

99 if self.curScityIndex

103 elif self.curPositionIndex < len(self.positions) - 1:104 self.curPositionIndex += 1

105 self.curPage = 1

106 elif self.curScityIndex < len(self.scity) - 1:107 self.curScityIndex += 1

108 self.curPositionIndex =0109 self.curPage = 1

110

111 #随机停留时长,俗话说以时间换空间

112 time.sleep(20 + random.randint(30, 50))113 yield self.url_link() #返回url_link()

114

115 #将url拼装,并调用parse

116 defurl_link(self):117 returnscrapy.http.Request(118 self.start_urls[0] + self.scity[self.curScityIndex] +(119 '/?query=%s' % self.positions[self.curPositionIndex]) +(120 '&page=%d&ka=page-%d' %(self.curPage, self.curPage)),121 headers=self.headers, #此处可以引用settings中的headers

122 callback=self.parse123 )

zhipin_spider.py

1)、基础参数的设定

先看下Scrapy官方入门教程中关于Spider类中的介绍。Spider官方入门教程

按照教程我们可以定义Spider中的要素信息如下:

简单说明下,name、allowed_domains和start_urls的定义,这个官网教程已经给解释和参考规则,此处不再赘述。

scity是用来设定需检索的城市的列表,‘c101010100/h_101010100’为Boss链接中的地址参数,代表:北京;

positions是用来定义需检索的职位信息列表,后续可以添加多个职位信息,可针对作战方针进行有针对的spider信息;

curPage、curScityIndex和curPositionIndex三个参数,分别用来标记页面、城市和岗位的当前spider情况,后续parse函数中用到。

headers,模拟浏览器访问,该部分信息的获取方式如下:

2)、start_requests(self)的方法介绍

在Scrapy官方入门教程中没有用到这个方法,而是直接在start_urls中存入要爬虫的网页链接,但是如果我们要爬虫的链接很多,而且是有一定规律的,我们就需要重写start_requests这个方法了,首先我们看看这个方法的技能:Scrapy官网入门教程

教程中有三点需要注意:

该方法必须返回一个可迭代对象

当指定了URL时,make_requests_from_url() 将被调用来创建Request对象。 该方法仅仅会被Scrapy调用一次;

修改最初爬取某个网站的Request对象,我们需要重写(override)该方法

由此可见,我们单独使用该方法可能没有办法满足要求,一会打一枪试一下就明白了。

3)、Boss页面的链接结构

url的结构分为两部分:招聘信息列表页和招聘详情页

①招聘信息列表页

首页:https://www.zhipin.com/?ka=header-home-logo;在顶部检索位置,有四个检索条件可以配置,分别为:

地区:当前默认北京;

内容:待输入项,此处为了更大范围能进行职位的爬取,所以此处输入:‘产品’,可以筛选出经理、高级经理、总监的全部职称的招聘信息

职位类型:全部为空

公司行业:全部为空

检索产品招聘信息后,第一页面的链接为:https://www.zhipin.com/job_detail/?query=产品经理&scity=101010100&industry=&position=

“query=”查询的内容,“scity=”城市,后两个检索条件中的后两个,为空可忽略

当点到第二页后,链接变为:https://www.zhipin.com/c101010100/h_101010100/?query=产品经理&page=2&ka=page-2

对比后,与第一页的链接相差较大,尝试使用第二页面的样式模拟第一页面的请求。

请求链接为:https://www.zhipin.com/c101010100/h_101010100/?query=产品经理&page=1&ka=page-1

可以正常打开第一页面,展示信息与之前的官网链接相同。

所以,第一页和第二页,以及后续的页面,均可以使用相同的url模版来模拟查询招聘信息。

另外,如果需要查询其他地区的招聘信息,则需要变更地区码。

如上海地区链接如下:https://www.zhipin.com/c101020100/h_101020100/?query=产品经理&page=1&ka=page-1

综上所述:

请求招聘信息列表的url公共模版如下:

https://www.zhipin.com'+'/'+'地区码'+'/?query'+'职位'+'&page='+'页码'+'&ka=page-'+'页码'

②招聘详情页

招聘的详细信息,主要存在详情页面展示,例如,职位描述、任职要求、公司简介等信息。

以该链接为例:https://www.zhipin.com/job_detail/cea321961162ff3e1Xd539W-GVE~.html?ka=search_list_3

其中:ka=search_list_3为请求的页面来源标示,所以去掉不影响页面的正常访问。

另为,cea321961162ff3e1Xd539W-GVE~,想一个ID的标示,打开Html源码确认一下。

这个ID在Html中定义为data-jid,所以我们可以将详情页的url使用如下规则进行拼装。

请求招聘详情页的url公共模版如下:

'https://www.zhipin.com/job_detail/'+'jid'+'.html'

ps:jid为我们在item.py中定义的要素值,爬取该参数,等后期我们可以有针对性的了解单个有意向的招聘职位时,再进行招聘详情信息的爬取。

4)、重写start_requests(self)方法

根据招聘信息列表的url规则,我们可以对start_requests方法进行重写。

我们直接将参数url的配置方法写在start_requests()中,因其必须返回一个可迭代Itearable对应,所以参考官网教程使用列表(list)的方式'[ ]',将返回内容转换成可迭代对象。

理想总是美好的,但是现实确实残酷的,因为此处有坑,所以我们跳过后续的介绍(parse(self,response)代码见上文的zhipin_spider.py),我们先放一枪整体运行试一下...

运行结果:第一页可以在正常爬取,但是到第二页就报错。

报错报文如下:

所以借鉴参考例子的方式,利用另外一个函数专门来拼装url,将拼装后的url返回至start_requests(),同时使用列表list将返回值包装成可迭代对象,并发起第一次的Request请求。

同时在url_link(self)的callback回调parse,再parse()的callback回调url_link(),实现循环请求我们陆续拼装的url的页面,并爬取页面信息。

调整后的代码如下:

调整完,再执行时,则没有再报该问题。start_requersts(self)重写完成。

另外,此处涉及到一个Requset对象,官网给的用法如下。Scrapy官网入门教程

5)、定义parse(self,response)函数

本次参考例子中的CSS的方法摘取相关的信息,当然很有使用Xpath的方式,这两种方式均可以。

ps:有现成的当然是使用现成的效率高些,下次可以试一下Xpath的方式。Scarpy入门教程推荐Xpath教程

CSS的方式,我对比着Html页面的写了,给出了步骤分解,可供参考。

有几处地方进行了数据的特殊处理,分别介绍下:

①salary的数据处理

页面中的salary的是Xk-Xk的形式,但是基于个人定制化的需求,将薪水拆分出:high、low、avg三个档位,拆分出来就可以针对薪水进行规则设置。

代码中对low和high的薪水分别进行判断,如果low低于20k,或者high低于25k则直接跳出for循环,继续寻找下个招聘信息,如果符合条件,则将招聘信息按照item设定,同时输出至指定路径。

ps:想想自己的可怜的薪水,说多了都是泪啊...

②招聘信息发布时间

代码中也标注了,发布时间主要分为三种形式,我们将时间统一转换成:年-月-日,也是为后续的数据放方便处理。

③轮循规则

我是按照城市优先、职位次之、页面再次之的顺序轮训。

这样设定,是基于这样的考虑:是按照变换的难易程度来设定嵌套层级。

爬取招聘信息,首先会考虑城市的因素,一般情况下,是会不会轻易更换所在城市,除非一些特殊情况;另外,我们也会尝试去爬取互联网相对发达的城市,可以对比下不同城市的就业情况。另外,在相同城市,会爬取每个职位的10页检索信息,因为Boss一次检索只会返回10页的招聘信息。

总之,anyway...你可以尝试相同职位的不同城市的轮循规则,爬取的数据应该是相同的。

④睡眠💤很重要

设定的爬取的睡眠时间,如果没有节制的一顿乱射,Boss只会告诉你,你过频了,然后...给你返回403,封你的IP...

设置长的睡眠时间,这也是我为什么没用代理也可以正常爬取的一个原因吧。

ps:我真正看一个页面的招聘列表信息,最多也就1分钟,所以设置1分钟的休眠时间,对Boss来说还是可以接受的...

(4)、好戏,开整

代码已经备好,随时可以发动总攻。Scrapy官方入门教程

但是在执行语句前还又一些小事情需要搞定

1)、设置 UTF-8 编码

为了让爬取到信息以utf-8的形式保存,需要添加一个设置。

在setting.py中添加(ps:在代码执行中也可以添加)

FEED_EXPORT_ENCODING = 'utf-8'

2)、修改async关键字

在首次执行scrapy crawl zhipin -o item.json时,程序报错,报文如下:

错误原因:SyntaxError: invalid syntax

然后寻找相应的文件夹,路径如下:

Macintosh HD->资源库->Frameworks->Python.framework->Versions->3.7->lib->python3.7->site-packages->twisted->conch->manhole.py

使用安装python3时带的IDLE打开manhole.py文件,按照参考方案中修改async关键字。修改完,command+s保存即可。

万事具备,开整~

在bash中输入:cd www_zhipin_com,然后回车,使命令切到zhipin的项目下

输入:scrapy crawl boss -o item.json,然后回车

然后看着程序开始biubiu的执行,一阵舒爽....

执行的截图如下:

该命令会在程序执行完成后,在项目中生成一个对应的item.json文件。

同时,该文件也可以在python3中进行查看。

三、打扫战场

上文只是按照入门教程中提到的最简单的方式保存数据,只是保存到本地的json文件中,保存的数据格式如下:

{'city': '北京 海淀区 西北旺','companyShortName': '爱米欢','companySize': '100-499人','education': '本科','financeStage': 'A轮','industryField': 'O2O','jid': '79cea95a1818b1591XR409u-GVA~','pid': '20286392','positionName': '产品经理','salary': {'avg': 30000, 'high': 40000, 'low': 20000},'spider_at': '2018-10-06 21:28:09','time': '2018-10-05','workYear': '1-3年'}

另外,入门教程页提到将数据保存只数据库,

因按照好几个教程尝试将数据保存只MongoDB中都未能成功,所以只好作罢。

如有大神不吝赐教,还望留言联系,不胜感激~

四、不足

因这是第一次使用Scrapy框架进行spider,从10.2到10.6日写完本博客,已经用去将近整个十一假期,虽说投入了很长一段时间,但是对自己来说还有一定提升,从完全看不明白经验贴,到可以花3天时间写完整个博客分析,觉得自己还是有所长进。

但仍有一些不足,有待日后提升:

1、研究1天未能使用pipelines.py将数据保存至MongoDB中,略有些遗憾

2、关于招聘详细页的信息,还没有进行抓取

3、settings.py中的通用配置,以及middlewares.py中的代理IP等,未能好好研究使用一下

4、未能将spider到的数据,进行数据可视化的统计、精准的分析,还是导出数据后,一条一条的看(ps:还不如在Boss官网上查看招聘信息的效率高,哭丧脸...)

五、后记

查看招聘网站的种种不错的职位和待遇,真有一种跃跃欲试的冲动,找机会试试吧,希望能有能不错的机会...

同时也希望,这篇关于Scrapy的基础入门博客,能为你在python入门学习的到路上扫除一些障碍,也不枉花时间整理博客。

六、鸣谢

在本次的学习中,参考了一下的文档

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值