这是第三个大四综合实践——数据处理与分析。我们小组打算爬取各个招聘网站进行数据分析。
我负责其中的爬虫模块,教了两个队友怎么使用scrapy,打算我解决完爬取数据的一些难题之后,剩余的解析就交给他们。
我觉得解析数据只是苦力活,只要爬取到带有数据的html,剩下的就很轻松了。最后我选择使用selenium,这样就不用分析接口了,两个刚学爬虫的队友也能轻松搞定。
很快地搞定了前程无忧网和拉勾网之后,我在爬取智联招聘网 遇到了很多问题。本文将这些问题以及解决方案记录下来,供读者参考。
参考链接 以下链接为我在发现问题和解决问题的过程中参考的资料。
登录 智联招聘这个网站,如果不登陆,一个职位都不给你显示,所以第一步是登录。
使用cookies登录 一开始打算手动登录之后保存cookies到文件(全自动的吃力不讨好,于是弄成半自动的了),然后启动scrapy时从文件里面读取cookies并加载刷新。
先用下面这个工具模块登录后获取到cookies,保存到文件当中,需要的时候再读取出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ''' 工具模块 ''' import jsonfrom selenium import webdriverdef getCookies (websiteName ): ''' 获取cookies,会打开一个浏览器,然后手动登录,会获取登录后的cookies写入cookies.json中 @return 是否成功 ''' website_map = { 'zhaopin' : "https://sou.zhaopin.com/?jl=543" } website_url = website_map.get(websiteName) if website_url == None : return False browser.get(website_url) input('请随便输入点什么代表你已经登录完成:' ) dictCookies = browser.get_cookies() jsonCookies = json.dumps(dictCookies, sort_keys=True , indent=2 ) print(jsonCookies) with open(websiteName+'.json' , 'w' ) as f: f.write(jsonCookies) print('获取成功' ) return True if __name__ == "__main__" : getCookies('zhaopin' )
读取cookies的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class EmploymentDownloaderMiddleware (object ): spider_need_login = [ 'zhaopin' ] isLogin = False def __init__ (self ): self.browser = webdriver.Chrome() def login (self, websiteName: str ): ''' 利用cookies登录 @param websiteName:网站名字,目前支持的网站:'zhaopin'(智联招聘) ''' with open(websiteName+'.json' , 'r' , encoding='utf8' ) as f: listCookies = json.loads(f.read()) print('===============================================cookies===============================' ) print(listCookies) print('===============================================cookies===============================' ) for cookie in listCookies: self.browser.add_cookie(cookie) self.isLogin = True
去除selenium特征 但是即使我读取cookies成功了,页面右上角都显示我用户名了,仍然一直显示加载动画。甚至连“登录之后再搜索,海量职位等你挑!”这一句提示都没有。
用chrome开发者工具比较手动打开的网页以及selenium打开的网页,发现selenium打开的网页发送给接口https://fe-api.zhaopin.com/c/i/sou
的请求并没有收到Response,多试几次之后发现了另一个已经收到Response,但是状态码为400 Bad Request。
猜测是selenium打开的网页传递的参数会与人工打开的网页有所区别。
查询资料后,发现是因为selenium打开的网页会有特征变量window.navigator.webdriver
(参考:selenium的检测与突破 ),网站只要检测那个特征变量,就可以判断这次访问是不是selenium打开的,如果这个变量为True,则不加载数据,从而实现反爬虫的目的。看来智联招聘真的是被各种爬虫爬怕了。
找了两三个方法,修改了selenium的启动选项,都无法修改这个变量,因为他们的方法是针对旧版本的chrome的,不过我在博客的评论下找到了一位老哥,他写了一篇不需要退版本的解决方案。(参考:(新)关于修改window.navigator.webdriver代码失效问题(不必退版本的解决方案) )
解决方案是:在selenium启动浏览器之前,让它执行js脚本,修改掉那个变量,详细方法见参考链接。
最后middleware.py
里面的核心代码是这样的:
1 2 3 4 5 6 7 8 9 10 11 class EmploymentDownloaderMiddleware (object ): def __init__ (self ): self.browser = webdriver.Chrome() self.browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument" , { "source" : """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) """
无奈使用半自动登录 解决特征变量的问题后,终于不再是无尽的加载动画了,但是读取cookies登录显示“登录之后再搜索,海量职位等你挑!”这样的提示信息,不给数据。
可能是因为当次的cookies只能当次使用,之后即使没有过期,也会因为sessionid对不上或者别的什么参数对不上而不给数据。
最后在Middleware获取selenium模拟浏览器网页的源代码之前,加了一个input函数等待我手动在模拟浏览器内登录好了之后再继续执行,方法比较low,但是管用。缺点是每次启动都得登录,有点担心被封号,不过后面频繁爬取时并没有被封过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class EmploymentDownloaderMiddleware (object ): spider_need_login = [ 'zhaopin' ] isLogin = False def __init__ (self ): self.browser = webdriver.Chrome() self.browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument" , { "source" : """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) """ self.loadTime = 3 def login (self, websiteName: str ): ''' 利用cookies登录 @param websiteName:网站名字,目前支持的网站:'zhaopin'(智联招聘) ''' with open(websiteName+'.json' , 'r' , encoding='utf8' ) as f: listCookies = json.loads(f.read()) print('===============================================cookies===============================' ) print(listCookies) print('===============================================cookies===============================' ) for cookie in listCookies: self.browser.add_cookie(cookie) self.isLogin = True def process_request (self, request, spider ): if request.meta.get('next_page_css' ) != None : nextPageBtn = self.browser.find_element_by_css_selector( request.meta['next_page_css' ]) nextPageBtn.click() time.sleep(self.loadTime) page_text = self.browser.page_source else : self.browser.get(request.url) if spider.name in self.spider_need_login and not self.isLogin: input('请手动登录,登录好了之后输入1:' ) self.isLogin = True dictCookies = self.browser.get_cookies() jsonCookies = json.dumps(dictCookies, sort_keys=True , indent=2 ) print(jsonCookies) with open(spider.name+'.json' , 'w' ) as f: f.write(jsonCookies) print('cookies保存成功' ) time.sleep(self.loadTime) page_text = self.browser.page_source return HtmlResponse(url=self.browser.current_url, body=page_text, encoding='utf-8' , request=request)
翻页 按照我以前的方式来爬取详情页并没有什么问题,但是问题在于爬取详情页的时候,当前标签页会跳转到详情页,之后就回不去目录页了。
开启两个标签页 当爬取完第一页的所有职位数据后,selenium模拟浏览器的页面是停留在最后一个职位的详情页的,无法找到“下一页按钮”,也就无法翻页。如果此时手动跳转回目录页,那么之前翻的页是无法保留的,又回到第一页。
解决方案:打开两个标签页,如果正在爬取详情页,则切换到第二个标签页来进行爬取,第一个标签页始终是目录页。
等到详情页爬取完毕,则切换回第一个标签页,翻页并继续解析。
以下是开启第二个标签页并获取句柄的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class EmploymentDownloaderMiddleware (object ): spider_need_login = [ 'zhaopin' ] isLogin = False homePageHandle = None subPageHandle = None def __init__ (self ): self.browser = webdriver.Chrome() self.browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument" , { "source" : """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) """ }) self.loadTime = 3 self.homePageHandle = self.browser.current_window_handle js = 'window.open("https://www.baidu.com/")' self.browser.execute_script(js) time.sleep(self.loadTime) all_handles = self.browser.window_handles self.subPageHandle = all_handles[-1 ]
接着在process_request
函数中判断当前request是否为爬取详情页的请求,如果是,那么就切换到第二个标签页,如果不是,那么就回到目录页进行翻页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 def process_request (self, request, spider ): if request.meta.get('item' ) != None : self.browser.switch_to.window(self.subPageHandle) self.browser.get(request.url) time.sleep(self.loadTime) page_text = self.browser.page_source else : self.browser.switch_to.window(self.homePageHandle) time.sleep(self.loadTime) if request.meta.get('next_page_css' ) != None : nextPageBtn = self.browser.find_element_by_css_selector( request.meta['next_page_css' ]) self.browser.execute_script( "arguments[0].click();" , nextPageBtn) time.sleep(self.loadTime) page_text = self.browser.page_source else : self.browser.get(request.url) if spider.name in self.spider_need_login and not self.isLogin: input('请手动登录,登录好了之后输入1:' ) self.isLogin = True dictCookies = self.browser.get_cookies() jsonCookies = json.dumps( dictCookies, sort_keys=True , indent=2 ) print(jsonCookies) with open(spider.name+'.json' , 'w' ) as f: f.write(jsonCookies) print('cookies保存成功' ) time.sleep(self.loadTime) page_text = self.browser.page_source return HtmlResponse(url=self.browser.current_url, body=page_text, encoding='utf-8' , request=request)
多线程问题 默认情况下,scrapy开启多个线程来进行爬取,这就导致上一个问题的解决方案会出现问题——一个线程正在爬取详情页的时候,另一个线程开始切换回目录页进行翻页了。
解决方法是,在settings.py
里面将同时允许的线程数设置为1,也就是单线程运行,反正我对速度没有要求:
1 2 3 CONCURRENT_REQUESTS = 1
下一页按钮被遮挡 翻页时,会出现“下一页按钮”被遮挡而无法点击的情况。
也就是出现Message: element not interactable
元素不可交互的问题,解决方法参考:Other element would receive the click:解决之一
将模拟点击的代码改为:
1 2 3 4 nextPageBtn = self.browser.find_element_by_css_selector(request.meta['next_page_css' ]) self.browser.execute_script("arguments[0].click();" , nextPageBtn)
这里arguments[0]
代指后面跟着的第一个参数,也就是nextPageBtn
。
这个方法虽然解决了翻页,但是新的问题又出现了。
翻页后,数据并没有刷新,虽然显示跳转到了下一页,但是数据仍然是第一页的数据,延长等待时间仍然不行,一看开发者工具,好嘛,request又变红了。
这个问题我现在也没有解决。