本篇文章内容较长,可根据需要阅读,大纲如下:
回顾当下
为什么开发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.获取所有分类 接口:
1 http://m obile.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.数据爬取 看下数据爬取的大致流程,思路可能会更清晰一些。 数据爬取的流程如下: 首先获取所有的分类,然后查询每个分类的所有专栏,接着查询每个专栏的所有音频和该作者,通过音频查询每个音频对应的介绍和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.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, distinctimport 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) 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
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 tag s: tag_new.append ({'_id' : tag ['tagId' ], 'name' : tag ['tagName' ]}) return tag_new def to_tag_id(tags ): tag_new = [] for tag in tag s: 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_real = to_album(album, album_extra, user, category_id) tag_real = to_tag(data['data' ]['detail' ]['showTagList' ]) user_real = to_user(user) albums_cid.append(album_real) tag_cid += tag_real user_cid.append(user_real) else : print ('fail---------------------' ) 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万多,这对于后续请求音频,有着特别重要的意义,防止重复请求相同的专栏,费时费力。
代码如下:
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 ')
得到如图的内容:
6.2.分析获取音频接口
拥有去重的专栏,我们支持爬取共3.6万个专栏的所有音频。要注意这里的音频分为三类,一类是免费的,一类是VIP的,还有一类是即使是VIP也需要花钱购买的精品。
研究之后发现前面两类能够通过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集合,另一个为所有专栏的tracksId集合。这个tracksId集合为后面放入到每个专栏的外键值所使用。
4.3.整理数据成mongodb导入的形式 在4.2中我们已经爬取需要的所有json数据,并持久化到本地磁盘中,它们的的格式如下:
1 { "data" : [{xxx}, {xxx}, {xxx}] }
但是呢,mognodb支持导入的json格式并非object,而是array,格式如下:
因此我们需要将前后两边的括号及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 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存入本地数据库中。 导入命令如下:
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
导入音频会花一些时间,比较数据量大,耐心等待即可:
3万6千多个专栏、340多万个音频,数据到手,就可以实现对应的接口,返回对应的值,然后使用啦~ 后续接口的实现,RN的内容,在这就不多作介绍了…想看如何实现后续的东西的,可以参考我的另一篇文章《全桟知识体系(二) 》。
5.总结 本篇文章主要介绍如何将爬取的数据存入数据库整套流程。只是提供一下思路,知道怎么处理需要的数据为我所用,感谢阅读到最后。
最后说说题外话,讲下自己最近的状态吧~
我特别喜欢一句话,今天的生活状态是你5年前决定的,今天的选择同样决定你5年后的生活状态
。
尽管最近的心态不怎么好,拥有买房后无形的压力、独自一人在上海生活的压力、思念家人的压力、认识的同事各种离职、工作时间长的压力…但是,我相信自己的决定。
在成都,可以选择肉身,家人朋友都在成都,美食、环境、旅游各种巴适; 在上海,可以选择灵魂,孤独只是暂时的,但在这里会觉得自由,能给予自己更多发展的机会。 鱼和熊掌不可兼得,选择肉身还是选择灵魂?在我看来,我会选择灵魂,尽管各种不适,但是我从未后悔过。
孤独之前是迷茫,孤独之后是成长。 一个人有多谦卑,就有多自由。 只要怀揣着对生活的美好期许,并且一直努力向上,不断的奔跑和前进,拥抱爱人和家庭,用心工作,那么你走的这条路就是最好的人生之路。