多线程与多进程爬虫

threading

Thread类与线程函数

如果使用Thread类处理线程就方便得多了,可以直接使用Thread对象的join方法等待线程函数执行完毕再往下执行,也就是说,在主线程(main函数)中调用Thread对象的join方法,并且Thread对象的线程函数没有执行完毕,主线程会处于阻塞状态。

使用Thread类也很简单,首先需要创建Thread类的实例,通过Thread类构造方法的target关键字参数执行线程函数,通过args关键字参数指定传给线程函数的参数。然后调用Thread对象的start方法启动线程。

样例:

import threading
from time import sleep, ctime
# 线程函数,index表示整数类型的索引,sec表示休眠时间,单位:秒
def fun(index, sec):
    print('开始执行', index, ' 时间:', ctime())
    # 休眠sec秒
    sleep(sec)
    print('结束执行', index, '时间:', ctime())
def main():
    # 创建第1个Thread对象,通过target关键字参数指定线程函数fun,传入索引10和休眠时间(4秒)
    thread1 = threading.Thread(target=fun,
            args=(10, 4))
    # 启动第1个线程
    thread1.start()
    # 创建第2个Thread对象,通过target关键字参数指定线程函数fun,传入索引20和休眠时间(2秒)
    thread2 = threading.Thread(target=fun,
            args=(20, 2))
    # 启动第2个线程
    thread2.start()
    # 等待第1个线程函数执行完毕
    thread1.join()
    # 等待第2个线程函数执行完毕
    thread2.join()

if __name__ == '__main__':
    main()

Thread 类与线程对象

Thread类构造方法的target关键字参数不仅可以是一个函数,还可以是一个对象,可以称这个对象为线程对象。其实线程调用的仍然是函数,只是这个函数用对象进行了封装。这么做的好处是可以将与线程函数相关的代码都放在对象对应的类中,这样更能体现面向对象的封装性。

线程对象对应的类需要有一个可以传入线程函数和参数的构造方法,而且在类中还必须有一个名为“_ call _” 的方法。当线程启动时,会自动调用线程对象的“_ call _”方法,然后在该方法中调用线程函数。

代码:

import threading
from time import sleep, ctime
# 线程对象对应的类
class MyThread(object):
    # func表示线程函数,args表示线程函数的参数
    def __init__(self, func, args):
    # 将线程函数与线程函数的参数赋给当前类的成员变量
        self.func = func
        self.args = args
    # 线程启动时会调用该方法
    def __call__(self):
    # 调用线程函数,并将元组类型的参数值分解为单个的参数值传入线程函数
        self.func(*self.args)
# 线程函数
def fun(index, sec):
    print('开始执行', index, ' 时间:', ctime())
    # 延迟sec秒
    sleep(sec)
    print('结束执行', index, '时间:', ctime())
def main():
    print('执行开始时间:', ctime())
    # 创建第1个线程,通过target关键字参数指定了线程对象(MyThread),延迟4秒
    thread1 = threading.Thread(target = MyThread(fun,(10, 4)))
    # 启动第1个线程
    thread1.start()
    # 创建第2个线程,通过target关键字参数指定了线程对象(MyThread),延迟2秒
    thread2 = threading.Thread(target = MyThread(fun,(20, 2)))
    # 启动第2个线程
    thread2.start()
    # 创建第3个线程,通过target关键字参数指定了线程对象(MyThread),延迟1秒
    thread3 = threading.Thread(target = MyThread(fun,(30, 1)))
    # 启动第3个线程
    thread3.start()
    # 等待第1个线程函数执行完毕
    thread1.join()
    # 等待第2个线程函数执行完毕
    thread2.join()
    # 等待第3个线程函数执行完毕
    thread3.join()
    print('所有的线程函数已经执行完毕:', ctime())
if __name__ == '__main__':
    main()

从Tread 类继承

为了更好地对与线程有关的代码进行封装,可以从Thread类派生一个子类。然后将与线程有关的代码都放到这个类中。Thread类的子类的使用方法与Thread相同。从Thread类继承最简单的方式是在子类的构造方法中通过
super( )函数调用父类的构造方法,并传入相应的参数值。

示例:

import threading
from time import sleep, ctime


# 从Thread类派生的子类
class MyThread(threading.Thread):
    # 重写父类的构造方法,其中func是线程函数,args是传入线程函数的参数,name是线程名
    def __init__(self, func, args, name=''):
        # 调用父类的构造方法,并传入相应的参数值
        super().__init__(target=func, name=name,
                         args=args)

    # 重写父类的run方法
    def run(self):
        self._target(*self._args)


# 线程函数
def fun(index, sec):
    print('开始执行', index, '时间:', ctime())
    # 休眠sec秒
    sleep(sec)
    print('执行完毕', index, '时间:', ctime())


def main():
    print('开始:', ctime())
    # 创建第1个线程,并指定线程名为“线程1”
    thread1 = MyThread(fun, (10, 4), '线程1')
    # 创建第2个线程,并指定线程名为“线程2”
    thread2 = MyThread(fun, (20, 2), '线程2')
    # 开启第1个线程
    thread1.start()
    # 开启第2个线程
    thread2.start()
    # 输出第1个线程的名字
    print(thread1.name)
    # 输出第2个线程的名字
    print(thread2.name)
    # 等待第1个线程结束
    thread1.join()
    # 等待第2个线程结束
    thread2.join()

    print('结束:', ctime())


if __name__ == '__main__':
    main()

线程锁

线程锁的目的是将一段代码锁住,一旦获得了锁权限,除非释放线程锁,否则其他任何代码都无法再次获得锁权限。为了使用线程锁,首先需要创建Lock类的实例,然后通过Lock对象的acquire方法获取锁权限,当需要完成原子操作的代码段执行完后,再使用Lock对象的release方法释放锁,这样其代码就可以再次获得这个锁权限了。

要注意的是,锁对象要放到线程函数的外面作为一个全局变量,这样所有的线程函数实例都可以共享这个变量,如果将锁对象放到线程函数内部,那么这个锁对象就变成局部变量了,多个线程函数实例使用的是不同的锁对象,所以仍然不能有效保护原子操作的代码。

示例:

from atexit import register
import  random
from threading import  Thread,Lock,currentThread
from  time import sleep,ctime

#创建线程锁对象
lock = Lock()

def fun():
    #获取线程锁权限
    lock.acquire()
    #for循环已经变成了原子操作
    for i in range(5):
        print('Thread Name','=',currentThread().name,'i','=',i)
        # 休眠一段时间1~4
        sleep(random.randint(1,5))
        
    #释放线程锁
    lock.release()
    

def main():
    for i in range(3):
        Thread(target=fun).start()

#当线程结束时调用这个函数
@register  #路由

def exit():
    print('线程执行完毕:',ctime())

if __name__ == "__main__":
    main()

信号量

信号量是最古老的同步原语之一,它是一个计数器,用于记录资源消耗情况。当资源消耗时递减,当资源释放时递增。可以认为信号量代表资源是否可用。消耗资源使计数器递减的操作习惯上称为P,当一个线程对一个资源完成操作时,该资源需要返回资源池,这个操作一般称为V。

Python语言统一了所有的命名,使用与线程锁同样的方法名消耗和释放资源。acquire方法用于消耗资源,调用该方法计数器会减1,release方法用于释放资源,调用该方法计数器会加 1。

使用信号量首先要创建Bounded Semaphore类的实例,并且通过该类的构造方法传入计数器的最大值,然后就可以使用BoundedSemphor对象的acquire方法和release方法获取资源(计数器减1)和释放资源(计数器加1)了。

示例:

from threading import BoundedSemaphore
MAX = 3
# 创建信号量对象,并设置了计数器的最大值(也是资源的最大值),计数器不能超过这个值
semaphore = BoundedSemaphore(MAX)
# 输出当前计数器的值,输出结果:3
print(semaphore._value)
# 获取资源,计数器减1
semaphore.acquire()
# 输出结果:2
print(semaphore._value)
# 获取资源,计数器减1
semaphore.acquire()
# 输出结果:1
print(semaphore._value)
# 获取资源,计数器减1
semaphore.acquire()
# 输出结果:0
print(semaphore._value)
# 当计数器为0时,不能再获取资源,所以acquire方法会返回False
# 输出结果:False
print(semaphore.acquire(False))
# 输出结果:0
print(semaphore._value)
# 释放资源,计数器加1
semaphore.release()
# 输出结果:1
print(semaphore._value)
# 释放资源,计数器加1
semaphore.release()
# 输出结果:2
print(semaphore._value)
# 释放资源,计数器加1
semaphore.release()
# 输出结果:3
print(semaphore._value)
# 抛出异常,当计数器达到最大值时,不能再次释放资源,否则会抛出异常
semaphore.release()

要注意的是信号量对象的acquire方法与release方法。当资源枯竭(计数器为0)时调用acquinte方法会有两种结果。

第1种是acquire方法的参数值为True或不指定参数时, acquire方法会处于阻塞状态,直到使用release释放资源后,acquire方法才会往下执行。

第2种acquire方法的参数值为False,当计数器为0时调用acquire方法并不会阻塞,而是直接返回False,表示未获得资源,如果成功获得资源,会返回True。

release方法在释放资源时,如果计数器已经达到了最大值(本例是3),会直接抛出异常,表示已经没有资源释放了。

信号量与锁结合

示例:

from atexit import register
from random import randrange
from threading import BoundedSemaphore, Lock, Thread
from time import sleep, ctime
# 创建线程锁
lock = Lock()
# 定义糖果机的槽数,也是信号量计数器的最大值
MAX = 5
# 创建信号量对象,并指定计数器的最大值
candytray = BoundedSemaphore(MAX)
# 给糖果机的槽补充新的糖果(每次只补充一个槽)
def refill():
    # 获取线程锁,将补充糖果的操作变成原子操作
    lock.acquire()
    print('重新添加糖果...', end=' ')
    try:
    # 为糖果机的槽补充糖果(计数器加1)
        candytray.release()
    except ValueError:
        print('糖果机都满了,无法添加')
    else:
        print('成功添加糖果')
    # 释放线程锁
    lock.release()
# 顾客购买糖果
def buy():
    # 获取线程锁,将购买糖果的操作变成原子操作
    lock.acquire()
    print('购买糖果...', end=' ')
    # 顾客购买糖果(计数器减1),如果购买失败(5个槽都没有糖果了),返回False
    if candytray.acquire(False):
        print('成功购买糖果')
    else:
        print('糖果机为空,无法购买糖果')
    # 释放线程锁
    lock.release()
# 产生多个补充糖果的动作
def producer(loops):
    for i in range(loops):
        refill()
        sleep(randrange(3))
# 产生多个购买糖果的动作
def consumer(loops):
    for i in range(loops):
        buy()
        sleep(randrange(3))

def main():
    print('开始:', ctime())
    # 参数一个2到5的随机数
    nloops = randrange(2, 6)
    print('糖果机共有%d个槽!' % MAX)
    # 开始一个线程,用于执行consumer函数
    Thread(target=consumer, args=(randrange(
        nloops, nloops+MAX+2),)).start()
    # 开始一个线程,用于执行producer函数
    Thread(target=producer, args=(nloops,)).start()

@register
def exit():
    print('程序执行完毕:', ctime())

if __name__ == '__main__':
    main()

运行结果:
在这里插入图片描述

生产者–消费者问题与queue模块

本节使用线程锁以及队列来模拟一个典型的案例:生产者一消费者模型。在这个场景下,商品或服务的生产者生产商品、然后将其放到类似队列的数据结构中,生产商品的时间是不确定的.同样消费者消费生产者生产的商品的时间也是不确定的。

这里使用queue模块来提供线程间通信的机制,也就是说,生产者和消费者共享一个队列。生产者生产商品后,会将商品添加到队列中。消费者消费商品,会从队列中取一个商品。由于向队列中添加商品和从队列中获取商品都不是原子操作,所以需要使用线程锁将这两个操作锁住。

代码:


```python
from  random import  randrange
from time import sleep,time,ctime
from threading import  Lock,Thread
from queue import  Queue

# 创建线程锁对象
lock = Lock()

# 从Therad 派生的子类
class MyTherad(Thread):
    def __init__(self,func,args):
        super().__init__(target= func , args= args)

# 向队列添加商品
def writeQ(queue):
    # 获取线程锁
    lock.acquire()
    print('生产了一个对象,并将其添加到队列中', end='  ')
    # 向队列中添加商品
    queue.put('商品')
    print("队列尺寸", queue.qsize())
    # 释放线程锁
    lock.release()

# 从队列中获取商品
def readQ(queue):
    # 获取线程锁
    lock.acquire()
    # 从队列中获取商品
    val = queue.get(1)
    print('消费了一个对象,队列尺寸:', queue.qsize())
    # 释放线程锁
    lock.release()

#生产若干个生产者者
def writer(queue,loops):
    for i in range(loops):
        writeQ(queue)
        sleep(randrange(1,4))

# 生产若干个消费者

def reader(queue,loops):
    for i in range(loops):
        readQ(queue)
        sleep(randrange(2,4))


funcs =[writer,reader]
nfuncs = range(len(funcs))

def main():
    nloops = randrange(2,6)
    q = Queue(32)

    threads = []
    #创建2个线程运行writer 函数与reder函数
    for i in nfuncs:
        t = MyTherad(funcs[i],(q,nloops))
        threads.append(t)

    # 开始线程
    for i in nfuncs:
        threads[i].start()

    #等待两个线程结束
    for i in nfuncs:
        threads[i].join()
    print('所以工作已经完成')

if __name__ =='__main__':
    main()





效果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/875beae28106452482ec1161b12048b6.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5ODM4NjA3,size_16,color_FFFFFF,t_70)

# 多进程
尽管多线程可以实现并发执行,不过多个线程之间是共享当前进程的内存的,也就是说,线程可以申请到的资源有限。要想进一步发挥并发的作用,可以考虑使用多进程。

如果建立的进程比较多,可以使用`multiprocessing模块的进程池(Pool类)`,通过Pool类构造方法的processes函数,可以指定创建的进程数。

Pool类有一个map方法,用于将回调函数与要给回调
函数传递的数据管理起来,代码如下:

```python
pool = Pool(processes=4)
pool.map(callback_fun,values)

上面的代码利用Pool对象创建了4个进程,并通过map方法指定了进程回调函数,当进程执行时,就会调用这个函数,values是一个可迭代对象,每次进程运行时,就会从values中取一个值传递给callback _ fun,也就是说,callback fun函数至少要有一个参数接收values中的值。

示例:

from  multiprocessing import  Pool
import time

# 进程回调函数
def get_value(value):
    i = 0
    while i <3:
        #休眠一秒
        time.sleep(1)
        print(value,i)
        i+=1

if __name__ =='__main__':
    #产生5个值,供多线程获取
    values =['value{}'.format(str(i)) for i in range(0,5)]
    # 创建4个进程
    pool = Pool(processes=4)
    #将进程回调函数与values关联
    pool.map(get_value,values)

爬取豆瓣电影详情

在这里插入图片描述
在这里插入图片描述

网页分析

在这里插入图片描述

因为电影分类上的数据是异步的所以我们,在XHR中找到真实的网址

https://movie.douban.com/j/chart/top_list?type=11&interval_id=100%3A90&action=&start=20&limit=20

发现每一个分类中的网址只有两个地方是不一样的

  • type=11
  • start=20

而 type = 11 ,这个11是和分类这个连接中的type是一样的,
在这里插入图片描述
start=20 是什么意思呢,通过分析,这个是每一次会获取20个电影信息,就是说每一次下滑,会一次性返回20个;
在这里插入图片描述
每一个对应一个电影的数据,是json格式。需要转换。

代码:

import json, threading
import re, requests
from lxml import etree
from queue import Queue


class DouBan(threading.Thread):
    #重写父类的构造函数
    def __init__(self, q=None):
        super().__init__()
        self.base_url = 'https://movie.douban.com/chart'
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
            'Referer': 'https://movie.douban.com/explore'
        }
        self.q = q
        self.ajax_url = 'https://movie.douban.com/j/chart/top_list?type={}&interval_id=100%3A90&action=&start={}&limit=20'

    # 获取网页的源码
    def get_content(self, url, headers):
        response = requests.get(url, headers=headers)
        return response.text

    # 获取电影指定信息
    def get_movie_info(self, text):
        # 将json格式转换为Python的字典
        text = json.loads(text)
        item = {}
        for data in text:
            score = data['score']
            image = data['cover_url']
            title = data['title']
            actors = data['actors']
            detail_url = data['url']
            vote_count = data['vote_count']
            types = data['types']
            item['评分'] = score
            item['图片'] = image
            item['电影名'] = title
            item['演员'] = actors
            item['详情页链接'] = detail_url
            item['评价数'] = vote_count
            item['电影类别'] = types
            print(item)

    # 获取电影api数据的
    def get_movie(self):
        headers = {
            'X-Requested-With': 'XMLHttpRequest',
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
        }

        # 获取api数据,并判断分页
        while True:
            if self.q.empty():
                break
            n = 0
            while True:
                # 拼接成一个完整的网址
                text = self.get_content(self.ajax_url.format(self.q.get(), n), headers=headers)
                if text == '[]':
                    break
                self.get_movie_info(text)
                n += 20

    # 获取所有类型的type——id
    def get_types(self):
        html_str = self.get_content(self.base_url, headers=self.headers)  # 分类页首页
        html = etree.HTML(html_str)
        types = html.xpath('//div[@class="types"]/span/a/@href')  # 获得每个分类的连接,但是切割type
        # print(types)
        type_list = []
        for i in types:
            p = re.compile('type=(.*?)&interval_id=')  # 筛选id,拼接到api接口的路由
            type = p.search(i).group(1)
            type_list.append(type)
        return type_list

    def run(self):
        self.get_movie()


if __name__ == '__main__':
    # 创建消息队列
    q = Queue()
    # 将任务队列初始化,将我们的type放到消息队列中
    t = DouBan()
    types = t.get_types()
    for tp in types:
        q.put(tp[0])
    # 创建一个列表,列表的数量就是开启线程的树木
    crawl_list = [1, 2, 3, 4]
    for crawl in crawl_list:
        # 实例化对象
        movie = DouBan(q=q)
        movie.start()



解释:

  1. p = re.compile('type=(.*?)&interval_id='):返回的是一个匹配对象,它单独使用就没有任何意义,需要和findall(), search(), match()搭配使用。就是可以用这个对象去匹配字符
  2. type = p.search(i).group(1):匹配 ,group(1)是因为(.*?) 会返回这个括号匹配到的内容。

效果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落春只在无意间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值