BOSS直聘职位信息爬取

从11月开始,断断续续几个月,BOSS直聘的行业信息爬取数据量已经初具规模,目前程序稳步运行,日增量在六七万左右,最终将达到两三百万,数据爬取的工作也算是告一段落,下面对项目进行一个简单的小结。

作为一个即将进入职场并认准大数据方向的研究生而言,没有互联网公司的实习经历,只能通过项目经验来为自己加分。初期并没有想过能有如此大的规模,只是想通过爬取一些东西来巩固python的学习,积累一些爬虫的经验,随着项目一步步迭代,数据的爬取工作进行的十分顺利,期间学习了大量的机器学习算法,正好也能运用到爬取的数据上,对行业信息进行分析,也能帮助自己提前了解行业形式,一举多得。

源码链接:https://github.com/lxm909055383/bosszhipin

一、爬取内容的选择

我们的目的是爬取该网站所有的招聘信息,由于招聘信息的网址组合方式为(主网址+城市url+行业url)这样一种形式,所以事先要将城市和对应的url行业和对应的url信息分别爬取出来,最后采用排列组合的方式将网址挨个进行爬取。

所有城市与对应区的信息

目前该网站支持的城市只有以下十几个,其对应的区也一起爬取备用,在后面分析阶段可能会细化到某些区。

所有行业分类

该网站的行业信息涉及面非常广,我们需要爬取某个行业及对应的一级分类、二级分类。

招聘信息

这是BOSS直聘网站一条行业信息记录,包括岗位名称、薪资 、公司、城市、经验、学历、所属行业、融资情况、公司规模、关键字、联系人名字、联系人职务、联系人图片、发布时间这14个属性值。

二、爬虫框架的选取

scrapy作为一个爬虫主流框架,上手非常容易,本人通过爬取豆瓣电影TOP250这个小例子对scrapy框架的使用有了一定的认识与理解,scrapy的安装与项目创建可以参考scrapy入门教程。下面是BOSS直聘的一个scrapy项目,给出了相关文件的简单介绍:

spiders:放置spider代码的目录,里面可以有多个spideritems.py:是种简单的容器,保存了爬取到的数据,提供了类似于字典的API以及用于声明可用字段的简单语法middlewares.py:主要是管理User-Agent和IP这一块内容pipelines.py:管道文件,爬取到的数据会传输到这里settings.py:设置文件

与最初创建的项目相比多了start和model两个文件夹,其中的start文件夹通过cmdline设置了每个spider的启动代码,这样就不需要每次都用scrapy crawl命令来启动,更加方便;model文件夹中的mysql_db文件成功建立scrapy与数据库的连接,部分代码如下

三、爬虫主体

通过(一)中我们知道该项目涉及到三个spider文件的编写,在下文我们只选择其中的招聘信息来进行介绍,另外两个的方法类似。

(1)爬取内容的item定义

确定好在网页中爬取的内容之后,需要在item.py文件中进行定义:

(2)spider文件编写

spider文件的代码编写是最最重要和核心的部分,在进行数据提取前需要对网页的构成、html以及xpath知识有一定的了解,相关内容可以在w3school网站上进行学习,推荐一个谷歌浏览器插件,XPath Helper,可以在网页面及时发现xpath是否书写正确,是一大神器。考虑到网页的相似性,初期只爬取一页的内容,然后在此基础上进行翻页爬取,具体方法后文会讲到,spider文件部分代码如下:

(3)异常情况处理

期间遇到很多异常情况,需要单独处理,也是因为这些接触到了python的捕获异常以及异常处理机制,异常信息的获取对于程序的调试非常重要,尤其是在代码量比较大的情况下,可以有助于快速定位有错误程序语句的位置。在此给大家提个醒,有些网站的反爬手段做的十分巧妙,往往就在一个细小的标签上,所以对html及xpath一定要有很清晰的认识与辨别力(Amazon的反爬就非常出名,有兴趣的可以了解下,毕竟知己知彼才能百战百胜嘛),为了大家少踩坑,部分罗列如下:

存在某些招聘信息的融资情况(Financing)缺失,而CompanySize、Financing、 Industry这三个元素的信息在一个标签里,故导致爬取信息出现紊乱

解决办法:将Financing Industry这两个单独考虑,采用if判断

在翻页的时候由于第二页中个别招聘信息的PubTime标签丢失,移植到其他标签里,导致程序终止,无法爬去所有的

解决办法:利用python的异常处理机制将PubTime异常的设置为其他可标识的东西,之后统一处理数据

KeyWord包含0到3个信息不等,要将这几个用一个数组表示,如果空的话就输出None

解决办法:当KeyWord为空时,对应span里保存的是PubTime;当KeyWord不为空时,对应span里保存的是KeyWord值,此时PubTime保存在.//div[@class="job-time"]/span/text(),顺带解决了第二个问题

在用第三个方法处理KeyWord时,关键字缺失的情况没法按预期显示“关键字未知”,原因是if语句没有进入,“发布”是字符型,后面的文本信息是列表型,不能直接用in

解决办法:将列表型转化为字符型 ,.join(list)

对于PubTime这一栏,有的是“发布于某月某日”,有的是“发布于今天”,有的是“发布于昨天”

解决办法:为了统一,利用时间日期函数将形式变为“2017-9-12”这种固定的格式,加入年份是考虑到跨年爬取带来的误区

(4)递归爬取

前面提到的翻页爬取其实是一种递归爬取,涉及到scrapy spider的parse方法,理解起来比较难,可以返回两种值:Item或者Request,通过Request可以实现递归抓取,主要有以下几种情况:

如果要抓取的数据在当前页,可以直接解析返回item;def parse(self, response): item = MyItem() item[a] = aaa item[b] = bbb yield item 如果要抓取的数据在当前页指向的页面,则返回Request并指定parse_item作为callback;def parse(self, response): item = MyItem() url = 当前页指向的页面 yield Request(url, callback=self.parse_item) def parse_item(self, response): item[c] = ccc item[d] = ddd yield item如果要抓取的数据当前页有一部分,指向的页面有一部分(比如博客或论坛,当前页有标题、摘要和url,详情页面有完整内容)这种情况需要用Request的meta参数把当前页面解析到的数据传到parse_item,后者继续解析item剩下的数据。def parse(self, response): item = MyItem() item[a] = aaa item[b] = bbb url = 当前页指向的页面 yield Request(url, meta={item: item} , callback=self.parse_item) def parse_item(self, response): item = response.meta[item] item[c] = ccc item[d] = ddd yield item要抓完当前页再抓其它页面(比如下一页),本项目就需要使用这种方法来进行递归爬取,可以返回Request,callback为parse。next_url = response.xpath(//*[@id="main"]/div[3]/div[2]/div[2]/a[@ka="page-next"]/@href).extract() if next_url: next_url = + next_url[0] yield Request(next_url)

参考链接:scrapy递归抓取网页数据

不断的抓取下一个链接如何实现,items如何保存?

四、数据入库

在spiders中爬取所需字段后,如何保存数据成为亟待解决的问题?将数据保存在文件中是一个方法,但是数据量庞大时该方法并不是最佳选择,通常会选择保存在数据库中,接下来将介绍一个python的ORM框架——Peewee,这属于一款轻量级的框架,能有效建立scrapy与数据库的连接,详细内容可查看peewee官方文档。

在对数据进行存储之前,要清楚整个过程数据的流向,大致如下:

在item.py中定义要爬取的字段在spiders中爬取所需字段启用一个item_pipeline组件(在settings.py的ITEM_PIPELINES配置中),数据被传输到pipeline(因为涉及到多个spider,使用该方法需要来回切换,所以参考了如下的方法为每一个spider设置自己的pipeline通过peewee建立与数据库的连接(peewee连接数据库将pipeline里的数据传输到数据库

本文使用的是MySql数据库,在入库之前首先要设计好数据表,这里需要将城市信息和行业信息分别存储在两个表中,在进行招聘信息网址组合时再从数据库里分别提取,期间会用到在数据库里查找、修改等操作的知识,可以参考数据库增删查改操作。

(1)表结构设计

城市信息表

在设计城市信息表时,因为涉及到城市和对应区两种类型数据,为了将两者放在一个表中,特添加father_id和type两个字段来将两者进行区分

行业信息表招聘信息表

(2)去重

后期程序会一直运行,将存在大量重复的记录,为了保证数据库中数据的唯一性和有效性,在入库的时候加入去重这个步骤:

城市和区通过name和url可以唯一标识行业通过name和url可唯一标识职位信息因为涉及到的属性较多,在去重时稍微复杂一点,通过post_name,salary,company,city,experience,education,contact_name,publish_time这些属性生成一个唯一的MD5值,即可将一条记录唯一标识,其中属性的选择根据个人需求进调整。

数据库部分数据情况如下,招聘信息表太长分割成图三和图四:

图一 城市信息表图二 行业信息表图三 招聘信息表图四 招聘信息表

五、scrapy防止IP被封

在进行大量爬取工作时,网站会识别出是爬虫程序,然后封锁IP,这种情况本人在初期遇到过很多次,当时采取了下面两种scrapy自带措施:

设置下载等待时间/频率

setting.py文件中的DOWNLOAD_DELAY设置在3以上

禁止cookies

setting.py文件中的COOKIES_ENABLES = FALSE

结果表明效果并不明显,当数据量稍微大点时,还是会被封IP(少量数据可以使用上述方法),于是继续探索了更一般的解决方法,如下:

使用User-Agent池

大多数情况下,网站都会根据我们的请求头信息来区分你是不是一个爬虫程序,如果一旦识别出这是一个爬虫程序,很容易就会拒绝我们的请求,因此我们需要给我们的爬虫手动添加请求头信息,来模拟浏览器的行为,但是当我们需要大量的爬取某一个网站的时候,一直使用同一个User-Agent显然也是不够的,因此需要scrapy设置随机User-Agent

使用IP池

在scrapy中使用IP池的方法可以参考IP池设置,这需要我们去寻找大量可用的IP, 网上有一些免费的可以使用,比如码农很忙代理IP搜索,米扑代理等等。

需要注意的是,不管使用User-Agent池还是IP池都是通过下载器中间件(Downloader Middleware)来实现的,因此需要在setting.py文件进行设置,如下:

因为使用IP池的方法有非常好的效果,故本项目没有再继续使用随机User-Agent,若有需求大家可以自行选择。

六、断点爬取

以上五点完成后,程序已经可以正常运行,但是由于数据量非常庞大,粗略计算了一下笔记本的爬取周期大概为二十多天(考虑过租用阿里服务器,但是目前对于数据的需求还不太大,不至于到那一步),而且由于网站时刻会有数据更新,程序需要多周期迭代。那么问题来了,电脑不可能一天24小时开机,一个周期二十多天不间断,如果程序突然中断了,就需要重头开始,从第一个网址再进行爬取,而排在前面的网址都是之前已经爬取过的,很费时不说,可能后面的网址一次也爬取不了。遇到这个问题,很自然的想法就是程序能从上次中断的地方继续爬,这就涉及到断点爬取的知识,核心思想是记录好上次已经爬的状态,具体实现方法多种多样。在本项目中,采用的方法是将每次爬取网址的城市id和行业id分别保存在两个文件中,随着程序不但的更新,下次再启动程序时,直接读取文件中的id,将之后的进行组合即可。

——————————————————————————————————————

以上就是本项目数据爬取部分的流程,用了一天的时间终于整理完了,涉及到的知识点非常多,都是本人在项目中亲自踩过的坑,非常有借鉴意义,有相关的问题可以随时私信与我交流探讨!

数据后续分析部分:

BOSS直聘职位信息数据分析报告

未经许可,禁止转载!!!