python3爬虫系列16之多线程爬取汽车之家批量下载图片

python3爬虫系列16之多线程爬取汽车之家批量下载图片

1.前言

上一篇呢,python3爬虫系列14之爬虫增速多线程,线程池,队列的用法(通俗易懂),主要介绍了线程,多线程,和两个线程池的使用。

这一篇,我们就来实战一下下了~

鼠标我最近飘了,都敢去看车网浏览了,看着这么多车,又买不起,心中伤感,于是抓紧把他们拿下来,偷偷欣赏。

打开了一个看车网站,
https://car.autohome.com.cn/pic/series/66-1.html

在这里插入图片描述

这么多车呢?不爬取下,岂不是浪费技能?

2.网页分析

首先我们来分析一下;
宝马三系,其它细节图

https://car.autohome.com.cn/pic/series/66-12-p1.html

打开这个看车的网站图片页面

在这里插入图片描述

可以看到一共有 10 页,

其中每一页都是不同的细节图,而且每页基本上60张图。

够了,够了,现在点击下一页,

  • 第1页: https://car.autohome.com.cn/pic/series/66-12-p1.html
  • 第2页: https://car.autohome.com.cn/pic/series/66-12-p2.html
  • 第3页: https://car.autohome.com.cn/pic/series/66-12-p3.html

可以发现 URL 变了

https://car.autohome.com.cn/pic/series/66-12-pX,

所以这个变化的地方,就是表示页数,后面直接当做变量处理,爬完一页就传下一页的参数,这样就可以一直快乐了。

在这里插入图片描述

紧接着,F12打开网页审查,看一下源代码,

我们要爬车的名字,所以先找到我们的车名的地址:
在这里插入图片描述

copy下他的xpath路径:所有车图片的div

/html/body/div[2]/div/div[2]/div[7]/div/div[2]/div[2]

再找到我们的车的细节图片img:

在这里插入图片描述

copy下他的xpath路径:所有车图片的img

/html/body/div[2]/div/div[2]/div[7]/div/div[2]/div[2]/ul/li[1]/a/img

3.写代码了

本次使用queue队列,和多线程threading一起来,还有咱们的lxml+xpath的方式。

不记得的,回头去看看:python3爬虫系列11之xpath和css selector方式的内容提取介绍

不说了,开撸

先定义一下线程的数量:

concurrent = 3           # 采集线程数
conparse = 3             # 解析线程数

再构造每一页的url

def main():
    # 生成请求队列
    req_list = queue.Queue()
    # 生成数据队列 ,请求以后,响应内容放到数据队列里
    data_list = queue.Queue()
    # 创建文件对象
    f = open('qichezhijia.json','w',encoding='utf-8')
    # 循环生成多个请求url
    for i in range(1,10 + 1):  # 10页
        base_url='https://car.autohome.com.cn/pic/series/66-12-p%d.html' % i
        # 加入到请求队列中
        req_list.put(base_url)

生成N个采集线程:用来调用Crawl创造线程方法

    # 生成N个采集线程
    req_thread = []
    for i in range(concurrent):
        t = Crawl(i + 1, req_list, data_list)  # 1.调用创造线程方法
        t.start()
        req_thread.append(t)

生成N个解析线程:用来调用Parse创造解析线程方法

    # 生成N个解析线程
    parse_thread = []
    for i in range(conparse):
        t = Parse(i + 1,data_list,req_thread,f)     # 2.创造解析线程方法
        t.start()
        parse_thread.append(t)
    for t in req_thread:

然后记得join一下线程:

    for t in req_thread:
        t.join()
        for t in parse_thread:
            t.join()

然后新建一个采集线程类:class Crawl(threading.Thread):


'''
采集线程类
'''
class Crawl(threading.Thread):
    # 初始化
    def __init__(self,number,req_list,data_list):
        # 调用Thread 父类方法
        super(Crawl,self).__init__()
        # 初始化子类属性
        self.number = number
        self.req_list = req_list
        self.data_list = data_list
        self.headers = {
            'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36'
        }
        # 线程启动的时候调用
    def run(self):
        # 输出启动线程信息
        print('启动采集线程%d号' % self.number)
        # 如果请求队列不为空,则无限循环,从请求队列里拿请求url
        while self.req_list.qsize() > 0:
            # 从请求队列里提取url
            url = self.req_list.get()
            print('%d号线程采集:%s' % (self.number,url))
            # 防止请求频率过快,随机设置阻塞时间
            time.sleep(random.randint(1,3))
            # 发起http请求,获取响应内容,追加到数据队列里,等待解析
            response = requests.get(url,headers=self.headers)
            if response.status_code == 200:
                self.data_list.put(response.text) # 向数据队列里追加    
 

用来发送网页请求,get到网页源代码的。

在写一个解析线程类class Parse(threading.Thread):


'''
解析线程类
'''
class Parse(threading.Thread):
    # 初始化属性
    def __init__(self,number,data_list,req_thread,f):
        super(Parse ,self).__init__()
        self.number = number                # 线程编号
        self.data_list = data_list          # 数据队列
        self.req_thread = req_thread        # 请求队列,为了判断采集线程存活状态
        self.f = f                          # 获取文件对象
        self.is_parse = True                # 判断是否从数据队列里提取数据
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36'
        }

    def run(self):
        print('启动%d号解析线程' % self.number)
        # 无限循环,
        while True:
            # 如何判断解析线程的结束条件
            for t in self.req_thread:           # 循环所有采集线程
                if t.is_alive():                # 判断线程是否存活
                    break
            else:                           # 如果循环完毕,没有执行break语句,则进入else
                if self.data_list.qsize() == 0:             # 判断数据队列是否为空
                    self.is_parse = False                   # 设置解析为False

             # 判断是否继续解析
            if self.is_parse:   # 解析
                try:
                    data = self.data_list.get(timeout=3)            # 从数据队列里提取一个数据
                except Exception as e:      # 超时以后进入异常
                    data = None
                # 如果成功拿到数据,则调用解析方法
                if data is not None:
                    self.parse(data)        # 3.调用页面解析方法
            else:
                break       # 结束while 无限循环
        print('退出%d号解析线程' % self.number)

    # 页面解析函数
    def parse(self,data):
        html = etree.HTML(data)

        # 获取所有车图片的div==/html/body/div[2]/div/div[2]/div[7]/div/div[2]/div[2]
        duanzi_div = html.xpath('/html/body/div[2]/div/div[2]/div[7]/div/div[2]/div[2]')

        for duanzi in duanzi_div:
            # 获取车名
            carName = duanzi.xpath('./ul/li/div/a/text()')
            print('车名:',carName,len(carName))

            # 获取车图==/html/body/div[2]/div/div[2]/div[7]/div/div[2]/div[2]/ul/li[1]/a/img
            imgList= duanzi.xpath('./ul/li/a/img/@src')
            print('图片:',imgList,type(imgList),len(imgList))  #  <class 'list'>
            
 			downfiles(imgList,carName)
         

这是用来提取内容,采用的
html = etree.HTML(data)来解析请求到的html源代码,然后通过xpath路径来抓取需要的数据,然后封装成为字典,便于持久化入库等操作。

            # 内容封装到字典
            item = {
                'carname': carName,
                'img': imgList,
            }
            # 转为json对象-写入文件
            self.f.write(json.dumps(item,ensure_ascii=False) + '\n')
            
print('爬虫完毕。')

最后调用一下 downfiles(imgList,carName)下载这个图片。

新建一个downfiles下载图片的函数:

 # 创建文件对
  
f = open('qichezhijia.json','w',encoding='utf-8')
'''
下载图片文件
'''
def downfiles(imglist, carName):
    x = 0  # 计数
    for name in carName:
        folder = './pic/01/' + name + '_' + str(x)   # 设置图片保存的路径
    if not os.path.exists(folder):
        #os.makedirs(folder)
        print(folder + ' => 创建成功')
        # 从imglist中遍历拿到单个的imgurl
        for imgurl in imglist:
            # 发起网络请求来下载图片!!!(很重要)
            imgres = requests.get("http:" + imgurl)
            if imgres.status_code == 200:
                # img.content用来返回二进制数据,wb是二进制流写入
                fname = imgurl.split('/')[-1]
                with open(folder + fname, "wb") as f:
                    f.write(imgres.content)
                    x += 1
                    print("下载第", x, "张")
            else:
                print('图片下载失败:',imgres.status_code)
    else:
        pass
        print('路径已创建,请忽略 => ' + carName)


在启动一下:

if __name__ == '__main__':
    start = time.time()
    main()                  # 运行爬虫程序
    end = time.time()
    spendtime = end - start
    print("用时 " + str(spendtime) + "秒")

运行:
在这里插入图片描述
耗时:
在这里插入图片描述

爬了:21秒。

最终效果:
在这里插入图片描述
可以看到我们爬了564张。
网站上也是564张。

在这里插入图片描述
同时我们还存了json数据,方便入库。
在这里插入图片描述

报错处理

  1. requests.exceptions.MissingSchema: Invalid URL ‘None’: No schema supplied. Perhaps you meant http://None?

或者这样:

  1. requests.exceptions.MissingSchema: Invalid URL
    ‘//car2.autoimg.cn/cardfs/product/g2/M05/9D/F6/240x180_0_q95_c42_autohomecar__ChsEml3YCWaASLG6AAGfAWdgL9E414.jpg’: No schema supplied. Perhaps you meant http:////car2.autoimg.cn/cardfs/product/g2/M05/9D/F6/240x180_0_q95_c42_autohomecar__ChsEml3YCWaASLG6AAGfAWdgL9E414.jpg?

图示:
在这里插入图片描述
在这里插入图片描述
原因:
爬虫中,原网页的图片img路径为不规则形式,//路径字符,自动带有的,
在这里插入图片描述
是这样的:
[’//car3.autoimg.cn/cardfs/product/g27/M0B/56/28/240x180_0_q95_c42_autohomecar__ChcCQF3ST7uAJQgYAAUBwmZly3E432.jpg’] <class ‘list’>

虽然网页请求是没有问题。:在这里插入图片描述

但是这种情况,我们要处理:
列表插入的方式,加个https的头,

解决方案 :
我的img是个list。

#如果url正常的,则这样就可下载图片 img = requests.get(imgSrc[0],headers=self.headers)

#如果不是正常的,则发起网络请求来下载图片!!!(很重要) img = requests.get(“http:”+imgSrc[0],headers=self.headers)

  1. 批量下载list集图片工具类
'''
下载文件
'''
def downfiles(imglist, title):
   x = 0
   folder = 'D:\\xxxxxx\\'+ time.strftime("%Y%m%d", time.localtime()) +'\\' + strreplace(title) + '\\'
   
   if not os.path.exists(folder):
      os.makedirs(folder)
      print(folder+' => 已创建')
  
      # 遍历
      for imgurl in imglist:
          # 获取从imglist中遍历得到的imgurl
          imgres = requests.get(imgurl)
          fname = imgurl.split('/')[-1]
          with open(folder + fname, "wb") as f:
              f.write(imgres.content)
              x += 1
              print("第", x ,"张")              
   else:
      pass      
      print('忽略 => ' + title);

实际上源码已经附在文章,拼接即可。

也提供源码下载地址:多线程爬虫汽车之家

没有积分留下邮箱,发给你。


番外篇:

现在假如你的CPU有8核,每核CPU都可以用1个进程,每个进程可以用1个线程来进行计算。那么8核CPU同时只能对8个任务进行操作了,这样来爬虫就会非常高效。

多进程爬虫:
想要充分利用多核的CPU还是用多进程。可以做到并行爬取。

多线程爬虫:
多线程下的 GIL 锁会让多线程显得有点鸡肋。

通俗点说 GIL锁 就是一把超级大锁,即全局排他锁,保护了数据安全性的同时,使得多线程提高效率的能力几乎丧失(Python 里一个进程永远只能同时执行一个线程,拿到 GIL 的线程才能执行),这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。

结果方法之一是python提供了mutiprocess(多进程)来弥补这个问题,

所以,在 Python 里面推荐使【用多进程】而【不用多线程】。

发布了212 篇原创文章 · 获赞 934 · 访问量 106万+

猜你喜欢

转载自blog.csdn.net/ITBigGod/article/details/103246368