爬取網站的流程:
-
確定網站哪個url是數據的來源
-
簡要分析網站結構,查看數據在哪裏
-
查看是否有分頁,解決分頁問題
-
發送請求,查看response.text裏面是否有我們想要的數據
-
如果有數據,提取,保存
-
如果沒有,我們就可以通過以下兩種方式來實現爬取
-
分析數據來源,查看是否可以通過一些接口獲取數據(首推)
應該首先想到,數據可能是從ajax接口中獲取的。
分析接口的步驟:
- 查看改接口返回的數據是否是我們想要的
- 重點查看該接口的請求參數,瞭解哪些請求參數的變化的,以及是怎麼變化的
-
selenium+phantomjs來獲取頁面內容
-
格式化字符串的三種方法
- ‘……%s’%i
- ‘…{3}…{2}…{1}’.format(a,b,c)
- 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
'''
五種狀態
- 新建:線程創建
- 就緒狀態:當啓動線程後,線程就進入就緒狀態,就緒狀態的線程會被放在一個cpu調度隊列中,cpu會負責讓其中的線程運行,變爲運行狀態
- 運行狀態:cpu調度一個就緒狀態的線程,該線程就變爲了運行狀態
- 阻塞狀態:當運行狀態的線程被阻塞就變爲了阻塞狀態,阻塞狀態的線程要重新變爲就緒狀態才能繼續執行
- 死亡:線程執行完畢
(三)問題
多個線程對公有變量處理的時候,容易造成數據的混亂,造成數據不安全的問題。
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()
互斥鎖
通過互斥鎖確保線程之間數據的正確
步驟
-
創建鎖對象
mutex = threading.Lock()
-
上鎖,釋放鎖
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開闢一個子進程,每個子進程開啓多個線程,可以對若干同類型的數據進行處理。
四、死鎖
原因
產生死鎖的情況有兩種:
- 當一個線程獲取了鎖之後,還未釋放鎖的前提下,試圖獲取另一把鎖,此時會產生死鎖
- 線程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