最近面临找工作,想看看现在深圳前端招聘环境怎么样,想着写个爬虫获取一下boss上面的前端岗位招聘信息,然后做一个简单的数据分析
结果爬取数据的时候发现有3个主要问题:
网站搜索最多只能查询10页的数据,超过10页就查找不了
这个网站反反爬好难,本菜鸡能力有限暂时还解决不了这种,而且zp_stoken的生成验证了好多环境实在也不会弄
请求频繁了还会触发图形验证,而且还据说很容易封ip,处理起来都好麻烦(╥﹏╥)
但是条条大路通罗马,没有好办法,总还有笨办法可以进行解决的嘛,本菜鸡翻来覆去,勉强想到几个笨办法:
既然只能查找10页数据,那我就多换几个筛选条件,最后综合综合一下数据嘛,本来我也只是想搜搜深圳的前端岗位,干脆就按区域和工作年限来分别查数据好了,这样总数据量不就上来了嘛
虽然zp_stoken生成时验证的环境我弄不好,但是我可以直接用rpc远程调用来生成zp_stoken,直接用浏览器的环境,我不就可以不用解决环境问题啦(◔౪◔)
感觉请求如果没有很快,好像也不那么容易触发图形验证,而且我本身爬取数据量也不大,所以碰到图形验证,就直接手动验证就可以啦,然后通过生成随机数,每次请求间隔一些时间,感觉应该能够避免些被封ip的风险吧
1. zp_stoken生成 请求信息的时候,cookie中zp_stoken这个参数很重要,它就是反爬的关键参数。如果请求携带的zp_stoken不对,返回的数据只能是您的访问行为异常,如图
而zp_stoken的生成,就是来源于上面请求返回的zpData中的seed和ts
那么接下来,就是找到生成zp_stoken调用的函数方法,然后注入ws服务,这样之后每次调用requests请求的时候,如果zp_stoken失效,直接调用方法传入seed和ts,就能得到新的有效的zp_stoken啦~
直接全局搜索__zp_stoken__,如图,可以发现zp_stoken生成调用的方法就是圈出的位置
接下来就是使用rpc远程调用,具体操作方法可以参考js逆向之rpc远程调用
浏览器注入ws服务如上,然后就是python开启调用ws服务,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import asyncioimport websocketsconnected = set () async def socket_server (websocket ): connected.add(websocket) try : async for message in websocket: for conn in connected: if conn != websocket: await conn.send(message) finally : connected.remove(websocket) start_server = websockets.serve(socket_server, "127.0.0.1" , 8080 ) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
最后就是通过flask部署到本地服务上,供后续请求调用,如下:
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 import asyncioimport jsonimport websocketsfrom flask import Flask,requestapp = Flask(__name__) loop = asyncio.get_event_loop() async def hello (message ): async with websockets.connect("ws://127.0.0.1:8080" ) as websocket: await websocket.send(message) return await websocket.recv() def get_token (seed, ts ): return str (loop.run_until_complete(hello(json.dumps({ "seed" : seed, "ts" : ts })))) @app.route('/' ,methods = ['POST' , 'GET' ] ) def hello_world (): if request.method == 'GET' : print (request.args) seed = request.args.get('seed' ) ts = request.args.get('ts' ) return get_token(seed, ts) if __name__ == '__main__' : app.run(host="0.0.0.0" ,port=80 ,debug=True )
调用效果如下:
2. 请求封装和图形验证提示 每一次请求都需要有效的zp_stoken,那当然是用函数统一封装一下请求比较好啦,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def request_method (url, data ): print (f'params: {data} ' ) responses = requests.get(url, params=data,cookies=cookies, headers=headers) print (f'res: {responses.text} ' ) if responses.json()['code' ] == 37 : res = requests.get(f'http://127.0.0.1:80' , params={ 'seed' : responses.json()['zpData' ]['seed' ], 'ts' : responses.json()['zpData' ]['ts' ], }) cookies['__zp_stoken__' ] = res.text return request_method(url, data) elif responses.json()['code' ] == 35 : print ('请进行手动图形验证!!!!' ) play_mp3() return request_method(url, data) data = responses.json()['zpData' ] responses.close() return data
返回code为35的时候,说明触发图形验证了,需要到浏览器里手动进行验证,因为爬取数据时间较长,所以我用pygame来播放音频来进行提示,免得我注意不到哈哈哈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def play_mp3 (): pygame.mixer.init() pygame.mixer.music.load("temp/心电心-王心凌.mp3" ) pygame.mixer.music.play(-1 ) while True : text = input ('是否已经手动验证完毕?' ) if text == 'y' : pygame.mixer.music.stop() print ("音频已停止播放" ) pygame.mixer.quit() pygame.quit() print ("pygame 已成功清理和退出" ) break
3. 请求岗位数据 由于只能请求10页数据,我要先获取城市各个区域列表和工作年限的列表数据,然后再通过循环遍历的方式获取岗位数据,所有获取的数据我会先放入csv文件中
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 csv_head = ['jobName' ,'brandName' ,'cityName' ,'areaDistrict' ,'businessDistrict' ,'salaryDesc' ,'jobExperience' ,'jobDegree' ,'bossName' ,'bossTitle' ,'brandIndustry' ,'brandStageName' ,'brandScaleName' ,'skills' ,'welfareList' ,'securityId' ,'lid' ,'jobLabels' ,'postDescription' ,'address' ,'activeTimeDesc' ] headers = { 'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' , } cookies = { '__zp_stoken__' : '' , } joblist_params = { 'scene' : '1' , 'query' : '前端' , 'city' : '101280600' , 'experience' : '' , 'multiBusinessDistrict' : '' , 'page' : '1' , 'pageSize' : '30' , } if __name__ == '__main__' : responses = request_method('https://www.zhipin.com/wapi/zpgeek/businessDistrict.json' , { 'cityCode' : joblist_params['city' ], }) district_code = [] for obj in responses['businessDistrict' ]['subLevelModelList' ]: district_code.append({ "code" : obj['code' ], "name" : obj['name' ] }) experience_code = request_method('https://www.zhipin.com/wapi/zpgeek/search/job/condition.json' , {})['experienceList' ][3 :7 ] with open ('temp/bossData.csv' , 'w' ,newline='' , encoding='utf-8' ) as f: writer = csv.writer(f) writer.writerow(csv_head) for district in district_code: for experience in experience_code: joblist_params['multiBusinessDistrict' ] = district['code' ] joblist_params['experience' ] = experience['code' ] joblist_params['page' ] = '1' print (f"开始请求数据,区域:{joblist_params['multiBusinessDistrict' ]} ;工作经验:{joblist_params['experience' ]} " ) query_joblist(joblist_params, writer) print (f"数据请求完毕,区域:{joblist_params['multiBusinessDistrict' ]} ;工作经验:{joblist_params['experience' ]} " )
岗位数据获取方法如下,防止请求过于频繁被封ip啥的,所以我会生成0-5的随机数,在每次请求完成后,休眠几秒,这样子虽然会把爬取数据时间拉长,但是起码稳定些也不用怕被封ip啦
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 def get_content (security_id, lid ): response = request_method('https://www.zhipin.com/wapi/zpgeek/job/card.json' , { 'securityId' : security_id, 'lid' : lid, 'sessionId' : '' , }) return response['jobCard' ] def sleep (): random_integer = random.randint(0 , 5 ) print (f'等待{random_integer} 秒' ) time.sleep(random_integer) def query_joblist (params, writer ): responses = request_method('https://www.zhipin.com/wapi/zpgeek/search/joblist.json' , params) for i in responses['jobList' ]: obj = re.compile (f"{joblist_params['query' ]} " , re.S) if len (obj.findall(i['jobName' ])) == 0 : continue sleep() i.update(get_content(i['securityId' ], i['lid' ])) data = [] for key in csv_head: data.append(i[key]) writer.writerow(data) if responses['hasMore' ] == True : params['page' ] = int (params['page' ]) + 1 sleep() query_joblist(params, writer)
4. 运行效果 个人实际体验爬取数据两千多条,大概花费可能2小时左右,触发图形验证频率,还算能接受,具体的爬取效果如下:
爬取完成生成文件效果:
5. 数据分析 前面爬取的数据都是直接存入csv文件中,如果对数据进行进一步处理,存入到数据库中(可选),就可以用这些数据进行简单的数据分析啦,如图是我用爬取的数据做的一些简单的数据分析(不知为啥好大一部分爬取的岗位数据活跃度不高耶,半年前活跃?)
以上就是我实现的一个简单爬取boss直聘岗位数据并且进行数据分析的案例
如果文章中有哪里没有讲明白,或者讲解有误的地方,以及如果有更优美的实现方式的话,欢迎在评论区指导。
感谢阅读 ( ´▽`)