分类目录归档:Python

Python中JSON操作

Python中操作JSON的模块是json,导入即可用:

1
import json

json模块主要提供两个方法:loads()和dumps(),通过这两个方法对JSON进行操作,前者用来将JSON字符串转换成Python基本类型(dict, list),后者用来将Python基本类型(dict, list, tuple)转换成JSON字符串。试几个例子就明白了:

1. json.loads()

1
2
3
4
5
6
7
8
9
10
11
#json object string to dict
>>> str = '{"url": "www.fengchangjan.com", "method": "GET"}'
>>> data_dict = json.loads(str)
>>> data_dict['url']
u'www.fengchangjan.com'
 
# json array string to list
>>> str = '[{"url": "www.fengchj.com"}, {"method": "GET"}]'
>>> data_list = json.loads(str)
>>> data_list[0]['url']
u'www.fengchj.com'

2. json.dumps()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# dict to json object string
>>> data = {'url' : 'www.fengchangjan.com', 'method' : 'GET'}
>>> json.dumps(data)
'{"url": "www.fengchangjan.com", "method": "GET"}'
 
# list to json array string
>>> data = [{'url' : 'www.fengchj.com'}, {'method' : 'GET'}]
>>> json.dumps(data)
'[{"url": "www.fengchj.com"}, {"method": "GET"}]'
 
# tuple to json array string
>>> data = ({'url' : 'www.fengchj.com'}, {'method' : 'GET'})
>>> json.dumps(data)
'[{"url": "www.fengchj.com"}, {"method": "GET"}]'

json模块还有很多其他的方法,即便是loads()和dumps()也有许多选项参数,需要时可以查看手册,此处不举例。我觉得单凭示例中的两个方法已经能满足平时JSON操作中的80%需求了。

--EOF--

虾米收藏歌曲下载器

使用虾米的时间不是很长,但多少也积累了一些收藏歌曲,很想把它们都下下来,趁这两天有空,就开始付诸实践,顺便可以巩固下Python。现在基本功能已经实现,代码已上传到GitHub:https://github.com/fengchj/xiami-favsong-downloader

虾米收藏歌曲下载器的基本功能包括:
1. 下载指定用户收藏的歌曲。
2. 自定义歌曲下载目录。
3. 格式化歌曲ID3信息。

后续会在本地曲库中维护一个元数据文件,加入判断当前歌曲是否已经在本地曲库中的功能,避免每次运行程序都从头开始下载。另外可能会加入下载收藏专辑、收藏歌手的Top100歌曲的功能。

用法:
1. 启动程序: python downmusic.py。
2. 输入用户在虾米网的userid。userid是一串数字,进入用户首页,如http://www.xiami.com/u/3270716, 3270716就是userid。
3. 输入歌曲的下载目录。默认是程序所在目录。

下载完成后,会分别显示下载成功和失败的次数:

这里先说说虾米收藏歌曲下载器的基本思路:
1. 获取用户收藏的歌曲列表。获取用户收藏的歌曲列表目的是拿到歌曲ID列表。虾米无需登陆就能查看任意用户收藏的歌曲列表,查看地址为:

1
http://www.xiami.com/space/lib-song/u/{userid}/page/{pageno}

{userid}表示用户id,{pageno}表示指定页的收藏歌曲列表。这个地址返回的是个HTML文件,其中每首歌都以"<a title="Set Fire to the Rain" href="/song/1769915737">Set Fire to the Rain</a>"的格式呈现,我们目的是拿到歌曲ID,所以只需正则匹配出"/song/1769915737"部分就可以了,其他信息可以从歌曲的元信息XML中拿到。遍历用户的所有收藏歌曲页面,匹配"/song/\d{1,20}",直到匹配结果为空结束,得到用户收藏的歌曲ID列表。

2. 获取歌曲元信息(包含歌曲名、歌手和链接地址等)。在试听一首歌时,虾米先到http://www.xiami.com/song/playlist/id/{song_id}/object_name/default/object_id/0请求一个包含歌曲元信息(歌名、歌手、专辑名和加密链接地址等)的XML文件,这个地址对所有歌曲有效,用的时候只需把{song_id}替换成歌曲的ID即可。

比如,Adele的『Set Fire to the Rain』歌曲页面为:http://www.xiami.com/song/1769915737,歌曲ID为1769915737,元信息XML地址为:

1
http://www.xiami.com/song/playlist/id/1769915737/object_name/default/object_id/0

在浏览器打开它,返回的XML信息为:

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
<?xml version="1.0" encoding="utf-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track>
<title><![CDATA[Set Fire to the Rain]]></title>
<song_id>1769915737</song_id>
<album_id>253320</album_id>
<album_name><![CDATA[21]]></album_name>
<object_id>1</object_id>
<object_name>default</object_name>
<insert_type>1</insert_type>
<background>http://img.xiami.com/res/player/bimg/bg-5.bak.jpg</background>
<grade>-1</grade>
<artist><![CDATA[Adele]]></artist>
<location>
4h%2Fxit2522%519383.t3Ffi.%3%5%2E7179%mtA%1an24235F565_25pp%2.meF8F3E%_9724E3
</location>
<ms></ms>
<lyric>http://img.xiami.com/./lyric/upload/37/1769915737_1356124042.lrc</lyric>
<pic>http://img.xiami.com/images/album/img85/23485/2533201341353998_1.jpg</pic>
</track>
</trackList>
<type>default</type>
<type_id>1</type_id>
<clearlist></clearlist>
</playlist>

用正则匹配找出歌曲名(title节点),歌手(artist节点),歌曲链接(location节点)。

3. 获取歌曲的真实链接地址。从XML中location节点得到的地址是加过密的,不过虾米只对歌曲链接进行了简单的加密,网上已有大把分析文章,很容易破解。歌曲链接采用的是栅栏加密算法,将明文以列顺序写为几行,然后按照行的顺序读出来生成密文。
比如明文"attack-at-once"的加密过程为:

1
2
3
4
5
1) 将attack-at-once以列序写成三行。
aa--e
tcao
tktc
2) 按行读取生成密文"aa--etcaotktc"。

知道了栅栏加密的原理后,只需得到加密时的行数,就能从密文中反解出明文。虾米用location的第一个字符表示行数,因此歌曲『Set Fire to the Rain』的location反解过程分为:

1
2
3
4
5
6
7
8
9
10
11
1) 将密文分行:
密文:4h%2Fxit2522%519383.t3Ffi.%3%5%2E7179%mtA%1an24235F565_25pp%2.meF8F3E%_9724E3
 
4
h%2Fxit2522%519383.
t3Ffi.%3%5%2E7179%m
tA%1an24235F565_25p
p%2.meF8F3E%_9724E3
 
2) 按列读取明文:
http%3A%2F%2Ff1.xiami.net%2F23485%2F25332%5E%2F%5E5_1769915737_289243%5E.mp3

将明文进行URLDecoder后,得到:

1
http://f1.xiami.net/23485/25332^/^5_1769915737_289243^.mp3

将链接中的'^'符号替换成'0'后,就得到了最终的歌曲下载地址:

1
http://f1.xiami.net/23485/253320/05_1769915737_2892430.mp3

4. 格式化歌曲的ID3信息。通过程序下载下来的虾米歌曲ID3信息丢失严重,对于喜欢从手机或者电脑客户端通过Last.FM Scrobbler同步音乐播放记录的人来说,这是个严重问题。解决这个问题的方法是从歌曲元信息XML文件中得到信息较为准确的歌手和歌名信息,再以标准方式写入音频文件。

以上是基本思路,接下是Python代码层面上处理的一些问题:
1. 使用urllib2模块模拟浏览器操作,获取HTTP响应内容,它提供的API可以像读取本地文件一样获取网络上的数据。程序中获取用户收藏歌曲列表和获取歌曲元信息XML都用到这个模块的API。

1
2
3
4
5
6
7
headers = {
    'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) Chrome/24.0'
}
request = urllib2.Request(address, headers = headers)
response = urllib2.urlopen(request)
text = response.read()
print text

HTTP Header中User-Agent信息必不可少,少了它服务器会返回一个503: Service Temporarily Unavailable,估计虾米用它来挡掉一部分无聊的流量吧。

2. 下载歌曲使用了urllib模块的urlretrieve API。这里也能用urllib2 API来实现,两个模块虽有一些区别,但是可以用任何一个实现下载文件功能。选择urllib因为用它代码少:

1
urllib.urlretrieve(location ,path)

其中,location为歌曲的下载地址,path为本地歌曲的存放路径。

3. 正则匹配。从网上抓数据少不了正则匹配(re模块)。程序中有两个地方用到正则匹配,一是从歌曲元信息XML中获取感兴趣的信息:歌名、歌手、歌曲链接地址。二是从用户收藏歌曲页面获取歌曲ID列表。

1
2
3
4
5
6
7
8
9
10
11
##1. 匹配歌曲元信息
song_title_reg=r"<title><\!

</span>CDATA<span style=" />

!
(.*)</span><span style=" />></title>"
match = re.search(song_title_reg, text) if match: song_title = match.group(1) print song_title   ##2. 获取用户收藏歌曲ID列表 song_reg=r"\"/song/(\d{1,20})\"" result = re.findall(song_reg, text, re.S) print result

re.findall API返回一个列表,因为匹配模式中只有一个分组,所以返回的result就是歌曲ID列表。正则匹配要仔细考虑贪婪和非贪婪、单行匹配和多行匹配问题,re模块都有相应的解决方案。

4. 写入ID3信息。Python有很多处理音频文件ID3信息的第三方模块,这里用mutagen,因为它支持的文件类型多,接口较丰富。因为同步Last.FM只需要歌名和歌手信息准确就可以,所以也没必要用mutagen那些高级的API,用EasyID3类就足够。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from mutagen.easyid3 import EasyID3
 
def write_id3(path, song_name):
    #write id3 info to song.
    try:
        audio = EasyID3(path)
    except Exception, e:
        #while source track doesn't have ID3 structure. malloc new EasyID3().
        audio = EasyID3()
    song_info = song_name.split(' - ')
    artist = song_info[0]
    title = song_info[1]
    audio["artist"] = unicode(artist, 'utf-8')
    audio["title"] = unicode(title, 'utf-8')
    audio.save(path)

mutagen在构造EasyID3对象时,如果发现音频文件中缺少ID3信息,会抛出一个异常,因此需要在异常处理中new一个EasyID3对象,然后填入歌手和歌名信息,存到文件中。

5. 反解栅栏加密算法。歌曲的真实链接地址被栅栏加密算法加密过,前面已经分析过栅栏加密算法的原理,很容易写出反解算法。在已知行数row的情况下,如果字符串长度能被行数整除,则每行字符数相等;如果字符串长度无法被行数整除,假如余数为r,则前r行的字符数比剩下(row-r)行字符数多1,根据这些规律构造二维数组,按列读取即可。核心代码如下:

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
##行数已知,计算密文长度、列数、余数等。
encypt_loc_len = len(encypt_location)
column = encypt_loc_len/row 
remainder = encypt_loc_len% row
 
src_list = []
unencypt_loc = ''
iter = 0
##前余数行的二维数组构造方法。
for i in range(0, remainder):
	sub_list = encypt_location[iter:iter + column + 1]
	iter = iter + column + 1
	src_list.append(sub_list)
 
##剩下的(row-余数)行的二维数组构造方法。
for i in range(0, row - remainder):
	sub_list = encypt_location[iter:iter+column]
	iter = iter + column
	src_list.append(sub_list)
 
#二维数组构造出来后,按列读取。
for i in range(0, column + 1):
	for j in range(0, row):
		if i < len(src_list[j]):
			unencypt_loc += src_list[j][i]
print unencypt_loc

6. 下载失败重试。这里的失败主要是指反解出的歌曲真实链接地址不正确。其实能出现这个错误也算幸福的烦恼,在获取歌曲元信息XML文件时,如果间隔太短,虾米服务器返回的信息是无效的(格式仍合法),这样反解出的歌曲真实链接地址就有问题。本来是单线程,大部分时间程序应该阻塞在下载歌曲的API(urllib.urlretrieve)上,但是如果网速太快(=.=!!),就会造成请求歌曲元信息XML间隔太短。现在采用的方法是失败重试,最多重试三次,每次重试之前有个退避时间,退避时间算法为2失败次数秒。失败重试的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if errors ocurs during the download processing, re-try 3 times.
RETRY_TIME_LIMIT  = 3
isFail = True
trytime = 0
while isFail and  trytime < RETRY_TIME_LIMIT:
    try:
        (location, song_title, artist) = parse_xml(song_id)
        target = parse_location(location)
        download_music(target, artist + ' - ' + song_title, base_dir)
        count = count + 1
        isFail = False
    except Exception, e:
        print e
        time.sleep(math.pow(2, trytime))
        isFail = True
        trytime = trytime + 1

--EOF--