python实现爬虫批量下载图片

本文介绍了一个使用Python编写的简易爬虫程序,用于从特定网站批量下载图片。通过分析网页结构,利用正则表达式抓取图片链接,同时采用多线程技术提升下载效率。文章还详细记录了从需求分析、代码实现到功能完善的全过程。
摘要由CSDN通过智能技术生成

一、准备工作

上周无意间(真的是无意间)发现了一个奇怪的网站,上面有一些想要的图片,谷歌浏览器上有批量下载图片的插件,但是要把所有页面都打开才能下载,比较麻烦。于是想着能不能写个爬虫程序,刚好自己也一直想学一下这个东西。

秋招面试小红书的时候,二面的面试官问我怎么实现一个分布式爬虫软件,我之前根本不知道爬虫是什么原理,只是听说过而已。所以后来也一直想学一下。

先上网搜索了一下,发现都是python的爬虫。于是周末用了一天时间学了python语法基础,其实只要你学过一门语言,再去学其他语言都会快很多,只要把数据类型,语法格式,函数定义等掌握一下,基本就能看懂代码。

接着找到网上一个下载百度图片的爬虫例子,代码很简单,原理也很好理解。因为我暂时只想下载一些图片,没有必要追求太多。所以就以这个教程作为模板,结合我的目标网站做了相应的修改。

爬虫下载百度图片教程

import re
import requests

def dowmloadPic(html, keyword):
    pic_url = re.findall('"objURL":"(.*?)",', html, re.S)
    i = 1
    print('找到关键词:' + keyword + '的图片,现在开始下载图片...')
    for each in pic_url:
        print('正在下载第' + str(i) + '张图片,图片地址:' + str(each))
        try:
            pic = requests.get(each, timeout=10)
        except requests.exceptions.ConnectionError:
            print('【错误】当前图片无法下载')
            continue

        dir = '../images/' + keyword + '_' + str(i) + '.jpg'
        fp = open(dir, 'wb')
        fp.write(pic.content)
        fp.close()
        i += 1


if __name__ == '__main__':
    word = input("Input key word: ")
    url = 'http://image.baidu.com/search/flip?tn=baiduimage&ie=utf-8&word=' + word + '&ct=201326592&v=flip'
    result = requests.get(url)
    dowmloadPic(result.text, word)

二、写代码

首先分析了一下我要下载的网站的前端代码,然后基于上面的代码开始写。

  1. 先利用这个网站的搜索功能,搜索的统一url格式为公共前缀/关键词,获得搜索结果页面。
  2. 搜索结果页面上就有很多相册,点击相册后会进入具体的相册地址,相册的url的统一格式为公共前缀/相册编号/
  3. 进入每个相册后,是分页展示的很多图片。而且由于分页页数可能很多,中间一些分页标签用省略号代替了,所以这里需要找到分页的最大页面编号。分页的地址统一格式为公共前缀/编号.html。所以匹配出所有分页地址后,把编号的最大值找出来。
  4. 在每一个分页地址中,可以进行图片下载,图片的地址也是有规律的,公共前缀/相册编号/a/图片编号,所以只要找到这些链接然后进行下载就好了。

因为我忘了保存中间代码,所以这部分只说一下思路。下次记得用git保存一下。不过上面这些其实也只是一些简单过程。

这部分花的最多的时间是在写正则表达式,之前都没用过这个知识,这次才发现原来会写正则表达式很省事

三、改进代码

改进的部分主要是用多线程增加下载速度,修改搜索的不足。

  1. 首先是用多线程增加下载速度。因为我会以文件的格式输入多个关键词,
    - 因此在处理关键词的时候,采用多线程形式处理。每个线程处理一个关键词。这里主要用queue.Queue()的同步队列。
    - 然后对每个关键词进行搜索之后会得到很多相册,再设置多线程每个线程负责处理一个相册。刚开始这里也是用了一个queue.Queue(),后来发现这样的话,不同关键词的相册就混在一起了,于是又改成字典形式{关键词:queue.Queue()}
  2. 另外,我发现网站搜索得到的相册不全。需要进入相册后点击标签上的人名,进入对应的主页才能获得所有相册。相册展示也可能是分页的。这里又做了一步处理。
  3. 添加了日志输出,关键点的操作输出到控制台和文件中。

完整的代码

import logging
import queue
import threading

from spider_download import spider_download

global_data = threading.local()
threadLock = threading.Lock()
all_albums = set()

LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"    # 日志格式化输出
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"                        # 日期格式
fp = logging.FileHandler('logging.txt', encoding='utf-8', mode='a')   # 日志写入文件中
fs = logging.StreamHandler()  # 日志写到控制台
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT, handlers=[fp, fs])


class myThread(threading.Thread):
    def __init__(self, name, q):
        threading.Thread.__init__(self)
        self.name = name
        self.q = q

    def run(self):
        logging.info("开启关键字线程:" + self.name)
        global_data.num = 0
        process_download(self.name, self.q)
        logging.info("退出关键字线程:" + self.name)


def process_download(threadName, q):
    while not workQueue.empty():
        keyword = q.get(True, 3)
        logging.info("%s processing %s" % (threadName, keyword))
        spider_download(keyword, all_albums)


def read_files():
    file_name = "46.txt"
    try:
        f = open(file_name, "r", encoding="utf-8")
        lines = f.readlines()
        print(lines)
        for line in lines:
            line = line.strip('\n')
            workQueue.put(line)
        logging.info("共添加了%d个关键词" % len(lines))
        return len(lines)
    finally:
        f.close()


workQueue = queue.Queue()
threads = []

if __name__ == "__main__":
    """
    读取文件,获取关键词,每个线程负责处理一个关键词
    """
    read_files()  # 读取文件,填充队列
    thread_num = 5  # 也可以根据read_files()返回值进行调整
    logging.info("将创建%d个线程" % thread_num)
    for num in range(1, thread_num + 1):
        thread = myThread("Thread-" + str(num), workQueue)
        threads.append(thread)  # 添加线程到线程列表
        thread.start()  # 开启线程
    # 等待所有线程完成
    for t in threads:
        t.join()
    logging.info("退出主线程")

import logging
import queue
import re
import threading
from os import mkdir, makedirs, path
import requests

global_data = threading.local()
mapLock = threading.Lock()
workQueueMap = dict()  # 相册队列字典,以关键词作为同步队列名
threads = []
all_album = set()

LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"  # 日志格式化输出
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"  # 日期格式
fp = logging.FileHandler('logging.txt', encoding='utf-8', mode='a')  # 日志写入文件中
fp.setLevel(logging.WARNING)
fs = logging.StreamHandler()  # 日志写到控制台
logging.basicConfig(level=logging.INFO, handlers=[fp, fs])


class myThread2(threading.Thread):
    def __init__(self, name, q, keyword):
        threading.Thread.__init__(self)
        self.name = name
        self.q = q
        self.keyword = keyword

    def run(self):
        logging.info("开启下载相册线程:" + self.name)
        global_data.num = 0
        process_download(self.name, self.q, self.keyword)
        logging.info("退出下载相册线程:" + self.name)


def process_download(threadName, qmap, keyword):
    """
    从队列中取出一个相册地址,进行后续处理,每一个相册对应一个线程
    """
    if keyword not in qmap:
        # print(qmap)
        logging.error("字典为空或关键词不在字典中,线程退出%s--%s" % (threadName, keyword))
        return
    while not qmap[keyword].empty():
        album = qmap[keyword].get(True, 3)  # 从队列中取出一个相册地址,完整地址
        logging.info("%s processing album %s" % (threadName, album))
        nums = re.findall('(\d+)/', album)  # 直接从地址中匹配出相册编号
        filepath = "images/" + keyword + "/"
        if not path.exists(filepath):  # 如果目录不存在时,创建目录
            makedirs(filepath)
        record = "images/" + keyword + "/record.txt"  # 记录重复的文件夹,不再重复下载
        if nums[0] in all_album:
            fp = open(record, 'a+')  # 以二进制形式写文件
            fp.write(nums[0] + "\n")
            fp.close()
            continue
        else:
            all_album.add(nums[0])
        filepath = "images/" + keyword + "/" + nums[0] + "/"  # 拼接目录
        if not path.exists(filepath):  # 如果目录不存在时,创建目录
            makedirs(filepath)
        result = requests.get(album)
        reg = album + '\S+"'
        page_url = re.findall(reg, result.text)  # 根据正则表达式找到分页链接
        pages = set(page_url)  # 去重
        pages.add(album)  # 把第一页加进去
        # 找到页面数量最大值,进行循环,否则可能存在一些下载不到的图片
        max_num = 0
        for p in pages:
            rst = re.findall('(\d+).html', p)
            if rst:
                page_num = int(rst[0])
                max_num = max(max_num, page_num)  # 找到分页编号最大的

        global_data.i = 0  # 每个相册重新编号和计数
        global_data.failed = 0
        global_data.success = 0
        logging.info("找到了关于%s的%s号相册的%d个链接" % (keyword, nums[0], max_num))
        getimgs(album, nums[0], max_num, keyword, filepath)  # 根据每个分页链接获取图片地址
        logging.warning("下载完成: %s-%s 相册, 共下载%d张图片,其中%d张成功,%d张失败" % (keyword, str(nums[0]),
                                                                    global_data.i, global_data.success,
                                                                    global_data.failed))


def download_img(images, keyword, num, filepath):
    """
    下载每个页面上的图片,并且更改名字
    """
    logging.info('关键词:' + keyword + '的图片,现在开始下载图片...')
    for each in images:
        global_data.i += 1
        logging.info('正在下载第%d张图片,图片地址:%s' % (global_data.i, each))
        try:
            pic = requests.get(each, timeout=10)  # get请求,超时时间10s
            global_data.success += 1
        except requests.exceptions.ConnectionError:
            logging.info('【错误】当前图片无法下载,地址:%s' % each)
            global_data.failed += 1
            continue
        img_name = filepath + '/' + keyword + '_' + str(num) + '_' + str(global_data.i) + '.jpg'  # 给图片重命名
        fp = open(img_name, 'wb')  # 以二进制形式写文件
        fp.write(pic.content)
        fp.close()


def getimgs(each, num, max_num, keyword, filepath):
    """
    根据每个分页地址,最大分页数,对每个分页访问,获得所有图片地址
    """
    for page_num in range(1, max_num + 1):
        if page_num == 1:
            url = each
        else:
            url = each + str(page_num) + ".html"
        result = requests.get(url)
        reg = '"(公共前缀/' + num + '\S+)"'
        img_url = re.findall(reg, result.text)  # 根据正则表达式找到图片地址链接
        images = set(img_url)
        logging.info("在第%d页上找到%d张图片" % (page_num, len(images)))
        download_img(images, keyword, num, filepath)


def getpages(albums, nums, keyword):
    """
    进入每个图册中后,获取分页链接,找到最多分页数目
    """
    albums = set(albums)
    nums = set(nums)
    logging.warning("关键词:%s共找到%d个相册" % (keyword, len(albums)))
    if keyword not in workQueueMap:
        workQueueMap[keyword] = queue.Queue()
    for each in albums:
        workQueueMap[keyword].put(each)  # 将相册地址加入队列中
    # print(workQueueMap)
    thread_num = min(5, len(albums))  # 线程太多会导致下载请求超时
    # 创建新线程
    for num in range(1, thread_num + 1):
        thread = myThread2(threading.currentThread().getName() + "-2-" + str(num), workQueueMap, keyword)
        threads.append(thread)  # 添加线程到线程列表
        thread.start()  # 开启线程
    # 等待所有线程完成
    for t in threads:
        t.join()
    logging.info("退出关键字线程" + threading.currentThread().getName())


def search(keyword):
    """
    输入关键词进行搜索得到一个页面并返回给下一步
    """
    url = '公共前缀/search/' + keyword
    result = requests.get(url)
    if result:
        logging.info("关于%s的搜索成功" % keyword)
    else:
        logging.info("关于%s的搜索失败", keyword)
        raise Exception("搜索失败")
    return result


def getalbum(html, keyword):
    """
    需要根据关键词找到的第一个相册获得关键词主页
    主页相册可能包括多页,需要进行判断处理
    然后返回相册地址集合
    """
    album_url = re.findall('(公共前缀/a\S+)"', html)
    album_num = []
    if album_url:
        result = requests.get(album_url[0])
        result.encoding = "utf-8"
        reg = 'href="(\S+)"\s+target="_blank"\>' + keyword
        home_url = re.findall(reg, result.text)  # 主页链接
        if home_url:
            homepage = requests.get(home_url[0])
            homepage.encoding = "utf-8"
            reg = '已收录<span>(\d+)</span>套写真集'
            all_num = re.findall(reg, homepage.text)
            logging.warning("%s的主页收录了%s个相册" % (keyword, all_num[0]))
            get_allAlbums(keyword, homepage.text, album_url, album_num)
            nextpage = home_url[0] + "index_1.html"
            hasnext = re.findall(nextpage, homepage.text)
            if hasnext:
                get_allAlbums(keyword, hasnext.text, album_url, album_num)
        else:
            logging.error("没有搜索到关键词为%s的主页" % keyword)
    else:
        logging.error("没有找到关键词%s相关的任何相册" % keyword)
    return album_url, album_num


def get_allAlbums(keyword, html, album_url, album_num):
    urls = re.findall('(公共前缀/a\S+)"', html)
    nums = re.findall('公共前缀/a/(\d+)/', html)
    logging.info("搜索页获得%s相册的链接:%s" % (keyword, album_url))
    logging.info("搜索页获得%s相册的编号: %s" % (keyword, album_num))
    album_url.extend(urls)
    album_num.extend(nums)


def spider_download(word, all_albums):
    global all_album
    all_album = all_albums
    result = search(word)  # 搜索页面
    albums, nums = getalbum(result.text, word) # 获取相册
    getpages(albums, nums, word)	# 下载相册

四、结束

其实这个程序还是很简单的,并没有用到一些高大上的框架。之后可能研究一些更深入的知识,用来获取一些更有趣的东西。
最后分享几张爬到的图片


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值