Blog Logo

Python之爬虫实战篇-ChangePro

写于2018-12-03 09:07 阅读耗时20分钟 阅读量


前面章节都是概念和入门,本篇讲解下实战。 我先从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

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:

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:

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:

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:

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:

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.组成项全是{}形式的值对,前后不能有[]数组,链接出也不能有逗号

# 错误mongodb的json格式
[{ }, { }, { }]

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

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


效果如下:

get_detail_monogdb


3.使用mongoimport命令导入json文件

前提把mongodb数据库链接打开,执行以下命令实现导入:

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

// 安装
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。

// 在/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数据库查询数据

// 在/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.配置接口路由,实现接口访问

// 在/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服务

// 本地启动默认端口7001,如果想更改端口在package.json中
{
  "scripts": {
    "dev": "egg-bin dev --port 7001"
  }
}

// 运行
npm run dev

看完整篇文章恭喜你,你离爬虫工程师更进一步,想抓取什么样的app或网页数据流程都大同小异。 抓包看请求,分析请求结果找规律,获取资源链接持久化到本地,最后下载资源或存数据库。 感谢大家浏览,我是爬虫小白,在成为全栈工程师的路上艰苦奋斗着...

Headshot of Maxi Ferreira

怀着敬畏之心,做好每一件事。