理论分析
主要工作:获取网页中的歌曲信息,并将对应的歌曲热门评论数据保存下来。因此主要需要如下信息:歌曲名称、歌手名称、评论信息。获取到这些信息后需要存储数据。
根据这些需求分析,首先需要一个网络请求库,这里采用requests库来完成。获取到的数据html文档、json文档两种格式,如图1图2所示。对于处理html文档可以使用正则表达式来处理,对于json数据可以由Python自带的json库处理。由于json文件的获取需要一个加密数据,因此需要安装AES加密功能。
图1 获取到的HTML文档
图2 获取到的JSON文档
获取到的数据需要保存为更利于观看的图文,因此这里采用将文本文档转化为markdown文件的形式,这样生成的文件在未渲染时文本依旧有很好的可读性,在渲染后可保证多个文档的样式一致性。
实施方法
在程序最开始需要先导入如下库:re,json,base64,codecs,requests,如图3所示。通过这些库可以实现网络请求的发送接收,数据的处理,加密数据的计算。为了处理一些网络异常,防止因为网络状况问题导致程序崩溃,这里将requests库里的异常处理功能导入。这样,当网络请求出现异常时,程序会跳转到异常处理部分,发出网络问题的提示,保证程序不会中断执行。
图3 导入需要的函数库
整个程序的运行流程如图4所示,程序开始运行后首先读取待爬取列表,将列表中的每一个网址依次进行如下处理。首先根据网址信息获取到歌曲ID,根据得到的歌曲ID作为参数构造请求获取HTML文档,然后从HTML文档中获取网页标题信息,完成歌曲名称的爬取。再将歌曲ID作为参数构造请求来获取评论信息。当歌曲信息评论信息获取完毕后将所需的数据保存到文本文档中完成当前网页的爬取。程序将判断是否爬取完成所有的网址,如果所有网址都爬取完成后程序结束。
对于一些异常情况,本程序也有相应的处理机制。程序中主要处理两类异常情况:网络请求异常和文件读写异常。Python的异常处理能力是很强大的,可向用户准确反馈出错信息。一旦引发而且没有捕捉异常,程序执行就会终止运行。对于网络异常,通常通过重试请求即可得到正确的结果,而不需要终止整个程序的运行,需要处理网络异常,使得程序能够在出错时重试请求。对于文件读写异常,可能重试读写文件也不会成功,这时就需要向用户报告问题。
发生异常时,Python能“记住”引发的异常以及程序的当前状态。Python还维护着traceback对象,其中含有异常发生时与函数调用堆栈有关的信息。异常可能在一系列嵌套较深的函数调用中引发。程序调用每个函数时,Python会在“函数调用堆栈”的起始处插入函数名。一旦异常被引发,Python会搜索一个相应的异常处理程序。如果当前函数中没有异常处理程序,当前函数会终止执行,Python会搜索当前函数的调用函数,并以此类推,直到发现匹配的异常处理程序,或者Python抵达主程序为止。这一查找合适的异常处理程序的过程就称为“堆栈辗转开解”(Stack Unwinding)。解释器一方面维护着与放置堆栈中的函数有关的信息,另一方面也维护着与已从堆栈中“辗转开解”的函数有关的信息。通过这套成熟的机制可以保证整个程序运行的稳定性,确保得到的数据准确可靠。
图4 程序运行流程
对于一个网页首先将网址中的歌曲ID通过正则表达式筛选出来,如图5所示,函数将传入的网址通过正则表达式筛选出歌曲ID信息。根据获取的ID信息构造网络请求并发送网络请求,可以将所需的网页HTML文档获取到并保存到程序设置的指定变量内。
图5 获取歌曲ID
如图6所示,程序将网址作为输入,函数返回获取到的HTML文档返回。然后程序将文档通过正则表达式将需要的信息提取出来。提取得到的数据将会以返回值的形式返回。通过这样的设计可以更加高效的获取网页内容,使得整体程序效率大大提高。
图6 获取歌曲title标签
获取到标题后可以进一步构造请求,把歌曲对应的评论信息爬取到本地,如图7所示,函数首先计算请求地址,构造请求header,将请求的数据 post_data里,完成整个网络请求信息的构造。然后使用requests库提供的post方法执行网络请求,并将获取到的json数据返回。这个函数应用了异常处理,可以保证在网络通信出现故障时程序不崩溃导致整个爬取信息的丢失。
图7 获取评论信息
获取到的数据只存在于程序运行过程中,因此需要对数据进行保存。这里采用将信息输出到markdown文件的形式保存下来。Markdown文件具有可读性好、排版简洁明了的特点,同时它比纯文本更具表现力。
软件运行结果
程序中以对4首歌的爬取为例,分别爬取如下网页:
图8 待爬取网页列表
爬取过程中程序实时输出爬取进度,如图9所示,对比网页上的评论数目可以初步确定数据是有效的。
图9 爬取过程程序输出
程序运行完成后生成markdown文件,将markdown文件通过马克飞象工具生成PDF文档。
程序源码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Author : ATIME
# @License : GNU General Public License
# @Contact : atime2008@atime.org.cn
# @Time : 2017/11/21 10:33
# @File : wyyyy.py
# @Version : Python3.6
import re
import json
import base64
import codecs
import requests
from Crypto.Cipher import AES
from requests.exceptions import RequestException
FILE_NAME = 'wyyyy.md'
URL163 = ['http://music.163.com/#/song?id=186001',
'http://music.163.com/#/song?id=437250607',
'http://music.163.com/#/song?id=4172700',
'http://music.163.com/#/song?id=30953009'
]
class Netease(object):
def __init__(self):
# self.first_param = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"
# self.second_param = "010001"
# self.third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
# self.forth_param = "0CoJUm6Qyw8W8jud"
# self.csrf_token = '1c51dc5cedf74268eaedb744f431f27b'
self.header = {
"Referer": 'http://music.163.com/',
'Cookie': 'appver=1.5.0.75771;',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
}
def get_id(self, url):
pattern = re.compile('\d+\d', re.S)
song_id = re.findall(pattern, url)
return song_id[1] # str类型
def get_html(self, url):
request_url = 'http://music.163.com/song?id=' + self.get_id(url)
headers = self.header
try:
response = requests.get(request_url, headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException:
return None
def get_title(self, url):
title_html = self.get_html(url)
pattern = re.compile('(?<=title\>).*(?=</title)', re.S)
title = re.findall(pattern, title_html)
title[0].encode('utf-8')
return title[0] # str类型
def AES_encrypt(self, text, key, iv):
pad = 16 - len(text) % 16
text = text + chr(pad) * pad
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text)
encrypt_text = base64.b64encode(encrypt_text)
return encrypt_text
def get_params(self):
iv = "0102030405060708"
first_param = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"
# second_param = "010001"
# third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
forth_param = "0CoJUm6Qyw8W8jud"
first_key = forth_param
second_key = 16 * 'F'
h_encText = self.AES_encrypt(first_param, first_key, iv)
h_encText = self.AES_encrypt(h_encText, second_key, iv)
return h_encText
def get_csrf_token(self):
csrf_token = '1c51dc5cedf74268eaedb744f431f27b'
return csrf_token
def get_encSecKey(self):
encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
return encSecKey
def get_json(self, song_id, csrf_token, params, encSecKey):
base_url = 'http://music.163.com/weapi/v1/resource/comments/R_SO_4_'
request_url = base_url + song_id + '?csrf_token=' + csrf_token
headers = self.header
post_data = {
"params": params,
"encSecKey": encSecKey
}
try:
response = requests.post(request_url, headers=headers, data=post_data)
if response.status_code == 200:
return response.json() # .content#.decode('utf-8')
return None
except RequestException:
return None
def save_as_json(self, song_name, json_result):
# print(json_result)
line = json.dumps(json_result, ensure_ascii=False)
# print(line)
# print(type(line))
file_name = song_name + '.json'
with codecs.open(file_name, 'w', 'utf-8') as fp: # 写入Unicode字符
fp.write(line)
def set_title(self):
global FILE_NAME
with codecs.open(FILE_NAME, 'w', 'utf-8') as fp: # 写入Unicode字符
fp.write(u'## 网易云音乐热门评论')
def save_title(self, song_name):
global FILE_NAME
with codecs.open(FILE_NAME, 'a', 'utf-8') as fp: # 写入Unicode字符
fp.write('\n### ')
fp.write(song_name)
def save_comment(self, json_data):
global FILE_NAME
hot_comment = json_data['hotComments']
print("total:", len(hot_comment))
for i in range(len(hot_comment)):
with codecs.open(FILE_NAME, 'a', 'utf-8') as fp: # 写入Unicode字符
fp.write('\n@( ')
fp.write(hot_comment[i]['user']['nickname'])
fp.write(' )')
with codecs.open(FILE_NAME, 'a', 'utf-8') as fp: # 写入Unicode字符
fp.write('\n> ')
fp.write(hot_comment[i]['content'])
fp.write('\n')
# print(hot_comment[i]['user']['nickname'])
# print(hot_comment[i]['content'])
def claw(self, url_list):
self.set_title()
for url in url_list:
song_id = self.get_id(url)
song_name = self.get_title(url)
self.save_title(song_name)
print(song_name)
csrf_token = self.get_csrf_token()
params = self.get_params()
encSecKey = self.get_encSecKey()
json_data = self.get_json(song_id, csrf_token, params, encSecKey)
self.save_comment(json_data)
self.save_as_json(song_id, json_data)
def main():
netease = Netease()
netease.claw(URL163)
if __name__ == "__main__":
main()