多線程爬取表情包
有一個網站,叫做“鬥圖啦”,網址是:https://www.doutula.com/
。這裏麪包含了許許多多的有意思的鬥圖圖片,還蠻好玩的。有時候爲了鬥圖要跑到這個上面來找表情,實在有點費勁。於是就產生了一個邪惡的想法,可以寫個爬蟲,把所有的表情都給爬下來。這個網站對於爬蟲來講算是比較友好了,他不會限制你的headers
,不會限制你的訪問頻率(當然,作爲一個有素質的爬蟲工程師,爬完趕緊撤,不要把人家服務器搞垮了),不會限制你的IP地址,因此技術難度不算太高。但是有一個問題,因爲這裏要爬的是圖片,而不是文本信息,所以採用傳統的爬蟲是可以完成我們的需求,但是因爲是下載圖片所以速度比較慢,可能要爬一兩個小時都說不準。因此這裏我們準備採用多線程爬蟲,一下可以把爬蟲的效率提高好幾倍。
一、分析網站和爬蟲準備工作:
構建所有頁面URL列表:
這裏我們要爬的頁面不是“鬥圖啦”首頁,而是最新表情頁面https://www.doutula.com/photo/list/
,這個頁面包含了所有的表情圖片,只是是按照時間來排序的而已。我們把頁面滾動到最下面,可以看到這個最新表情使用的是分頁,當我們點擊第二頁的時候,頁面的URL
變成了https://www.doutula.com/photo/list/?page=2
,而我們再回到第一頁的時候,page
又變成了1
,所以這個翻頁的URL
其實很簡單,前面這一串https://www.doutula.com/photo/list/?page=
都是固定的,只是後面跟的數字不一樣而已。並且我們可以看到,這個最新表情總共是有869頁,因此這裏我們可以寫個非常簡單的代碼,來構建一個從1到869的頁面的URL
列表:
# 全局變量,用來保存頁面的URL的
PAGE_URL_LIST = []
BASE_PAGE_URL = 'https://www.doutula.com/photo/list/?page='
for x in range(1, 870):
url = BASE_PAGE_URL + str(x)
PAGE_URL_LIST.append(url)
獲取一個頁面中所有的表情圖片鏈接:
我們已經拿到了所有頁面的鏈接,但是還沒有拿到每個頁面中表情的鏈接。經過分析,我們可以知道,其實每個頁面中表情的HTML
元素構成都是一樣的,因此我們只需要針對一個頁面進行分析,其他頁面按照同樣的規則,就可以拿到所有頁面的表情鏈接了。這裏我們以第一頁爲例,跟大家講解。首先在頁面中右鍵->檢查->Elements
,然後點擊Elements
最左邊的那個小光標,再把鼠標放在隨意一個表情上,這樣下面的代碼就定位到這個表情所在的代碼位置了:
可以看到,這個img
標籤的class
是等於img-responsive lazy image_dtz
,然後我們再定位其他表情的img
標籤,發現所有的表情的img
標籤,他的class
都是img-responsive lazy image_dtz
:
因此我們只要把數據從網上拉下來,然後再根據這個規則進行提取就可以了。這裏我們使用了兩個第三方庫,一個是requests
,這個庫是專門用來做網絡請求的。第二個庫是bs4
,這個庫是專門用來把請求下來的數據進行分析和過濾用的,如果沒有安裝好這兩個庫的,可以使用以下代碼進行安裝(我使用的是python2.7的版本):
# 安裝requests
pip install requests
# 安裝bs4
pip install bs4
# 安裝lxml解析引擎
pip install lxml
然後我們以第一個頁面爲例,跟大家講解如何從頁面中獲取所有表情的鏈接:
# 導入requests庫
import requests
# 從bs4中導入BeautifulSoup
from bs4 import BeautifulSoup
# 第一頁的鏈接
url = 'https://www.doutula.com/photo/list/?page=1'
# 請求這個鏈接
response = requests.get(url)
# 使用返回的數據,構建一個BeautifulSoup對象
soup = BeautifulSoup(response.content,'lxml')
# 獲取所有class='img-responsive lazy image_dtz'的img標籤
img_list = soup.find_all('img', attrs={'class': 'img-responsive lazy image_dta'})
for img in img_list:
# 因爲src屬性剛開始獲取的是loading的圖片,因此使用data-original比較靠譜
print img['data-original']
這樣我們就可以在控制檯看到本頁中所有的表情圖片的鏈接就全部都打印出來了。
下載圖片:
有圖片鏈接後,還要對圖片進行下載處理,這裏我們以一張圖片爲例:http://ws2.sinaimg.cn/bmiddle/9150e4e5ly1fhpi3ysfocj205i04aa9z.jpg
,來看看Python
中如何輕輕鬆鬆下載一張圖片:
import urllib
url = 'http://ws2.sinaimg.cn/bmiddle/9150e4e5ly1fhpi3ysfocj205i04aa9z.jpg'
urllib.urlretrieve(url,filename='test.jpg')
這樣就可以下載一張圖片了。
結合以上三部分內容:
以上三部分,分別對,如何構建所有頁面的URL
,一個頁面中如何獲取所有表情的鏈接以及下載圖片的方法。接下來把這三部分結合在一起,就可以構建一個完整但效率不高的爬蟲了:
# 導入requests庫
import requests
# 從bs4中導入BeautifulSoup
from bs4 import BeautifulSoup
import urllib
import os
# 全局變量,用來保存頁面的URL的
PAGE_URL_LIST = []
BASE_PAGE_URL = 'https://www.doutula.com/photo/list/?page='
for x in range(1, 870):
url = BASE_PAGE_URL + str(x)
PAGE_URL_LIST.append(url)
for page_url in PAGE_URL_LIST:
# 請求這個鏈接
response = requests.get(page_url)
# 使用返回的數據,構建一個BeautifulSoup對象
soup = BeautifulSoup(response.content,'lxml')
# 獲取所有class='img-responsive lazy image_dtz'的img標籤
img_list = soup.find_all('img', attrs={'class': 'img-responsive lazy image_dta'})
for img in img_list:
# 因爲src屬性剛開始獲取的是loading的圖片,因此使用data-original比較靠譜
src = img['data-original']
# 有些圖片是沒有http的,那麼要加一個http
if not src.startswith('http'):
src = 'http:'+ src
# 獲取圖片的名稱
filename = src.split('/').pop()
# 拼接完整的路徑
path = os.path.join('images',filename)
urllib.urlretrieve(src,path)
以上這份代碼。可以完整的運行了。但是效率不高,畢竟是在下載圖片,要一個個排隊下載。如果能夠採用多線程,在一張圖片下載的時候,就完全可以去請求其他圖片,而不用繼續等待了。因此效率比較高,以下將該例子改爲多線程來實現。
二、多線程下載圖片:
這裏多線程我們使用的是Python
自帶的threading
模塊。並且我們使用了一種叫做生產者和消費者的模式,生產者專門用來從每個頁面中獲取表情的下載鏈接存儲到一個全局列表中。而消費者專門從這個全局列表中提取表情鏈接進行下載。並且需要注意的是,在多線程中使用全局變量要用鎖來保證數據的一致性。以下是多線程的爬蟲代碼(如果有看不懂的,可以看視頻,講解很仔細):
#encoding: utf-8
import urllib
import threading
from bs4 import BeautifulSoup
import requests
import os
import time
# 表情鏈接列表
FACE_URL_LIST = []
# 頁面鏈接列表
PAGE_URL_LIST = []
# 構建869個頁面的鏈接
BASE_PAGE_URL = 'https://www.doutula.com/photo/list/?page='
for x in range(1, 870):
url = BASE_PAGE_URL + str(x)
PAGE_URL_LIST.append(url)
# 初始化鎖
gLock = threading.Lock()
# 生產者,負責從每個頁面中提取表情的url
class Producer(threading.Thread):
def run(self):
while len(PAGE_URL_LIST) > 0:
# 在訪問PAGE_URL_LIST的時候,要使用鎖機制
gLock.acquire()
page_url = PAGE_URL_LIST.pop()
# 使用完後要及時把鎖給釋放,方便其他線程使用
gLock.release()
response = requests.get(page_url)
soup = BeautifulSoup(response.content, 'lxml')
img_list = soup.find_all('img', attrs={'class': 'img-responsive lazy image_dta'})
gLock.acquire()
for img in img_list:
src = img['data-original']
if not src.startswith('http'):
src = 'http:'+ src
# 把提取到的表情url,添加到FACE_URL_LIST中
FACE_URL_LIST.append(src)
gLock.release()
time.sleep(0.5)
# 消費者,負責從FACE_URL_LIST提取表情鏈接,然後下載
class Consumer(threading.Thread):
def run(self):
print '%s is running' % threading.current_thread
while True:
# 上鎖
gLock.acquire()
if len(FACE_URL_LIST) == 0:
# 不管什麼情況,都要釋放鎖
gLock.release()
continue
else:
# 從FACE_URL_LIST中提取數據
face_url = FACE_URL_LIST.pop()
gLock.release()
filename = face_url.split('/')[-1]
path = os.path.join('images', filename)
urllib.urlretrieve(face_url, filename=path)
if __name__ == '__main__':
# 2個生產者線程,去從頁面中爬取表情鏈接
for x in range(2):
Producer().start()
# 5個消費者線程,去從FACE_URL_LIST中提取下載鏈接,然後下載
for x in range(5):
Consumer().start()
寫在最後:
本教程採用多線程來完成表情的爬取,可以讓爬取效率高出很多倍。Python
的多線程雖然有GIL
全局解釋器鎖,但在網絡IO
處理這一塊表現還是很好的,不用在一個地方一直等待。以上這個例子就很好的說明了多線程的好處。另外,如果你對Python
和爬蟲或者框架有相關的興趣,那麼可以學習下這個課程,21天讓你從小白迅速成長爲爬蟲老司機:零基礎:21天搞定Python分佈式爬蟲