这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
早就说过要爬取一下招聘网站,一直以来没时间搞,用大数据来看一下 VR 方向的行情如何?都有哪些岗位?哪些岗位比较吃香?哪些岗位薪资比较高?又有哪些技术要求?等等等等,只有了解了这些,才能更准确地去把握自己的定位,找到自己未来发展的方向。
本周,我爬取了《拉勾网》网站,以 VR 为关键词,搜索得到 450 条招聘信息匹配结果。爬取其关键信息并保存至本地,留作后续分析使用。本文只讲爬虫部分,后续的数据分析及可视化部分,再说再说。
拉勾网,其实爬起来比我想象中要稍微困难一点,就是它设置有反爬虫机制(可能是总被爬虫光顾,给服务器带来不小的压力吧),什么样的反爬虫机制呢,后面我们细讲。
一、调研目标网站,制定爬取策略
这一步,我们需要去目标网站《拉勾网》逛逛,了解以下几件事情:
- 在这里我们可以获取到哪些数据?
- 这些数据是存放在哪儿的?
- 如何获取所有数据(如何翻页)?
带着这些问题,我们去《拉勾网》(www.lagou.com/)看一看。
通过简单的了解,我们可以获取到以下信息:
- 搜索关键词 VR 得到的匹配结果有 500+ 条,不过页面最多展示 30 页,每页 15 条,共计 450 条。
- 每条招聘信息包含以下数据:工作地点、公司名称、职位名称、职位要求,薪资待遇等。
- 职位列表的翻页不是通过 URL 控制的,因为在翻页过程中,网站的 url 没有变化过,猜测应该是采用某种动态加载的技术。
按 F12
召唤出开发者工具,继续了解更加详细的信息。
职位信息存放在 ul
标签下的 li
标签中。
点击翻页按钮时,后台会发出这样的 Post
请求,请求返回的结果是一个 Json
文件。
仔细观察后我们可以发现,json
文件中包含了该页中页面上显示的 15
条招聘信息中的所有数据,甚至还有一些数据是页面中没有展现出来的。看到这里其实我是特别兴奋的,因为这样意味着我们找到了一个比在 html
页面中筛选信息更为便捷有效的方法,通过这个方法,我们可以很方便的获取到关于招聘信息的几乎所有详细内容。
(PS:在目标网站分析的阶段,这种情况其实是比较常见的,就是同样的数据我们可以有多种途径去获取它,此时我们便可以有多种爬取策略可供选择,然后在其中选择一种最为合适的方法即可。没有被选用的方法也不能说明它不好,毕竟网站各不相同,使用的方法也各有差异,这个网站不适合用可能换一个网站就很好用也说不定)
最终,我们调研得出的结论为:
- 需要获取的数据为:工作地址、公司名称(及规模)、职位名称、职位标签,第一二三意愿方向(备选职位)、学历要求、工作经验要求、薪水、福利待遇、发布日期。
- 数据存储在一个 Post 请求返回的 json 文件中,请求的格式为 www.lagou.com/jobs/positi… 。
- Post 请求的参数有三个
first:false
,pn:1
,kd:VR
。first
记录你是否第一次访问,pn
用来记录当前页数,kd
为你搜索的关键词。所以翻页功能我们只需要在循环中改变pn
的值,即可。(注,三个参数的含义是我通过经验以及他们的缩写来猜测的,如果和实际含义有偏差的话欢迎指正)
二、 尝试通过代码发送请求,获取数据
虽然我们已经将目标网站的结构分析的差不多了,但是毕竟是在浏览器中访问的,而网站的反爬机制(如果有的话)在浏览器访问时并不会触发,只有爬虫代码访问时才会启动,所以我们需要尝试通过代码去发送请求,看是否可以正常返回预期结果。
import requests
def get_data(url,data):
'''
功能:访问 url 的网页,获取网页内容并返回
参数:
url :目标网页的 url
返回:目标网页的 html 内容
'''
headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
}
try:
r = requests.post(url, headers=headers, data = data)
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except requests.HTTPError as e:
print(e)
print("HTTPError")
except requests.RequestException as e:
print(e)
except:
print("Unknown Error !")
if __name__ == '__main__':
url = 'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false'
data = {
'first': 'true',
'pn': 1,
'kd': 'VR',
}
html = get_data(url,data)
print(html)
打印请求返回的响应,发现并没有按照我们的预期,返回存储有招聘信息的 json 文件,而是只有干巴巴的一句话:
{"success":false,"msg":"您操作太频繁,请稍后再访问","clientIp":"111.***.***.*"}
访问失败了,原因是操作太频繁。。。这不扯呢嘛,我还没开始爬呢,怎么就太过频繁了?
思索一下原因,我怀疑是请求头没有伪装好,被发现是爬虫了,于是打开浏览器的开发者工具,将其中的请求头放入爬虫代码的请求头中,重新运行。
#修改 get_data 函数代码中的 headers
headers = {
'Host': 'www.lagou.com',
'Origin': 'https://www.lagou.com',
'Referer': 'https://www.lagou.com/jobs/list_VR?px=default&city=%E5%85%A8%E5%9B%BD',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
}
果然,在修改请求头之后,程序可以成功获取到了后端发来的招聘职位信息。这应该是此网站设置的第一道爬虫,可以初步检测访问的是否是爬虫,若发现是爬虫,则拒绝访问。
三、正式编写爬虫程序
经过上一个步骤,我们基本上可以使用爬虫代码获取到招聘职位信息了 ,而且改变 data
中的 pn
的值即可获取其他页码的招聘信息,达到了翻页的效果。至此,前期的准备工作基本全部完成,接下来,我们只需要编写解析函数,完善爬虫程序即可。
import json
import requests
import time
import pandas as pd
def get_data(url,data):
'''
功能:访问 url 的网页,获取网页内容并返回
参数:
url :目标网页的 url
data : 参数信息
返回:目标网页的 html 内容
'''
#修改 get_data 函数代码中的 headers
headers = {
'Host': 'www.lagou.com',
'Origin': 'https://www.lagou.com',
'Referer': 'https://www.lagou.com/jobs/list_VR?px=default&city=%E5%85%A8%E5%9B%BD',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
}
try:
r = requests.post(url, headers=headers, data = data)
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except requests.HTTPError as e:
print(e)
print("HTTPError")
except requests.RequestException as e:
print(e)
except:
print("Unknown Error !")
def parse_data(html):
'''
功能:提取 html 页面信息中的关键信息,并整合一个数组并返回
参数:html 根据 url 获取到的网页内容
返回:存储有 html 中提取出的关键信息的数组
'''
data = json.loads(html)['content']['positionResult']['result']
job_info = []
for item in data:
info = []
district = item['district'] if item['district'] != None else ''
city = item['city'] + ':' + district
if item['businessZones'] != None:
zones = ','.join(item['businessZones'])
city += '(' + zones + ')'
print(city)
info.append(city)
companyName = item['companyFullName']
companySize = item['companySize']
companyInfo = companyName + '(' + companySize + ')'
print(companyInfo)
info.append(companyInfo)
positionName = item['positionName']
print(positionName)
info.append(positionName)
firstType = item['firstType']
secondType = item['secondType']
thirdType = item['thirdType']
positionLables = ''
if item['positionLables'] != None:
positionLables = ','.join(item['positionLables'])
print(positionLables)
info.append(positionLables)
positionType = '1.' + firstType + ' 2.' + secondType + ' 3.' + thirdType
print(positionType)
info.append(positionType)
education = item['education']
salary = item['salary']
workYear = item['workYear']
info.append(education)
info.append(salary)
info.append(workYear)
positionAdvantage = item['positionAdvantage']
if item['hitags'] != None:
hitags = ','.join(item['hitags'])
positionAdvantage += ',' + hitags
print(positionAdvantage)
info.append(positionAdvantage)
createTime = item['createTime']
info.append(createTime)
print('---'*20)
job_info.append(info)
return job_info
def save_data(data):
'''
功能:将data中的信息输出到文件中/或数据库中。
参数:data 将要保存的数据
'''
filename = 'jobInfo.csv'
dataframe = pd.DataFrame(data)
dataframe.to_csv(filename, mode='a', index=False, sep=',', header=False)
def main():
for page in range(1, 31):
print('正在爬取:第' + str(page) + '页......\n')
url = 'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false'
data = {
'first': 'true',
'pn': page,
'kd': 'VR',
}
html = get_data(url,data)
info = parse_data(html)
save_data(info)
#print(html)
print('第' + str(page) + '页完成!')
#time.sleep(60)
if __name__ == '__main__':
print("爬虫启动成功!")
main()
print("爬虫运行结束!")
这样基本上就可以了,运行程序,可以看到数据开始输出,保存到本地文件中。
注:这里可以通过改变 pn
的值来调节页码,同样的道理,你也可以改变 kd
的值来搜索其他的职位信息,甚至,你可以将 kd
的值作为一个变量,由用户在程序运行之后动态输入。
四、程序运行过程中遇到的小问题
其实,上面写好的程序在实际运行过程中并不是一帆风顺的,它也会遇到一些异常,导致程序运行错误。比如说我遇到的(大家爬的时候,一定也会遇到)问题,就是爬取四五页左右之后,服务器便会拒绝访问。
{"success":false,"msg":"您操作太频繁,请稍后再访问","clientIp":"111.***.***.*"}
确实,人家没冤枉咱, 这次真的是咱爬的过于频繁了。
这就是我遇到的第二个反爬机制,他会检测客户端访问数据的频率,如果在一定时间内访问次数超过阈值,则将其界定为爬虫,拒绝访问。
解决方法页比较简单,就是在每爬取一页之后,使用 time.sleep
函数暂停一段时间(为了保险,我设置延迟为 60 秒),并且加入了异常捕获机制,如果发现拒绝访问的情况,就在等待一段时间之后再次访问。
经过很长的一段时间的运行,程序成功的爬取到了 30 页,共计 450 条招聘信息数据。
写在后面的话
此前虽然爬取了很多网站,也遇到了一些反爬机制,但是如此灵敏的反爬机制还是第一次见到,触发率太高了,估计也是经常被爬虫困扰吧。因此建议大家使用爬虫时,尽量采取一些温和的方式,爬取一段时间之后添加等待时间,以免给对方服务器造成太大的负担。
不过借由此网站,我们可以更加直观的了解到网站的反爬虫机制以及我们响应的应对策略。也算是一个不错的爬虫实战案例吧。
2020年9月11 日 更新
这篇爬虫是两年前写的了,很久没有维护过了。最近有读者学习这篇爬虫,然后跟我反馈说代码已经失效了。于是借此机会,维护一下代码。
拉勾网确实做了一些调整,提高了爬虫的难度,主要在于两个方面,一个是会去验证请求头中 Cookie
信息,二是在参数表中添加了一个 sid
的加密参数。
在源码中进行如下的修改之后,重新运行即可成功爬取。
headers = {
'Host': 'www.lagou.com',
'Origin': 'https://www.lagou.com',
'Referer': 'https://www.lagou.com/jobs/list_VR?px=default&city=%E5%85%A8%E5%9B%BD',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'cookie': '这里填Cookie信息',
}
data = {
'first': 'true',
'pn': page,
'kd': 'VR',
'sid' : '这里填sid参数',
}
注1:这两个参数在浏览器的开发者工具中可以找到(参考文章第一节中分析网站的方法)
注2:这两个参数会失效(可能是跟时间有关,可能跟爬取频率有关),失效后会导致爬取失败。如果失效了,重新在浏览器中找到这两个参数,替换一下即可。
如果文章中有哪里没有讲明白,或者讲解有误的地方,欢迎在评论区批评指正,或者扫描下面的二维码,加我微信,大家一起学习交流,共同进步。