爬虫笔记23:线程的创建、线程间的资源竞争、线程队列、案例应用对比(普通方式爬取小米应用信息、多线程爬取)

多进程是什么?
进程:系统中正在运行的一个应用程序。
它有什么特点?
1个CPU核心职能执行1个进程,其它进程就处于阻塞状态 。多个CPU核心就可以同时执行N个任务。

线程是什么?
进程中包含的执行单元
它有什么特点?
1个进程可包含多个线程
1次只能执行1个线程
线程锁(资源竞争问题)

有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的
(凡是程序一上线程,那么复杂度就提升了,出BUG的频率也就提升了)
在这里插入图片描述

一、线程的创建,有2种方式:
import threading
(1)通过函数:threading.Thread(函数对象)
使用threading模块当中的一个Thread类,调用 threading.Thread 之后,会创建一个新的线程,参数 target 指定线程将要运行的函数function,args 和 kwargs 则指定function函数的参数。

import threading
import time

def demo():	
	# 子线程
    print('hello 我是子线程')

if __name__ == '__main__':
    for i in range(5):
        t = threading.Thread(target=demo)
        time.sleep(1)
        t.start()

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

(2)通过类;
自定义一个类,并继承父类 threading.Thread ,再重新run()方法

import threading
import time
class A(threading.Thread):

    def run(self):
        for i in range(5):
            print('hello 我是子线程')

if __name__ == '__main__':
    a = A()
    a.start()

结果:
在这里插入图片描述
1、主线程会等待子线程结束之后再结束

import time
import threading

def demo1():
    for i in range(5):
        print('hello everybody')
        time.sleep(1)


if __name__ == '__main__':
    t = threading.Thread(target=demo1)
    t.start()
    print(1)

结果:(主线程在打印1后应该结束,但没有结束)
在这里插入图片描述
2、join():等待子线程执行结束之后,主线程再继续执行

import time
import threading

def demo1():
    for i in range(5):
        print('hello everybody')
        time.sleep(1)


if __name__ == '__main__':
    t = threading.Thread(target=demo1)
    t.start()
    t.join()  # 等待子线程执行结束之后再执行
    print(1)

结果:
在这里插入图片描述
3、setDaemon(True) : 守护进程,即主线程不会等待子线程结束

import time
import threading

def demo1():
    for i in range(5):
        print('hello everybody')
        time.sleep(1)


if __name__ == '__main__':
    t = threading.Thread(target=demo1)
    t.setDaemon(True) # 守护进程
    t.start()
    print(1)

结果:
在这里插入图片描述
4、enumerate()查看线程的数量
enumerate()的一般应用:
在这里插入图片描述
enumerate()在线程中的使用:threading.enumerate()
在这里插入图片描述
第1个print(threading.enumerate()),当前只有一个主线程;
第2个print(threading.enumerate()),仍然只有一个主线程;(因为t.start() 才是表示线程demo1创建并执行
第3个print(threading.enumerate()),有2个线程,一个是主线程,一个是线程demo1

5、线程中的全局变量
(1)在函数内部修改全局变量

a = 20

def fn():
    a = 10
    print('函数内部:','a =',a)

fn()
print('函数外部: a =',a)

在这里插入图片描述
‘’在这里插入图片描述
(2)线程中的全局变量:线程之间共享全局变量

import threading
import time

num = 100
def demo1():
    global num
    num += 1
    print('demo1-num-%d' % num)

def demo2():
    print('demo2-num-%d' % num)

def main():
    t1 = threading.Thread(target=demo1,name='demo1')
    t2 = threading.Thread(target=demo2,name='demo2')
    t1.start()
    t2.start()
    print('main-num-%d' % num)

if __name__ == '__main__':
    main()

结果:(线程demo1执行后,全局变量num的值是101)
在这里插入图片描述
二、线程间的资源竞争
使用threading模块当中的一个Thread类,调用 threading.Thread 之后,会创建一个新的线程,参数 target 指定线程将要运行的函数function,args 和 kwargs 则指定function函数的参数。

一个线程写入,一个线程读取,没问题,如果两个线程都写入呢?
在这里插入图片描述
当argv设置大值时,
在这里插入图片描述
解决方法:有2种
(1)sleep强制性的等线程1完全结束后,再执行线程2
在这里插入图片描述
或者用join(),会比sleep()更快;
在这里插入图片描述
(2)线程锁:threading.Lock()
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为"锁定",其他线程不能改变,直到该线程释放资源,将资源的状态变成"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

第一步:创建一个线程锁,lock = threading.Lock()
第二步:上锁,lock.acquire()
第三步:解锁,lock.release()
在这里插入图片描述

三、线程队列
from queue import Queue #从库里导入类
q=Queue() #实例化对象,既然是对象,那就有方法。

队列的特点:先进先出

常见的队列方法:
empty() 判断队列是否为空,True代表是空的
full() 判断队列是否满了,True代表队列满了
get() 从队列当中取出数据
put() 将一个数据添加到队列当中
在这里插入图片描述
为了防止程序长时间的没有反应,
在这里插入图片描述
我们设置一个超时时间timeout,
在这里插入图片描述
或者用put_nowait()
在这里插入图片描述
四、案例应用对比
1、普通方式爬取小米应用信息
需求:爬取 小米应用商城中聊天社交应用的信息 应用的名称 应用的类别 应用的详情页的url 并且 翻页爬取
在这里插入图片描述
在这里插入图片描述
第一步 页面分析
我们发现数据不在网页的源码当中 证明数据是动态加载的 ajax加载的数据
在这里插入图片描述
解决方案: 1 、分析真正的数据接口 2 、通过selenium来爬取数据
(我们用的第1种)

我们右键检查-network-刷新-并翻页后,发现requests的数量在增加;
在这里插入图片描述
并在XHR中,发现有几个是很相似的,如下图:
在这里插入图片描述
我们点开第一个,可以发现有displayName:LOFTER,预示着我们可能找对方向了。
在Preview中data下,有更全的信息:
在这里插入图片描述
由此,我们复制Headers中的url地址(之后我们确定了这就是目标url),新页面中打开(需要提前加载好扩展程序jsonview,方法后面补充),得到:
在这里插入图片描述
由此,我们可以得到每个应用的目标信息;我们打开一个应用的下载地址,可以看到:
在这里插入图片描述
再打开另一个应用的下载地址,可以确定,就是后缀的不同。

以上,我们就确定了目标url是:https://app.mi.com/categotyAllListApi?page=0&categoryId=2&pageSize=30(相应的要将其中的page=0,一直替换到66)

import requests
import time
import random
from fake_useragent import UserAgent

class XiaomiSpider():
    def __init__(self):
        self.url = 'https://app.mi.com/categotyAllListApi?page={}&categoryId=2&pageSize=30 '
        # self.headers = {
        #     'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36'
        # }

    # 获取响应内容
    def get_html(self,url):
        headers = {'User-Agent':UserAgent().random}
        html = requests.get(url,headers=headers).json()
        #print(html)
        self.parse_html(html)

    # 解析响应内容
    def parse_html(self,html):
        item = {}
        for app_dict in html['data']:
            item['app_name'] = app_dict['displayName']
            item['app_type'] = app_dict['level1CategoryName']
            item['app_link'] = 'https://app.mi.com/details?id=' + app_dict['packageName']
            print(item)


    def main(self):
        for page in range(67):
            url = self.url.format(page)
            self.get_html(url=url)
            # 控制一下抓取的频率
            time.sleep(random.randint(1,2))  # n = random.randint(1,2) 1 <= n <= 2

if __name__ == '__main__':
    x = XiaomiSpider()
    x.main()

结果:
在这里插入图片描述
2、多线程爬取
知识点:
在这里插入图片描述

import threading
from queue import Queue
import time

q=Queue()
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)

def parse_html():
	a=q.get()
	print(a)

for i in range(4):	#创建了4个线程,每个线程轮流启动
	t=threading.Thread(target=parse_html)	#每个线程的作用是,从q中取出一个数并打印,该线程便结束。
	t.start()
	print(t)

问题:如何取出第5个数?
答案是在target=parse_html函数中,无限循环。
在这里插入图片描述
程序没有finish提示结束的原因是,主线程在等待子线程结束后再结束,而子线程无限循环地在空队列中取数,将一直挂着,不结束。所以要注意防范当队列被取空以后,再向该空队列取数时,被一直挂着的情况。
在这里插入图片描述
可以在get中添加一个参数:timeout=3(即3秒取不到数,将报错)
那么要怎样才出现finish呢?有2种方式,如下图:
(1)当线程中的目标函数不是无限循环模式时:
在这里插入图片描述

(2)当线程中的目标函数是无限循环模式时:t.setDaemon(True),也可以解决上述(1)的问题。
在这里插入图片描述
或者:
在这里插入图片描述

补充:
(1)
在这里插入图片描述
也就是说,上图中,for循环没有循环下去,只有子线程1成功创建并启动。
那么要如何修改呢?
在这里插入图片描述
(2)

在这里插入图片描述
解决办法:
在这里插入图片描述
原因:如果线程里每从队列里取一次,但没有执行task_done(),则join无法判断队列到底有没有结束,在最后执行join()是等不到结果的,会一直挂起。
可以理解为,每task_done一次 就从队列里删掉一个元素,这样在最后join的时候根据队列长度是否为零来判断队列是否结束,从而执行主线程。
在这里插入图片描述

又见下面的案例。

import requests
import time
import random
import threading
from queue import Queue
from fake_useragent import UserAgent

class XiaomiSpider():
    def __init__(self):
        self.url = 'https://app.mi.com/categotyAllListApi?page={}&categoryId=2&pageSize=30'
        #创建队列
        self.q = Queue()
        #创建线程锁
        self.lock = threading.Lock()

    def url_in(self):
        #将目标url放到队列当中
        for page in range(67):
            url = self.url.format(page)
            self.q.put(url)

    #线程的事件函数 获取url 请求 解析 处理
    def parse_html(self):
        while True: #无限循环,是为了让线程循环下去
            # 上锁
            self.lock.acquire()#线程间有竞争,假设最后一个url,一下个线程已进行到28行,而下一个线程又判断不是空,就会堵塞
            if not self.q.empty():
                url = self.q.get()
                # 解锁
                self.lock.release()
                headers = {'User-Agent': UserAgent().random}
                html = requests.get(url, headers=headers).json()
                item = {}
                for app_dict in html['data']:
                    item['app_name'] = app_dict['displayName']
                    item['app_type'] = app_dict['level1CategoryName']
                    item['app_link'] = 'https://app.mi.com/details?id=' + app_dict['packageName']
                    print(item)
            else:
                self.lock.release()
                break

    def run(self):
        self.url_in()
        t_lst = []
        for i in range(5):
            t = threading.Thread(target=self.parse_html)
            t_lst.append(t)
            t.start()

if __name__ == '__main__':
    spider = XiaomiSpider()
    spider.run()

# JSONDecodeError 抓取频率太快了 页面没有及时返回

补充:
1、chrome浏览器扩展程序
第一步,下载好程序包,永久的放在桌面;
第二步,加载已解压的扩展程序。
在这里插入图片描述
2、import json 和 requests模块当中的 response.json(),效果是一样的。
(1)import json是python内置的json模块提供的,和requests库没关系。

import json
import requests

response = requests.get(url) 
data1 = json.loads(response.text) 

(2)requests库提供的

import requests

response = requests.get(url) 
data2 = response.json() 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值