使用 python 批量下载又拍(yupoo)照片

去年某个时候,HM 说要把照片放到网上,于是我替她觅了一个照片服务:又拍(yupoo)。在 HM 把所有照片上传到又拍之后,我才发现又拍没有提供批量下载照片的功能,如果照片太多的话,手动一张一张下载是很恐怖的,可以说,又拍是只进不出的。正好 HM 电脑硬盘里又没有保留照片的副本,所以我感到非常愧疚。

愧疚之余,花了一个晚上,用 python 写了一个批量下载又拍照片的脚本。

理所当然的,又拍没有提供 API ,我只能从分析它的 html 开始。

一,身份认证

我直接使用了浏览器 Cookies 做认证,一来这样省事,二来怕哪个有同样需要的网友下载到这段代码,因为对我的不信任而放弃使用。

python 提供了一个模块 cookielib 可以读取浏览器的 cookies 文件。不过 firefox 3 的 cookies 使用了 SQLite 数据库存储。

Noah Fontes 写了一段代码可以将 sqlite 数据库转换成 cookies 文件。这段代码使用 MIT 许可证发布,意味着我可以直接使用了。

#! /usr/bin/env python
# Protocol implementation for handling gsocmentors.com transactions
# Author: Noah Fontes nfontes AT cynigram DOT com
# License: MIT
 
def sqlite2cookie(filename):
    from cStringIO import StringIO
    from pysqlite2 import dbapi2 as sqlite
 
    con = sqlite.connect(filename)
 
    cur = con.cursor()
    cur.execute("select host, path, isSecure, expiry, name, value from moz_cookies")
 
    ftstr = ["FALSE","TRUE"]
 
    s = StringIO()
    s.write("""\
# Netscape HTTP Cookie File
# http://www.netscape.com/newsref/std/cookie_spec.html
# This is a generated file!  Do not edit.
""")
    for item in cur.fetchall():
        s.write("%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (
            item[0], ftstr[item[0].startswith('.')], item[1],
            ftstr[item[2]], item[3], item[4], item[5]))
 
    s.seek(0)
 
    cookie_jar = cookielib.MozillaCookieJar()
    cookie_jar._really_load(s, '', True, True)
    return cookie_jar

代码返回一个 CookieJar 对象,可以直接被 urllib2 的 HTTPCookieProcessor 类使用,然后抓取认证后的 HTML 代码。

二,抓取 HTML 代码

这个其实很容易,urllib2 模块就行了。但是在测试的时候发现偶尔会触发 “Connection Time Out” 的 URLErorr ,所以要俘获一个异常,然后重试。

def fetch_html(url):
    while True:
    	req = urllib2.Request(url)
        req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) Gecko/20101209 Firefox/3.6.13')
    	opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(sqlite2cookie('cookies.sqlite')))
        try:
            f = opener.open(req)
        except Exception, e:
            print u'获取 HTML 代码出错,重试……'
        else:
            break
 
    result = f.read()
    f.close()
    return result

三,解析 HTML 代码

python 有一个轻量级的 HTML 解析模块——HTMLParser ,用法也很简单。遇到开始的 html 标签会触发 handle_starttag() 这个方法,遇到结束的 html 标签会触发 handle_endtag() 方法,遇到普通数据触发 handle_data() 方法,等等……

1,从 yupoo 主页解析相册入口地址

相册的入口地址在这一段 html 代码中:

python 代码:

class fetchAlbumPortal(HTMLParser.HTMLParser):
    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.link = ''
        self.album_link = ''
    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            for name,value in attrs:
                if name == 'href':
                    self.link = value
 
    def handle_data(self, data):
        if data == u'我的相册':
            self.album_link = self.link

在遇到每一个超链接时将链接存入临时变量 “self.link” ,当遇到 “我的相册” 时,此时变量 “self.link” 的值即为 “我的相册” 的地址。将 “self.link” 赋值给 “self.album_link” 即可。这里本来该在赋值前进行判断的,防止用户内容中恰好有一个叫做 “我的相册” 的链接,但由于这段 html 代码在用户内容之后,所以判断就不需要了。

这写这篇日志的时候,我悲剧地发现,其实这一步完全是多余的,直接进入 “http://www.yupoo.com/albums” 就跳转到相册地页面了。

2,解析每个相册的名称和地址

相册名称和地址的 html 代码:

python 代码:

class fetchAlbumsList(HTMLParser.HTMLParser):
    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.c = ''
        self.h = ''
        self.t = ''
        self.albums_list = {}
    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            for name,value in attrs:
                if name == 'class':
                    self.c = value
                if name == 'href':
                    self.h = value
                if name == 'title':
                    self.t = value
            if self.c == 'Seta':
                self.albums_list[self.t] = 'http://www.yupoo.com' + self.h
            self.c = ''
            self.h = ''
            self.t = ''

3,解析相册中图片的名称和地址

html 代码:

python 代码:

class fetchPicsList(HTMLParser.HTMLParser):
    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.c = ''
        self.h = ''
        self.t = ''
        self.pics_list = {}
    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            for name,value in attrs:
                if name == 'class':
                    self.c = value
                if name == 'href':
                    self.h = value
                if name == 'title':
                    self.t = value
            if self.c == 'img' and self.t != '':
                pic_url = 'http://www.yupoo.com' + self.h
                self.pics_list[self.t] = thumb2orig(pic_url)
            self.c = ''
            self.h = ''
            self.t = ''

得到的图片地址 “pic_url” 后交给函数 orig_link 处理的原因是,”pic_url” 中的图片只是一个缩略图,并不是原始图片。

缩略图的地址:

http://www.yupoo.com/photos/handsomecheung/albums/1997785/79548434/

原始图的地址:

http://www.yupoo.com/photos/handsomecheung/79548434/zoom/

利用 python 的 re 模块转换 URL:

def thumb2orig(url):
    orig_url = re.sub('/albums/\d+/', '/', url) + 'zoom/' + 'original'
    return orig_url

4,抓取图片的下载地址

html 代码:

python 代码:

class fetchImgLink(HTMLParser.HTMLParser):
    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.c = ''
        self.s = ''
        self.img_link = ''
    def handle_starttag(self, tag, attrs):
        if tag == 'img':
            for name,value in attrs:
                if name == 'class':
                    self.c = value
                if name == 'src':
                    self.s = value
            if self.c == 'Photo':
                self.img_link = self.s
            self.c = ''
            self.s = ''

5,处理相册/照片分页的情况

当相册/照片超出一定数量时又拍会分页显示,所以还得判断是否存在分页的情况。

相册页面和照片页面分页的 html 代码是相同的,可以用一个类来处理。

html 代码:

python 代码:

class checkNextPage(HTMLParser.HTMLParser):
    def __init__(self):
        HTMLParser.HTMLParser.__init__(self)
        self.c = ''
        self.h = ''
        self.next_link = ''
    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            for name,value in attrs:
                if name == 'class':
                    self.c = value
                if name == 'href':
                    self.h = value
            if self.c == 'nextprev':
                self.next_link = 'http://www.yupoo.com' + self.h
            self.c = ''
            self.h = ''
 
    def handle_data(self, data):
        if data == u'上一页':
            self.next_link = ''

在遇到 “上一页” 时将 “self.next_link” 清空是因为无法从 html 标签上区分链接是属于 “上一页” 还是 “下一页”,所以,在遇到 “上一页” 时将变量 “self.next_link” 清空,以确保其储存的是 “下一页” 的链接。

四,下载图片

为了稳定性和速度,本来想调用 wget 或是 aria2c 来下载照片,不过考虑到跨平台,还是用 urllib 模块的 urlretrieve() 方法来实现了。反正都是一行的代码的事。

测试证明 urlretrieve() 还是很给力的。

urllib.urlretrieve(img_link, local_path)

五,使用方法

1,下载代码

完整的代码在这里(需代理)。

2,登陆又拍

使用 firefox 3.x 登陆又拍,登陆的时候一定要勾选 “记住我” 。然后复制 firefox 的个人文件夹下的 cookies.sqlite 文件,放到和下载后的代码相同的位置。

我知道这样太不智能,太别扭了,将就一下吧。反正这个程序也不会经常使用的。

3,安装 python 和 pysqlite 模块

Linux 大多都默认包含 python,只需要安装 pysqlite 模块。在 ArchLinux 下,pysqlite 模块的包名称是:python-pysqlite 。

Windows 则要单独安装 python 。并且因为 windows 下最新的 pysqlite 模块只支持到 python2.6,所以必须安装 python2.6 。下载 python2.6pysqlite

3,运行程序

在终端执行:

python2 yupoo_pic_download.py

六,已知问题

1,同名相册和图片

在处理相册和图片列表的时候,很简单的使用了字典。导致遇到同名相册/图片时会直接覆盖上一个相册/图片。不过这种情况应该不多吧。

2,图片的扩展名

又拍只显示了图片的文件名,所以下载下来的图片没有扩展名。这对于 Linux 用户来说应该问题不大,很多文件管理器都是优先根据文件内容判断文件类型。或者高级点的用户可以先用 file 判断一下文件类型,再用 for 循环批量重命名。

对于 windows 用户可能麻烦一点。一个不怎么 perfect 的办法是,管它什么类型,下载时通通命名成 “.jpg” 文件。把代码 “local_path = path + os.sep + p_name” 改为 “local_path = path + os.sep + p_name + ‘.jpg'” 就可以了。

七,结尾

写这篇博客仅仅用于技术交流,因为这样通过解析 html 来获得图片的下载地址非常不可靠,哪天又拍稍稍改变一下代码这个程序就得重写了。

我不知道为什么国内很多互联网企业喜欢以一种非常不开放的方式做服务,总之也挺不靠谱的。我更愿意把照片都放到 picasaweb 和 flickr ,即使哪天通向他们的道路被阻挡,我还有办法跨过长城。虽然道路曲折一点,也好过只出不进和有去无回。

(完)

使用 python 批量下载又拍(yupoo)照片》上有7条评论

  1. 瓜子

    太好了!原来Flickr和Picasa是支持批量下载的啊! 我正想把这些照片都下载下来呢.

    [回复]

    瓜子 回复:

    不好意思… 我没找到Flickr的批量下载功能在什么地方。博主能告诉我怎么批量下载在Flickr上的照片吗?

    [回复]

    hc 回复:

    flickr 有提供相应的 API ,你可以寻找相应的应用。

    [回复]

    瓜子 回复:

    原来是API 感谢感谢!这样的话Yupoo的API也可以这么做对吧?

    hc 回复:

    是的,但前提是 yupoo 要提供这样的 API ,可惜它似乎没有提供。

发表评论

电子邮件地址不会被公开。