前面章节都是概念和入门,本篇讲解下实战。 我先从ChangePro入手,ChangePro是潮流文化社区,健身视频每日更新的一款app,之前有所接触,毕竟把健身当作兴趣是我所追求的目标。 健身的成本和习惯是不容易养成的,因为工作和家庭,能给到自己健身的时间和机会真的不多。 change pro官网:https://change.so ,提倡改变是一种习惯
。
介绍完毕后,我要来爬取change社区的所有资源,包括它们的数据、视频、海报等
。 开始之前,想说一句,爬虫是具有时效性的,今天写完爬完整个资源,也许明天就不能使用
,毕竟别人也会增强反爬虫机制,导致你写的爬虫程序失效,唯一能做的就是学习新的爬虫技能。
说下本篇主要知识点:
使用抓包工具charles分析change app内的请求
使用python模拟http请求、io写入、解析json文件
将json数据直接导入monogdb数据库
使用企业级框架egg.js做简单分页,id查询
1.Charles操作流程 1.下载安装Charles Charles是一款功能强大的app抓包神器,下载地址:https://www.charlesproxy.com/download 。
2.安装pc端ac证书 下载安装后,需配置证书,因为想截取https请求需安装Charles的CA证书。 首先我们需要在 Mac电脑上安装证书。点击 Charles 的顶部菜单,选择 “Help” -> “SSL Proxying” -> “Install Charles Root Certificate”,然后输入系统的帐号密码,即可在 钥匙串访问 看到添加好的证书。如下图所示:
接着打开“Launchpad”,点击“其他”,找到“钥匙串访问”,打开它,能看到一个名叫“Charles Proxy CA”的证书,移上去点击右键,查看“显示简介”,打开“信任”,选择“始终信任”,输入密码完成整个证书安装。
3.安装移动端ac证书 截取移动设备中的https请求,需要在手机上安装相应的证书。点击 Charles 的顶部菜单,选择 “Help” -> “SSL Proxying” -> “Install Charles Root Certificate on a Mobile Device or Remote Browser”,然后就可以看到 Charles 弹出的简单的安装教程。如下图所示:
图上大致说明的步骤如下: 1.链接与电脑同局域网的WiFi,设置代理输入192.168.59.70:8888 iPhone:打开WiFi,点进去,拉到最底部,看到“HTTP代理”,选择配置代理,输入电脑ip和端口8888
PC:点击保存后,电脑上会弹出以下字样,选择Allow允许
2.用Safari浏览器访问chls.pro/ssl去下载证书,弹出以下提示,点击允许,且安装
安装成功后,可通过“ 设置 ”-> “ 通用 ”-> “ 描述文件与设备管理 ”中查看,且描述文件是绿色已验证状态。
3.证书信任设置 在 iPhone 的 “ 设置 “->” 通用 “-> “ 关于本机 ”-> “ 证书信任设置 ” 中,可以看到安装的证书,将其开关打开启用完全信任。
至此关于Charles的所有安装就全部结束,接下来讲解如何使用Charles来抓change app的所有请求信息。
4.charles常用操作和说明 我们从app store下载change,打开change app,可以看到很多https的请求,找到https://api.change.so,默认是看不到https请求的,所以我们需要启用ssl代理。
如果发现启用ssl代理点不动,或者还是看不到https里面的请求内容,需要进行下一步操作,设置ssl代理允许的url。 因为我们需要访问https://api.change.so,因此,点击 Charles 的顶部菜单,选择 “Proxy” -> “SSL Proxying Settings”,将“Enable SSL Proxying”勾选上,然后就可以点击 add 按钮,添加*.change.so
,端口*
。
操作完毕后,就可以看到app内访问的https请求
,开始进行分析请求头或请求内容,方便解析下一步,为爬取数据做铺垫。
2.Python操作流程 通过Charles解析https请求后,可以进行下一步的操作,用python模拟用户通过手机操作app发起请求
。最近发现change版本更新了2.5.0,已经屏蔽获取所有列表的接口请求,请求进行了加密操作。 还好我之前爬数据的change版本是2.4.0,在老版本里我发现三个重要请求接口。 接口一,获取所有资源的分页接口:https://api.change.so/v2/timelines?page=%d&type=video
参数page从1开始进行分页查询。 接口二,获取具体某个资源的接口:https://api.change.so/v2/videos/%d
参数videos/后面是具体的资源id。 接口三,用户登录的接口:https://api.change.so/v2/users/sign_in
通过这三个接口,我们就能获取到该app里面所有的视频、图片和数据,分析发现调用接口一,尽管能获取到大部分数据,但视频的url和描述等数据都没有,必须调用接口二才能得到。 那么思路就比较简单: 1.通过接口一获取所有列表数据
; 2.通过接口一得到的列表数据得到的id,进行接口二获取单个详细数据
; 3.调用接口一、接口二发现,有些视频是需要vip才能访问的,我购买了年费vip,所以按理能正常获取所有列表,因此还得带上用户token,需要在调用这两个接口之前使用接口三进行模拟登陆
。
看下我大致的目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Download.py : 简单封装的工具类,实现公用的功能,比如change 登陆、下载资源等。 spider_list.py : 用自己的账号登陆成功后,通过递归/死循环,一直调用接口一获取列表数据,直到无数据返回。 spider_detail.py : 解析接口一获取的数据,用自己的账号登陆成功后,通过id进行接口二的调用,获取到更详细的数据。 spider_download.py : 通过接口二获取的视频url和图片url,将健身视频和海报下载到本地电脑。 spider_json_to_mongodb.py 通过接口二获取的数据,需处理下id字段,换成_id字段,方便直接导入mongodb数据库为我所用。
python的强大之处在于代码简洁
,以上所有功能通过1、2KB,最多不超过50行代码实现,简直就是帅到爆~
下面一一来讲解和观看每个python文件运行的实际效果:
工具类Download.py: 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 import requests, os, click def get_source(url, name, folder ='./' ): if not os.path.exists(folder): os.makedirs(folder) fpath = os.path.join(folder, name) if not os.path.exists(fpath): print (fpath) resp = requests.Session().get (url, stream =True ) length = int(resp.headers.get ('content-length' )) label = 'Downloading {} {}kb' .format(name, int(length/1024)) with click.progressbar(length =length, label =label) as progressbar: with open(fpath, 'wb' ) as f: for chunk in resp.iter_content(chunk_size =1024): if chunk: f.write(chunk) progressbar.update(1024) def login(): url = 'https://api.change.so/v2/users/sign_in' data = { 'country_code' :86, 'jpush_registration_id' :'18171adc0326d0a4fc6' , 'telephone' : 15828274523, 'password' : '就不告诉你' } headers = { 'app-version' : '2.4.0' , 'api-version' : '1538323200' , 'app-device' : 'iPhone 7 (CDMA)' , 'device-id' : '45EA3966-AE09-4A82-AEBF-6FD1E2B60405' , 'accept-language' : 'zh-Hans-CN;q=1' , 'accept-encoding' : 'br,gzip,deflate' , 'user-agent' : 'Change/2.4.0 (iPhone; iOS 12.1; Scale/2.00)' , 'os-version' : '12.1' } response = requests.post(url =url, data =data, headers =headers) data = response.json() return data['data' ].get ('authentication_token' )
详解知识点: 该文件get_source方法可以实现视频下载、图片下载功能,下载二进制文件通用的; login方法模拟用户app请求,因此带上请求头伪装成正常访问,请求成功后返回token用于其他地方使用。
1.执行spider_list.py: 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 import requests import json from utils.Download import loginopen_file = open('video-list.json' , 'w+' ) def get_url(url, token): headers = { 'host' : 'api.change.so' , 'accept' : '*/*' , 'authorization' : 'Token token=%s' % token, 'app-version' : '2.4.0' , 'api-version' : '1538323200' , 'app-device' : 'iPhone 7 (CDMA)' , 'device-id' : '45EA3966-AE09-4A82-AEBF-6FD1E2B60405' , 'accept-language' : 'zh-Hans-CN;q=1' , 'accept-encoding' : 'br,gzip,deflate' , 'user-agent' : 'Change/2.4.0 (iPhone; iOS 12.1; Scale/2.00)' , 'os-version' : '12.1' } print (url) response = requests.get (url, headers) data = response.json() return data['data' ]['videos' ] def spider_start(token): page = 1 while True : url = 'https://api.change.so/v2/timelines?page=%d&type=video' % page video_list = get_url(url, token) if video_list: videos = {'videos' : video_list} open_file.write((json.dumps(videos) + '\n' )) page += 1 else : print ('ending' ) break open_file.close() print ('OK' ) access_token = login() print ('登录成功' , access_token, sep =':' )spider_start(access_token)
详解知识点: 使用while True,实现递归操作,判断响应的video_list为[]的时候,跳出死循环; 新建一个video_list.json存储每次请求的结果,使用open_file.write逐个写入,写入结束后记得把IO操作close。
效果如下:
2.执行spider_list.py: 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 import requests import json from utils.Download import loginnew_file = open('video-detail.json' , 'w+' ) def read_list(): with open('video-list.json' , 'rb+' ) as f: video_list = f.readlines() print ('请求次数' , len(video_list), sep =':' ) num = 0 # 登录获取token access_token = login() print ('登录成功' , access_token, sep =':' ) # 遍历读取的json for line in video_list: video = json.loads(line.decode('utf-8' )) videos = video['videos' ] for film in videos: num += 1 detail = get_video_detail(film['id' ], access_token) videos = {'video' : detail} # 放入video-detail.json new_file.write((json.dumps(videos) + '\n' )) print ('视频总数' , num, sep =':' ) def get_video_detail(video_id, token): url = 'https://api.change.so/v2/videos/%d' % video_id headers = { 'host' : 'api.change.so' , 'accept' : '*/*' , 'authorization' : 'Token token=%s' % token, 'app-version' : '2.4.0' , 'api-version' : '1538323200' , 'app-device' : 'iPhone 7 (CDMA)' , 'device-id' : '45EA3966-AE09-4A82-AEBF-6FD1E2B60405' , 'accept-language' : 'zh-Hans-CN;q=1' , 'accept-encoding' : 'br,gzip,deflate' , 'user-agent' : 'Change/2.4.0 (iPhone; iOS 12.1; Scale/2.00)' , 'os-version' : '12.1' } print (url) response = requests.get (url =url, headers =headers) data = response.json() return data['data' ] read_list() new_file.close()
详解知识点: 读取json文本时可以使用rb+
模式读取,read byte add模式,简单说就是通过二进制流不断读取video_list.json里面的内容,读取大型文件比较好的方式;with open ...
语法糖读取结束后自动关闭IO流; 读取video_list.json后,将每行接口返回的videos的数据里面的id挨个传给接口二,获取更详细的数据后,写入到新的video-detail.json文件里; 写入的时候记得加上\n
可自动换行,好处是为一下步再次解析读取做准备。
挨个getId获取详细数据,一共有3449次请求接口,效果如下:
3.执行spider_list.py: 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 json from utils.Download import get_sourceimport time def read_list(): with open ('video-detail.json' , 'rb+' ) as f: video_list = f.readlines() for line in video_list: video = json.loads(line .decode('utf-8' )) video = video['video' ] time .sleep(1 ) video_id = video['id' ] folder = '../change_pro/%d' % video_id poster_url = video['poster' ] post_name = poster_url[poster_url.rfind('/' )+1 :] video_url = video['url' ] video_name = video_url[video_url.rfind('/' ) + 1 :] get_source(poster_url, post_name, folder ) get_source(video_url, video_name, folder ) print('ID:' , video_id, sep=':' ) print('图片:' , poster_url, sep=':' ) print('视频:' , video_url, sep=':' ) read_list()
详解知识点: 请求结束后,得到最终数据video-detail.json,里面包含了视频的地址,海报图片的地址,这样就可以通过get_source方法进行下载保存,为我所用; 下载资源的目录可以提前定义好,我是让它们放在爬虫项目change_pro_spider的同一级,生成change_pro目录,且每次下载资源时先新建一个id目录,里面在放视频和图片资源。
效果如下:
至此,通过python你就可以爬到change pro所有的海报及所对应的视频共400多g
,下载下来以后慢慢学习健身,强身健体啦!
3.Egg.js、MongoDB操作流程 尽管持久化数据(json)和资源(mp4、jpeg)到了硬盘,但是入数据库和实现接口为我所用才是终极目标,有了数据库的数据和实现的接口,就可以展示到自己的网页和app中
,尽管有点不厚道。
数据库存储我用的MongoDB、接口实现我用的Egg.js,都是做最简单的查询和分页操作,技术不值得一提。 Egg.js是继承于Koa2的,专为企业而生的Node.js框架。 MongoDB是no sql数据库的一种,存储很方便。
先看下实现后的效果:
简单说下实现步骤:
1.将video-detail.json转成mongdb支持导入的json格式
2.使用mongoimport
命令将json数据直接导入mongdb数据库
3.实现Egg.js实现getList分页及getId单个查询接口
1.mongdb的_id键 mongodb中存储的文档必须有一个”_id”键。这个键的值可以是任何类型的,默认类型是ObjectId,共12个字节,由时间戳+机器+PID+计数器构成,确保唯一性。 mongodb有个好玩的玩法是可以通过mongoimport命令导入json文件的数据到数据库,_id默认类型是ObjectId
,类型可换成String或Number
。 这样一来,通过python进行数据爬取,将获取到的数据可直接导入到mongodb数据库中进行接口调用。因此,在对数据进行爬取前,可以先定义好Schema,尤其是定义好_id
。
2.用python转义成mongodb支持的json 执行spider_json_to_mongodb.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import jsonnew _file = open('video-detail-mongodb.json' , 'wb+' )with open('video-detail.json' , 'rb+' ) as f: video_list = f.readlines() print('请求次数' , len(video_list), sep=':' ) # 遍历读取的json num = 0 for line in video_list: video = json.loads(line.decode('utf-8' )) num += 1 json_txt = video['video' ] json_txt['_id' ] = json_txt['id' ] json_txt.pop('id' ) new _str = json.dumps({'video' : json_txt }) # 将处理后的json文本存入到新文本,好做mongodb到导入 new _str = new _str [new _str .index(':' )+2 :new_str .rfind('}' )] new _file .write(new _str .encode('utf-8' )) print('视频总数' , num, sep=':' ) print('OK' ) new _file .close()
详解知识点: mongodb支持的json格式有两点需要注意: 1.组成项全是{}
形式的值对,前后不能有[]
数组,链接出也不能有,
逗号
1 2 3 4 5 [{ }, { }, { }] { }{ }{ }
2.将id键全部替换成mongdb支持的_id键
效果如下:
3.使用mongoimport命令导入json文件 前提把mongodb数据库链接打开,执行以下命令实现导入:
1 sudo mongoimport -h 127.0.0.1:27017 -d spider -c changepros ./video-detail-mongodb.json --mode upsert
-d指数据库名称,自定义spider; -c指数据库表名称,自定义changepros,需注意的点是记得加s
复数,否则接口实现后数据出不来。
效果如下:
4.使用egg.js实现简单的分页查询和单个获取接口 数据库有数据后,接下来就是根据业务去实现接口了,这里使用到egg.js。 egg.js快速入门可参考官网:https://eggjs.org/zh-cn/intro/quickstart.html ,这里就不一一说明了。 大致文件及结构如下:
1.安装egg.js完毕后,安装一下egg-mongoose插件 该插件实现egg.js与mongodb的交互,插件github官网:https://github.com/eggjs/egg-mongoose
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 // 安装npm i egg-mongoose --save // 配置mongoose// 在/config/ config.default.js中新增mongoose配置config.mongoose = { client: { url: 'mongodb://127.0.0.1:27017/spider' , options: { server: { socketOptions: { keepAlive: true, keepAliveInitialDelay: 300000 , }, reconnectTries: Number.MAX_VALUE, reconnectInterval: 500 , poolSize: 20 , }, }, }, DEBUG: true, // 是否输出查询日志 }; // 启用mongoose插件// 在/config/ plugin.js中启用mongoose插件exports.mongoose = { enable: true, package: 'egg-mongoose' , };
2.创建model模型,与之前的表名称对应 之前导入的数据库表名称是changepros,单数是changepro,因此创建一个名为ChangePro
的model。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module .exports = app => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const ChangeProSchema = new Schema({ _id : Number }); return mongoose.model('ChangePro' , ChangeProSchema); }
创建Schema的时候,一定要定义_id,且让它的值为Number
。
3.创建controller服务,操作mongodb数据库查询数据
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 'use strict' ;const Controller = require ('egg' ).Controller;class ChangeProController extends Controller { async info ( ) { const { ctx } = this ; ctx.body = await ctx.model.ChangePro.findById(ctx.params.id); }; async getList ( ) { const { ctx } = this ; let pageSize = Number (ctx.query.pageSize) || 5 ; let currentPage = Number (ctx.query.pageNow) || 1 ; let skipnum = (currentPage - 1 ) * pageSize; let condition = {}; let sort = {}; let opt = 'name describes url poster tag_list share' ; let list = await ctx.model.ChangePro.find(condition, opt) .skip(skipnum) .limit(pageSize) .sort(sort); let total = await ctx.model.ChangePro.countDocuments(condition); ctx.body = { data : list, total : total, success : true }; } } module .exports = ChangeProController;
4.配置接口路由,实现接口访问
1 2 3 4 5 6 7 8 9 10 11 12 13 'use strict' ;module .exports = app => { const { router, controller } = app; router.get('/' , controller.home.index); router.get('/changepro/row/:id' , controller.changepro.info); router.get('/changepro/getList' , controller.changepro.getList); };
5.启动egg.js server服务
1 2 3 4 5 6 7 8 9 // 本地启动默认端口7001 ,如果想更改端口在package.json中{ "scripts" : { "dev" : "egg-bin dev --port 7001" } } // 运行npm run dev
看完整篇文章恭喜你,你离爬虫工程师更进一步,想抓取什么样的app或网页数据流程都大同小异。 抓包看请求,分析请求结果找规律,获取资源链接持久化到本地,最后下载资源或存数据库。 感谢大家浏览,我是爬虫小白,在成为全栈工程师的路上艰苦奋斗着…