簡單的實現一個python3的多線程爬蟲,爬取p站上的每日熱門排行榜,python3多線程
大概半年前我開始學習python,也就是半年前,我半抄半改的同樣的爬蟲寫了出來,由於是單線程的程式,當中出了一點的小錯就會崩潰,但是那個爬蟲中的header之類的東西現在依舊還是能夠使用的,於是我就把之前那份的保留了下來。由於有一半是抄的,自己得到的並不多,這次重寫,我相當於又重新學習了一遍。當中有可能有認識不足的,歡迎指正。
首先我們要想登陸p站,得構造一個請求,p站登陸的請求包括:
request = urllib.request.Request( #建立請求 url=login_url, #連結 data=login_data, #資料 headers=login_header #頭)
url通過猜測就可以得到是https://www.pixiv.net/login.php,但是由於採用了https加密data和headers卻不好獲得,這裡我採用了之前的那份的,沒想到還能用:
data = { #構建請求資料 "pixiv_id": self.id, #帳號 "pass": self.passwd, #密碼 "mode": "login", "skip": 1}
login_header = { #構建要求標頭 "accept-language": "zh-cn,zh;q=0.8", "referer": "https://www.pixiv.net/login.php?return_to=0", "user-agent": "mozilla/5.0 (windows nt 10.0; win64; x64; rv:45.0) gecko/20100101 firefox/45.0"}
因為是要爬取多個頁面的圖,我這裡採用cookie登陸的方式,不過因為可能cookie會變每次運行還得重新登陸:
cookie = http.cookiejar.MozillaCookieJar(".cookie") #建立cookie每次都覆蓋,進行更新handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open(request)print("Log in successfully!")cookie.save(ignore_discard=True, ignore_expires=True)response.close()print("Update cookies successfully!")
登陸解決了,cookie登陸其實是很簡單的,只要載入本地的cookie檔案就行了:
def cookie_opener(self): #使用cookie登陸, 建立opener cookie = http.cookiejar.MozillaCookieJar() cookie.load(".cookie", ignore_discard=True, ignore_expires=True) handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) return opener
一開始我的想法是先將所有的連結中的圖片連結解析出來,然後再下載,在統籌學看來這樣的做法就是完全的浪費時間的,因為解析和下載所用的時間是不一樣的,解析可能會花上3,4分鐘,而單獨的下載只要10秒以內。在電腦允許的情況下,一個線程專門負責解析,另外的線程專門負責下載,效率會非常的高。
後來通過學習,我改成了生產者消費者模式:
1. 一個Crawler爬取連結中的圖片連結,放入處理隊列中
2. n個Downloader下載爬出到的圖片
本文的生產者和消費者模式
可以做到邊解析,邊下載,由於通常解析的速度是快於下載的速度的,一開始可能下載的速度是快過解析的,但是後來會被反超,採用一個解析器對多個下載器的模式效率並沒有多小的差別。
具體的實現,我是將downloader作為threading.Thread的一個衍生類別:
class downloader (threading.Thread): #將一個下載器作為一個線程 def __init__(self, q, path, opener): threading.Thread.__init__(self) self.opener = opener #下載器用的opener self.q = q #主隊列 self.sch = 0 #進度[0-50] self.is_working = False #是否正在工作 self.filename = "" #當前下載的檔案名稱 self.path = path #檔案路徑 self.exitflag = False #是否退出的訊號 def run(self): def report(blocks, blocksize, total): #回呼函數用於更新下載的進度 self.sch = int(blocks * blocksize / total * 50) #計算當前下載百分比 self.sch = min(self.sch, 50) #忽略溢出 def download(url, referer, path): #使用urlretrieve下載圖片 self.opener.addheaders = [ #給opener添加一個頭 ('Accept-Language', 'zh-CN,zh;q=0.8'), ('User-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0'), ('Referer', referer) #p站的防盜鏈機制 ] pattern = re.compile(r'([a-zA-Z.0-9_-]*?)$') #正則匹配處理模式 filename = re.search(pattern, url).group(0) #匹配圖片連結產生本地檔案名稱 if filename.find("master") != -1: #去除多圖的master_xxxx的字串 master = re.search(re.compile(r'_master[0-9]*'), filename) filename = filename.replace(master.group(0), '') self.filename = filename urllib.request.install_opener(self.opener) #添加更新後的opener try: urllib.request.urlretrieve(url, path + filename, report) #下載檔案到本地 except: os.remove(path + filename) #如果下載失敗,將問題檔案刪除,並將referer和url重新放入隊列 self.q.put((referer, url)) while not self.exitflag: if not self.q.empty(): #當隊列非空擷取隊列首部元素,開始下載 links = self.q.get() self.is_working = True download(links[1], links[0], self.path) self.sch = 0 #置零 self.is_working = False
封裝的downloader類作為一個單獨的線程起到了和threading.Thread一樣的作用,同時對下載器任務的一些說明,可以在後面的啟動並執行過程中顯示各個下載器的進度。
爬取地址的時候,先把熱門排行榜首頁的掃一遍,擷取所有作品的地址,以下都用到了beautifulsoup模組:
response = opener.open(self.url)html = response.read().decode("gbk", "ignore") #編碼,忽略錯誤(錯誤一般不存在在連結上)soup = BeautifulSoup(html, "html5lib") #使用bs和html5lib解析器,建立bs對象tag_a = soup.find_all("a")for link in tag_a: top_link = str(link.get("href")) #找到所有<a>標籤下的連結 if top_link.find("member_illust") != -1: pattern = re.compile(r'id=[0-9]*') #過濾存在id的連結 result = re.search(pattern, top_link) if result != None: result_id = result.group(0) url_work = "http://www.pixiv.net/member_illust.php?mode=medium&illust_" + result_id if url_work not in self.rankurl_list: self.rankurl_list.append(url_work)
解析由於只有一個線程,所以我就用了一般的用法:
def _crawl(): while len(self.rankurl_list) > 0: url = self.rankurl_list[0] response = opener.open(url) html = response.read().decode("gbk", "ignore") #編碼,忽略錯誤(錯誤一般不存在在連結上) soup = BeautifulSoup(html, "html5lib") imgs = soup.find_all("img", "original-image") if len(imgs) > 0: self.picurl_queue.put((url, str(imgs[0]["data-src"]))) else: multiple = soup.find_all("a", " _work multiple ") if len(multiple) > 0: manga_url = "http://www.pixiv.net/" + multiple[0]["href"] response = opener.open(manga_url) html = response.read().decode("gbk", "ignore") soup = BeautifulSoup(html, "html5lib") imgs = soup.find_all("img", "image ui-scroll-view") for i in range(0, len(imgs)): self.picurl_queue.put((manga_url + "&page=" + str(i), str(imgs[i]["data-src"]))) self.rankurl_list = self.rankurl_list[1:] self.crawler = threading.Thread(target=_crawl) #開第一個線程用於爬取連結,生產者消費者模式中的生產者 self.crawler.start()
與此同時產生和之前所設的最大線程數相等的線程:
for i in range(0, self.max_dlthread): #根據設定的最大線程數開闢下載線程,生產者消費者模式中的消費者 thread = downloader(self.picurl_queue, self.os_path, opener) thread.start() self.downlist.append(thread) #將產生的線程放入一個隊列中
接下來就是顯示多線程每一個線程的下載進度,同時等待所有的事情處理結束了:
flag = Falsewhile not self.picurl_queue.empty() or len(self.rankurl_list) > 0 or not flag:#顯示進度,同時等待所有的線程結束,結束的條件(這裡取相反):#1 下載隊列為空白#2 解析列表為空白#3 當前所有的下載任務完成 os.system("cls") flag = True if len(self.rankurl_list) > 0: print(str(len(self.rankurl_list)) + " urls to parse...") if not self.picurl_queue.empty(): print(str(self.picurl_queue.qsize()) + " pics ready to download...") for t in self.downlist: if t.is_working: flag = False print("Downloading " + '"' + t.filename + '" : \t[' + ">"*t.sch + " "*(50-t.sch) + "] " + str(t.sch*2) + " %") else: print("This downloader is not working now.") time.sleep(0.1)
下面是實際啟動並執行圖:
多線程,每個線程的進度都不同,上面顯示的分別是需要解析的連結和已經準備好要下載的連結
等到所有的任務結束,給所有的線程發送一個退出指令:
for t in self.downlist: #結束後給每一個下載器發送一個退出指令 t.exitflag = True
等所有任務結束,系統將會給出下載所用的時間:
def start(self): st = time.time() self.login() opener = self.cookie_opener() self.crawl(opener) ed = time.time() tot = ed - st intvl = getTime(int(tot)) os.system("cls") print("Finished.") print("Total using " + intvl + " .") #統計全部工作結束所用的時間
2016年12月14日,p站每日榜全部爬取所用的時間:
下面給出coding上的地址:
https://coding.net/u/MZI/p/PixivSpider/git