STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

全桟知识体系(三)

本篇文章内容较长,可根据需要阅读,大纲如下:

  • 回顾当下
  • 为什么开发app
  • 享学习的由来
  • 享学习数据
    • 解析返回值json
    • 数据爬取
    • 整理数据成mongodb导入的形式
    • 导入json到mongodb数据库
  • 总结

1.回顾当下

apps

很高兴能再次和大家分享“全栈”这个词,从2017年的全栈一、到2018年的全栈二,再到今年2019年的全栈三,每篇的侧重点都是不同的。

全桟知识体系(一):主要介绍什么是全栈、大牛们对全栈的看法及全栈的意义,找车场就此诞生。

全桟知识体系(二):主要介绍全栈的实际操作,将一款真正意义上的产品从无到有培育出来,享健身就此诞生。

全桟知识体系(三)主要介绍全栈的另一面,数据准备工作,享学习就此诞生。

享学习是我研发的最后一个app,在自己奋斗的年龄不想留下青春遗憾,随着年龄的增长,没什么精力折腾了。


2.为什么开发app

有许多读者或同事问我这样一个问题,为什么你那么如此喜欢开发app呢?怎么不是开发页面、管理后台、或游戏呢?
第一点,也是最重要的一点,因为我热爱开发app呀~
出于兴趣做事,只是过程特别的漫长和坎坷,结果是迟早的事情,没什么大不了的。
hard

身为前端开发工程师,选择的方向其实也蛮多,可以做小程序、可以做H5小游戏、可以做PC管理后台、可以做H5页面(app内、客户端内、浏览器内)、可以做后台node、可以做canvas效果、可以做3D效果webgl、可以做app等等…选择一个自己感兴趣的方向,并为此努力,会有很好的提升~

第二点,通过js能开发与原生相媲美的app,学习java、object-c成本太高,精力有限,即使学习,也没有专门从事iOS、android开发的同事强。

第三点,通过app的开发,能体验到最新的技术潮流。比如react、react-native、react-navigation、redux、immutable、thunk、saga、styled-components…除了react技术栈之外,最近比较火的还有用dart语言、flutter开发app…

第四点,将自己的想法变成现实,是一件特别酷的事情,开发app能兑现这句话。
找车场:帮助司机找到附近的停车场,解决停车问题;
享健身:帮助健身小白坚持锻炼身体,解决健康问题;
享学习;帮助大家享受学习享受文化,解决学习问题;


3.享学习的由来

首页:

sredy


详情:

sredy


理由有以下四点:
1.希望通过react native做个app音频播放器
2.爬取过某在线听书平台的大数据,不用蛮可惜的
3.去年注册的享学习商标到手,不用蛮可惜的
4.一句流浪汉说过的话

good

距离这个浩如烟海的文化本身来说,我们都是井底之蛙,还不够还不够,所以一定要不断的学习,不断的学习。

谁能想象这句话竟出自一个衣衫褴褛,其貌不扬的流浪汉之口。
相信很多人和我一样,浩如烟海这个成语第一次听到~

在app的介绍里面也有说明:
app

引用抖友的评论:

你满嘴诗意,却落魄街头,你纯情之心,却事态百凉,念中华之精华,阅沧海之琼书,你有用,却也无用。

小丑在殿堂,大师在流浪。

有时感觉我们挺残酷的,中华名族的传统文化正在慢慢淡出上班族的视线,《左传》、《尚书》、《史记》、《庄子》、《老子》、《论语》…
希望大家能多关注关注中国文化~享受学习

年轻的时候以为不读书不足以了解人生,直到后来才发现,如果不了解人生是读不懂书的,读书的意义大概就是,用生活所感去读书,用读书所得去生活吧。

接下来正式开始干货分享~


4.享学习数据

python当爬虫语言,在我看来是最合适的,尽管js、java等编程语言都有类似的爬虫框架,但是python语言的简洁态度,几行代码搞定你需要的IO操作、HTTP请求,变得十分的easy。

4.1.解析返回值json

通过Charles,可以看到喜马拉雅app里面的许多接口及其返回值。筛选自己需要的接口,观察其返回值,分析json然后为我所用。
test

享学习app的核心功能是播放,因此获取mp3地址存到自己的数据库里就是最最核心的点。


看完大致的接口,得到以下接口信息及返回值,最后分析可以创建所需的model对象:

1.获取所有分类

接口:

1
http://mobile.ximalaya.com/m/category_tag_menu

返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
id: 39,
name: 'renwen',
title: '人文',
tag_list: null(没啥用)
}

{
id: 8,
name: 'finance',
title: '商业财经',
tag_list: ["股指期货", "互联网金融", "创业密码", "商业聚焦", "投资理财", "财经评论", "财经资讯", "消费指南"](没啥用)
}

title属于一级分类,tag_list应该属于二级分类即标签,后面在具体的专栏里有三级分类showTagList,因此二级分类用处不大。

引申出第一个model,
类别表category,格式如下:

1
2
3
4
5
{
id: 0, //分类ID
name: 'xxx', //分类昵称
title: 'xxx', //分类标题
}

2.通过分类id获取该分类下的所有专栏

接口:

1
/mobile/discovery/v2/category/metadata/albums/%s?calcDimension=hot&categoryId=%d&device=iPhone&pageId=%d&pageSize=20&version=6.5.30

参数:

1
2
3
4
(ts, cid, page)
ts:当前时间戳
cid:分类id
page:分页的索引

返回值:

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
{
id: 6855034,
title: "猩猩时间:超级财智养成记",
uid: 45158622,
cover: [
"http://imagev2.xmcdn.com/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg!op_type=5&upload_type=album&device_type=ios&name=small",
"http://imagev2.xmcdn.com/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg!op_type=5&upload_type=album&device_type=ios&name=medium",
"http://imagev2.xmcdn.com/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg!op_type=5&upload_type=album&device_type=ios&name=large",
"http://imagev2.xmcdn.com/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg!op_type=5&upload_type=album&device_type=ios&name=large_pop",
"http://imagev2.xmcdn.com/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg!op_type=5&upload_type=album&device_type=ios&name=web_large",
"http://fdfs.xmcdn.com/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg"
],
categoryId: 8,
lastUptrack: {
id: 139781155,
cover: "group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446.jpg",
title: "尽管股市跌成狗,可我还是赚到了钱 [关键词:敬畏]",
at: 1543246161000
},
intro: "重塑金融思维,打造财智大脑",
tracks: 332,
playCounts: 17432248,
playTrackId: 139781155,
showTagList: [
{"tagId":187,"tagName":"脱口秀"},
{"tagId":358,"tagName":"理财"},
{"tagId":882,"tagName":"午休"},
{"tagId":151,"tagName":"睡前"}]
}

研究可以发现,接口里返回20条数据,每条数据代表一个专栏,一个专栏就返回以上很多的信息。


我们可以通过showTagList获取到该专栏的标签,如果把所有专栏的showTagList都获取到的话,去重就能得到喜马拉雅所有的标签。

引申出第二个model,
标签表tag,格式如下:

1
2
3
4
{
id: 0, //标签ID
name: '' //标签名称
}

我们发现返回的cover是个数组,返回的是不同大小的专栏图片,我们只需要获取类似/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg的地址,host和!后面自定义拼接就能显示不同大小的图片,因此cover只需要定义成一个字符串即可。

引申出第三个model,
专栏表album,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
id: 0, //专栏ID
title: 'xxx', //标题
subTitle: 'xxx', //子标题
uid: 0, //作者ID 外键
cover: 'group44/M05/3A/FC/wKgKjFsOyb-AsbJXAAXoNNozuKA220.jpg', //图标 小中大(正方)、原大、网页大、原图
categoryId: 0, //分类ID 外键
intro: '',//简介
tracks: [trackId], //音频ID 外键
playCounts: 0, //总播放次数
showTagList: [tagId] //标签列表 标签ID外键
}

3.通过专栏id获取该专栏下的所有音频

接口:

1
/mobile/v1/album/track/%s?albumId=%d&device=iPhone&isAsc=true&isQueryInvitationBrand=true&pageId=%d&pageSize=20

参数:

1
2
3
4
5
(ts, aid, page)
需要3个参数
ts:当前时间戳
aid:专栏id
page:分页索引

返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
id: 139781155,
title: "尽管股市跌成狗,可我还是赚到了钱 [关键词:敬畏]",
cover: [(后台逻辑可实现)
"http://fdfs.xmcdn.com/group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446_web_meduim.jpg",
"http://fdfs.xmcdn.com/group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446_web_large.jpg",
"http://fdfs.xmcdn.com/group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446_mobile_large.jpg"
],
duration: 649,
playUrl: [
"http://audio.xmcdn.com/group53/M04/16/E9/wKgLfFv8LHyzje50ACejORZEQu0586.mp3",
"http://audio.xmcdn.com/group52/M06/15/5A/wKgLcFv8ESbRYU9ZAE9GKbkecHw907.mp3",
"http://audio.xmcdn.com/group52/M06/15/83/wKgLe1v8ETGSFCa7AFA8wQLY3h0062.m4a",
"http://audio.xmcdn.com/group52/M05/16/88/wKgLcFv8HiSxFDW1AB6vnJV1zIw706.m4a"
],
download: {
aacSize: 2011036,
aacUrl: "http://download.xmcdn.com/group52/M05/16/88/wKgLcFv8HiSxFDW1AB6vnJV1zIw706.m4a",
size: 2597756,
url: "http://download.xmcdn.com/group52/M06/15/7C/wKgLe1v8ER6zewDiACejfHiT2yg864.aac"
},
playtimes: 12316,
image: "http://imagev2.xmcdn.com/group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446.jpg!op_type=3&columns=640&rows=640",
}

引申出第四个model,
音频表track,格式如下:

1
2
3
4
5
6
7
8
{
id: 0, //音频ID
title: '', //音频标题
cover: 'group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446', //图标 小中大(正方)图片640
duration: 0, //持续时间(秒)
play: '', //播放地址
playtimes: 0, //播放次数
}

4.通过专栏id获取该专栏的作者信息

接口:

1
/mobile/v1/album/detail/%s?albumId=%d&device=iPhone

参数:

1
2
3
4
(ts, aid)
需要2个参数
ts:当前时间戳
aid:分类id

返回值:

1
2
3
4
5
6
7
8
9
10
11
12
{
id: 45158622,
nickname: "猩猩来了",
followers: 201338,
followings: 3,
personDescribe: "一汐财经",
personalSignature: "猩猩来了工作室致力于开创新的内容品类,为新一代消费者提供真实、好玩,有观点的财经内容产品。",
ptitle: "一汐财经",
smallLogo: "http://fdfs.xmcdn.com/group42/M02/B8/8B/wKgJ9FqnVDXDE82cAAC4nX6nYKE96_mobile_small.jpeg",
albums: 6,
tracks: 612
}

引申出第五个model,
作者表user,格式如下:

1
2
3
4
5
{   
id: 0, //作者ID
nickname: '' //作者昵称
avator: '', //作者头像
}

至此,我们发现有基本的5个modle对象,分别是category分类表tag标签表album专栏表track音频表user作者表五类。
关系映射可简单理解为分类里有专栏;专栏里有音频、作者;标签通过所有专栏标签去重获取。分类与专栏一对多,专栏对音频一对多、专栏对标签一对多、专栏对作者一对一。


4.2.数据爬取

看下数据爬取的大致流程,思路可能会更清晰一些。
数据爬取的流程如下:
spider
首先获取所有的分类,然后查询每个分类的所有专栏,接着查询每个专栏的所有音频和该作者,通过音频查询每个音频对应的介绍和mp3地址。
获取完所有专栏的时候,可以获取每个专栏的标签,最后获取到所有专栏的标签。

数据爬取阶段主要将返回的数据,持久化到json文件中,为后续做准备。

1.爬取喜马拉雅所有分类,生成category.json
1
2
3
4
5
6
7
8
9
10
def save_category():
folder = '../xmly/'
file = os.path.join(folder, 'category.json')
if not os.path.exists(folder):
os.makedirs(folder)
if not os.path.exists(file):
with open(file, 'wb') as f:
url = 'http://mobile.ximalaya.com/m/category_tag_menu'
category = requests.get(url).json()
f.write(json.dumps(category).encode('utf-8'))

定义一个save_category方法,判断xmly目录是否存在,如果不存在则创建该目录;判断category.json文件是否存在,如果不存在则发起http请求,将返回的json内容写入category.json,存在说明已经请求过了,无需再次请求。

仅仅10行代码执行了创建xmly目录,打开io流,发起http请求,将返回值解析成json字符串、写入json字符串流数据到category.json、关闭io流、最后生成带数据的cagegory.json文件。python厉害吧~

得到如图的内容:
category


category.json:
category_json


2.爬取喜马拉雅分类的所有专栏,生成n个album.json

这个地方的步骤可能会稍复杂些,具体实现下:
1.读取刚刚cagegory.json里面的分类,获取所有分类的id

1
2
3
4
5
6
7
8
9
def read_category():
with open('../xmly/category.json', 'rb') as f:
line_bytes = f.read()
data = json.loads(line_bytes.decode('utf-8'))
category_total = data['data']['category_count']
category_list = data['data']['category_list']
print('分类共', category_total, sep=':个')
for category in category_list:
print(category['id'], category['title'], sep='-------')

2.通过请求获取具体一个分类的所有的专栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def save_albums(cid):
page = 1
albums = []
while True:
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/discovery/v2/category/metadata/albums/%s?calcDimension=hot&categoryId=%d&device=iPhone&pageId=%d&pageSize=20&version=6.5.30' % (ts, cid, page)
data = get_url(url)
albums_list = data.get('list', [])
if albums_list:
page += 1
albums = albums + albums_list
else:
break
print(cid, '----------------专栏数:', len(albums))
return albums

3.写入到对应的json中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if not is_existed('albums/%d.json' % cid):
print(cid, '------------------start')
while True:
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/discovery/v2/category/metadata/albums/%s?calcDimension=hot' \
'&categoryId=%d&device=iPhone&pageId=%d&pageSize=20&version=6.5.30' \
% (ts, cid, page)
data = get_url(url)
albums_list = data.get('list', [])
if albums_list:
page += 1
albums = albums + albums_list
else:
print(cid, '------------------ending')
break
print(cid, '----------------专栏数:', len(albums))
save_json('albums/', '%d.json' % cid, {'albums': albums})

2和3有个相同的while True,意思是一直获取分页list的数据,直到无list数据的时候,退出死循环,获取所有栏目的专辑结束。


3.优化一下代码

我们会发现,很多地方都用到了获取请求写入json读取json判断目录和文件是否存在等功能,因此我们可以提取封装成一个utils工具库。
utils/index.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
53
54
55
56
57
58
59
60
61
62
import requests, random, json, os


# 通用的获取接口
def get_url(url):
headers = {
'host': 'mobile.ximalaya.com',
'accept': '*/*',
'user-agent': 'ting_v6.5.30_c5(CFNetwork, iOS 12.1, iPhone9,1)',
'accept-language': 'zh-cn',
'accept-encoding': 'gzip, deflate',
'connection': 'keep-alive'
}
host_ips = [
'http://180.153.255.6',
'http://114.80.170.74',
'http://114.80.161.20',
'http://114.80.161.18',
'http://114.80.142.163'
]
index = random.randint(0, 4)
new_ip = host_ips[index]
if 'mobile.ximalaya.com' in url:
url = url
else:
url = new_ip + url
try:
print(url)
response = requests.get(url=url, headers=headers)
return response.json()
except Exception as e:
print('request Exception')
print('sleep--------------------------------------10 seconds')
time.sleep(10)
os.system('python3 /Users/wuwei/python/xmly_spider/review.py')


# 通用的写入json
def save_json(folder, file_name, json_data):
folder = '../xmly/' + folder
file = os.path.join(folder, file_name)
if not os.path.exists(folder):
os.makedirs(folder)
if not os.path.exists(file):
with open(file, 'wb') as f:
f.write(json.dumps(json_data).encode('utf-8'))


# 通用的读取json
def read_json(folder, file_name):
folder = '../xmly/' + folder
file = os.path.join(folder, file_name)
if os.path.exists(file):
with open(file, 'rb') as f:
line_bytes = f.read()
data = json.loads(line_bytes.decode('utf-8'))
return data['data']


# 通用的是否存在
def is_existed(file):
return os.path.exists('../xmly/'+ file)

讲解一下get_url方法,将喜马拉雅的分布式服务器的主机IP通过随机的方式,任一获取,这样每次发出的请求,服务器接收是不一样的。这样不容易被反爬虫机制给处置,比如将自己的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
37
38
39
40
41
42
from utils.index import get_url, is_existed, save_json, read_json, distinct
import time


# 获取所有分类
def save_category():
if not is_existed('category.json'):
url = 'http://mobile.ximalaya.com/m/category_tag_menu'
data = get_url(url)
save_json('', 'category.json', data)


# 获取所有分类id
def get_category_ids():
data = read_json('', 'category.json')
print('分类共', data['category_count'], sep=':个')
for category in data['category_list']:
print(category['id'], category['title'], sep='-------')
save_albums(category['id'])


# 获取某类的所有专栏
def save_albums(cid):
page = 1
albums = []
if not is_existed('albums/%d.json' % cid):
print(cid, '------------------start')
while True:
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/discovery/v2/category/metadata/albums/%s?calcDimension=hot' \
'&categoryId=%d&device=iPhone&pageId=%d&pageSize=20&version=6.5.30' \
% (ts, cid, page)
data = get_url(url)
albums_list = data.get('list', [])
if albums_list:
page += 1
albums = albums + albums_list
else:
print(cid, '------------------ending')
break
print(cid, '----------------专栏数:', len(albums))
save_json('albums/', '%d.json' % cid, {'data': albums})

得到如图的内容:
albums


albums里面0.json:
album_json


4.获取所有专栏作者和标签的基本信息

上一步我们通过查询单个分类,获取到所有的专栏。接下来,我们需要遍历读取整个分类category.json,再遍历读取单个分类获取到的专栏json,最后获取到所有专栏id。
思路大致如下:
1.获取所有专栏id

1
2
3
4
5
6
7
8
9
def get_all_albums():
data = read_json('', 'category.json')
# 遍历所有分类
for category in data['category_list']:
category_id = category['id']
albums = read_json('albums/', '%d.json' % category_id)
# 遍历该分类的所有专栏
for album in albums:
print(album['albumId')

2.获取一个专栏的作者和标签信息

1
2
3
4
5
def get_album_info(aid):
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/v1/album/detail/%s?albumId=%d&device=iPhone' % (ts, aid)
data = get_url(url)
return data

3.将前两步合在一起,完成所有专栏的获取。
4.尽管我们有专栏的许多信息,但是我们只需要其中的一部分,那么我们可以提前定义好自己需要的json文件,后续可直接通过mongodb命令将json直接导入数据库,生产表。
album:

1
2
3
4
5
6
7
8
9
10
11
12
{
id: 0, //专栏ID
title: 'xxx', //标题
subTitle: 'xxx', //子标题
cover: 'group44/M05/3A/FC/wKgKjFsOyb-AsbJXAAXoNNozuKA220.jpg', //图标 小中大(正方)、原大、网页大、原图
intro: '',//简介
playCounts: 0, //总播放次数
uid: 0, //作者ID 外键
categoryId: 0, //分类ID 外键
showTagList: [tagId] //标签列表 标签ID外键
tracks: [trackId], //音频ID 外键
}

tracks,暂时获取不到,但其余字段都有了,因此没太大关系,后续加上tracks外键即可。


5.继续优化代码

继续提取封装方法,放到utils/index.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
53
54
55
56
57
58
# 通用的json数组去重
def distinct(json_arr):
return [dict(t) for t in set([tuple(d.items()) for d in json_arr])]


# 通用的json转换
def to_album(album, album_extra, user, category_id):
# 无标签,特殊处理,默认小说
if 'showTagList' not in album_extra.keys():
album_extra['showTagList'] = [{
'tagId': 1,
'tagName': '小说'
}]
# 无图片,特殊处理,默认为空字符串
cover = album.get('coverSmall', '')
if cover:
if 'imagev2.xmcdn.com' in cover:
cover = cover[cover.index('group'):cover.rindex('!')]
data_album = {
'_id': album['albumId'],
'title': album['title'],
'subTitle': album.get('intro', ''),
'playsCounts': album['playsCounts'],
'cover': cover,
'categoryId': category_id,
'uid': user['uid'],
'showTagList': to_tag_id(album_extra['showTagList']),
'intro': album_extra['intro']
}
return data_album


def to_user(user):
avator = user.get('smallLogo', '')
if avator:
if 'imagev2.xmcdn.com' in avator:
avator = avator[avator.index('group'):avator.rindex('!')]
user_new = {
'_id': user['uid'],
'nickname': user['nickname'],
'avator': avator,

}
return user_new


def to_tag(tags):
tag_new = []
for tag in tags:
tag_new.append({'_id': tag['tagId'], 'name': tag['tagName']})
return tag_new


def to_tag_id(tags):
tag_new = []
for tag in tags:
tag_new.append(tag['tagId'])
return tag_new

优化后的获取所有专栏:

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
# 获取所有专栏
def get_all_albums():
data = read_json('', 'category.json')
# 遍历所有分类
for category in data['category_list']:
category_id = category['id']
albums = read_json('albums/', '%d.json' % category_id)
# 判断目录是否存在
if not is_existed('detail/%d' % category_id):
# 遍历该分类的所有专栏
get_albums_for_cid(albums, category_id)
else:
print('category_id:%d------------------is_existed' % category_id)


# 获取该分类下的所有专栏
def get_albums_for_cid(albums, category_id):
# 定义两个集合获取该分类的所有专栏和标签
albums_cid = []
tag_cid = []
user_cid = []
# 遍历该分类的所有专栏
for album in albums:
data = get_album_info(album['albumId'])
if data['ret'] == 0:
user = data['data']['user']
album_extra = data['data']['detail']
# album model数据
album_real = to_album(album, album_extra, user, category_id)
# tag model数据
tag_real = to_tag(data['data']['detail']['showTagList'])
# user model数据
user_real = to_user(user)
albums_cid.append(album_real)
tag_cid += tag_real
user_cid.append(user_real)
else:
print('fail---------------------')
# 获取成功后存入json
print('category_id:%d------------------finish' % category_id)
save_json('detail/%d/' % category_id, 'album.json', {'data': albums_cid})
save_json('detail/%d/' % category_id, 'tag.json', {'data': distinct(tag_cid)})
save_json('detail/%d/' % category_id, 'user.json', {'data': distinct(user_cid)})


# 获取专栏的作者和标签信息
def get_album_info(aid):
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/v1/album/detail/%s?albumId=%d&device=iPhone' % (ts, aid)
data = get_url(url)
return data

得到如图的内容:
albumInfo


detail里面0分类的album.json:
albumInfo


detail里面0分类的tag.json:
albumInfo


detail里面0分类的user.json:
albumInfo


6.获取单个专栏的所有音频

终于到最后一步,该操作也是耗时最长的阶段。因为共71个分类,假如每个分类有1000个专栏,每个专栏有1000集的话,那总共爬取的内容有700万之多。

6.1.所有专栏去重

在开始之前,建议先将所有的专栏、标签、作者去重,因为一个分类里面的专栏可能会在其他分类里面,标签和作者也是同样的道理。妹没去重之前有5万多个专栏,去重后剩下3万多,这对于后续请求音频,有着特别重要的意义,防止重复请求相同的专栏,费时费力。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def read_distinct(file_type):
data = read_json('', 'category.json')
all_new_data = []
ids = []
if not is_existed('detail/%s.json' % file_type):
# 遍历所有分类
for category in data['category_list']:
category_id = category['id']
all_data = read_json('detail/%d/' % category_id, '%s.json' % file_type)
# 遍历所有专栏
for one in all_data:
if one['_id'] not in ids:
ids.append(one['_id'])
print('add-----------------%d' % one['_id'])
all_new_data.append(one)
save_json('detail/', '%s.json' % file_type, {'data': all_new_data})
print('len:%d-------%s finish' % (len(all_new_data), file_type))
else:
print('all_%s------------------is_existed' % file_type)

# 过滤掉重复的专栏、标签、作者
read_distinct('album')
read_distinct('tag')
read_distinct('user')

得到如图的内容:
read_distinct


6.2.分析获取音频接口

album_type

拥有去重的专栏,我们支持爬取共3.6万个专栏的所有音频。要注意这里的音频分为三类,一类是免费的,一类是VIP的,还有一类是即使是VIP也需要花钱购买的精品。


album_no_download

研究之后发现前面两类能够通过VIP爬取,第三类精品即使是VIP也无法获取。因为精品这类音频播放的时候,都有接口获取该音频是否支付,支付后下发签名,然后通过加密的key获取音频流,用一次链接就失效了。所以第三类精品这类资源,无法爬取。


6.3.获取track音频

思路就是遍历去重后的所有专栏,然后每个专栏继续遍历20分页的查询音频,获取到所有的音频,在获取音频的时候判断免费的url是否存在,存在说明该专栏是免费的,直接获取音频url,不存在说明该专栏是VIP的,需要通过另外一个专门的接口获取url。

代码如下:

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
53
54
# 获取所有专栏
def read_all_albums():
albums = read_json('detail/', 'album.json')
for album in albums:
aid = album['_id']
if not is_existed('track_ids/%d.json' % aid):
album_tracks = get_tracks(aid)
save_json('track_ids/', '%d.json' % aid, album_tracks[0])
save_json('tracks/', '%d.json' % aid, album_tracks[1])
else:
print('album_id:%d------------------album is_existed' % aid)


# 获取该专栏的所有音频
def get_tracks(aid):
page = 1
tracks = []
tracks_id = []
print(aid, '------------------start')
while True:
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/v1/album/track/%s?albumId=%d&device=iPhone&isAsc=true&isQueryInvitationBrand=true&' \
'pageId=%d&pageSize=20' % (ts, aid, page)
data = get_url(url)
if data['ret'] == 0:
tracks_list = data['data'].get('list', [])
if tracks_list:
page += 1
for track in tracks_list:
tracks_id.append(track['trackId'])
play_free = track.get('playPathAacv224', track.get('playUrl64', ''))
# 没有免费的音频,通过VIP爬取
if not play_free:
track_url = get_track_url(aid, track['trackId'])
track['playPathAacv224'] = track_url
tracks.append(to_track(track))
else:
print(aid, '------------------ending')
break
else:
print(aid, '------------------ending已下架')
break
print(aid, '----------------音频数:', len(tracks))
return [tracks_id, tracks]

# 获取单个音频链接(VIP)
def get_track_url(aid, tid):
ts = 'ts-' + str(int(round(time.time() * 1000)))
url = '/mobile/download/v1/%d/track/%d/%s?trackQualityLevel=0' % (aid, tid, ts)
data = get_url(url)
if data['ret'] == 0:
return data.get('downloadAacUrl', data.get('downloadUrl', ''))
else:
return ''

得到如图的内容:
tracks_ok

一个为所有专栏的tracks集合,另一个为所有专栏的tracksId集合。这个tracksId集合为后面放入到每个专栏的外键值所使用。


4.3.整理数据成mongodb导入的形式

在4.2中我们已经爬取需要的所有json数据,并持久化到本地磁盘中,它们的的格式如下:

1
{ "data": [{xxx}, {xxx}, {xxx}] }

但是呢,mognodb支持导入的json格式并非object,而是array,格式如下:

1
[{xxx}, {xxx}, {xxx}]

因此我们需要将前后两边的括号及data键去掉。
除此之外,我们还需要将albums.json里面的每个album重新遍历出来,将track_ids目录下的所有值放进每个album的tracks属性中。

代码如下:

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
# 保存所有专栏
def save_albums_mongodb():
albums = read_json('detail/', 'album.json')
albums_tracks_all = []
if not is_existed('mongodb/albums.json'):
for album in albums:
aid = album['_id']
tracks_ids = read_arr('track_ids/', '%d.json' % aid)
album['tracks'] = tracks_ids
albums_tracks_all.append(album)
albums_str = arr_to_str(albums_tracks_all)
save_arr('mongodb/', 'albums.json', albums_str)


# 保存所有音频
def save_tracks_mongodb():
albums = read_json('detail/', 'album.json')
tracks_all = []
if not is_existed('mongodb/tracks.json'):
for album in albums:
aid = album['_id']
album_tracks = read_arr('tracks/', '%d.json' % aid)
for track in album_tracks:
tracks_all.append(track)
tracks_str = arr_to_str(tracks_all)
save_arr('mongodb/', 'tracks.json', tracks_str)


# 保存所有分类
def save_categories_mongodb():
if not is_existed('mongodb/categories.json'):
data = read_json('', 'category.json')
category_arr = data['category_list']
category_str = arr_to_str(category_arr)
save_arr('mongodb/', 'categories.json', category_str)


# 保存所有标签
def save_tags_mongodb():
if not is_existed('mongodb/tags.json'):
data = read_json('detail/', 'tag.json')
tag_str = arr_to_str(data)
save_arr('mongodb/', 'tags.json', tag_str)


# 保存所有作者
def save_users_mongodb():
if not is_existed('mongodb/users.json'):
data = read_json('detail/', 'user.json')
user_str = arr_to_str(data)
save_arr('mongodb/', 'users.json', user_str)

utils新增:

1
2
3
4
# arr转换成str arr,支持mongodb导入
def arr_to_str(json_data):
str_data = json.dumps(json_data)
return str_data[str_data.index('['):str_data.rindex(']') + 1]

最终得到mongodb目录,里面就是所有可用的数据啦

mongodb_ok


4.4.导入json到mongodb数据库

终于到最最激动人心的时刻了,那就是把爬取的json存入本地数据库中。
导入命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在monogdb目录,执行mongoimport命令

// 分类、标签、作者 数据较小 使用--jsonArray
mongoimport -h 127.0.0.1:27017 -d sredy -c categories ./categories.json --jsonArray --upsert

mongoimport -h 127.0.0.1:27017 -d sredy -c tags ./tags.json --jsonArray --upsert

mongoimport -h 127.0.0.1:27017 -d sredy -c users ./users.json --jsonArray --upsert

// 专栏、音频 数据很大 需要批量导入,需要加上--batchSize 1配置
mongoimport -h 127.0.0.1:27017 -d sredy -c albums ./albums.json --jsonArray --mode upsert --batchSize 1

mongoimport -h 127.0.0.1:27017 -d sredy -c tracks ./tracks.json --jsonArray --mode upsert --batchSize 1

mongodb_imoport


导入音频会花一些时间,比较数据量大,耐心等待即可:

mongodb_end

3万6千多个专栏、340多万个音频,数据到手,就可以实现对应的接口,返回对应的值,然后使用啦~
后续接口的实现,RN的内容,在这就不多作介绍了…想看如何实现后续的东西的,可以参考我的另一篇文章《全桟知识体系(二)》。


5.总结

本篇文章主要介绍如何将爬取的数据存入数据库整套流程。只是提供一下思路,知道怎么处理需要的数据为我所用,感谢阅读到最后。

最后说说题外话,讲下自己最近的状态吧~

我特别喜欢一句话,今天的生活状态是你5年前决定的,今天的选择同样决定你5年后的生活状态

尽管最近的心态不怎么好,拥有买房后无形的压力、独自一人在上海生活的压力、思念家人的压力、认识的同事各种离职、工作时间长的压力…但是,我相信自己的决定。

在成都,可以选择肉身,家人朋友都在成都,美食、环境、旅游各种巴适;
在上海,可以选择灵魂,孤独只是暂时的,但在这里会觉得自由,能给予自己更多发展的机会。
鱼和熊掌不可兼得,选择肉身还是选择灵魂?在我看来,我会选择灵魂,尽管各种不适,但是我从未后悔过。

孤独之前是迷茫,孤独之后是成长。
一个人有多谦卑,就有多自由。
只要怀揣着对生活的美好期许,并且一直努力向上,不断的奔跑和前进,拥抱爱人和家庭,用心工作,那么你走的这条路就是最好的人生之路。