多线程爬虫快速上手

2023-11-08

多线程爬虫

在实现网页爬虫的时候,经常会因为代理问题掉线导致爬虫失败,还又很多时候下载的文件略大,比如下载图片,因为下载图片是一个耗时的操作。如果采用之前那种同步的方式下载。那效率肯会特别慢。这时候我们就可以考虑使用多线程的方式来下载图片。

我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理。

在这里只说明多线程爬虫,在多线程基础上可以使用多进程来进行爬虫。多线程多进程在python中的使用可以参考我的另一篇博客

多线程介绍:

多线程是为了同步完成多项任务,通过提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。
最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也可以有多节车厢。多线程的出现就是为了提高效率。同时它的出现也带来了一些问题。更多介绍请参考:https://baike.baidu.com/item/多线程/1190404?fr=aladdin

threading模块介绍:

threading模块是python中专门提供用来做多线程编程的模块。threading模块中最常用的类是Thread

查看线程数:

使用threading.enumerate()函数便可以看到当前线程的数量。

查看当前线程的名字:

使用threading.current_thread()可以看到当前线程的信息。

创建Thread类并指定target来创建多线程

以下看一个简单的多线程程序:

import time
import threading


"""
传统的方式
def coding():
    for x in range(3):
        print("正在写代码....")
        time.sleep(1)

def drawing():
    for x in range(3):
        print("正在画图....")
        time.sleep(1)


def main():
    coding()
    drawing()


if __name__ == '__main__':
    main()
"""

"""
采用多线程的方式
"""
def coding():
    for x in range(3):
        print(threading.current_thread().name,"--正在写代码....")
        time.sleep(1)

def drawing():
    for x in range(3):
        print(threading.current_thread().name,"--正在画图....")
        time.sleep(1)


def main():
    print(threading.main_thread().name)
    threading.Thread(name="coding thread",target=coding).start() # 创建子线程coding thread
    threading.Thread(name="drawing thread",target=drawing).start() # 创建子线程drawing thread


if __name__ == '__main__':
    main()
    print("总共的线程数:",threading.enumerate())

继承自threading.Thread类来创建多线程:

为了让线程代码更好的封装。可以使用threading模块下的Thread类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码。示例代码如下:

import threading
import time


class CodingThread(threading.Thread):
    # 需要重写run 方法
    def __init__(self,name):
        super().__init__()
        self.name = name
    def run(self):
        for x in range(3):
            print(threading.current_thread().name, "--正在写代码....")
            time.sleep(1)


class DrawingThread(threading.Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name
    def run(self):
        for x in range(3):
            print(threading.current_thread().name, "--正在画图....")
            time.sleep(1)


def main():
    print(threading.main_thread().name)
    t1 = CodingThread("coding-thread")
    t2 = DrawingThread("drawing-thread")

    t1.start()
    t2.start()

if __name__ == '__main__':
    main()
    print("总共的线程数:", threading.enumerate())

多线程共享全局变量的问题:

多线程都是在同一个进程中运行的。因此在进程中的全局变量所有线程都是可共享的。这就造成了一个问题,因为线程执行的顺序是无序的。有可能会造成数据错误。比如以下代码:

import threading
import time
"""
多线程引发线程安全问题之--全局变量共享问题
"""

tickets = 0

def product_tickets():
    global tickets
    """生产者线程"""
    for x in range(1000000):
        tickets += 1
        # print("车票+1")
    print("剩余票数:",tickets)

def consume_tickets():
    global tickets
    """消费者线程"""
    for x in range(1000000):
        tickets -=1
        # print("车票-1")
        # time.sleep(0.1)
    print("剩余票数:", tickets)

def main():
    t1 = threading.Thread(name="productor:",target=product_tickets)
    t2 = threading.Thread(name="consumer:",target=consume_tickets)
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

以上结果正常来讲应该是6,但是因为多线程运行的不确定性。因此最后的结果可能是随机的。

在这里插入图片描述

锁机制:

一个比较高效的比喻:锁机制的引入就像是上公共厕所,坑位有限,前面的人进去了在上厕所,外面的人就只能等里面的人出来的再进去,而这个“坑位”就是我们要谈的锁对象,谁拿到了锁对象,就占住了坑位,谁就可以再厕所里把事情解决了再让出这个坑位,下一个人才能进去

为了解决以上使用共享全局变量的问题。threading提供了一个Lock类,这个类可以在某个线程访问某个变量的时候加锁,其他线程此时就不能进来,直到当前线程处理完后,把锁释放了,其他线程才能进来处理。示例代码如下:

import threading

VALUE = 0

gLock = threading.Lock()

def add_value():
    global VALUE
    gLock.acquire()
    for x in range(1000000):
        VALUE += 1
    gLock.release()
    print('value:%d'%VALUE)

def main():
    for x in range(2):
        t = threading.Thread(target=add_value)
        t.start()

if __name__ == '__main__':
    main()

Lock版本生产者和消费者模式:

生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。但是因为要使用中间变量,中间变量经常是一些全局变量,因此需要使用锁来保证数据完整性。

在爬虫中。我们可以把抓取页面的方法嵌入生产者模块。而把解析页面的方法嵌入消费者模块,从而大大提高爬虫效率

以下是使用threading.Lock锁实现的“生产者与消费者模式”的一个例子:

import threading
import time
import random

# 全局的变量保存金钱
MONEY = 0
# 生产者和消费者应该使用同一个锁对象
GLOCK = threading.Lock()
# 保存总共需要生产和消费的次数
GTOTAL_TIMES = 7
GTIMES = 0


class Producer(threading.Thread):
    def run(self):
        global MONEY
        global GTOTAL_TIMES
        global GTIMES
        while True:
            money = random.randint(100, 1000)  # 每次随机赚100~1000块钱
            GLOCK.acquire()
            if GTIMES>=GTOTAL_TIMES: # 出门上了一个星期的班。就休息,结束生产
                GLOCK.release()
                break
            MONEY += money
            print('%s刚刚挣了%d元钱,剩余%d元钱' % (threading.current_thread(), money, MONEY))
            GTIMES += 1
            GLOCK.release()
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global MONEY
        while True:
            money = random.randint(100, 1000)  # 每次随机消费100~1000块钱
            GLOCK.acquire()
            if MONEY >= money:
                MONEY -= money
                print('%s刚刚消费了%d元钱,剩余%d元钱' % (threading.current_thread(), money, MONEY))
            else:
                if GTIMES>=GTOTAL_TIMES: # 生产者不再生产了,消费者就停止消费,缩衣减食
                    GLOCK.release()
                    break
                print("还不快去挣钱!没奶粉钱啦!!")
            GLOCK.release()
            time.sleep(0.5)


def main():
    # 定义3个生产者和3个消费者
    for x in range(3):
        t_consumer = Consumer(name="消费者线程%d" % x)
        t_consumer.start()
    for x in range(3):
        t_producer = Producer(name="生产者线程%d" % x)
        t_producer.start()


if __name__ == '__main__':
    main()

Condition版的生产者与消费者模式:

Lock版本的生产者与消费者模式可以正常的运行。但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为。因此这种方式不是最好的。还有一种更好的方式便是使用threading.Condition来实现。threading.Condition可以在没有数据的时候处于阻塞等待状态。一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的操作。可以提高程序的性能。首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。以下将一些常用的函数做个简单的介绍:

  1. acquire:上锁。
  2. release:解锁。
  3. wait:将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notifynotify_all函数唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码。
  4. notify:唤醒某个正在等待的线程,默认是第1个等待的线程。
  5. notify_all:唤醒所有正在等待的线程。notifynotify_all不会释放锁并且需要在release之前调用。

Condition版的生产者与消费者模式代码如下:

import threading
import time
import random

# 全局的变量保存金钱
MONEY = 0
# 生产者和消费者应该使用同一个Condition对象
GCONDITION=threading.Condition()
# 保存总共需要生产和消费的次数
GTOTAL_TIMES = 7
GTIMES = 0


class Producer(threading.Thread):
    def run(self):
        global MONEY
        global GTOTAL_TIMES
        global GTIMES
        while True:
            money = random.randint(100, 1000)  # 每次随机赚100~1000块钱
            GCONDITION.acquire()
            if GTIMES>=GTOTAL_TIMES: # 出门上了一个星期的班。就休息,结束生产
                GCONDITION.release()
                break
            MONEY += money
            print('%s刚刚挣了%d元钱,剩余%d元钱' % (threading.current_thread(), money, MONEY))
            GTIMES += 1
            GCONDITION.notify_all()
            GCONDITION.release()
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global MONEY
        while True:
            money = random.randint(100, 1000)  # 每次随机消费100~1000块钱
            GCONDITION.acquire()
            """直接这样做会导致数据错误,因为condition在被唤醒后不是第一时间拿到锁,其他消费者线程会先进行消费,
            然后本线程排到号之后才会进行下面的代码运行,无法恢复现场。
            类似场景就像是你去吃火锅要排号,终于轮到你,结果服务员指着一桌子剩菜让你吃一样(因为没有时间去准备新的饭菜)
            所以采用while循环来解决这个问题,表示除非账户里的钱实际上大于要消费的钱时,才会跳出while循环,执行下面的代码
            
            if MONEY<=money:
                print("%s消费者准备消费%d元钱,剩余%d元钱,不足!进入阻塞状态,等待生产者线程唤醒" % (threading.current_thread(), money, MONEY))
                GCONDITION.wait()
            """
            while MONEY <= money:
                if GTIMES>=GTOTAL_TIMES:
                    GCONDITION.release()
                    return # 退出所有循环
                print("%s消费者准备消费%d元钱,剩余%d元钱,不足!进入阻塞状态,等待生产者线程唤醒" % (threading.current_thread(), money, MONEY))
                GCONDITION.wait()

            MONEY -= money
            print('%s刚刚消费了%d元钱,剩余%d元钱' % (threading.current_thread(), money, MONEY))
            GCONDITION.release()
            time.sleep(0.5)


def main():
    # 定义3个生产者和3个消费者
    for x in range(3):
        t_consumer = Consumer(name="消费者线程%d" % x)
        t_consumer.start()
    for x in range(3):
        t_producer = Producer(name="生产者线程%d" % x)
        t_producer.start()


if __name__ == '__main__':
    main()

Queue线程安全队列:

在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:

  1. 初始化Queue(maxsize):创建一个先进先出的队列。
  2. qsize():返回队列的大小。
  3. empty():判断队列是否为空。
  4. full():判断队列是否满了。
  5. get():从队列中取最后一个数据。(即最先放入的那个数据)
  6. put():将一个数据放到队列中。

一个Demo理解queque

import time
import threading
from queue import Queue

def set_value(q):
    index = 0
    while True:
        q.put(index)
        index +=1
        time.sleep(3)

def get_Value(q):
    while True:
        print(q.get()) # 如果无法从Queue中get到值,就会一直等待,所以打印的效果是3秒打印一次

def main():
    q = Queue(4)
    threading.Thread(name="生产队列",target=set_value,args=(q,)).start() # args可以传target目标函数的参数列表或元组
    threading.Thread(name="消费队列",target=get_Value,args=(q,)).start()


if __name__ == '__main__':
    main()


实例

使用生产者与消费者模式多线程下载表情包:

import queue

import requests
from lxml import etree
from urllib import request
import os
import re
import threading


class Producer(threading.Thread):
    def __init__(self, page_queue, img_queue, *args, **kwargs):
        super(Producer, self).__init__(*args, **kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            if self.page_queue.empty():  # 如果已经把page_queue里所有的url都取出来处理掉了,就退出死循环
                break
            url = self.page_queue.get()  # 获取queue队列中的url
            self.parse_page(url)

    def parse_page(self, url):
        """获取表情的url并放入img_queue"""
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
                          " Chrome/81.0.4044.129 Safari/537.36",
            "Referer": "https://www.doutula.com/photo/list/"
        }
        response = requests.get(url, headers=headers)
        text = response.text
        html = etree.HTML(text)
        imgs = html.xpath("//div[@class='page-content text-center']//img[@class!='gif']")
        for img in imgs:
            img_url = img.get('data-original')  # 获取图片的下载路径
            alt = img.get('alt')  # 获取图片的描述
            img_alt = re.sub(r'[\W]', '', alt)
            img_suffix = os.path.splitext(img_url)[1]  # 获取图片文件后缀名
            file_name = img_alt + img_suffix
            self.img_queue.put((img_url, file_name))  # 将解析出来的表情包所对应的下载链接和文件名传入img_queue


class Consumer(threading.Thread):
    def __init__(self, page_queue, img_queue, *args, **kwargs):
        super(Consumer, self).__init__(*args, **kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            if self.img_queue.empty() and self.page_queue.empty():  # 满足条件说明生产者生产完毕了,就不再消费了
                break
            img_url, filename = self.img_queue.get()
            request.urlretrieve(img_url, 'images/' + filename)
            print(filename + "下载完成...")


def main():
    """在这个实例中说明了queue在多线程中的典型用法:作为全局变量供多个线程消费"""
    page_queue = queue.Queue(100)  # 爬取100页的数据
    img_queue = queue.Queue(500)  # 缓存500张表情在img_queue,不影响爬取的结果数量,但是影响运行的速度,可以自定义
    for x in range(1, 101):
        url = 'https://www.doutula.com/photo/list/?page={}'.format(x)
        page_queue.put(url)

    for x in range(5):
        Producer(page_queue, img_queue).start()
        Consumer(page_queue, img_queue).start()


if __name__ == '__main__':
    main()

GIL全局解释器锁:

Python自带的解释器是CPythonCPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫做GIL(Global Intepreter Lock),叫做全局解释器锁。这个解释器锁是有必要的。因为CPython解释器的内存管理不是线程安全的。当然除了CPython解释器,还有其他的解释器,有些解释器是没有GIL锁的,见下面:

  1. Jython:用Java实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/Jython
  2. IronPython:用.net实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/IronPython
  3. PyPy:用Python实现的Python解释器。存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/PyPy
    GIL虽然是一个假的多线程。但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程。

既然Python是假多线程,但是为什么还是可以提高爬虫(或者其他IO程序)的运行效率呢?希望大家思考

多线程下载百思不得姐段子:

import requests
from lxml import etree
import threading
from queue import Queue
import csv


class BSSpider(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,joke_queue,*args,**kwargs):
        super(BSSpider, self).__init__(*args,**kwargs)
        self.base_domain = 'http://www.budejie.com'
        self.page_queue = page_queue
        self.joke_queue = joke_queue

    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            response = requests.get(url, headers=self.headers)
            text = response.text
            html = etree.HTML(text)
            descs = html.xpath("//div[@class='j-r-list-c-desc']")
            for desc in descs:
                jokes = desc.xpath(".//text()")
                joke = "\n".join(jokes).strip()
                link = self.base_domain+desc.xpath(".//a/@href")[0]
                self.joke_queue.put((joke,link))
            print('='*30+"第%s页下载完成!"%url.split('/')[-1]+"="*30)

class BSWriter(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }

    def __init__(self, joke_queue, writer,gLock, *args, **kwargs):
        super(BSWriter, self).__init__(*args, **kwargs)
        self.joke_queue = joke_queue
        self.writer = writer
        self.lock = gLock

    def run(self):
        while True:
            try:
                joke_info = self.joke_queue.get(timeout=40)
                joke,link = joke_info
                self.lock.acquire()
                self.writer.writerow((joke,link))
                self.lock.release()
                print('保存一条')
            except:
                break

def main():
    page_queue = Queue(10)
    joke_queue = Queue(500)
    gLock = threading.Lock()
    fp = open('bsbdj.csv', 'a',newline='', encoding='utf-8')
    writer = csv.writer(fp)
    writer.writerow(('content', 'link'))

    for x in range(1,11):
        url = 'http://www.budejie.com/text/%d' % x
        page_queue.put(url)

    for x in range(5):
        t = BSSpider(page_queue,joke_queue)
        t.start()

    for x in range(5):
        t = BSWriter(joke_queue,writer,gLock)
        t.start()

if __name__ == '__main__':
    main()
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多线程爬虫快速上手 的相关文章

随机推荐

  • Qt使用动态库

    三种方式 方式一 编译时就链接 必须在exe的pro文件中配置动态库的include和lib 而且这种方式要 dll so 和 lib a 都需要 在QtCreator中建立共享库的时候 它会自动定义Q DECL EXPORT Q DECL
  • 基于MATLAB,使用SVM和ANN实现车牌识别

    基于MATLAB 使用SVM和ANN实现车牌识别 WHY HOW 一 输入图像 二 三 图像处理 四 识别车牌矩形图像 五 字符切割 六 字符识别 七 MATLAB App UI ISSUE WHY 本人一直对计算机图像识别和机器学习以及人
  • 看雪学习笔记-[原创]编写第一个Exploit

    学习 https bbs kanxue com thread 226970 htm source 1 Immunity Debugger https debugger immunityinc com ID register py mona
  • Python:等待用户输入(input),带有超时功能(Windows可用)

    from threading import Timer import os input msg 啥也没输入 def work msg input msg print n你输入信息为 msg os exit 0 执行完成 退出程序 def i
  • 随笔MySQL:Searching rows for update状态解析

    欢迎关注我的 深入理解MySQL主从原理 32讲 如下 1 限制条件 一般不能是唯一键和主键 也不能是全表 代码如下 if used index MAX KEY 不能是唯一键 主键 和 全表 Check if we are modifyin
  • python 简便写法汇总

    python 1 如果要用到数组的值和下标 1 1找出数组中大于0的值的下标 return x 1 for x in range len nums if nums x gt 0 return i 1 for i num in enumera
  • 第一章:计算机基础知识——知识点整理

    第一章 计算机基础知识 知识点整理 第一章 计算机基础知识 知识梳理 高频考点 1 1 信息与信息技术 1 1 1 信息与数据 1 1 2 信息社会 1 1 3 信息技术 1 1 4 计算机文化 的内涵 1 2 计算机技术概论 1 2 1
  • 对JDBC的认识

    JDBC Java DataBase Connectivity 是Java和数据库的桥梁 是一个规范而不是一个实现 能够执行SQL语句 它由一组用Java语言编写的类和接口组成 JDBC API 由一个驱动程序管理器实现对连接到不同数据库的
  • 录播系统的服务器,录播系统服务器ip地址

    录播系统服务器ip地址 内容精选 换一换 当您在使用VPC的路由表功能时 需要在弹性云服务器上部署SNAT 使得VPC内其他没有绑定EIP的弹性云服务器可以通过它访问Internet 该配置对VPC内所有子网生效 已拥有需要部署SNAT的弹
  • linux下 查看vsftp是否启动状态

    linux 查看vsftp是否启动状态 1 使用ps命令 ps ef grep ftp 如果显示ftp的进程号 表示ftp为启动状态 2 使用service命令 service vsftpd status 显示信息为is running 表
  • 【HCIP-生成树】

    文章目录 1 生成树引入 2 802 1D 标准生成树 3 802 1W RSTP 快速生成树 4 802 1S MST 多生成树 1 生成树引入 为了保证交换网络高可用性 在交换机之间使用冗余链路 由于网络中的泛洪机制可能造成二层的桥接环
  • XShell直接拖拽文件

    在看视频的时候 看到讲师直接拖拽文件到服务器上 觉得好牛逼 之前自己都是另外开一个Xhttp 但是自己试了一下不可以 原因是缺少一个包 lrzsz 懒人找事做 输入命令 apt install lrzsz 我的服务器是Unbuntu系统 不
  • IDEA-Arraylist数组的基本使用

    package demo04 import java util ArrayList 数组的长度不可以发生改变 但是ArrayList集合的长度是可以随意变化的 对于ArrayList来说 有一个尖括号
  • selenium 自动化测试工具(五)UnitTest介绍

    encoding utf 8 import unittest 被测试类 class myclass object classmethod def sum cls a b return a b 将两个传入参数进行相加操作 classmetho
  • HICA(OSI部分总结)

    OSI参考模型 开放式参考互联模型 OSI是由ISO 国际标准化组细 在1979定颁布的 定义了数据产生过程的标准格式 丌同的系统丌同的软件在产生数据时定义了统一的标准 将数据的产生过程分为了7局 提出了分局的思想 分局 丌同局实现丌同的功
  • 从头到尾说一次 Spring 事务管理(器)

    事务管理 一个被说烂的也被看烂的话题 还是八股文中的基础股之一 本文会从设计角度 一步步的剖析 Spring 事务管理的设计思路 都会设计事务管理器了 还能玩不转 为什么需要事务管理 先看看如果没有事务管理器的话 如果想让多个操作 方法 类
  • URL(统一资源定位符)

    2023年8月28日 周一上午 目录 概述 URL的组成 举例说明 示例 CSDN官网 我的博客 极简Vim教程 在百度搜索CSDN 相关资料 概述 URL 统一资源定位符 是用于标识和定位互联网上的资源的字符串 它是一种标准化的格式 由多
  • Pytest框架:测试用例setup和teardown(续)

    背景 上次我们聊了为什么要使用setup和teardown以及其应用场景 接着聊了如何单独使用模块级 setup module teardown module 函数级 setup function teardown function 类级
  • python的闭包和装饰器

    1 什么是闭包 内外函数嵌套 内部函数引用外部函数作用域下的非全局变量 外函数返回内函数对象 优点 为变量续命 缺点 浪费内存 创建一个闭包必须满足以下几点 1 必须有一个内嵌函数 2 内嵌函数必须引用外部函数中的变量 3 外部函数的返回值
  • 多线程爬虫快速上手

    多线程爬虫 在实现网页爬虫的时候 经常会因为代理问题掉线导致爬虫失败 还又很多时候下载的文件略大 比如下载图片 因为下载图片是一个耗时的操作 如果采用之前那种同步的方式下载 那效率肯会特别慢 这时候我们就可以考虑使用多线程的方式来下载图片