1 需求分析
想要一個能爬取拉鈎網職位詳情頁的爬蟲,來獲取詳情頁內的公司名稱、職位名稱、薪資待遇、學歷要求、崗位需求等信息。該爬蟲能夠通過配置搜索職位關鍵字和搜索城市來爬取不同城市的不同職位詳情信息,並將爬取下來的信息存入數據庫。
2 目標站點分析
目標站點:https://www.lagou.com/。可以看見在左上角可以切換搜索城市,在正中央可以輸入搜索職位關鍵字,選擇好城市和輸入搜索職位關鍵字后點擊搜索按鈕,就可以跳轉到相應職位的列表頁,每個列表頁有15個詳情項(最后一頁可能不足15個)。點擊每個詳情項,就可以跳轉到對應公司的詳情頁,而要爬取的數據就在詳情頁中。
Tips:有可能同一個公司會由不同的HR發出相同的招聘信息,例如搜索Python爬蟲,會發現公司Eigen發布了兩條招聘信息,分別由雲衫和Casey分別發布。
3 流程分析
為了復習Scrapy和Selenium,沒有使用requests庫來實現這個爬蟲,具體流程:
1.切換城市和輸入搜索關鍵字:用Selenium驅動瀏覽器模擬點擊左上角的切換城市,然后輸入搜索關鍵字,最后點擊搜索按鈕,跳轉到相應職位的列表頁。
2.解析列表頁並模擬翻頁:解析首個列表頁,拿到整個職位列表頁的頁碼數,用Selenium模擬翻頁,在翻頁的同時拿到各個詳情頁的url。
3.解析詳情頁並提取數據:解析每個公司的詳情頁,用Scrapy的ItemLoader來獲取各個字段信息,並進行相應的數據處理工作。
4.存儲數據到MongoDB:將獲取到的每個公司詳情信息存儲到MongoDB數據庫。
4 代碼實現
用Scrapy框架來組織整個代碼。整個程序流程圖:

大致的流程圖是這樣的,拉鈎網的數據爬取是不需要Cookie的,加了Cookie反而會被識別出來是爬蟲。流程圖中關於模擬登陸、保存cookie到本地和從本地加載cookie只是加強https://www.cnblogs.com/strivepy/p/9233389.html的練習。
4.1 在Scrapy中一個Request是如何在DownloadMiddleware傳遞的
Scrapy官方文檔https://doc.scrapy.org/en/master/topics/settings.html#std:setting-DOWNLOADER_MIDDLEWARES_BASE對於DOWNLOADER_MIDDLEWARES_BASE的說明:
1 { 2 'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100, 3 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300, 4 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350, 5 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400, 6 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500, 7 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550, 8 'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560, 9 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580, 10 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590, 11 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600, 12 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700, 13 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750, 14 'scrapy.downloadermiddlewares.stats.DownloaderStats': 850, 15 'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900, 16 }
序號越小說明這個中間件離引擎越近,序號越大說明這個中間件離下載器越近。
每個Middleware都有process_request(request, spider)、prcess_response(request, response, spider)、process_exception(request, exception, spider)三個函數(哪怕沒有實現)。
當一個request由調度器調度經過各個下載器中間件時(沒有異常的情況下),request依次穿過序號從小到大的每個Middleware的process_request()函數到達下載器Downloader,下載器完成下載后即獲取到response后,response依次穿過序號從大到小的每個Middleware的process_response()函數到達引擎。
而Scrapy項目setting.py中DOWNLOADER_MIDDLEWARES自定義的各個Middleware會在項目運行時,框架自動和DOWNLOADER_MIDDLEWARES_BASE進行合並而得到一個完成的下載中間件列表。
4.2 實現城市切換和輸入搜索關鍵字
基本思路:由start_request()函數發出帶有flag的的初始請求,該flag只為在middleware中篩選出最初始的request,然后在process_request()函數中實現模擬登陸、加載本地cookie、切換城市、輸入搜索關鍵字然后點擊搜索按鈕跳轉到職位列表頁。
4.2.1 start_request()發起的帶有flag的請求
在meta中設置屬性index_flag來在middleware中過濾出初始請求,brower來接收Chrome實例,wait來接收WebDriverWait實例,pagenumber來接收整個列表頁的頁數。
1 # Location: LagouCrawler.spider.lagoucrawler.LagouCrawlerSpder 2 3 def start_requests(self): 4 base_url = 'https://www.lagou.com' 5 index_flag = {'index_flag': 'fetch index page', 'brower': None, 'wait': None, 'pagenumber': None} 6 yield scrapy.Request(url=base_url, callback=self.parse_index, meta=index_flag, dont_filter=True)
4.2.2 process_request()函數過濾出初始請求
在process_request()函數中過濾出初始請求后,判斷是否已經登陸,若已經登陸,則直判斷是否需要切換城市,然后輸入搜索關鍵字最后點擊搜索跳轉到列表頁,否則判斷本地是否存在cookie文件,如果存在則直接加載本地cookie文件,否則進行模擬登陸並將cookie保存為本地文件,然后再判斷是否需要切換城市,並輸入搜索關鍵字然后點擊搜索跳轉到列表頁。
1 # Location: LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware 2 def process_request(self, request, spider): 3 """ 4 middleware的核心函數,每個request都會經過該函數。此函數過濾出初始request和詳情頁request, 5 對於初始request進行驗證登陸、cookies等一系列操作,然后將最后獲取到的索引頁response返回,對 6 於詳情頁的request則,不做任何處理。 7 :param request: 8 :param spider: 9 :return: 10 """ 11 # 過濾出初始的登陸、切換索引頁的request 12 if 'index_flag' in request.meta.keys(): 13 # 判斷是否為登陸狀態,若未登陸則判斷是否有cookies文件存在 14 if not self.is_logined(request, spider): 15 path = os.getcwd() + '/cookies/lagou.txt' 16 # 若cookies文件存在,則加載cookie文件,否則進行登陸操作 17 if os.path.exists(path): 18 self.load_cookies(path) 19 else: 20 # 登陸lagou網 21 self.login_lagou(spider) 22 # 登陸成功后的索引頁的響應體,若不登錄,請求響應提詳情頁面的url時,會重定向到登陸頁面 23 response = self.fetch_index_page(request, spider) 24 return response
用Chrome驅動瀏覽器時,總會先彈出切換城市的窗口,所以需要先將其關掉,再通過獲取右上角的登陸狀態元素的為本內容,來判斷是否已經為登陸狀態。
1 # Location: LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware 2 def is_logined(self, request, spider): 3 """ 4 初始請求時,總會彈出切換城市的窗口,所以先關掉它,然后通過判斷右上角是否顯示 5 用戶名判斷是否為登陸狀態,並初始化整個程序的brower實例 6 :param request: 初始請求request,其meta包含index_page屬性 7 :param spider: 8 :return: 已經登陸返回True, 否則返回False 9 """ 10 self.brower.get(request.url) 11 try: 12 # 關掉城市選擇窗口 13 box_close = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="cboxClose"]'))) 14 box_close.click() 15 # 獲取右上角的登錄狀態 16 login_status = self.wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="lg_tbar"]/div/ul/li[1]/a'))) 17 # 若右上角顯示為登陸,則說明用戶還沒有登陸 18 if login_status.text == '登錄': 19 return False 20 else: 21 return True 22 except TimeoutException as e: 23 # 二次請求,不會出現地址框,需要重新設計 24 spider.logger.info('Locate Username Element Failed:%s' % e.msg) 25 return False
加載本地cookie文件到Chrome實例中。
1 # Location: LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware 2 def load_cookies(self, path): 3 """ 4 加載本地cookies文件,實現免登錄訪問 5 :param path: 本地cookies文件路徑 6 :return: 7 """ 8 with open(path, 'r') as f: 9 cookies = json.loads(f.read()) 10 for cookie in cookies: 11 cookies_dict = {'name': cookie['name'], 'value': cookie['value']} 12 self.brower.add_cookie(cookies_dict)
若沒有本地cookie文件,則需要登錄拉鈎網,並將cookie保存為本地文件,待以后使用:
1 # Location: LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware 2 def login_lagou(self, spider): 3 """ 4 用selenium模擬登陸流程,並將登陸成功后的cookies保存為本地文件。 5 :param spider: 6 :return: 7 """ 8 try: 9 # 設置等待時間,否則會出現登陸元素查找不到的異常 10 time.sleep(2) 11 # 點擊進入登錄頁面 12 login_status = self.wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="loginToolBar"]//a[@class="button bar_login passport_login_pop"]'))) 13 login_status.click() 14 # 輸入用戶名 15 username = self.wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@data-propertyname="username"]/input'))) 16 username.send_keys(self.username) 17 # 輸入用戶密碼 18 password = self.wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@data-propertyname="password"]/input'))) 19 password.send_keys(self.password) 20 # 點擊登陸按鈕 21 submit_button = self.wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@data-propertyname="submit"]/input'))) 22 submit_button.click() 23 # time.sleep(1) 24 # 獲取登錄成功后的cookies 25 cookies = self.brower.get_cookies() 26 # 保存登陸后的cookies 27 self.save_cookies(cookies) 28 except TimeoutException as e: 29 spider.logger.info('Locate Login Element Failed: %s' % e.msg)
在完成登陸后使用save_cookies()函數將cookie保存為本地文件:
1 # Location: LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware 2 @staticmethod 3 def save_cookies(cookies): 4 """ 5 登陸成功后,將cookie保存為本地文件,供下次程序運行或者以后使用 6 :param cookies: 7 :return: 8 """ 9 path = os.getcwd() + '/cookies/' 10 if not os.path.exists(path): 11 os.mkdir(path) 12 with open(path + 'lagou.txt', 'w') as f: 13 f.write(json.dumps(cookies))
最后在完成所有關於cookie的操作后,進行城市切換和輸入搜索關鍵字並點擊搜索按鈕,使用WebDriverWait后,有些元素獲取還是會報元素不可點擊的異常,所以在前面加上time.sleep(1):
1 # Location: LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware 2 def fetch_index_page(self, request, spider): 3 """ 4 該函數使用selenium完成城市切換,搜索關鍵字輸入並點擊搜索按鈕操作。如果點擊搜索按鈕后, 5 頁面沒有成功跳轉,則會因為31行的代碼,拋出NoSuchElementException,而在load_cookies() 6 函數報一個NoneType沒有get_cookies()的錯誤。原因是response是空的。 7 :param request: 8 :param spider: 9 :return: 10 """ 11 try: 12 # 判斷是否需要切換城市 13 city_location = self.wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="lg_tnav"]/div/div/div/strong'))) 14 if city_location.text != self.city: 15 time.sleep(1) 16 city_change = self.wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="changeCity_btn"]'))) 17 city_change.click() 18 # 根據搜索城市定位到相應元素並點擊切換 19 # time.sleep(1) 20 city_choice = self.wait.until(EC.presence_of_element_located((By.LINK_TEXT, self.city))) 21 city_choice.click() 22 time.sleep(1) 23 # 定位關鍵字輸入框並輸入關鍵字 24 keywords_input = self.wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="search_input"]'))) 25 keywords_input.send_keys(self.job_keywords) 26 # time.sleep(1) 27 # 定位搜索按鈕並點擊,有時候點擊后頁面不會發生跳轉,原因是被重定向了。 28 keywords_submit = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="search_button"]'))) 29 keywords_submit.click() 30 # 跳轉到列表頁等待待抓取的內容元素加載完成,如果被重定向,則跳轉不到該頁面,會報NoSuchElementException 31 self.wait.until(EC.visibility_of_all_elements_located((By.XPATH, '//*[@id="s_position_list"]'))) 32 pagenumber = self.wait.until(EC.presence_of_element_located(( 33 By.XPATH, '//*[@id="s_position_list"]/div[@class="item_con_pager"]/div/span[@class="pager_next "]/preceding-sibling::span[1]' 34 ))) 35 # 獲取一共有多少頁,供通過response傳遞到parse_detail函數,進行后續的翻頁解析使用 36 request.meta['pagenumber'] = pagenumber.text 37 # 將brower和wait通過response傳遞到parse_detail函數,進行后續的翻頁解析使用 38 request.meta['brower'] = self.brower 39 request.meta['wait'] = self.wait 40 body = self.brower.page_source 41 # 返回初始搜索頁面,在parse_detail函數中進行相關信息的解析 42 response = HtmlResponse( 43 url=self.brower.current_url, 44 body=body, 45 encoding='utf-8', 46 request=request 47 ) 48 return response 49 except TimeoutException: 50 spider.logger.info('Locate Index Element Failed And Use Proxy Request Again') 51 # except NoSuchElementException: 52 # 如果捕捉到該異常,說明頁面被重定向了,沒有正常跳轉,重新請求輸入關鍵字頁面 53 return request
在跳轉到列表頁后,將brower(Chrome實例), wait(WebDriverWait實例)和pagenumber放入request.meta中。然后返回response(HtmlResponse實例),該response由start_request()函數指定的回調函數parse_index()進行解析,在該函數中通過response.meta取出剛剛存放進request.meta中的brower,wait和pagenumber,來進行接下來的翻頁操作。
4.3 實現解析列表頁和模擬翻頁
當Selenium驅動Chrome完成切換城市、輸入搜索關鍵字、點擊搜索按鈕並成功跳轉到列表頁后,會將第一頁列表頁的response傳遞到parse_index()回調函數進行解析:
1 # Location: LagouCrawler.spider.lagoucrawler.LagouCrawlerSpder 2 def parse_index(self, response): 3 """ 4 解析第一頁列表頁,拿到各個招聘詳情頁url,並發起請求;然后進行翻頁做操,拿到每頁 5 列表頁各個詳情頁的url,並發起請求。注意:以杭州Python爬蟲職位為例詳情頁請求發起 6 大概55個后(抓取的時候,一共有4頁,每頁15個招聘,供60個招聘詳情),最后5個總是 7 被重定向到最初始輸入搜索關鍵字的頁面,即使設置了DOWNLOAD_DELAY也是沒用。應該是 8 被服務器識別出了是機器人了,初步思路是在middlewares的process_response()函數中, 9 通過判斷response的status_code,對重定向的request加上代理后,再次發起request, 10 但是這個思路沒能實現,需要更深層次的理解框架,所以使用了阿布雲代理的動態代理, 11 讓每個request都通過代理服務器發出請求。 12 :param response: 經middleware篩選並處理后的第一頁詳情頁response 13 :return: 14 """ 15 self.pagenumber = response.meta['pagenumber'] 16 # 初始化spider中的brower和wait 17 self.brower = response.meta['brower'] 18 self.wait = response.meta['wait'] 19 # 解析索引頁各項招聘詳情頁url 20 for url in self.parse_url(response): 21 yield scrapy.Request(url=url, callback=self.parse_detail, dont_filter=True) 22 # 翻頁並解析 23 for pagenumber in range(2, int(self.pagenumber) + 1): 24 response = self.next_page() 25 for url in self.parse_url(response): 26 yield scrapy.Request(url=url, callback=self.parse_detail, dont_filter=True)
在parse_index()函數中有兩個函數,一個是解析單個列表頁中各個詳情項url的parse_url()函數,和模擬翻頁的next_page()函數。在解析出詳情項的url后,發起請求。Scrapy會對request的url去重(RFPDupeFilter),將dont_filter設置為True,則告訴Scrapy這個url不參與去重。
解析詳情項url的parse_url()函數,返回詳情項url列表:
1 # Location: LagouCrawler.spider.lagoucrawler.LagouCrawlerSpder 2 @staticmethod 3 def parse_url(response): 4 """ 5 解析出每頁列表頁各項招聘信息的url 6 :param response: 列表頁response 7 :return: 該列表頁各項招聘詳情頁的url列表 8 """ 9 url_selector = response.xpath('//*[@id="s_position_list"]/ul/li') 10 url_list = [] 11 for selector in url_selector: 12 url = selector.xpath('.//div[@class="p_top"]/a/@href').extract_first() 13 url_list.append(url) 14 return url_list
模擬翻頁的next_page()函數,返回下一頁列表頁的response。將翻頁速速控制為2秒,在尋找下一頁這個元素時,需要注意的地方如代碼所示:
1 # Location: LagouCrawler.spider.lagoucrawler.LagouCrawlerSpder 2 def next_page(self): 3 """ 4 用selenium模擬翻頁動作。用xpath獲取next_page_button控件時,花了很久時間,原因是 5 span標簽的class="pager_next "后引號前面有一個空格!!! 6 :return: 7 """ 8 try: 9 # 用xpath找這個下一頁按鈕居然花了半天的時間居然是這個程序員大哥在span標簽的class="pager_next "加了個空格,空格!!! 10 next_page_button = self.wait.until(EC.presence_of_element_located(( 11 By.XPATH, '//*[@id="s_position_list"]/div[@class="item_con_pager"]/div/span[@class="pager_next "]' 12 ))) 13 next_page_button.click() 14 self.wait.until(EC.visibility_of_all_elements_located((By.XPATH, '//*[@id="s_position_list"]'))) 15 # 控制翻頁速度 16 time.sleep(2) 17 body = self.brower.page_source 18 response = HtmlResponse(url=self.brower.current_url, body=body, encoding='utf-8') 19 return response 20 except TimeoutException: 21 pass
每解析完一個列表頁,都會對解析出來url發起請求,來請求詳情頁。
4.4 實現詳情頁解析並提取數據
每個詳情項的url請求返回的response都會由parse_detail()函數來解析,使用ItemLoader完成數據提取和格式化操作:
1 # Location: LagouCrawler.spider.lagoucrawler.LagouCrawlerSpder 2 @staticmethod 3 def parse_detail(response): 4 """ 5 解析每一頁各個招聘信息的詳情 6 :param response: 每個列表頁的HtmlResponse實例 7 :return: 各個公司招聘詳情生成器 8 """ 9 item_loader = CompanyItemLoader(item=CompanyItem(), response=response) 10 item_loader.add_xpath('company_name', '//*[@id="job_company"]/dt/a/div/h2/text()') 11 item_loader.add_xpath('company_location', 'string(//*[@id="job_detail"]/dd[@class="job-address clearfix"]/div[@class="work_addr"])') 12 item_loader.add_xpath('company_website', '//*[@id="job_company"]/dd/ul/li[5]/a/@href') 13 item_loader.add_xpath('company_figure', '//*[@id="job_company"]/dd/ul//i[@class="icon-glyph-figure"]/parent::*/text()') 14 item_loader.add_xpath('company_square', '//*[@id="job_company"]/dd/ul//i[@class="icon-glyph-fourSquare"]/parent::*/text()') 15 item_loader.add_xpath('company_trend', '//*[@id="job_company"]/dd/ul//i[@class="icon-glyph-trend"]/parent::*/text()') 16 item_loader.add_xpath('invest_organization', '//*[@id="job_company"]/dd/ul//p[@class="financeOrg"]/text()') 17 item_loader.add_xpath('job_position', '//*[@class="position-content-l"]/div[@class="job-name"]/span/text()') 18 item_loader.add_xpath('job_salary', '//*[@class="position-content-l"]/dd[@class="job_request"]/p/span[@class="salary"]/text()') 19 item_loader.add_xpath('work_experience', '//*[@class="position-content-l"]/dd[@class="job_request"]/p/span[3]/text()') 20 item_loader.add_xpath('degree', '//*[@class="position-content-l"]/dd[@class="job_request"]/p/span[4]/text()') 21 item_loader.add_xpath('job_category', '//*[@class="position-content-l"]/dd[@class="job_request"]/p/span[5]/text()') 22 item_loader.add_xpath('job_lightspot', '//*[@id="job_detail"]/dd[@class="job-advantage"]/p/text()') 23 item_loader.add_xpath('job_description', 'string(//*[@id="job_detail"]/dd[@class="job_bt"]/div)') 24 item_loader.add_xpath('job_publisher', '//*[@id="job_detail"]//div[@class="publisher_name"]/a/span/text()') 25 item_loader.add_xpath('resume_processing', 'string(//*[@id="job_detail"]//div[@class="publisher_data"]/div[2]/span[@class="tip"])') 26 item_loader.add_xpath('active_time', 'string(//*[@id="job_detail"]//div[@class="publisher_data"]/div[3]/span[@class="tip"])') 27 item_loader.add_xpath('publish_date', '//*[@class="position-content-l"]/dd[@class="job_request"]/p[@class="publish_time"]/text()') 28 item = item_loader.load_item() 29 yield item
Item字段和ItemLoader的定義,此處定義中完成提取字段的格式化處理(去掉空格、換行符等):
1 # Location: LagouCrawler.items 2 # -*- coding: utf-8 -*- 3 4 # Define here the models for your scraped items 5 # 6 # See documentation in: 7 # https://doc.scrapy.org/en/latest/topics/items.html 8 9 import datetime 10 from scrapy import Item, Field 11 from scrapy.loader import ItemLoader 12 from scrapy.loader.processors import TakeFirst, MapCompose, Join 13 14 15 def formate_date(value): 16 """ 17 根據提取到的發布時間,若是當天發布則是H:M格式,則獲取當天日期的年月日然后返回,否則是Y:M:D格式, 18 則直接返回該年月日 19 :param value: 提取到的時間字符串 20 :return: 格式化后的年月日 21 """ 22 if ':' in value: 23 now = datetime.datetime.now() 24 publish_date = now.strftime('%Y-%m-%d') 25 publish_date += '(今天)' 26 return publish_date 27 else: 28 return value 29 30 31 class CompanyItemLoader(ItemLoader): 32 default_output_processor = TakeFirst() 33 34 35 class CompanyItem(Item): 36 # define the fields for your item here like: 37 # name = scrapy.Field() 38 39 # 公司名稱 40 company_name = Field( 41 input_processor=MapCompose(lambda x: x.replace(' ', ''), lambda x: x.strip()) 42 ) 43 # 公司地址 44 company_location = Field( 45 input_processor=MapCompose(lambda x: x.replace(' ', ''), lambda x: x.replace('\n', ''), lambda x: x[:-4]) 46 ) 47 # 公司官網 48 company_website = Field() 49 # 公司規模 50 company_figure = Field( 51 input_processor=MapCompose(lambda x: x.replace(' ', ''), lambda x: x.replace('\n', '')), 52 output_processor=Join('') 53 ) 54 # 公司領域 55 company_square = Field( 56 input_processor=MapCompose(lambda x: x.replace(' ', ''), lambda x: x.replace('\n', '')), 57 output_processor=Join('') 58 ) 59 # 公司階段 60 company_trend = Field( 61 input_processor=MapCompose(lambda x: x.replace(' ', ''), lambda x: x.replace('\n', '')), 62 output_processor=Join('') 63 ) 64 # 投資機構 65 invest_organization = Field() 66 67 # 崗位名稱 68 job_position = Field() 69 # 崗位薪資 70 job_salary = Field( 71 input_processor=MapCompose(lambda x: x.strip()) 72 ) 73 # 經驗需求 74 work_experience = Field( 75 input_processor=MapCompose(lambda x: x.replace(' /', '')) 76 ) 77 # 學歷需求 78 degree = Field( 79 input_processor=MapCompose(lambda x: x.replace(' /', '')) 80 ) 81 # 工作性質 82 job_category = Field() 83 84 # 職位亮點 85 job_lightspot = Field() 86 # 職位描述 87 job_description = Field( 88 input_processor=MapCompose(lambda x: x.replace('\xa0\xa0\xa0\xa0', '').replace('\xa0', ''), lambda x: x.replace('\n', '').replace(' ', '')) 89 ) 90 91 # 職位發布者 92 job_publisher = Field() 93 # 發布時間 94 publish_date = Field( 95 input_processor=MapCompose(lambda x: x.replace('\xa0 ', '').strip(), lambda x: x[:-6], formate_date) 96 ) 97 # # 聊天意願 98 # chat_will = Field() 99 # 簡歷處理 100 resume_processing = Field( 101 input_processor=MapCompose(lambda x: x.replace('\xa0', '').strip()) 102 ) 103 # 活躍時段 104 active_time = Field( 105 input_processor=MapCompose(str.strip) 106 )
ItemLoader的具體使用可以參考https://blog.csdn.net/zwq912318834/article/details/79530828
4.5 存儲數據到MongoDB
通過使用itempipline將抓取到的數據存儲到MongoDB:
1 # Location: LagouCrawler.piplines 2 3 # -*- coding: utf-8 -*- 4 5 # Define your item pipelines here 6 # 7 # Don't forget to add your pipeline to the ITEM_PIPELINES setting 8 # See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html 9 10 from pymongo import MongoClient 11 12 13 class LagoucrawlerPipeline(object): 14 15 def __init__(self, host=None, db=None, collection=None): 16 self.mongo_uri = host 17 self.mongo_db = db 18 self.mongo_collection = collection 19 self.client = None 20 self.db = None 21 self.collection = None 22 23 @classmethod 24 def from_crawler(cls, crawler): 25 """ 26 通過該函數,獲取在settings.py文件中定義的Mongodb地址、數據庫名稱和表名 27 :param crawler: 28 :return: 29 """ 30 return cls( 31 host=crawler.settings.get('MONGO_URI'), 32 db=crawler.settings.get('MONGO_DB'), 33 collection=crawler.settings.get('MONGO_COLLECTION') 34 ) 35 36 def open_spider(self, spider): 37 """ 38 在spider打開時,完成mongodb數據庫的初始化工作。 39 :param spider: 40 :return: 41 """ 42 self.client = MongoClient(host=self.mongo_uri) 43 self.db = self.client[self.mongo_db] 44 self.collection = self.db[self.mongo_collection] 45 46 def process_item(self, item, spider): 47 """ 48 pipeline的核心函數,在該函數中執行對item的一系列操作,例如存儲等。 49 :param item: parse_detail函數解析出來的item 50 :param spider: 抓取該item的spider 51 :return: 返回處理后的item,供其它pipeline再進行處理(如果有的話) 52 """ 53 # 以公司名稱做為查詢條件 54 condition = {'company_name': item.get('company_name')} 55 # upsert參數設置為True后,若數據庫中沒有該條記錄,會執行插入操作; 56 # 同時,使用update_one()函數,也可以完成去重操作。 57 result = self.collection.update_one(condition, {'$set': item}, upsert=True) 58 spider.logger.debug('The Matched Item is: {} And The Modified Item is: {}'.format(result.matched_count, result.modified_count)) 59 return item 60 61 def close_spider(self, spider): 62 """ 63 在spider關閉時,關閉mongodb數據連接。 64 :param spider: 65 :return: 66 """ 67 self.client.close()
Tips:在查看MongoDB數據時,會發現數據數量會比抓取下來的數據數量少,原因是數據插入是用MongoDB的更新函數update_one()以公司名稱為查詢條件完成的,而所有詳情項中會出現同一個公司不同的hr發布相同職位的招聘信息。
然后在setting.py文件中配置一下該pipline:
1 # Location: LagouCrawler.settings 2 3 # Configure item pipelines 4 # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html 5 ITEM_PIPELINES = { 6 'LagouCrawler.pipelines.LagoucrawlerPipeline': 300, 7 } 8 9 # Mongodb地址 10 MONGO_URI = 'localhost' 11 # Mongodb庫名 12 MONGO_DB = 'job' 13 # Mongodb表名 14 MONGO_COLLECTION = 'works'
到此,運行爬蟲,不出意外的話,程序能夠完成城市的切換、搜索關鍵字的輸入、索引頁的翻頁以及詳情項的字段提取。事實是能到一部分數據,然后就是302重定向和各種40X和50X響應。原因是拉鈎網的反爬蟲措施。
4.6 爬取拉鈎網IP被禁解決方案
如上所訴,在整個程序框架完成后,並沒有如願的拿到完整數據,而是在拿到部分數據后,請求被重定向,最后IP被禁。所以需要采取一些措施。
4.6.1 爬取拉鈎網不需要cookie
拉鈎網的爬取不是不需要cookie,而是不能有cookie,拉鈎網根據cookie來識別是否是爬蟲在訪問,所以在settings.py中禁掉cookie:
1 # Location: LagouCrawler.settings 2 3 # Disable cookies (enabled by default) 4 COOKIES_ENABLED = False
4.6.2 爬取拉鈎網的請求頭Headers
需要在settings.py中配置一下請求頭,來偽裝成瀏覽器(此處沒有配置User-Agent):
1 # Location: LagouCrawler.settings 2 3 # Override the default request headers: 4 DEFAULT_REQUEST_HEADERS = { 5 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 6 'Accept-Encoding': 'gzip, deflate, br', 7 'Accept-Language': 'zh-CN,zh;q=0.9', 8 'Host': 'www.lagou.com', 9 'Referer': 'https://www.lagou.com/', 10 'Connection': 'keep-alive' 11 }
4.6.3 給每個request配置隨機User-Agent
為了防止IP因為User-Agent被禁,所以定義一個下載中間件,用fake_useragent第三方包來給每個request添加一個隨機的User-Agent:
1 # Location: LagouCrawler.middlewares 2 3 class RandomUserAgentMiddleware(object): 4 """ 5 給每一個request添加隨機的User-Agent 6 """ 7 8 def __init__(self, ua_type=None): 9 super(RandomUserAgentMiddleware, self).__init__() 10 self.ua = UserAgent() 11 self.ua_type = ua_type 12 13 @classmethod 14 def from_crawler(cls, crawler): 15 """ 16 獲取setting.py中配置的RANDOM_UA_TYPE,如果沒有配置,則使用默認值random 17 :param crawler: 18 :return: 19 """ 20 return cls( 21 ua_type=crawler.settings.get('RANDOM_UA_TYPE', 'random') 22 ) 23 24 def process_request(self, request, spider): 25 """ 26 UserAgentMiddleware的核心方法,getattr(A, B)相當於A.B,也就是獲取A 27 對象的B屬性,在這就相當於ua.random 28 :param request: 29 :param spider: 30 :return: 31 """ 32 request.headers.setdefault('User-Agent', getattr(self.ua, self.ua_type)) 33 # 每個請求禁止重定向 34 request.meta['dont_redirect'] = True 35 request.meta['handle_httpstatus_list'] = [301, 302] 36 spider.logger.debug('The <{}> User Agent Is: {}'.format(request.url, getattr(self.ua, self.ua_type)))
在settings.py中配置一下該下載中間件,注意需要禁掉Scrapy框架自帶的UserAgentMiddleWare:
1 # Location: LagouCrawler.settings 2 3 DOWNLOADER_MIDDLEWARES = { 4 # 啟用阿布雲代理服務器中間件 5 'LagouCrawler.middlewares.AbuYunProxyMiddleware': 1, 6 # 在DownloaderMiddleware之前啟用自定義的RandomUserAgentMiddleware 7 'LagouCrawler.middlewares.RandomUserAgentMiddleware': 542, 8 # 禁用框架默認啟動的UserAgentMiddleware 9 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, 10 'LagouCrawler.middlewares.LagoucrawlerDownloaderMiddleware': 543, 11 }
Tips:在給每個request添加隨機User-Agent的同時,讓該request禁止重定向,原因是在后面使用代理服務器的時候,如果有一個請求通過代理服務器請求失敗,該請求會被重定向,結果導致程序進入該重定向頁面的死循環。
4.6.4 使用阿布雲代理服務器來發起請求
相對於自己維護一個代理池,使用阿布雲代理的動態版(適用於爬蟲業務,需要付費)來為每一個request分配一個隨機IP更加方便靈活。
阿布雲官網:https://center.abuyun.com
阿布雲接口教程:https://www.jianshu.com/p/90d57e7a545a?spm=a2c4e.11153940.blogcont629314.15.59f8319fWrMVQK
同樣寫一個下載中間件,讓每一個request通過阿布雲來發起請求:
1 # Location: LagouCrawler.middlewares 2 3 class AbuYunProxyMiddleware(object): 4 """ 5 接入阿布雲代理服務器,該服務器動態IP1秒最多請求5次。需要在setting中設置下載延遲 6 """ 7 8 def __init__(self, settings): 9 self.proxy_server = settings.get('PROXY_SERVER') 10 self.proxy_user = settings.get('PROXY_USER') 11 self.proxy_pass = settings.get('PROXY_PASS') 12 self.proxy_authorization = 'Basic ' + base64.urlsafe_b64encode( 13 bytes((self.proxy_user + ':' + self.proxy_pass), 'ascii')).decode('utf8') 14 15 @classmethod 16 def from_crawler(cls, crawler): 17 return cls( 18 settings=crawler.settings 19 ) 20 21 def process_request(self, request, spider): 22 request.meta['proxy'] = self.proxy_server 23 request.headers['Proxy-Authorization'] = self.proxy_authorization 24 spider.logger.debug('The {} Use AbuProxy'.format(request.url))
服務器的地址、阿布雲的用戶名和密碼(可以1塊錢買1個小時,默認每秒5個請求)都配置在settings.py中:
1 # Location: LagouCrawler.settings 2 3 # 阿布雲代理服務器地址 4 PROXY_SERVER = "http://http-dyn.abuyun.com:9020" 5 # 阿布雲代理隧道驗證信息,注冊阿布雲購買服務后獲取 6 PROXY_USER = 'H9470L5HEARXXXXX' 7 PROXY_PASS = '02E02D1D773XXXXX' 8 # 啟用限速設置 9 AUTOTHROTTLE_ENABLED = True 10 AUTOTHROTTLE_START_DELAY = 0.2 # 初始下載延遲
同樣在settings.py中配置一下該下載中間件,如4.6.3所示。
5 Scrapy的斷點調試
為了方便Scrapy的斷點調試,在scrapy.cfg同級目錄下新建一個run.py文件:
1 from scrapy.cmdline import execute 2 3 4 def main(): 5 spider_name = 'lagoucrawler' 6 cmd_string = 'scrapy crawl {spider_name}'.format(spider_name=spider_name) 7 execute(cmd_string.split()) 8 9 10 if __name__ == '__main__': 11 main()
這樣就可以通過Debug run.py,來進行Scrapy項目的斷點調試。
6 項目代碼
完整代碼的Github地址:https://github.com/StrivePy/LaGouCrawler/tree/recode/LagouCrawler
7 參考資料
- Selenium官方文檔:https://selenium-python.readthedocs.io/
- Selenium+Python手冊大全:https://www.jianshu.com/nb/25338984
- Scrapy官方文檔:https://docs.scrapy.org/en/latest/
- Scrapy中文文檔:https://scrapy-chs.readthedocs.io/zh_CN/0.24/
- 如何讓你的Scrapy爬蟲不被ban(1):http://www.cnblogs.com/rwxwsblog/p/4575894.html
- 如何讓你的Scrapy爬蟲不被ban(2):http://www.cnblogs.com/rwxwsblog/p/4582127.html
- 阿布雲官方接入文檔:https://www.abuyun.com/http-proxy/dyn-manual.html
- 阿布雲Python3使用教程:https://www.jianshu.com/p/90d57e7a545a
