上一次的抓取豆瓣高分計算機書籍的案例,采用的是完全同步的方式。即單個線程依次執(zhí)行完所有的邏輯,這樣存在的問題就是我們的爬蟲程序會非常的慢。
所以本文作為上一次案例的升級版本,通過循序漸進、動手實踐的方式來達到更好的學習效果。
相對于上次的案例,本次主要采用多線程+隊列的方式來實現(xiàn)。
用到的包:
import requests
from bs4 import BeautifulSoup
import re
import numpy as np
import csv
import time
import threading
import queue
本次新增了兩個包,threading 和 queue。threading 是用來進行多線程編程的,queue 也就是用來創(chuàng)建隊列。至于更詳細的使用方法,可以上網自行學習。這里就不多做介紹了。
主要流程:
- 生成 URL
- 創(chuàng)建兩個隊列,一個用保存生成的URL(隊列1),一個保存HTML文檔(隊列2)
- 創(chuàng)建若干個線程來下載 HTML,并且保存到隊列2
- 創(chuàng)建若干個線程解析文檔
- 排序并保存
代碼:
生成分頁URL地址
def make_url(page)
根據(jù)評分排序
def _sort(result)
保存到csv
def save(data)
請求url,下載html
def req_page()
解析html,獲取評分
def get_content()
以上前三個方法都沒有改動,主要是第四個和第五個。
req_page(): 用來請求url。
def req_page():
while True:
try:
url = url_task.get(block=False)
resp = requests.get(url)
html = resp.text
task_html.put(html)
time.sleep(1)
except:
break
以上代碼會被若干個線程執(zhí)行,每一個線程的流程都是不段的從 url_task 也就是我們創(chuàng)建的隊列1中取出一個URL,然后執(zhí)行請求,并把下載到的 HTML 放入隊列2。這里有兩點要注意的。第一個點就是通過 url_task.get() 方法從隊列里拿出任務的時候,由于我們的隊列1是提前設定好的,也就是說當下載線程取任務的時候并不會發(fā)生 queue.Empty 的異常。只有當隊列中的數(shù)據(jù)被處理完的時候才會執(zhí)行 except,那么線程就可以通過這個來退出。第二點是sleep這塊 ,因為請求太頻繁會被豆瓣封掉IP。
get_content():
def get_content():
if task_html.qsize() > 10:
while True:
try:
html = task_html.get(block=False)
bs4 = BeautifulSoup(html, "lxml")
book_info_list = bs4.find_all('li', class_='subject-item')
if book_info_list is not None:
for book_info in book_info_list:
list_ = []
try:
star = book_info.find('span', class_='rating_nums').get_text()
if float(star) < 9.0:
continue
title = book_info.find('h2').get_text().replace(' ', '').replace('\n', '')
comment = book_info.find('span', class_='pl').get_text()
comment = re.sub("\D", "", comment)
list_.append(title)
list_.append(comment)
list_.append(star)
task_res.append(list_)
except:
continue
except:
break
這個函數(shù)首先判斷一下 HTML 文檔隊列(隊列2)的大小是不是大于10,目的是防止解析線程比下載線程執(zhí)行的快,如果解析線程快于下載線程,那么再還沒有下載完所有的URL時,就觸發(fā)隊列的 queue.Empty異常,從而過早退出線程。中間的代碼也是上次案例中的代碼,不同之處也就是以前是從列表中讀取,現(xiàn)在是從隊列中讀取。同時這個函數(shù)也是由多個解析線程執(zhí)行。
主函數(shù):
# 生成分頁url
url_list = make_url(50)
# url 隊列 (隊列1)
url_task = queue.Queue()
for url in url_list:
url_task.put(url)
# 下載好的html隊列 (隊列2)
task_html = queue.Queue()
# 最終結果列表
task_res = []
threads = []
# 獲取html線程
for i in range(5):
threads.append(threading.Thread(target=req_page))
# 解析html線程
threads.append(threading.Thread(target=get_content))
threads.append(threading.Thread(target=get_content))
for i in threads:
i.start()
i.join()
# 主線程排序保存
save(_sort(task_res))
主函數(shù)的流程也就是最開始寫的五個流程。因為我們創(chuàng)建的所有線程都調用了 join() 方法,那么在最后執(zhí)行排序和保存操作的時候,所有的子線程都已經執(zhí)行完畢了。