爬虫(七)--程序,多进程,多线程

爬取网站的流程:

  1. 确定网站哪个url是数据的来源

  2. 简要分析网站结构,查看数据在哪里

  3. 查看是否有分页,解决分页问题

  4. 发送请求,查看response.text里面是否有我们想要的数据

  5. 如果有数据,提取,保存

  6. 如果没有,我们就可以通过以下两种方式来实现爬取

    1. 分析数据来源,查看是否可以通过一些接口获取数据(首推)

      应该首先想到,数据可能是从ajax接口中获取的。

      分析接口的步骤:

      1. 查看改接口返回的数据是否是我们想要的
      2. 重点查看该接口的请求参数,了解哪些请求参数的变化的,以及是怎么变化的
    2. selenium+phantomjs来获取页面内容

格式化字符串的三种方法

  1. ‘……%s’%i
  2. ‘…{3}…{2}…{1}’.format(a,b,c)
  3. f’……filename’

一、程序、进程和线程

定义

程序:一个应用可以当做一个程序,比如qq。

进程:程序运行最小的资源分配单位,一个程序可以有多个进程。

线程:cpu调度的最小单位,必须依赖进程而存在。线程没有独立的资源,所有线程共享他所属进程的资源。

一个程序至少有一个进程,一个进程至少有一个线程。

二、多线程

多线程是指一个程序包含多个并行的线程来完成不同的任务。

优点:可以提高cpu的利用率。

(一)创建

1.创建多线程的第一种方法

(1)导包

import threading

(2)创建一个线程

t = threading.Thread(
	target = 方法名,
	args = (1,)    # 方法的参数(元组类型)
)

(3)启动线程

t.start()

例:下载文件(单线程)

import time
import random
import threading

# 单线程爬虫
def download(fileName):
    print(f"{fileName}文件开始下载")
    time.sleep(random.random()*10)
    print(f"{fileName}文件完成下载")

# 单线程 默认主线程
if __name__ == '__main__':
    for i in range(5):
        download(i)

例:下载文件(多线程)

import time
import random
import threading

def download(fileName):
    print(f"{fileName}文件开始下载")
    time.sleep(random.random()*10)
    print(f"{fileName}文件完成下载")

# 多线程
if __name__ == '__main__':
    for i in range(5):
        t = threading.Thread(target=download,args=(i,))
        t.start()
    print(threading.enumerate())    # 加主线程,共6个
线程生存期

当我们启动一个线程到这个线程的任务方法执行完毕的过程,就是该线程的生存周期。

查看线程数量
threading.enumerate()    # 可以查看当前进程下的运行的线程

例:多线程

import random,time,threading

def sing():
    for i in range(3):
        print(f'{i}正在唱歌')
        time.sleep(random.random())
def dance():
    for i in range(3):
        print(f'{i}正在跳舞')
        time.sleep(random.random())

if __name__ == '__main__':
    # 创建线程来启动这两个任务
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()

    while True:
        length = len(threading.enumerate())
        print(f'当前运行的线程数量:{length}')
        time.sleep(random.random())
        if length <= 1:
            break

2.创建多线程的第二种方法(通过线程类创建)

(1)继承threading.Thread

(2)重写run方法

(3)实例化这个类就可以创建线程,之后再调用start方法启动即可

线程类传参

必须在线程类的__init__方法中调用父类的__init__方法

两种方法:

# 1
super().__init__()
# 2
threading.Thread.__init__(self)

import threading,time

class MyThread(threading.Thread):
    def __init__(self,filename):
        self.filename = filename
        print('线程开始启动----')
        threading.Thread.__init__(self)
    def run(self):
        print(f'线程开始下载{self.filename}====')

if __name__ == '__main__':
    t = MyThread('log.png')
    t.start()
线程类中,我们实例化线程类的时候,可以通过指定name这个参数,给线程起名。
t = MyThread(name = 'download')
t.start()

在线程类中调用self.name,使用线程名称。

如果不设置名称,默认就是Thread-1,Thread-2,……

import threading

class MyThread(threading.Thread):
    def run(self):
        print('%s正在下载...'%self.name)

if __name__ == '__main__':
    t = MyThread(name='download')
    t.start()
    # 如果不传则默认线程名称Thread-1,Thread-2...以此类推
    for i in range(5):
        t = MyThread()
        t.start()

(二)执行顺序

线程的执行顺序是不固定的,主要是由线程的状态决定。

from threading import Thread
import time

class MyThread(Thread):
    def __init__(self,filename):
        super(MyThread, self).__init__()
        self.filename = filename

    def run(self):
        for i in range(3):
            time.sleep(1)
            print(f'当前的线程是:{self.name},正在下载:{self.filename}')

if __name__ == '__main__':
    for i in range(1,4):
        t = MyThread(i)
        t.start()

'''
当前的线程是:Thread-3,正在下载:3
当前的线程是:Thread-1,正在下载:1
当前的线程是:Thread-2,正在下载:2
当前的线程是:Thread-2,正在下载:2
当前的线程是:Thread-3,正在下载:3
当前的线程是:Thread-1,正在下载:1
当前的线程是:Thread-2,正在下载:2
当前的线程是:Thread-3,正在下载:3
当前的线程是:Thread-1,正在下载:1
'''

五种状态

  1. 新建:线程创建
  2. 就绪状态:当启动线程后,线程就进入就绪状态,就绪状态的线程会被放在一个cpu调度队列中,cpu会负责让其中的线程运行,变为运行状态
  3. 运行状态:cpu调度一个就绪状态的线程,该线程就变为了运行状态
  4. 阻塞状态:当运行状态的线程被阻塞就变为了阻塞状态,阻塞状态的线程要重新变为就绪状态才能继续执行
  5. 死亡:线程执行完毕

(三)问题

多个线程对公有变量处理的时候,容易造成数据的混乱,造成数据不安全的问题。

from threading import Thread
import time
import random
g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1
        time.sleep(random.random())
        print('in work1,gum=%d' % g_num)

def work2():
    global g_num
    for i in range(3):
        g_num += 1
        time.sleep(random.random())
        print('in work2,gum=%d' % g_num)

if __name__ == '__main__':
    t1 = Thread(target=work1)
    t2 = Thread(target=work2)
    t1.start()
    t2.start()
from threading import Thread
g_num = 0
def test1():
    global g_num
    for i in range(1000000):
        g_num += 1
    print("---test1---g_num=%d"%g_num)

def test2():
    global g_num
    for i in range(1000000):
        g_num += 1
    print("---test2---g_num=%d"%g_num)
if __name__ == '__main__':
    p1 = Thread(target=test1)
    p2 = Thread(target=test2)
    p1.start()
    p2.start()

互斥锁

通过互斥锁确保线程之间数据的正确

步骤
  1. 创建锁对象

    mutex = threading.Lock()
    
  2. 上锁,释放锁

    if mutex.acquire():    # 此函数默认参数为True,填写False时,不阻塞,互斥锁就失去意义了
    	'''
    	对公有变量的处理
    	'''
    mutex.release()    # 释放锁
    

使用互斥锁解决线程不安全

import threading
g_num = 0

def w1():
    global g_num
    for i in range(10000000):
        #上锁
        mutexFlag = mutex.acquire(True)
        if mutexFlag:
            g_num+=1
            #解锁
            mutex.release()
    print("test1---g_num=%d"%g_num)

def w2():
    global g_num
    for i in range(10000000):
        # 上锁
        mutexFlag = mutex.acquire(True)
        if mutexFlag:
            g_num+=1
            # 解锁
            mutex.release()
    print("test2---g_num=%d" % g_num)

if __name__ == "__main__":
    #创建锁
    mutex = threading.Lock()
    t1 = threading.Thread(target=w1)
    t2 = threading.Thread(target=w2)
    t1.start()
    t2.start()

三、多线程和多进程

(一)多线程

1.优点

程序逻辑和控制方式复杂。

所有线程可以直接共享内存和变量。

多线程消耗的总资源比多进程要少。

2.缺点

每个线程和主程序共用地址空间,受限于2GB的地址空间。

线程之间的同步和加锁控制比较麻烦。

一个线程的崩溃可能影响到整个程序的稳定性。

(二)多进程

1.优点

每个进程互相独立,子进程崩溃没关系,不影响主程序的稳定性。

增加cpu,容易扩充性能。

每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。

2.缺点

逻辑控制复杂,需要和主程序交互。

需要跨进进程边界,不适合大数据量传送,适合小数据量传送、密集运算。

多进程调度开销比较大。

在实际开发中,选择多线程和多进程应该通过具体实际开发情况进行选择。最好是多进程和多线程结合,即根据实际的需要,每个cpu开辟一个子进程,每个子进程开启多个线程,可以对若干同类型的数据进行处理。

四、死锁

原因

产生死锁的情况有两种:

  1. 当一个线程获取了锁之后,还未释放锁的前提下,试图获取另一把锁,此时会产生死锁
  2. 线程A获取锁1,线程B获取锁2,线程A还未释放锁1,想要继续获取锁2,线程B还未释放锁2,同时想要获取锁1

五、项目

腾讯招聘(ajax)

import requests,json,time

class Tencent(object):
    def __init__(self,url):
        self.url = url
        self.parse()

    def write_to_file(self,list_):
        for item in list_:
            with open('tencent_infos.txt','a',encoding='utf-8') as fp:
                fp.write(str(item)+'\n')

    def parse_json(self,text):
        infos = []
        dict = json.loads(text)
        for data in dict['Data']['Posts']:
            item = {}
            # 职位名
            item['RecruitPostName'] = data['RecruitPostName']
            # 职位类型
            item['CategoryName'] = data['CategoryName']
            # 职责
            item['Responsibility'] = data['Responsibility']
            # 发布时间
            item['LastUpdateTime'] = data['LastUpdateTime']
            # 详情页链接
            item['PostURL'] = data['PostURL']
            infos.append(item)
        self.write_to_file(infos)

    def parse(self):
        for i in range(1,51):
            response = requests.get(self.url %i)
            self.parse_json(response.text)

if __name__ == '__main__':
    start = time.time()
    base_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1572856544479&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex=1&pageSize=%s&language=zh-cn&area=cn'
    Tencent(base_url)
    print(time.time()-start)    # 17.27698802947998

多线程(一)

每页都使用了一个线程,速度很快,但会浪费资源

import requests,json,time,threading

class Tencent(object):
    def __init__(self,url):
        self.url = url

    def write_to_file(self,list_):
        for item in list_:
            with open('tencent_infos.txt','a',encoding='utf-8') as fp:
                fp.write(str(item)+'\n')

    def parse_json(self,text):
        infos = []
        dict = json.loads(text)
        for data in dict['Data']['Posts']:
            item = {}
            # 职位名
            item['RecruitPostName'] = data['RecruitPostName']
            # 职位类型
            item['CategoryName'] = data['CategoryName']
            # 职责
            item['Responsibility'] = data['Responsibility']
            # 发布时间
            item['LastUpdateTime'] = data['LastUpdateTime']
            # 详情页链接
            item['PostURL'] = data['PostURL']
            infos.append(item)
        self.write_to_file(infos)

    def parse(self):
        response = requests.get(self.url)
        self.parse_json(response.text)

if __name__ == '__main__':
    start = time.time()
    base_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1572856544479&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex=1&pageSize=%s&language=zh-cn&area=cn'
    crawl_list = []
    for i in range(1, 51):
        tencent = Tencent(base_url % i)
        # 用第一种方法开启线程
        t = threading.Thread(target=tencent.parse)
        t.start()
        crawl_list.append(t)
        # 将每个线程都调用join方法,保证所得的运行时间是在所有线程完毕之后的时间
    for t in crawl_list:
        t.join()
    print(time.time()-start)    # 1.9531118869781494

多线程(二)

使用消息队列

import requests,json,time,threading
from queue import Queue

class Tencent(threading.Thread):
    def __init__(self,url,name,q):
        super().__init__()
        self.url = url
        self.q = q
        self.name = name

    def run(self):
        self.parse()

    def write_to_file(self,list_):
        for item in list_:
            with open('tencent_infos.txt','a',encoding='utf-8') as fp:
                fp.write(str(item)+'\n')

    def parse_json(self,text):
        infos = []
        dict = json.loads(text)
        for data in dict['Data']['Posts']:
            item = {}
            # 职位名
            item['RecruitPostName'] = data['RecruitPostName']
            # 职位类型
            item['CategoryName'] = data['CategoryName']
            # 职责
            item['Responsibility'] = data['Responsibility']
            # 发布时间
            item['LastUpdateTime'] = data['LastUpdateTime']
            # 详情页链接
            item['PostURL'] = data['PostURL']
            infos.append(item)
        self.write_to_file(infos)

    def parse(self):
        while True:
            if self.q.empty():
                break
            page = self.q.get()
            print(f'======第{page}页======in{self.name}')
            response = requests.get(self.url%page)
            self.parse_json(response.text)

if __name__ == '__main__':
    start = time.time()
    base_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1572856544479&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex=1&pageSize=%s&language=zh-cn&area=cn'
    # 1.创建任务队列
    q = Queue()
    # 2.给队列添加任务,任务是每一页的页码
    for page in range(1,51):
        q.put(page)
    # print(q)    # <queue.Queue object at 0x000000000395F668>
    # while not q.empty():
    #     print(q.get())
    crawl_list = ['aa','bb','cc','dd','ee']
    list_ = []
    for name in crawl_list:
        t = Tencent(base_url,name,q)
        t.start()
        list_.append(t)
    for l in list_:
        l.join()
    print(time.time()-start)    # 3.4191956520080566

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值