LoveqSwift(一):Python爬虫抓取loveq.cn所有节目信息

前言

听《一些事一些情》已经十多年,近来通勤时间重温以往节目的时候发现,无论官方或民间的APP都或多或少有些遗憾,虽然也是个重复造轮子的活,但是自己动手写一个或许会更有乐趣呢。


缘由

首先,开发一款音乐播放器的移动APP,我们得有api才可以展示数据,下载音频文件,最终播放节目。如何获取到每一期节目的数据呢?

  • 方案一
    通过对现有的APP抓包,这个方案最简单快捷,使用charles之类的工具可以对官方和民间APP进行抓包,获取api。但,选取的几款APP均不是采用get、post请求的,没办法轻易拿到数据,pass。
  • 方案二
    通过APP内部加载单页web,获取网页源代码后,通过正则表达式提取节目信息以及下载链接并展示。但,经过测试后发现,此方案会有比较严重的性能问题,加载时间过久,用户体验较差,pass。(以下为两种加载web方式的测试代码)
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
//下标截取,蠢方法一
func subWebstring(HTMLSource: [String?]) -> Array<AnyObject> {
let reg = try! NSRegularExpression.init(pattern: parten, options: .CaseInsensitive)
let HTML = HTMLSource[0]! as NSString
let array0 = HTML.componentsSeparatedByString("节目名称</dt>")
let HTML2 = array0[1] as NSString
let array = reg.matchesInString(HTML2 as String, options: .ReportCompletion, range: NSMakeRange(0, HTML2.length))

var data = Array<AnyObject>()

for match: NSTextCheckingResult in array {
let range = match.range
let string: String = HTML2.substringWithRange(range)

let s1 = (string as NSString).substringWithRange(NSMakeRange(9, 26))
let s2 = (string as NSString).substringWithRange(NSMakeRange(173, 13))
let s3 = (string as NSString).substringWithRange(NSMakeRange(167, 5))


let ns3 = ["url":s1, "title":s2, "format":s3]

let user = Mapper<ProgrammerListModel>().map(ns3)
data.append(user!)
}

return data
}
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
//使用第三方框架Fuzi
func Fuzi(HTMLSource: [String?]){

let html = HTMLSource[0]!
let program = "program_download"

do{
let doc = try! HTMLDocument(string: html, encoding: NSUTF8StringEncoding)

for link in doc.css("a, link") {

let str: String? = link["href"]
if str != nil && str!.containsString(program) {
print(link["href"])
if !urlSource.contains(str!) {
if str!.hasPrefix(program) {
urlSource.append(str!)
}

}
}

}

print(urlSource)
for link in doc.css("span") {
let s = link.stringValue as NSString
if s.length > 0 {
let s1 = (s as NSString).substringToIndex(1)
if s1 == "【" {
print(s)
dataSource.append(s)
}
}

}

if let result = doc.eval(xpath: "count(/*/a)") {
print("anchor count : \(result.doubleValue)")
}
}
print(urlSource)
}
  • 方案三
    通过抓取官网所有的节目信息和下载地址,自己搭建服务器提供api。此方案要实行对于个人来说有两个难点:1.怎么抓取一千多期节目的数据;2.怎么搭建服务器并提供接口。经过分析各种方案后,第一点可以通过Python爬虫解决,第二点,因后端知识树暂时没点,所以暂时使用实时云服务来替代。一开始是目标为firebase,试用后发现,Google收购它后,慢慢添加Google一些服务接口进去了,由于众所周知的原因,最终选用了墙内的wilddog

动工

方案选定后,正式动工。
Python为现学现卖,so,现阶段的目标实现需求达到目标即可。
经过分析网页源代码,抓取流程如下:在界面列表抓取当前页所有节目信息–>跳转单期节目的下载页获取真正的音频文件地址–>将节目信息与真正的音频文件地址并为字典按照月份分类再存入数组–>抓取下一页·····所有页抓取完毕后–>再按照年份进行分类–>分别写入不同的JSON文件

需要用到的库:urllib2、re、json

抓取界面信息

定义Class,初始化确定好下载页面的url,以及全局接收的数组

1
2
3
4
5
6
class Spider:

#初始化
def __init__(self):
self.siteURL = 'http://www.loveq.cn/program.php?&cat_id=20&'
self.JSONMP3 = []
1
2
3
4
5
6
#获取索引页面的内容
def getPage(self,pageIndex):
url = self.siteURL + "page=" + str(pageIndex)
request = urllib2.Request(url)
response = urllib2.urlopen(request)
return response.read().decode('utf-8')

抓取当前页所有节目

抓取完毕之后过滤掉WMA格式,并对粤语和普通话双版本进行分类

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
#获取索引界面所有节目的信息
def getContents(self,pageIndex):
page = self.getPage(pageIndex)
pattern = re.compile('<dl.*?clearfix">.*?<dt.*?<a.*?"(.*?)".*?target.*?">(.*?)</a>.*?class="ct"><span>(.*?)</dd>',re.S)
items = re.findall(pattern,page)

jsonChinese = []
jsonMP3 = []
for item in items:
downloadURL = self.getDetailPage(item[0])
print item
name=item[2].encode("utf-8")

if 'MP3格式' in name or 'mp3格式' in name:
if '普通话' in name:
# 保存普通话节目列表
jsonChinese.append([downloadURL,item[1],0])
else:
#保存粤语节目列表
jsonMP3.append([downloadURL,item[1],1])
contents = {}
contents.setdefault('url',downloadURL)
contents.setdefault('title',item[1])
contents.setdefault('type',str(1))
self.JSONMP3.append(contents)
#2006-2009年的节目没有采用[MP3格式]xxxx这样的方式作为title,手动进行切换代码吧=.=
# jsonMP3.append([downloadURL, item[1], 1])
# contents = {}
# contents.setdefault('url', downloadURL)
# contents.setdefault('title', item[1])
# contents.setdefault('type', str(1))
# self.JSONMP3.append(contents)

跳转下载页面获取真正下载地址

1
2
3
4
5
6
7
8
9
10
#获取真实下载地址
def getDetailPage(self,infoURL):
url = 'http://www.loveq.cn/' + infoURL
response = urllib2.urlopen(url)
content = response.read().decode('utf-8')
pattern = re.compile('<div.*?more_downlink".*?style.*?<a.*?"(.*?)"',re.S)
items = re.findall(pattern,content)
for item in items:
downloadURL = item
return downloadURL

分类保存所有节目数据

1
2
3
4
5
6
7
8
9
#传入起止页码,获取界面json
def savePagesInfo(self,start,end):
for i in range(start,end+1):
print u"looking for page",i
self.savePageInfo(i)

#将整页节目列表保存起来
def savePageInfo(self,pageIndex):
self.getContents(pageIndex)

工具类

主要用于按照年份进行分类,并导出JSON文件

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
class Tools:
#工具初始化
def __init__(self):
self.JSONFile = {}

#传入起止月份,获取新json
def saveNewJSONSInfo(self,year,start,end,JSON):
self.JSONFile = {}
for i in range(start,end+1):
self.saveJSONInfo(year,i,JSON)

#将节目保存起来
def saveJSONInfo(self,year,Index,JSON):
moonJSON = []
moonstr = ''
moonstr2 = ''
if Index < 10:
moonstr = year + '.0' + str(Index)
moonstr2 = year + '0' + str(Index)
else:
moonstr = year + '.' + str(Index)
moonstr2 = year + str(Index)
for item in JSON:
if moonstr in item['title']:
moonJSON.append(item)
self.JSONFile.setdefault(moonstr2,moonJSON)

def store(self,year,JSON):
with open( year + 'JSONFile.json', 'w') as f:
f.write(json.dumps(JSON))

Let’s go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#传入起止页码即可,在此传入了1,71,表示抓取第2到10页的节目
spider = Spider()
spider.savePagesInfo(1,71)
tool = Tools()
totalJSON ={}
#设置年份,3代表2003年
for i in range(3,16):
year = ''
if i < 10 :
year = '200' + str(i)
else:
year = '20' + str(i)
tool.saveNewJSONSInfo(year,1,12,spider.JSONMP3)
totalJSON.setdefault(year,tool.JSONFile)
#保存JSON文件到当前文件夹
if __name__ == "__main__":
tool.store(year,totalJSON)

此处输入图片的描述
大功告成!然后将导出的JSON文件导入到wilddog。
接下来的工作就是整个APP,LoveqSwift项目的构建了。
最后附上github:http://t.cn/R5AVbTH