本篇文章内容较长,可根据需要阅读,大纲如下:
- 回顾当下
- 为什么开发app
- 享学习的由来
- 享学习数据
- 解析返回值json
- 数据爬取
- 整理数据成mongodb导入的形式
- 导入json到mongodb数据库
- 总结
1.回顾当下
很高兴能再次和大家分享“全栈”这个词,从2017年的全栈一、到2018年的全栈二,再到今年2019年的全栈三,每篇的侧重点都是不同的。
全桟知识体系(一):主要介绍什么是全栈、大牛们对全栈的看法及全栈的意义,找车场
就此诞生。
全桟知识体系(二):主要介绍全栈的实际操作,将一款真正意义上的产品从无到有培育出来,享健身
就此诞生。
**全桟知识体系(三)**主要介绍全栈的另一面,数据准备工作,享学习
就此诞生。
享学习是我研发的最后一个app,在自己奋斗的年龄不想留下青春遗憾,随着年龄的增长,没什么精力折腾了。
2.为什么开发app
有许多读者或同事问我这样一个问题,为什么你那么如此喜欢开发app呢?怎么不是开发页面、管理后台、或游戏呢? 第一点,也是最重要的一点,因为我热爱开发app呀~ 出于兴趣做事,只是过程特别的漫长和坎坷,结果是迟早的事情,没什么大不了的。
身为前端开发工程师,选择的方向其实也蛮多,可以做小程序、可以做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.享学习的由来
首页:
详情:
理由有以下四点: 1.希望通过react native做个app音频播放器 2.爬取过某在线听书平台的大数据,不用蛮可惜的 3.去年注册的享学习商标到手,不用蛮可惜的 4.一句流浪汉说过的话
距离这个浩如烟海的文化本身来说,我们都是井底之蛙,还不够还不够,所以一定要不断的学习,不断的学习。
谁能想象这句话竟出自一个衣衫褴褛,其貌不扬的流浪汉之口。
相信很多人和我一样,浩如烟海
这个成语第一次听到~
在app的介绍里面也有说明:
引用抖友的评论:
你满嘴诗意,却落魄街头,你纯情之心,却事态百凉,念中华之精华,阅沧海之琼书,你有用,却也无用。
小丑在殿堂,大师在流浪。
有时感觉我们挺残酷的,中华名族的传统文化正在慢慢淡出上班族的视线,《左传》、《尚书》、《史记》、《庄子》、《老子》、《论语》... 希望大家能多关注关注中国文化~享受学习
年轻的时候以为不读书不足以了解人生,直到后来才发现,如果不了解人生是读不懂书的,读书的意义大概就是,用生活所感去读书,用读书所得去生活吧。
接下来正式开始干货分享~
4.享学习数据
python当爬虫语言,在我看来是最合适的,尽管js、java等编程语言都有类似的爬虫框架,但是python语言的简洁态度,几行代码搞定你需要的IO操作、HTTP请求,变得十分的easy。
4.1.解析返回值json
通过Charles,可以看到喜马拉雅app里面的许多接口及其返回值。筛选自己需要的接口,观察其返回值,分析json然后为我所用。
享学习app的核心功能是播放
,因此获取mp3地址存到自己的数据库里就是最最核心的点。
看完大致的接口,得到以下接口信息及返回值,最后分析可以创建所需的model对象:
1.获取所有分类
接口:
http://mobile.ximalaya.com/m/category_tag_menu
返回值:
{
id: 39,
name: 'renwen',
title: '人文',
tag_list: null(没啥用)
}
{
id: 8,
name: 'finance',
title: '商业财经',
tag_list: ["股指期货", "互联网金融", "创业密码", "商业聚焦", "投资理财", "财经评论", "财经资讯", "消费指南"](没啥用)
}
title属于一级分类,tag_list应该属于二级分类即标签,后面在具体的专栏里有三级分类showTagList,因此二级分类用处不大。
引申出第一个model,
类别表category
,格式如下:
{
id: 0, //分类ID
name: 'xxx', //分类昵称
title: 'xxx', //分类标题
}
2.通过分类id获取该分类下的所有专栏
接口:
/mobile/discovery/v2/category/metadata/albums/%s?calcDimension=hot&categoryId=%d&device=iPhone&pageId=%d&pageSize=20&version=6.5.30
参数:
(ts, cid, page)
ts:当前时间戳
cid:分类id
page:分页的索引
返回值:
{
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
,格式如下:
{
id: 0, //标签ID
name: '' //标签名称
}
我们发现返回的cover是个数组,返回的是不同大小的专栏图片,我们只需要获取类似/group34/M04/7B/85/wKgJYFnu4JyhuhmXAACKM_kdY4A939.jpg
的地址,host和!后面自定义拼接就能显示不同大小的图片,因此cover只需要定义成一个字符串即可。
引申出第三个model,
专栏表album
,格式如下:
{
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获取该专栏下的所有音频
接口:
/mobile/v1/album/track/%s?albumId=%d&device=iPhone&isAsc=true&isQueryInvitationBrand=true&pageId=%d&pageSize=20
参数:
(ts, aid, page)
需要3个参数
ts:当前时间戳
aid:专栏id
page:分页索引
返回值:
{
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
,格式如下:
{
id: 0, //音频ID
title: '', //音频标题
cover: 'group52/M06/15/7E/wKgLe1v8ESLBrA7VAAPMA45wZvg446', //图标 小中大(正方)图片640
duration: 0, //持续时间(秒)
play: '', //播放地址
playtimes: 0, //播放次数
}
4.通过专栏id获取该专栏的作者信息
接口:
/mobile/v1/album/detail/%s?albumId=%d&device=iPhone
参数:
(ts, aid)
需要2个参数
ts:当前时间戳
aid:分类id
返回值:
{
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
,格式如下:
{
id: 0, //作者ID
nickname: '' //作者昵称
avator: '', //作者头像
}
至此,我们发现有基本的5个modle对象,分别是category分类表
、tag标签表
、album专栏表
、track音频表
、user作者表
五类。
关系映射可简单理解为分类里有专栏;专栏里有音频、作者;标签通过所有专栏标签去重获取。分类与专栏一对多,专栏对音频一对多、专栏对标签一对多、专栏对作者一对一。
4.2.数据爬取
看下数据爬取的大致流程,思路可能会更清晰一些。 数据爬取的流程如下: 首先获取所有的分类,然后查询每个分类的所有专栏,接着查询每个专栏的所有音频和该作者,通过音频查询每个音频对应的介绍和mp3地址。 获取完所有专栏的时候,可以获取每个专栏的标签,最后获取到所有专栏的标签。
数据爬取阶段主要将返回的数据,持久化到json文件中,为后续做准备。
1.爬取喜马拉雅所有分类,生成category.json
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.json:
2.爬取喜马拉雅分类的所有专栏,生成n个album.json
这个地方的步骤可能会稍复杂些,具体实现下: 1.读取刚刚cagegory.json里面的分类,获取所有分类的id
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.通过请求获取具体一个分类的所有的专栏
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中
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,代码如下:
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加入黑名单限制调用等。
优化后的获取所有分类、获取分类的所有专栏:
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里面0.json:
4.获取所有专栏作者和标签的基本信息
上一步我们通过查询单个分类,获取到所有的专栏。接下来,我们需要遍历读取整个分类category.json,再遍历读取单个分类获取到的专栏json,最后获取到所有专栏id。 思路大致如下: 1.获取所有专栏id
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.获取一个专栏的作者和标签信息
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:
{
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工具库
# 通用的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
优化后的获取所有专栏:
# 获取所有专栏
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
得到如图的内容:
detail里面0分类的album.json:
detail里面0分类的tag.json:
detail里面0分类的user.json:
6.获取单个专栏的所有音频
终于到最后一步,该操作也是耗时最长的阶段。因为共71个分类,假如每个分类有1000个专栏,每个专栏有1000集的话,那总共爬取的内容有700万之多。
6.1.所有专栏去重
在开始之前,建议先将所有的专栏、标签、作者去重,因为一个分类里面的专栏可能会在其他分类里面,标签和作者也是同样的道理。妹没去重之前有5万多个专栏,去重后剩下3万多,这对于后续请求音频,有着特别重要的意义,防止重复请求相同的专栏,费时费力。
代码如下:
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')
得到如图的内容:
6.2.分析获取音频接口
拥有去重的专栏,我们支持爬取共3.6万个专栏的所有音频。要注意这里的音频分为三类,一类是免费的,一类是VIP的,还有一类是即使是VIP也需要花钱购买的精品。
研究之后发现前面两类能够通过VIP爬取,第三类精品即使是VIP也无法获取。因为精品这类音频播放的时候,都有接口获取该音频是否支付,支付后下发签名,然后通过加密的key获取音频流,用一次链接就失效了。所以第三类精品这类资源,无法爬取。
6.3.获取track音频
思路就是遍历去重后的所有专栏,然后每个专栏继续遍历20分页的查询音频,获取到所有的音频,在获取音频的时候判断免费的url是否存在,存在说明该专栏是免费的,直接获取音频url,不存在说明该专栏是VIP的,需要通过另外一个专门的接口获取url。
代码如下:
# 获取所有专栏
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集合,另一个为所有专栏的tracksId集合。这个tracksId集合为后面放入到每个专栏的外键值所使用。
4.3.整理数据成mongodb导入的形式
在4.2中我们已经爬取需要的所有json数据,并持久化到本地磁盘中,它们的的格式如下:
{ "data": [{xxx}, {xxx}, {xxx}] }
但是呢,mognodb支持导入的json格式并非object,而是array,格式如下:
[{xxx}, {xxx}, {xxx}]
因此我们需要将前后两边的括号及data键去掉。 除此之外,我们还需要将albums.json里面的每个album重新遍历出来,将track_ids目录下的所有值放进每个album的tracks属性中。
代码如下:
# 保存所有专栏
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新增:
# 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目录,里面就是所有可用的数据啦
4.4.导入json到mongodb数据库
终于到最最激动人心的时刻了,那就是把爬取的json存入本地数据库中。 导入命令如下:
// 在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
导入音频会花一些时间,比较数据量大,耐心等待即可:
3万6千多个专栏、340多万个音频,数据到手,就可以实现对应的接口,返回对应的值,然后使用啦~ 后续接口的实现,RN的内容,在这就不多作介绍了...想看如何实现后续的东西的,可以参考我的另一篇文章《全桟知识体系(二)》。
5.总结
本篇文章主要介绍如何将爬取的数据存入数据库整套流程。只是提供一下思路,知道怎么处理需要的数据为我所用,感谢阅读到最后。
最后说说题外话,讲下自己最近的状态吧~
我特别喜欢一句话,今天的生活状态是你5年前决定的,今天的选择同样决定你5年后的生活状态
。
尽管最近的心态不怎么好,拥有买房后无形的压力、独自一人在上海生活的压力、思念家人的压力、认识的同事各种离职、工作时间长的压力...但是,我相信自己的决定。
在成都,可以选择肉身,家人朋友都在成都,美食、环境、旅游各种巴适; 在上海,可以选择灵魂,孤独只是暂时的,但在这里会觉得自由,能给予自己更多发展的机会。 鱼和熊掌不可兼得,选择肉身还是选择灵魂?在我看来,我会选择灵魂,尽管各种不适,但是我从未后悔过。
孤独之前是迷茫,孤独之后是成长。 一个人有多谦卑,就有多自由。 只要怀揣着对生活的美好期许,并且一直努力向上,不断的奔跑和前进,拥抱爱人和家庭,用心工作,那么你走的这条路就是最好的人生之路。