scrapy+selenium爬取智联招聘

scrapy+selenium爬取智联招聘

这是第三个大四综合实践——数据处理与分析。我们小组打算爬取各个招聘网站进行数据分析。

我负责其中的爬虫模块,教了两个队友怎么使用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 json
from selenium import webdriver

def 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):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.

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
})
""" # 在开启浏览器之前执行脚本去除selenium特征避免被发现是爬虫

无奈使用半自动登录

解决特征变量的问题后,终于不再是无尽的加载动画了,但是读取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):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.

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
})
""" # 在开启浏览器之前执行脚本去除selenium特征避免被发现是爬虫
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:
# 如果该爬虫需要登录且未登录
# self.login(spider.name)
# self.browser.refresh() # 刷新
input('请手动登录,登录好了之后输入1:')
self.isLogin = True
# 顺便保存一下cookies,不过后面还是没用到
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开启的浏览器也获取到了数据

翻页

按照我以前的方式来爬取详情页并没有什么问题,但是问题在于爬取详情页的时候,当前标签页会跳转到详情页,之后就回不去目录页了。

开启两个标签页

当爬取完第一页的所有职位数据后,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
})
"""
}) # 在开启浏览器之前执行脚本去除selenium特征避免被发现是爬虫
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):

# 如果meta中存在item,则代表需要爬取详情页,在新的页面打开并爬取
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)
# 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:
# 如果该爬虫需要登录且未登录
# self.login(spider.name)
# self.browser.refresh() # 刷新
input('请手动登录,登录好了之后输入1:')
self.isLogin = True
# 顺便保存一下cookies
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) # 给浏览器加载数据的时间
# self.browser.implicitly_wait(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
# settings.py
# Configure maximum concurrent requests performed by Scrapy (default: 16)
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'])

# nextPageBtn.click() # 原本的点击代码
self.browser.execute_script("arguments[0].click();", nextPageBtn) # 新的模拟点击代码

这里arguments[0]代指后面跟着的第一个参数,也就是nextPageBtn

这个方法虽然解决了翻页,但是新的问题又出现了。

翻页后,数据并没有刷新,虽然显示跳转到了下一页,但是数据仍然是第一页的数据,延长等待时间仍然不行,一看开发者工具,好嘛,request又变红了。

这个问题我现在也没有解决。

作者

憧憬少

发布于

2020-11-22

更新于

2020-11-22

许可协议