追逐繁星的孩子

お帰りなさい

首页 标签 归档 分类 关于
Python爬虫——爬P站美图
日期 2017-08-16   |    标签 Python   |    评论

可能是因为最近对于画画的兴致比较高,P站逛的比较多。每每看到那些大触的作品,总是不由的想让教练教我画画了!其实对于画画的兴趣从小就有,犹记得小时候被关在家里,趴在一张比自己还大的绘画纸上作画。不过后来就被无情的学业给压制了,兴趣也大不如前。

好吧,既然暂时无法自己画出美美的图画,那就先无耻的把P站上的美图保存下来吧。于是乎,花了几天的时间写了个爬虫,看着硬盘里萌萌的动漫小姐姐图片,内心很满足。所以想着写篇文章总结一下吧!点击这里查看源码

(一) 准备工作

以下是我整理的关于爬P站的一些要点(知己知彼,百战不殆)

  • P站下载图片需要登录
  • 暂时处理的是P站排行榜下的图片(至少可以放心图片质量)
  • P站的大图访问有防盗链
  • P站图片存在普通图片和GIF图(这也是我主要关注的)
  • 普通图像存在两种格式(JPG和PNG)
  • P站动图都是通过请求zip(里面是每一帧的图像)

(二) Let's go

登录(通过requests库)

P站的登录并不复杂,首先找到POST地址 https://accounts.pixiv.net/api/login,分析POST参数就行,但是你会看到其中的一个参数post_key,这个参数其实存在于P站登录页面(当然咱们需要之前登录过P站哈!),写个正则把post_key的内容抓过来就OK了。为了之后不需要频繁去登录,这边直接采用requsets的Session保持连接,之后的请求都通过这个Session去连接就行。至此,登录完毕!

# post请求内容
data = {
    'pixiv_id': '',
    'password': '',
    'captcha': '',
    'g_recaptcha_response': '',
    'source': 'pc',
    'ref': 'wwwtop_accounts_index',
    'return_to': 'https://www.pixiv.net/',
    'post_key': ''
}
def loginPixiv():
    pixiv_id = input('请输入账号: ')
    password = input('请输入密码: ')
    # 获取session实例
    p = requests.Session()
    # 获取post请求必要数据
    p.headers = headers
    r = p.get(url='https://accounts.pixiv.net/login', headers=headers)
    parrern = re.compile(r'name="post_key" value="(.*?)">')
    res = parrern.search(r.text)
    data['post_key'] = res.group()
    data['pixiv_id'] = pixiv_id
    data['password'] = password
    # post请求模拟登陆
    print('模拟登陆开始......')
    try:
        p.post(url='https://accounts.pixiv.net/api/login', data=data)
        print('模拟登陆成功......')
        return p
    except:
        print('模拟登陆失败......')
        raise

分析页面(BeautifulSoup)

我们想下载的图片在这边 https://www.pixiv.net/ranking.php?mode=daily,里面有好几个标签,处理方式都一样,变一下get请求参数就行了。

那么就分析下当前页面好了。当前页面上有许多排行图片,这些图片有共同的class(ranking-item),通过bs4下的BeautifulSoup分析即可,每个class下需要抓取2个值,一个是图片的链接地址(A标签内的href值),另一个是图片的缩略图地址(img下的data-src,这并不是实际我们需要抓取的图片URL)。当然仔细观察下来其实缩略图地址可以很容易的转换成实际图片地址,这边就不展开了。于是乎,我们有了一个链接地址和一个待下载图片地址。这个链接地址我们需要在访问图片地址的时候加在referer上(应对防盗链)。

以上是针对单图的页面分析。但是P站上还有很多多图和动图。

多图:与单图分析一样,只是增加了一个该图片下有多少张子图片的值而已,获取到这个值就OK了,其他处理一样。

动图:动图的分析也与单图一样,只不过咱们需要把获取到的缩略图转换为ZIP包的URL。

图片下载

现在我们已经拿到了图片的地址,但是由于P站图片后缀可以是JPG或者PNG,而且在实际请求图片之前并不清楚属于什么格式,只能都请求一遍来确定具体的格式。那么接下来我们模拟个请求头(headers),注意加上Referer参数。get即可,通过写文件的方式将图片下载下来。

至此,差不多实现图片抓取功能了。

但是~~~~这样速度实在太慢了。我们需要优化下————

断点下载图片(多线程)

这边我通过多线程同时请求图片的不同部分来达到加快下载图片的目的(通过在请求头内增加Range参数),类似迅雷下载的原理:比如我下载一张1MB的图片,那么其响应主体的长度约等于1000000,如果我起5个线程,分别下载0-200000,,200000-400000,400000-600000,600000-800000,800000-1000000,再往文件的不同seek处写进去就行。这里处理好线程共享变量的问题就OK了。

class downloader:
    def __init__(self, login, url, num, filename, referer):
        self.url = url
        self.num = num
        self.login = login
        self.lock = threading.Lock()
        self.filename = filename
        self.referer = referer
        self.total = int(self.login.head(self.url).headers['Content-Length'])

    def get_range(self):
        ranges = []
        offset = int(self.total / self.num)
        for i in range(self.num):
            if i == self.num - 1:
                ranges.append((i * offset, self.total - 1))
            else:
                ranges.append((i * offset, (i + 1) * offset - 1))
        return ranges
    def download(self, start, end, f):
        retry = 0
        success = False
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.109 Safari/537.36',
            'Referer': '%s' % self.referer,
            'Range': 'Bytes=%s-%s' % (start, end)
        }
        while retry < 3 and not success:
            try:
                res = self.login.get(self.url, headers=headers)
                success = True
            except Exception as e:
                print(e)
                retry += 1
                if retry == 3:
                    print('重试三次均失败,退出!!')
                    break
        if success:
            with self.lock:
                f.seek(start)
                f.write(res.content)
    def run(self):
        fn = open(self.filename, "wb")
        thread_list = []
        for ran in self.get_range():
            start, end = ran
            thread = threading.Thread(target=self.download, args=(start, end, fn))
            thread_list.append(thread)
        for i in thread_list:
            i.start()
        for i in thread_list:
            i.join()
        fn.close()

现在我们下载图片应该快了不少。还能优化吗?当然可以————

采用队列多张图片同时下载

以上的处理流程是这样的:请求图片地址,下载图片,请求图片地址,下载图片......(循环)

我们看到,只能在请求完前一张图片的时候,才能下载,才能继续处理下一张图片。这样浪费了太多的IO操作时间。

改善流程:请求完所有图片地址放入队列,开多个线程进行消费

这样我们可以达到多个图片同时下载的目的,极大的加快下载速度。

URL_QUEUE = queue.Queue()
def downLoad(login):
    while not URL_QUEUE.empty():
        file, ref, url = URL_QUEUE.get()
        file_downloader.downloader(login, url, THREAD_COUNT, file, ref).run()
        URL_QUEUE.task_done()
def do(login):
    t = []
    for i in range(THREAD_COUNT):
        th = threading.Thread(target=downLoad, args=(login,))
        t.append(th)
    for i in t:
        i.start()
    for i in t:
        i.join()
    URL_QUEUE.join()
    use = (datetime.now() - s).seconds

现在只需要仅仅4分钟的时间就能下载完500张图片,速度达到3M/S

(三) END

其实目前还存在一个可优化点,如何快速收集所有的图片URL。

那么就说到这把!

继续收集美图(小姐姐图片)去~~~~~~~~~