【干货】快速爬取某招聘站点的几个思路
严正声明:
本文仅用于记录爬虫技术研究学习,不提供爬取脚本,所爬数据已删除,读者用于非法用途造成损失,与本文无关。爬虫技术本身并无违法违规之处,爬什么,怎么爬才是导致锒铛入狱的罪魁祸首。
0x0、引言
感觉距离上次发文已经过了很久,结果一看才25天,得益于 娃的呱(gū)呱坠地,每天的时间被延长了许多。当然期间最深的感受莫过于:上班比在家奶娃要轻松太多!
认识杰哥的读者都知道,Python实战类的文章灵感都来源于生活,休完陪产假没多久,素材就来了。一位 资深产品兼原创音乐人 朋友在群里@我:
秒懂,就是想搞多点数据,恰逢金三银四,杰哥也想了解下Android岗位的行情,索性就折腾一下~
0x1、思路一:浏览器模拟访问
打开招聘站点,搜索 "产品经理",滑动到底部发现果然只有10页:
尝试切到第2页,页面URL发生变化:
https://xxx/web/geek/job?query=产品经理&city=101280600&page=2
尝试把page改成11,能请求,但页面数据和第10页是相同的,果然 一次搜索结果最多搞到300条数据。
不过注意,它是 条件搜索,这个条件能搜300条,换个条件又能搜到300条。
换而言之,我们可以通过 设置不同的条件,来采集更多的数据,留意网页顶部的 修改城市和区域:
很明显,我们只要覆盖尽可能多的 城市-区-地名 组合,就可以采集到大量数据。接着就是批量爬取这三层条件的可选值。F12打开 开发者工具 开始抓包,对应的接口都很好抓,顺带写出爬取代码,先是 → 城市编码:
https://xxx/wapi/zpCommon/data/cityGroup.json
然后到 → 区编码和地名编码:
https://xxx/wapi/zpgeek/businessDistrict.json?cityCode={}
可以,万事俱备,只欠拿这些参数去批量调 搜索岗位的接口 了:
https://xxx/wapi/zpgeek/search/joblist.json?scene=1&query=产品经理&city={}&experience=°ree=&industry=&scale=&stage=&position=&jobType=&salary=&multiBusinessDistrict={}:25&multiSubway=&page=1&pageSize=30
正当我以为可以挂机去泡杯茶喝喝,结果一运行:
笑死,调这个接口直接触发站点的 反爬,尝试解决:
- 复制粘贴浏览器请求的所有 请求头,设置到requests中 → 没用;
- ip访问次数限制?上 代理隧道 → 没用,淦,我还以为设置代理没生效;
- Cookies问题?复制浏览器请求里的Cookies塞requests中 → 没用;
又细看了一下,发现是 Cookies加密,每次请求Cookies中的 zp_stoken 都是变化的。
又试了下 M端接口,一样会触发反爬,逆向js太磨人,朋友要得也急,木得时间慢慢玩破解,直接上传统艺能 浏览器自动化。无脑 pyppeteer 模拟,先跳转登录页,预留足够的时间登陆:
登陆完就可以关页面了,这一步主要是让浏览器处于 登录态,接着就是 拼接url访问 → 获得页面源码 → 提取数据:
爬取后的数据:
遍历文件统计下总共爬取到多少条:
18663条,还凑合,不过混进来一些 脏东西:
需要对数据进行清洗,这里判断职位包含"**产品"**字眼为有效数据,朋友说最好是Excel,帮人帮到底:
运行后总共输出有效数据13928条,同时区分城市,写入到excel中~
→
时间关系,只爬取到 区,而且只包含了北上广深杭的数据,不过朋友说够用了,就先酱吧~
0x2、思路二:手机adb模拟访问
其实中途还想过从移动端入手,抓了一波包:
2333,接口加密 + 返回数据加密,最便捷的爬取思路还是自动化啊,在《杰哥带你玩转Android自动化》 中提到过:
所有Android自动化框架和工具中 操作Android设备的功能实现 都基于 adb 和 无障碍服务AccessibilityService。
这里直接用adb来模拟,思路也很简单:无限向上滑动,同时解析布局xml提取所需数据。
先写个导出当前布局xml的工具代码:
接着写个根据resourc_id递归,获取目标结点的方法:
打开导出的布局xml:
可以看到结点对应的资源id,岗位列表 → xxx:id/recyclerView_list,岗位名称 → xxx:id/tv_position_name。
接着干嘛?加上死循环滑动+解析xml提取数据,一步到位?兄嘚想多了,有个难搞的问题:
如何精确控制Recyclerview滑动距离?
怎么说?
你得保证每次滑动准确滑动到 下三项,而且每一项都要 显示完整,因为 uiautomator dump 出的是当前界面的xml。如果滑动到的位置不对,就会出现这种缺失结点的错误数据:
滑太多又会丢失数据,看了一圈没有找到计算RecyclerView滑动距离的公式,想搞清楚,估计得去啃Recyclerview的源码。
这里笔者取下巧:每次滑动尽量少的距离 (不超过一项) + 完整数据校验 (节点数>10) + 结点去重(key:岗位+薪资),直接肝出代码:
挂机午睡,睡醒发现脚本停了,一看数据量:
擦,APP端也限制了搜索结果的条数,只能查询30页,而且总数据还不够300条。
想采集更多数据,可以像思路一一样,通过切换不同的 搜索条件 来达成,检测到数据很久没增加了,就模拟点击切换城市:
0x3、思路三:Xposed Hook
说实话,思路二这种模拟访问解析xml的方式不太靠谱,而且还 慢。我们可以换个角度:
请求返回的数据是 加密 的,但设置到UI控件上的数据是 解密 过的。
所以最简单的思路就是 → Hook UI控件设置数据的方法,拿到解密过的数据。
看了下APK,没加密,直接 jadx反编译,adb获取当前页面的包名和类名:
定位到 GeekSearchActivity
结合APP页面,不难看出 GeekSearchResultFragment
用于显示搜索结果,跟下:
SearchPositionFragment
明显用于显示搜索到的岗位,尝试定位布局xml,搜 R.layout.
没找着,看来是做了 资源混淆。
在Fragment初始化布局,一般会把代码写到 onCreateView()
中,但没在这个类里找到,估计是在父类里进行了封装,跟一下:SearchBaseFragment
→ BaseAwareFragment
→ LazyLoadFragment
:
吼,定义了抽象方法 getLayoutResId()
用于设置布局,回到 SearchPositionFragment
搜下这个方法:
打开布局文件:
定位到了Recyclerview,跟上面我们adb导出的xml id一致,哈哈,从控件资源id着手定位目标代码也是一个小技巧。
Recyclerview设置数据,肯定是通过 Adapter,搜一下,发现了 SearchPositionAdapter
,不过源码中没找到设置数据的方法,又是封装了好几层。
溯源后发现,顶层父类是 com.chad.library.adapter.base.BaseQuickAdapter
,哈,这不就是开源库CymChad/BaseRecyclerViewAdapterHelper 么?这个库设置数据的方法很简单 setNewData()
和 addData()
,搜一下发现这里调用到了:
Hook这两个方法,就可以拿到数据啦,这里要注意一点:
因为子类SearchPositionAdapter并没有重写这两个方法,调用的是父类方法,所以需要 Hook父类。
直接写出Hook代码:
运行后打开招聘软件,可以看到陆续输出一些日志,毕竟Hook的是父类,几乎所有列表设置数据都会调这两个方法。
来到搜索页,输入搜索词搜索,上拉加载更多,可以看到岗位相关的数据被打印出来:
但都是自定义数据类型,我们需要的是它里面岗位信息的字段,直接定位 SearchPositionItemModel
:
明显具体数据类型是这个泛型父类:
data是父类的私有属性,这里通过 反射 来获取,需要修改下访问权限:
运行后重复之前的操作,发现还套了一层:
打开 SearchPositionBean
:
嗯哼,这就是岗位信息相关的字段,可以按需反射获取,也可以直接遍历获取,这里直接打印出来:
运行操作后的输出日志:
可以看到具体的岗位信息了,但依旧有嵌套的Bean,按上面的方法还得打开对应Bean的源码,抠字段,有些蠢了。完全可以写一个 递归获取字段的方法,这里顺带将输出信息组合成Json的形式,方便采集到的数据保存。
运行操作后的输出日志:
看着还不是很整齐,不过Copy到Json格式化工具是没问题的:
如果有数据采集需求,调个第三方json库格式化下,然后 导出文件 或者 调用自己写的数据上传接口(推荐,直接入库美滋滋)。
接着解决第二个问题 → 单次搜索结果30页限制,从哪里入手呢?现象:
列表滑动到底部,触发上拉加载Loading,然后列表添加数据,当加载到30页后,直接不显示上拉Loading。
跟下这个上拉加载的组件:ZPUIRefreshLayout
→ com.scwang.smart.refresh.layout.SmartRefreshLayout
哟,这不是刷新库么 scwang90/SmartRefreshLayout,老朋友了,它提供了一个禁用上拉加载的API:
refreshLayout.setEnableLoadMore(false); //是否启用上拉加载功能
因为SmartRefreshLayout自带混淆,所以没法直接搜这个方法,先找到控件实例:SearchPositionFragment
→ SearchBaseFragment
:
接着回到 SearchPositionFragment
,开启 正则搜索,搜 this.d.*?(false)
就两个匹配结果,很明显是第一个,判断 geekSearchCardResponse == null
就禁用上拉加载,而它是由 gVar.f41043b
赋值的,参数类型是 g
,跟下:
哦吼,不知道都是些啥,hook下这个方法,把入参的字段打印出来:
运行后,第一次加载和上拉加载更多输出日志如下:
不难看出:a → 当前页数,e → 数据总条数,每次加载15条数据。
滑动到30页时,b这个字段为空,有两种可能:后台真的没有返回 或者 后台有返回但客户端做了限制。
我在原先打日志的基础上,又加了判空,到第30页时,我发现有数据:
搜了下<30,<=30,<31,<=31,>30 都没匹配到想要的结果,em...不知道具体做了什么操作。
那就粗暴点,直接Hook SmartRefreshLayout#b(Boolean)
,把入参设置为true,即:禁止关闭上拉加载
接着adb无限滚动:
可以数据不止30页,看了下数据也没有重复,问题二解决~
非常简单,不过本节也到这了,读者感兴趣的话,可以自己试着往下扒,比如:请求的构造规则、响应数据的解密 等,谢谢~
转载自:https://juejin.cn/post/7205508989161013303