爬蟲(七)--程序,多進程,多線程

爬取網站的流程:

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章