过段时间要开始找新工做了,爬取一些岗位信息来分析一下吧。目前主流的招聘网站包括前程无忧、智联、BOSS直聘、拉勾等等。有段时间时间没爬取手机APP了,此次写一个爬虫爬取前程无忧手机APP岗位信息,其余招聘网站后续再更新补上……php
所用工具(技术):html
IDE:pycharmjava
Database:MySQLpython
抓包工具:Fiddlermysql
爬虫框架:scrapy==1.5.0git
信息抓取:scrapy内置的Selectorgithub
咱们先来感觉一下前程无忧的APP,当咱们在首页输入搜索关键词点击搜索以后APP就会跳转到新的页面,这个页面咱们姑且称之为一级页面。一级页面展现着咱们所想找查看的全部岗位列表。web
当咱们点击其中一条岗位信息后,APP又会跳转到一个新的页面,我把这个页面称之为二级页面。二级页面有咱们须要的全部岗位信息,也是咱们的主要采集目前页面。sql
分析完页面以后,接下来就能够对前程无忧手机APP的请求(request)和回复(response)进行分析了。本文所使用的抓包工具为Fiddler,关于如何使用Fiddler,请查看本文的博客《网络爬虫中Fiddler抓取PC端网页数据包与手机端APP数据包》,在该博文中已对如何配置Fiddler及如何抓取手机APP数据包进行了详细的介绍。连接以下:数据库
http://www.javashuo.com/article/p-ahhfrerz-z.html
本文的目的是抓取前程无忧APP上搜索某个关键词时返回的全部招聘信息,本文以“Python”为例进行说明。APP上操做以下图所示,输入“Python”关键词后,点击搜索,随后Fiddler抓取到4个数据包,以下所示:
事实上,当看到第2和第4个数据包的图标时,咱们就应该会心一笑。这两个图标分别表明传输的是json和xml格式的数据,而不少web接口就是以这两种格式来传输数据的,手机APP也不列外。选中第2个数据包,而后在右侧主窗口中查看,发现第二个数据包并无咱们想要的数据。在看看第4个数据包,选中后在右侧窗体,能够看到如下内容:
右下角的内容不就是在手机上看到的招聘信息吗,仍是以XML的格式来传输的。咱们将这个数据包的连接复制下来:
https://appapi.51job.com/api/job/search_job_list.php?postchannel=0000&&keyword=Python&keywordtype=2&jobarea=000000&searchid=&famoustype=&pageno=1&pagesize=30&accountid=&key=&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0
咱们爬取的时候确定不会只爬取一个页面的信息,咱们在APP上把页面往下滑,看看Fiddler会抓取到什么数据包。看下图:
手机屏幕往下滑动后,Fiddler又抓取到两个数据包,并且第二个数据包选中看再次发现就是APP上新刷新的招聘信息,再把这个数据包的url连接复制下来:
https://appapi.51job.com/api/job/search_job_list.php?postchannel=0000&&keyword=Python&keywordtype=2&jobarea=000000&searchid=&famoustype=&pageno=2&pagesize=30&accountid=&key=&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0
接下来,咱们比对一下先后两个连接,分析其中的异同。能够看出,除了“pageno”这个属性外,其余都同样。没错,就是在上面标红的地方。第一个数据包连接中pageno值为1,第二个pageno值为2,这下翻页的规律就一目了然了。
既然咱们已经找到了APP翻页的请求连接规律,咱们就能够在爬虫中经过循环赋值给pageno,实现模拟翻页的功能。
咱们再尝试一下改变搜索的关键词看看连接有什么变化,以“java”为关键词,抓取到的数据包为:
https://appapi.51job.com/api/job/search_job_list.php?postchannel=0000&&keyword=java&keywordtype=2&jobarea=000000&searchid=&famoustype=&pageno=1&pagesize=30&accountid=&key=&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0
对比后发现,连接中也只有keyword的值不同,并且值就是咱们在本身输入的关键词。因此在爬虫中,咱们彻底能够经过字符串拼接来实现输入关键词模拟,从而采集不一样类型的招聘信息。同理,你能够对求职地点等信息的规律进行寻找,本文不在叙述。
解决翻页功能以后,咱们再去探究一下数据包中XML里面的内容。咱们把上面的第一个连接复制到浏览器上打开,打开后画面以下:
这样看着就舒服多了。经过仔细观察咱们会发现,APP上每一条招聘信息都对应着一个<item>标签,每个<itme>里面都有一个<jobid>标签,里面有一个id标识着一个岗位。例如上面第一条岗位是<jobid>109384390</jobid>,第二条岗位是<jobid>109381483</jobid>,记住这个id,后面会用到。
事实上,接下来,咱们点击第一条招聘信息,进入二级页面。这时候,Fiddler会采集到APP刚发送的数据包,点击其中的xml数据包,发现就是APP上刚刷新的页面信息。咱们将数据包的url连接复制出来:
https://appapi.51job.com/api/job/get_job_info.php?jobid=109384390&accountid=&key=&from=searchjoblist&jobtype=0100&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0
如法炮制点开一级页面中列表的第二条招聘,而后从Fiddler中复制出对应数据包的url连接:
https://appapi.51job.com/api/job/get_job_info.php?jobid=109381483&accountid=&key=&from=searchjoblist&jobtype=0100&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0
对比上面两个连接,发现规律没?没错,就是jobid不一样,其余都同样。这个jobid就是咱们在一级页面的xml中发现的jobid。由此,咱们就能够在一级页面中抓取出jobid来构造出二级页面的url连接,而后采集出咱们所须要的全部信息。整个爬虫逻辑就清晰了:
构造一级页面初始url->采集jobid->构造二级页面url->抓取岗位信息->经过循环模拟翻页获取下一页面的url。
好了,分析工做完成了,开始动手写爬虫了。
本文编写前程无忧手机APP网络爬虫用的是Scrapy框架,下载好scrapy第三方包后,经过命令行建立爬虫项目:
scrapy startproject job_spider .
job_spider就是咱们本次爬虫项目的项目名称,在项目名后面有一个“.”,这个点无关紧要,区别是在当前文件之间建立项目仍是建立一个与项目名同名的文件而后在文件内建立项目。
建立好项目后,继续建立一个爬虫,专用于爬取前程无忧发布的招聘信息。建立爬虫命名以下:
scrapy genspider qcwySpider appapi.51job.com
注意:若是你在建立爬虫项目的时候没有在项目名后面加“.”,请先进入项目文件夹以后再运行命令建立爬虫。
经过pycharm打开刚建立好的爬虫项目,左侧目录树结构以下:
在开始一切爬虫工做以前,先打开settings.py文件,而后取消“ROBOTSTXT_OBEY = False”这一行的注释,并将其值改成False。
# Obey robots.txt rules ROBOTSTXT_OBEY = False
完成上述修改后,打开spiders包下的qcwySpider.py,初始代码以下:
# -*- coding: utf-8 -*- import scrapy class QcwyspiderSpider(scrapy.Spider): name = 'qcwySpider' allowed_domains = ['appapi.51job.com'] start_urls = ['http://appapi.51job.com/'] def parse(self, response): pass
这是scrapy为咱们搭好的框架,咱们只须要在这个基础上去完善咱们的爬虫便可。
首先咱们须要在类中添加一些属性,例如搜索关键词keyword、起始页、想要爬取得最大页数,同时也须要设置headers进行简单的反爬。另外,starturl也须要从新设置为第一页的url。更改后代码以下:
name = 'qcwySpider' keyword = 'python' current_page = 1 max_page = 100 headers = { 'Accept': 'text / html, application / xhtml + xml, application / xml;', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', 'Host': 'appapi.51job.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', } allowed_domains = ['appapi.51job.com'] start_urls = ['https://appapi.51job.com/api/job/search_job_list.php?postchannel=0000&&keyword='+str(keyword)+ '&keywordtype=2&jobarea=000000&searchid=&famoustype=&pageno=1&pagesize=30&accountid=97932608&key=a8c33db43f42530fbda2f2dac7a6f48d5c1c853a&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0']
而后开始编写parse方法爬取一级页面,在一级页面中,咱们主要逻辑是经过循环实现APP中屏幕下滑更新,咱们用上面代码中的current_page来标识当前页页码,每次循环后,current_page加1,而后构造新的url,经过回调parse方法爬取下一页。另外,咱们还须要在parse方法中在一级页面中采集出jobid,并构造出二级页面的,回调实现二级页面信息采集的parse_job方法。parse方法代码以下:
def parse(self, response): """ 经过循环的方式实现一级页面翻页,并采集jobid构造二级页面url :param response: :return: """ selector = Selector(response=response) itmes = selector.xpath('//item') for item in itmes: jobid = item.xpath('./jobid/text()').extract_first() url = 'https://appapi.51job.com/api/job/get_job_info.php?jobid='+jobid+'&accountid=&key=&from=searchjoblist&jobtype=0100&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0' yield scrapy.Request(url=url, headers=self.headers, dont_filter=False, callback=self.parse_job) if self.current_page < self.max_page: self.current_page += 1 neext_page_url = 'https://appapi.51job.com/api/job/search_job_list.php?postchannel=0000&&keyword=Python&keywordtype=2&jobarea=000000&searchid=&famoustype=&pageno=1' \ + str(self.current_page) + '&pagesize=30&accountid=97932608&key=a8c33db43f42530fbda2f2dac7a6f48d5c1c853a&productname=51job&partner=8785419449a858b3314197b60d54d9c6&uuid=6b21f77c7af3aa83a5c636792ba087c2&version=845&guid=bbb37e8f266b9de9e2a9fbe3bb81c3d0' time_delay = random.randint(3,5) time.sleep(time_delay) yield scrapy.Request(url=neext_page_url, headers=self.headers, dont_filter=True, callback=self.parse)
为了方便进行调试,咱们在项目的jobSpider目录下建立一个main.py文件,用于启动爬虫,每次启动爬虫时,运行该文件便可。内容以下:
import sys import os from scrapy.cmdline import execute if __name__ == '__main__': sys.path.append(os.path.dirname(os.path.abspath(__file__))) execute(["scrapy" , "crawl" , "qcwySpider"])
二级页面信息采集功能在parse_job方法中实现,由于全部咱们须要抓取的信息都在xml中,咱们直接用scrapy自带的selector提取出来就能够了,不过在提取以前,咱们须要先定义好Item用来存放咱们采集好的数据。打开items.py文件,编写一个Item类,输入如下代码:
class qcwyJobsItem(scrapy.Item): jobid = scrapy.Field() jobname = scrapy.Field() coid = scrapy.Field() #……item太多,省略部分 isapply = scrapy.Field() url = scrapy.Field() def get_insert_sql(self): """ 执行具体的插入 :param cursor: :param item: :return: """ insert_sql = """ insert into qcwy_job( jobid ,jobname ,coid ,coname ,issuedate ,jobarea ,jobnum ,degree ,jobareacode ,cityname , funtypecode ,funtypename ,workyearcode ,address ,joblon ,joblat ,welfare ,jobtag ,providesalary , language1 ,language2 ,cotype ,cosize ,indtype1 ,indtype2 ,caddr ,jobterm ,jobinfo ,isapply ,url) VALUES ( %s, %s, %s,%s , %s, %s, %s, %s, %s, %s, %s, %s , %s, %s, %s,%s , %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ param = ( self['jobid'], self['jobname'], self['coid'], self['coname'], self['issuedate'], self['jobarea'], self['jobnum'], self['degree'], self['jobareacode'], self['cityname'], self['funtypecode'], self['funtypename'], self['workyearcode'], self['address'], self['joblon'], self['joblat'], self['welfare'], self['jobtag'], self['providesalary'], self['language1'], self['language2'],self['cotype'], self['cosize'], self['indtype1'], self['indtype2'], self['caddr'], self['jobterm'], self['jobinfo'], self['isapply'], self['url'] ) return insert_sql , param
上面每个item都与一个xml标签对应,用于存放一条信息。在qcwyJobsItem类的最后,定义了一个do_insert方法,该方法用于生产将item中全部信息存储数据库的insert语句,之因此在items木块中生成这个insert语句,是由于往后若是有了多个爬虫,有多个item类以后,在pipelines模块中,能够针对不一样的item插入数据库,使本项目具备更强的可扩展性。你也能够将全部与插入数据库有关的代码都写在pipelines。
而后编写parse_job方法:
def parse_job(self, response): time.sleep(random.randint(3,5)) selector = Selector(response=response) item = qcwyJobsItem() item['jobid'] = selector.xpath('/responsemessage/resultbody/jobid/text()').extract_first() item['jobname'] = selector.xpath('/responsemessage/resultbody/jobname/text()').extract_first() item['coid'] = selector.xpath('/responsemessage/resultbody/coid/text()').extract_first() ……
item['jobinfo'] = selector.xpath('/responsemessage/resultbody/jobinfo/text()').extract_first() item['isapply'] = selector.xpath('/responsemessage/resultbody/isapply/text()').extract_first() item['url'] = selector.xpath('/responsemessage/resultbody/share_url/text()').extract_first() yield item
完成上述代码后,信息采集部分就完成了。接下来继续写信息存储功能,这一功能在pipelines.py中完成。
class MysqlTwistedPipline(object): def __init__(self, dbpool): self.dbpool = dbpool @classmethod def from_settings(cls, settings): dbparms = dict( host = settings["MYSQL_HOST"], db = settings["MYSQL_DBNAME"], user = settings["MYSQL_USER"], passwd = settings["MYSQL_PASSWORD"], charset='utf8', cursorclass=MySQLdb.cursors.DictCursor, use_unicode=True, ) dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms) return cls(dbpool) def process_item(self, item, spider): #使用twisted将mysql插入变成异步执行 query = self.dbpool.runInteraction(self.do_insert, item) query.addErrback(self.handle_error, item, spider) #处理异常 def handle_error(self, failure, item, spider): # 处理异步插入的异常 print ('发生异常:{}'.format(failure)) def do_insert(self, cursor, item): # 执行具体的插入 # 根据不一样的item 构建不一样的sql语句并插入到mysql中 insert_sql, params = item.get_insert_sql() cursor.execute(insert_sql, params)
编写完pipelines.py后,打开settings.py文件,将刚写好的MysqlTwistedPipline类配置到项目设置文件中:
ITEM_PIPELINES = { # 'jobSpider.pipelines.JobspiderPipeline': 300, 'jobSpider.pipelines.MysqlTwistedPipline':1 , }
顺便也把数据库配置好:
#MySQL数据库配置 MYSQL_HOST = '192.168.1.100' MYSQL_USER = 'root' MYSQL_PASSWORD = '123456' MYSQL_DBNAME = 'job_spider'
数据库配置你也能够之间嵌入到MysqlTwistedPipline类中,不过我习惯于把这些专属的数据库信息写在配置文件中。
最后,只差一步,建数据库、建数据表。部分表结构以下图所示:
完成上述全部内容以后,就能够运行爬虫开始采集数据了。采集的数据以下图所示:
整个过程下来,感受前程无忧网APP爬取要比网页爬取容易一些(彷佛不少网站都这样)。回顾整个流程,其实代码中还有诸多细节尚可改进完善,例如还能够在构造连接时加上求职地点等。本博文重在对整个爬虫过程的逻辑分析和介绍APP的基本爬取方法,博文中省略了部分代码,若须要完整代码,请在个人github中获取,后续将继续更新其余招聘网站的爬虫。