STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

Python之爬虫实战篇-ChangePro

前面章节都是概念和入门,本篇讲解下实战。
我先从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”,然后输入系统的帐号密码,即可在 钥匙串访问 看到添加好的证书。如下图所示:

charles

接着打开“Launchpad”,点击“其他”,找到“钥匙串访问”,打开它,能看到一个名叫“Charles Proxy CA”的证书,移上去点击右键,查看“显示简介”,打开“信任”,选择“始终信任”,输入密码完成整个证书安装。

charles_download


3.安装移动端ac证书

截取移动设备中的https请求,需要在手机上安装相应的证书。点击 Charles 的顶部菜单,选择 “Help” -> “SSL Proxying” -> “Install Charles Root Certificate on a Mobile Device or Remote Browser”,然后就可以看到 Charles 弹出的简单的安装教程。如下图所示:

ac_config

图上大致说明的步骤如下:
1.链接与电脑同局域网的WiFi,设置代理输入192.168.59.70:8888
iPhone:打开WiFi,点进去,拉到最底部,看到“HTTP代理”,选择配置代理,输入电脑ip和端口8888

charles_config_mible

PC:点击保存后,电脑上会弹出以下字样,选择Allow允许

charles_config_pc


2.用Safari浏览器访问chls.pro/ssl去下载证书,弹出以下提示,点击允许,且安装

charles_config_mible


charles_config_mible

安装成功后,可通过“ 设置 ”-> “ 通用 ”-> “ 描述文件与设备管理 ”中查看,且描述文件是绿色已验证状态。


3.证书信任设置
在 iPhone 的 “ 设置 “->” 通用 “-> “ 关于本机 ”-> “ 证书信任设置 ” 中,可以看到安装的证书,将其开关打开启用完全信任。
charles_config_mible

至此关于Charles的所有安装就全部结束,接下来讲解如何使用Charles来抓change app的所有请求信息。


4.charles常用操作和说明

我们从app store下载change,打开change app,可以看到很多https的请求,找到https://api.change.so,默认是看不到https请求的,所以我们需要启用ssl代理。
ssl

如果发现启用ssl代理点不动,或者还是看不到https里面的请求内容,需要进行下一步操作,设置ssl代理允许的url。
因为我们需要访问https://api.change.so,因此,点击 Charles 的顶部菜单,选择 “Proxy” -> “SSL Proxying Settings”,将“Enable SSL Proxying”勾选上,然后就可以点击 add 按钮,添加*.change.so,端口*

ssl

操作完毕后,就可以看到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,需要在调用这两个接口之前使用接口三进行模拟登陆

看下我大致的目录结构:
change_pro

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_strong


下面一一来讲解和观看每个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)


# change pro登录
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 login
open_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']


# 不确定页数,通过data返回的[]去判断
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')


# 登录获取token
access_token = login()
print('登录成功', access_token, sep=':')
# 开始爬数据
spider_start(access_token)

详解知识点:
使用while True,实现递归操作,判断响应的video_list为[]的时候,跳出死循环;
新建一个video_list.json存储每次请求的结果,使用open_file.write逐个写入,写入结束后记得把IO操作close。


效果如下:

git_list


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 login
new_file = open('video-detail.json', 'w+')


# 解析json文本
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次请求接口,效果如下:

get_id


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_source
import time


# 解析json文本
def read_list():
with open('video-detail.json', 'rb+') as f:
video_list = f.readlines()
# 遍历读取的json
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目录,里面在放视频和图片资源。


效果如下:

get_detail_download


file

至此,通过python你就可以爬到change pro所有的海报及所对应的视频共400多g,下载下来以后慢慢学习健身,强身健体啦!


3.Egg.js、MongoDB操作流程

尽管持久化数据(json)和资源(mp4、jpeg)到了硬盘,但是入数据库和实现接口为我所用才是终极目标,有了数据库的数据和实现的接口,就可以展示到自己的网页和app中,尽管有点不厚道。

数据库存储我用的MongoDB、接口实现我用的Egg.js,都是做最简单的查询和分页操作,技术不值得一提。
Egg.js是继承于Koa2的,专为企业而生的Node.js框架。
MongoDB是no sql数据库的一种,存储很方便。

先看下实现后的效果:

getListPage


简单说下实现步骤:

  • 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 json

new_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
# 错误mongodb的json格式
[{ }, { }, { }]

# 正确mongodb的json格式
{ }{ }{ }

2.将id键全部替换成mongdb支持的_id键


效果如下:

get_detail_monogdb


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复数,否则接口实现后数据出不来。


效果如下:

importJson


4.使用egg.js实现简单的分页查询和单个获取接口

数据库有数据后,接下来就是根据业务去实现接口了,这里使用到egg.js。
egg.js快速入门可参考官网:https://eggjs.org/zh-cn/intro/quickstart.html,这里就不一一说明了。
大致文件及结构如下:

pro_dir

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
// 在/app/model下创建一个change_pro.js
// 注意不能写成changPro.js,连词只能以_结尾
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
// 在/app/controller下创建一个changepro.js
'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
// 在/app/router.js新增changepro接口
'use strict';

/**
* @param {Egg.Application} app - egg application
*/
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或网页数据流程都大同小异。
抓包看请求,分析请求结果找规律,获取资源链接持久化到本地,最后下载资源或存数据库。
感谢大家浏览,我是爬虫小白,在成为全栈工程师的路上艰苦奋斗着…