網路爬蟲之scrapy爬取某招聘網手機APP釋出資訊
1 引言
2 APP抓包分析
3 編寫爬蟲昂
4 總結
1 引言
過段時間要開始找新工作了,爬取一些崗位資訊來分析一下吧。目前主流的招聘網站包括前程無憂、智聯、BOSS直聘、拉勾等等。有段時間時間沒爬取手機APP了,這次寫一個爬蟲爬取前程無憂手機APP崗位資訊,其他招聘網站後續再更新補上……
所用工具(技術):
IDE:pycharm
Database:MySQL
抓包工具:Fiddler
爬蟲框架:scrapy==1.5.0
資訊抓取:scrapy內建的Selector
2 APP抓包分析
我們先來感受一下前程無憂的APP,當我們在首頁輸入搜尋關鍵詞點選搜尋之後APP就會跳轉到新的頁面,這個頁面我們姑且稱之為一級頁面。一級頁面展示著我們所想找檢視的所有崗位列表。
當我們點選其中一條崗位資訊後,APP又會跳轉到一個新的頁面,我把這個頁面稱之為二級頁面。二級頁面有我們需要的所有崗位資訊,也是我們的主要採集目前頁面。
分析完頁面之後,接下來就可以對前程無憂手機APP的請求(request)和回覆(response)進行分析了。本文所使用的抓包工具為Fiddler,關於如何使用Fiddler,請檢視本文的部落格《網路爬蟲中Fiddler抓取PC端網頁資料包與手機端APP資料包》,在該博文中已對如何配置Fiddler及如何抓取手機APP資料包進行了詳細的介紹。連結如下:
https://www.cnblogs.com/chenhuabin/p/10150210.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。
好了,分析工作完成了,開始動手寫爬蟲了。
3 編寫爬蟲
本文編寫前程無憂手機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類中,不過我習慣於把這些專屬的資料庫資訊寫在配置檔案中。
最後,只差一步,建資料庫、建資料表。部分表結構如下圖所示:
完成上述所有內容之後,就可以執行爬蟲開始採集資料了。採集的資料如下圖所示:
4 總結
整個過程下來,感覺前程無憂網APP爬取要比網頁爬取容易一些(似乎很多網站都這樣)。回顧整個流程,其實程式碼中還有諸多細節尚可改進完善,例如還可以在構造連結時加上求職地點等。本博文重在對整個爬蟲過程的邏輯分析和介紹APP的基本爬取方法,博文中省略了部分程式碼,若需要完整程式碼,請在我的github中獲取,後續將繼續更新其他招聘網站的爬蟲。
github:https://github.com/ChenHuabin321/job_spider