scrapy由淺入深(三) selenium模擬爬取ajax動態頁面(智聯招聘)
爬取智聯招聘的網址:https://sou.zhaopin.com/?pageSize=60&jl=489&kw=python&kt=3
上一篇部落格爬取了前程無憂的職位招聘資訊,總體來說前程無憂的網站資訊並不難爬取,前程無憂的網站並沒有ajax,直接請求網站就能獲得職位資訊,但是智聯招聘的頁面涉及到ajax,直接get網站的url獲取不到任何有用的資訊,這也是反爬蟲的一種手段,因此我們需要藉助selenium模擬瀏覽器訪問智聯招聘網站。在爬取的過程中有一些非常有意思的問題,下面我會把這些問題以及解決的辦法一一列舉出來。
1.首先我在分析職位詳情(注意不是職位列表頁面)網頁的結構的時候遇到的一個問題,在分析網頁的原始碼構造xpath的時候,發現無論怎麼修改xpath及css選擇器,獲得的資料都是空([ ])。原來使用爬蟲獲取到的網頁原始碼與我們在網頁上看到的原始碼不一樣,使用scrapy請求網站的時候,網頁會將class屬性替換掉,所以直接通過網頁上的原始碼來構造xpath和css選擇器是不可行的。正確的做法是通過scrapy shell +""(請求的網址),開啟瀏覽器檢視正確的class屬性,然後再構造xpath及css選擇器。2.然後就是涉及到ajax的職位列表頁面,細心一點的同學會發現當輸入網址之後,下方的職位列表會載入一段時間才會展示出來,如果我們直接get網頁的原始碼,不會得到任何有用的資訊,使用scrapy shell + ""(職位列表頁面) 可以看到在瀏覽器中不會顯示查詢之後的結果,因此我們需要使用selenium模擬獲取職位列表頁面的所有資訊。3.編寫使用selenium模擬點選下一頁的中介軟體,職位的詳細資訊通過scrapy系統的中介軟體下載,這就會產生資料丟失的問題,因為點選下一頁這個動作執行的非常快,那麼在點選下一頁之後,scrapy會接受該頁面的所有職位連結,一個頁面有60個職位連結,我試驗的時候基本上當selenium中介軟體點選到將近30頁的時候,第一頁的所有職位連結才會爬取完,那麼就有一個問題,現在scrapy已經接受了幾百個職位的url,在請求這些url的時候很有可能會丟掉大部分的資料,造成很多頁面沒有爬取的漏洞,解決的辦法也很簡單,設定網頁跳轉的限制,當一個網頁的資料爬取的差不多的時候,比如爬取了50多條資料的時候就能跳轉到下一頁。
程式碼思路:1.定義一箇中間件處理兩種不同的請求,點選下一頁或者下載詳情頁。2.抽取職位列表的所有url,通過scrapy系統的中介軟體請求職位的詳細資訊頁,防止覆蓋掉senium的職位列表的url。3.判斷該職位列表頁的資料爬取了多少條,如果超過50頁,那麼點選到下一頁。4.將資料儲存到資料庫
一.建立專案
(1)
scrapy startproject zhipinSpider
建立一個名稱為zhipinSpider的專案
(2)手動建立job_detail.py檔案,實現爬蟲的主要邏輯
二.專案配置
(1)編寫items檔案
import scrapy class ZhipinspiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() title = scrapy.Field() salary = scrapy.Field() detail = scrapy.Field()
定義三個欄位,分別用來儲存職位的標題,薪資,職位的要求
(2)編寫pipelines檔案
import sqlite3 db = sqlite3.connect("./../../zhi.db") cursor = db.cursor() class ZhipinspiderPipeline(object): def process_item(self, item, spider): cursor.execute("insert into jobs(title, salary, detail) values (?,?,?)",[item["title"],item["salary"],item["detail"]]) db.commit()
將資料儲存到sqlite資料庫中,也可以儲存到MySQL資料庫中,寫法類似
(3)編寫selenium模擬中介軟體
class SeleniumMiddleware(object):
def __init__(self):
self.options = Options()
# self.options.add_argument('-headless')
self.browser = webdriver.Firefox(executable_path="D:\geckodriver\geckodriver.exe",firefox_options=self.options)
# self.option = Options()
# self.option.add_argument('-headless')
# self.browser1 = webdriver.Firefox(executable_path="D:\geckodriver\geckodriver.exe",firefox_options=self.options)
def process_request(self, request, spider):
"""
通過meta攜帶的資料判斷應該跳轉到下一頁還是下載詳情頁
這裡沒有處理詳情頁的函式,所以當下載詳情頁的時候會呼叫scrapy系統的中介軟體,也就不會覆蓋跳
轉頁面的url
:param request:
:param spider:
:return:
"""
if int(request.meta["page"]) == 0:
self.browser.get(request.url)
self.browser.execute_script("window.scrollTo(0,document.body.scrollHeight)")
time.sleep(1)
pages = self.browser.find_element_by_css_selector('button.btn:nth-child(8)')
pages.click()
time.sleep(5)
return HtmlResponse(url=self.browser.current_url,body=self.browser.page_source,encoding="utf-8",request=request)
if int(request.meta["page"]) == 2:
# 不需要get網頁的url否則瀏覽器會在第一第二頁之間來回跳轉
# self.browser.get(self.browser.current_url)
self.browser.execute_script("window.scrollTo(0,document.body.scrollHeight)")
time.sleep(1)
pages = self.browser.find_element_by_css_selector('button.btn:nth-child(8)')
pages.click()
time.sleep(5)
return HtmlResponse(url=self.browser.current_url,body=self.browser.page_source,encoding="utf-8",request=request)
(4)配置setting檔案
啟用selenium中介軟體
DOWNLOADER_MIDDLEWARES = {
'zhipinSpider.middlewares.SeleniumMiddleware': 543,
}
啟用管道檔案
ITEM_PIPELINES = {
'zhipinSpider.pipelines.ZhipinspiderPipeline': 300,
}
下載延遲
DOWNLOAD_DELAY = 3
RANDOMIZE_DOWNLOAD_DELAY = True
(5)編寫spider檔案
爬蟲的主要邏輯
import scrapy
from scrapy import Request
import lxml.html
from zhipinSpider.items import ZhipinspiderItem
import sqlite3
db = sqlite3.connect("./../../zhi.db")
cursor = db.cursor()
i = 0
def select_from_sql():
"""
:return: 當前資料庫中資料的總數
"""
count = cursor.execute("select * from jobs")
return len(count.fetchall())
class JobDetailSpider(scrapy.Spider):
name = "jobSpider"
def start_requests(self):
url_str = "https://sou.zhaopin.com/?pageSize=60&jl=489&kw=python&kt=3"
yield Request(url=url_str,callback=self.parse,meta={"page":"0"})
def parse(self, response):
"""
抽取出包含職位url的html,並通過函式分離url
:param response:
:return:
"""
html_str = response.xpath('//div[@class="listItemBox clearfix"]').extract()
for html in html_str:
job_url = self.parse_one_job(html)
yield Request(url=job_url,callback=self.parse_job_text,meta={"page":"1"})
def parse_one_job(self,html):
"""
分理處html中的職位url
:param html:
:return:
"""
xtree = lxml.html.fromstring(html)
job_url = xtree.xpath('//div[@class="jobName"]/a/@href')[0]
return job_url
def parse_job_text(self,response):
global i
count = select_from_sql()
# 使用selenium模擬的結果
# title = response.xpath('//ul/li/h1/text()').extract()
# detail = response.xpath('//div[@class="pos-ul"]/p/text()').extract()
# salary = response.xpath('//div[@class="l info-money"]/strong/text()').extract()
# 使用view(response)獲得的結果
title = response.xpath('//div[@class="inner-left fl"]/h1/text()').extract_first()
salary = response.xpath('//ul[@class="terminal-ul clearfix"]/li/strong/text()').extract_first()
detail = response.xpath('//div[@class="tab-inner-cont"]/*/text()').extract()
detail_span = response.xpath('//div[@class="tab-inner-cont"]/p/span/text()').extract()
if detail_span is not None:
detail = detail_span + detail
contents = ""
for content in detail:
contents += content
contents = ' '.join(contents.split())
item = ZhipinspiderItem()
item["title"] = title
item["salary"] = salary
item["detail"] = contents
# 判斷有沒有爬取完當前頁面的職位資訊(是否達到分頁的條件)
if count - i > 58:
i = count
yield Request(url="http://www.baidu.com", callback=self.parse, meta={"page": "2"}, dont_filter=True)
yield item
這裡分析職位要求的xpath之所以有兩個是因為智聯招聘所有的職位要求資訊,它的html標籤會有個別不一樣的情況,這裡我直接定義了兩個xpath處理兩種情況,將兩種情況得到的資訊合併,這樣就會減少爬取不到職位要求資訊的情況。另外最後這個請求url之所以使用www.baidu.com是因為這裡我們不需要使用其他的url,只需要點選下一頁就行了,這裡的url只作為佔位使用。
總結:爬取智聯招聘難點在於如何爬取ajax資料,如何使用selenium模擬區分職位列表頁跟職位詳情頁,以及為了防止資料丟失而查詢資料庫資料的條數,判斷是否達到了分頁的條件。