簡易多線程爬蟲框架

本文首發于知乎

本文使用多線程實現一個簡易爬蟲框架,讓我們只需要關注網頁的解析,不用自己設置多線程、隊列等事情。調用形式類似scrapy,而諸多功能還不完善,因此稱為簡易爬蟲框架。

這個框架實現了Spider類,讓我們只需要寫出下面代碼,即可多線程運行爬蟲

class DouBan(Spider):def __init__(self):super(DouBan, self).__init__()self.start_url = 'https://movie.douban.com/top250'self.filename = 'douban.json' # 覆蓋默認值self.output_result = False self.thread_num = 10def start_requests(self): # 覆蓋默認函數yield (self.start_url, self.parse_first)def parse_first(self, url): # 只需要yield待爬url和回調函數r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')movies = soup.find_all('div', class_ = 'info')[:5]for movie in movies:url = movie.find('div', class_ = 'hd').a['href']yield (url, self.parse_second)nextpage = soup.find('span', class_ = 'next').aif nextpage:nexturl = self.start_url + nextpage['href']yield (nexturl, self.parse_first)else:self.running = False # 表明運行到這里則不會繼續添加待爬URL隊列def parse_second(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')mydict = {}title = soup.find('span', property = 'v:itemreviewed')mydict['title'] = title.text if title else Noneduration = soup.find('span', property = 'v:runtime')mydict['duration'] = duration.text if duration else Nonetime = soup.find('span', property = 'v:initialReleaseDate')mydict['time'] = time.text if time else Noneyield mydictif __name__ == '__main__':douban = DouBan()douban.run()
復制代碼

可以看到這個使用方式和scrapy非常相似

  • 繼承類,只需要寫解析函數(因為是簡易框架,因此還需要寫請求函數)
  • 用yield返回數據或者新的請求及回調函數
  • 自動多線程(scrapy是異步)
  • 運行都一樣只要run
  • 可以設置是否存儲到文件等,只是沒有考慮可擴展性(數據庫等)

下面我們來說一說它是怎么實現的

我們可以對比下面兩個版本,一個是上一篇文章中的使用方法,另一個是進行了一些修改,將一些功能抽象出來,以便擴展功能。

上一篇文章版本代碼請讀者自行點擊鏈接去看,下面是修改后的版本代碼。

import requests
import time
import threading
from queue import Queue, Empty
import json
from bs4 import BeautifulSoupdef run_time(func):def wrapper(*args, **kw):start = time.time()func(*args, **kw)end = time.time()print('running', end-start, 's')return wrapperclass Spider():def __init__(self):self.start_url = 'https://movie.douban.com/top250'self.qtasks = Queue()self.data = list()self.thread_num = 5self.running = Truedef start_requests(self):yield (self.start_url, self.parse_first)def parse_first(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')movies = soup.find_all('div', class_ = 'info')[:5]for movie in movies:url = movie.find('div', class_ = 'hd').a['href']yield (url, self.parse_second)nextpage = soup.find('span', class_ = 'next').aif nextpage:nexturl = self.start_url + nextpage['href']yield (nexturl, self.parse_first)else:self.running = Falsedef parse_second(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')mydict = {}title = soup.find('span', property = 'v:itemreviewed')mydict['title'] = title.text if title else Noneduration = soup.find('span', property = 'v:runtime')mydict['duration'] = duration.text if duration else Nonetime = soup.find('span', property = 'v:initialReleaseDate')mydict['time'] = time.text if time else Noneyield mydictdef start_req(self):for task in self.start_requests():self.qtasks.put(task)def parses(self):while self.running or not self.qtasks.empty():try:url, func = self.qtasks.get(timeout=3)print('crawling', url)for task in func(url):if isinstance(task, tuple):self.qtasks.put(task)elif isinstance(task, dict):self.data.append(task)else:raise TypeError('parse functions have to yield url-function tuple or data dict')except Empty:print('{}: Timeout occurred'.format(threading.current_thread().name))print(threading.current_thread().name, 'finished')@run_timedef run(self, filename=False):ths = []th1 = threading.Thread(target=self.start_req)th1.start()ths.append(th1)for _ in range(self.thread_num):th = threading.Thread(target=self.parses)th.start()ths.append(th)for th in ths:th.join()if filename:s = json.dumps(self.data, ensure_ascii=False, indent=4)with open(filename, 'w', encoding='utf-8') as f:f.write(s)print('Data crawling is finished.')if __name__ == '__main__':Spider().run(filename='frame.json')
復制代碼

這個改進主要思路如下

  • 我們希望寫解析函數時,像scrapy一樣,用yield返回待抓取的URL和它對應的解析函數,于是就做了一個包含(URL,解析函數)的元組隊列,之后只要不斷從隊列中獲取元素,用函數解析url即可,這個提取的過程使用多線程
  • yield可以返回兩種類型數據,一種是元組(URL,解析函數),一種是字典(即我們要的數據),通過判斷分別加入不同隊列中。元組隊列是不斷消耗和增添的過程,而字典隊列是一只增加,最后再一起輸出到文件中
  • queue.get時,加入了timeout參數并做異常處理,保證每一個線程都能結束

這里其實沒有特別的知識,也不需要解釋很多,讀者自己復制代碼到文本文件里對比就知道了

然后框架的形式就是從第二種中,剝離一些通用的設定,讓用戶自定義每個爬蟲獨特的部分,完整代碼如下(本文開頭的代碼就是下面這塊代碼的后半部分)

import requests
import time
import threading
from queue import Queue, Empty
import json
from bs4 import BeautifulSoupdef run_time(func):def wrapper(*args, **kw):start = time.time()func(*args, **kw)end = time.time()print('running', end-start, 's')return wrapperclass Spider():def __init__(self):self.qtasks = Queue()self.data = list()self.thread_num = 5self.running = Trueself.filename = Falseself.output_result = Truedef start_requests(self):yield (self.start_url, self.parse)def start_req(self):for task in self.start_requests():self.qtasks.put(task)def parses(self):while self.running or not self.qtasks.empty():try:url, func = self.qtasks.get(timeout=3)print('crawling', url)for task in func(url):if isinstance(task, tuple):self.qtasks.put(task)elif isinstance(task, dict):if self.output_result:print(task)self.data.append(task)else:raise TypeError('parse functions have to yield url-function tuple or data dict')except Empty:print('{}: Timeout occurred'.format(threading.current_thread().name))print(threading.current_thread().name, 'finished')@run_timedef run(self):ths = []th1 = threading.Thread(target=self.start_req)th1.start()ths.append(th1)for _ in range(self.thread_num):th = threading.Thread(target=self.parses)th.start()ths.append(th)for th in ths:th.join()if self.filename:s = json.dumps(self.data, ensure_ascii=False, indent=4)with open(self.filename, 'w', encoding='utf-8') as f:f.write(s)print('Data crawling is finished.')class DouBan(Spider):def __init__(self):super(DouBan, self).__init__()self.start_url = 'https://movie.douban.com/top250'self.filename = 'douban.json' # 覆蓋默認值self.output_result = False self.thread_num = 10def start_requests(self): # 覆蓋默認函數yield (self.start_url, self.parse_first)def parse_first(self, url): # 只需要yield待爬url和回調函數r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')movies = soup.find_all('div', class_ = 'info')[:5]for movie in movies:url = movie.find('div', class_ = 'hd').a['href']yield (url, self.parse_second)nextpage = soup.find('span', class_ = 'next').aif nextpage:nexturl = self.start_url + nextpage['href']yield (nexturl, self.parse_first)else:self.running = False # 表明運行到這里則不會繼續添加待爬URL隊列def parse_second(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')mydict = {}title = soup.find('span', property = 'v:itemreviewed')mydict['title'] = title.text if title else Noneduration = soup.find('span', property = 'v:runtime')mydict['duration'] = duration.text if duration else Nonetime = soup.find('span', property = 'v:initialReleaseDate')mydict['time'] = time.text if time else Noneyield mydictif __name__ == '__main__':douban = DouBan()douban.run()
復制代碼

我們這樣剝離之后,就只需要寫后半部分的代碼,只關心網頁的解析,不用考慮多線程的實現了。

歡迎關注我的知乎專欄

專欄主頁:python編程

專欄目錄:目錄

版本說明:軟件及包版本說明

轉載于:https://juejin.im/post/5b129bd7e51d4506a74d22f4

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/452430.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/452430.shtml
英文地址,請注明出處:http://en.pswp.cn/news/452430.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【小松教你手游開發】【unity實用技能】給每個GameObject的打開關閉加上一個漸變...

在游戲開發中,經常會因為直接將GameObject,setActive的方式打開關閉,這種方式效果太過生硬而給它加上一個Tween 可能是AlphaTween或者ScaleTween。 再加上一個PlayTween來做控制。 這樣子需要在每個GameObject上加上這幾個Component不說&…

靜態網頁和動態網頁

靜態網頁是指不應用程序而直接或間接制作成html的網頁,這種網頁的內容是固定的,修改和更新都必須要通過專用的網頁制作工具,比如Dreamweaver。動態網頁是指使用網頁腳本語言,比如php、asp、asp.net等,通過腳本將網站內…

在微型計算機中 如果電源突然中斷,微型計算機在工作中電源突然中斷,則其中的信息全部丟失,再次通電后也不能恢復的..._考試資料網...

請根據下面的文字材料,完成一節課的教學設計。 絲綢之路 一座古樸典雅的“絲綢之路”巨型石雕,矗立在西安市玉祥門外。那馱著彩綢的一峰峰駱駝,高鼻凹眼的西域商人,精神飽滿,栩栩如生。商人們在這個東方大都市開了眼界…

Vmware上安裝RedHat Linux 7.3操作系統手冊

文章目錄1.點擊“創建新的虛擬機”,勾選“自定義”選項,點擊一步;2.默認選擇最高版本的workstations,點擊下一步;3.選擇“稍后安裝操作系統”,點擊下一步;4&a…

軟件開發的“三重門”

自從上次寫了“程序員技術練級攻略” 以來,就覺得似乎還有很多東西沒有談到,但當時沒有繼續思考了。而春節前有人問我,是做底層技術,還是做業務。這問題讓我思考了很多,不由自主地回顧了一 下我這十多年的軟件開發經歷…

軟件工程15 個人閱讀作業1

Task1:注冊個人博客賬號 個人博客地址 https://www.cnblogs.com/bmr666/ Task2:注冊碼云賬號 碼云賬號 https://gitee.com/bmr666 Task3:完成博客-閱讀與思考 閱讀參考材料,并回答下面幾個問題: (1&#xf…

Windows Server 2008操作系統安裝手冊

文章目錄1.輸入語言和其他首選項,然后單擊“下一步”繼續;2.點擊“現在安裝”,啟動安裝程序;3.選擇要安裝的操作系統,這里選擇Windows Server 2008 R2 Enterprise(完全安…

云計算機有哪些特征,你知道云計算有哪些核心特征嗎?

你知道云計算有哪些核心特征嗎?下面跟小編一起來了解下吧!!!1、敏捷:使用戶得以快速的,且以低價格的獲得技術架構資源。2、應用程序界面API的可達性是指允許軟件與云以類似“人機交互這種用戶界面設施交互相所相一致的方式”來交互。云計算系統典型的運…

從玩撲克到軟件開發

我以前不是做軟件開發的。在加入ThoughtWorks兩年之前,我主要靠玩撲克為生。當然,如果你曾跟我打聽過我前臂上的紋身,那你肯定已然聽過我的故事了。要是還沒有,等下次我們一起喝一杯時,我可以講給你聽。 我從未因為花…

什么是IPsec協議

IPSec 協議不是一個單獨的協議,它給出了應用于IP層上網絡數據安全的一整套體系結構,包括網絡認證協議 Authentication Header(AH)、封裝安全載荷協議Encapsulating Security Payload(ESP)、密鑰管理協議Int…

python 字符串、列表和元祖之間的切換

>>> s[http,://,www,baidu,.com] >>> url.join(s) >>> url http://wwwbaidu.com >>> 上面的代碼片段是將列表轉換成字符串>>> s(hello,world,!) >>> d .join(s) >>> d hello world ! >>> 以上代碼片段…

你真的懂函數嗎?

函數聲明方式 匿名函數 function后面直接跟括號,中間沒有函數名的就是匿名函數。 let fn function() {console.log(我是fn) } let fn2 fn console.log(fn.name) //fn console.log(fn2.name)//fn,fn和fn2指向的是同一個function。 復制代碼具名函數 fun…

靜態html的ajax如何發請求,靜態頁面ajax - 冥焱的個人空間 - OSCHINA - 中文開源技術交流社區...

1.靜態頁面$.ajax({type:"get",url:"http://localhost:8080/app/register/sendSMS",//請求地址必須帶http協議data:{"phone":phone},async:false,//是否異步dataType: "jsonp",//固定格式jsonp: "callback",//固定格式jsonp…

Diango博客--12.開發 Django 博客文章閱讀量統計功能

文章目錄0.models中增加新字段1.models中增加方法2.遷移數據庫3.修改視圖函數4.在模板中顯示閱讀量0.models中增加新字段 為了記錄文章的瀏覽量,需要在文章的數據庫表中新增一個用于存儲閱讀量的字段。 文件位置:blog/models.py class Post(models.Mo…

c++ try_catch throw

使用throw拋出異常 本人節選自《21天學通C》一書 拋出異常(也稱為拋棄異常)即檢測是否產生異常,在C中,其采用throw語句來實現,如果檢測到產生異常,則拋出異常。該語句的格式為: throw 表達式…

數字證書和數字簽名

什么是數字證書?由于Internet網電子商務系統技術使在網上購物的顧客能夠極其方便輕松地獲得商家和企業的信息,但同時也增加了對某些敏感或有價值的數據被濫用的風險. 為了保證互聯網上電子交易及支付的安全性,保密性等,防范交易及支付過程中的欺詐行為&a…

域名劫持

轉載于:https://www.cnblogs.com/xinghen1216/p/8548323.html

cesium html源碼,Cesium源碼的本地運行及調試

CesiumJS源碼運行有兩種方式:基于node.js運行官方下載地址:https://cesium.com/cesiumjs/下載解壓后,在根目錄安裝依賴后,就可直接運行npm initnpm start如果調試代碼呢,官方的示例都是在Sandcastle里放著,…

Diango博客--13.將“視圖函數”類轉化為“類視圖”

文章目錄0.思路引導1.ListView2.將 index 視圖函數改寫為類視圖3.將 category 視圖函數改寫為類視圖4.將 archive 視圖函數改寫成類視圖5.將 tag 視圖函數改寫成類視圖6.DetailView7.將DetailView視圖函數改寫成類視圖0.思路引導 1)在開發網站的過程中,…

es6之數據結構 set,WeakSet,mapWeakMap

{let list new Set();list.add(1);list.add(2);list.add(1);console.log(list); //Set(2) {1, 2} let arr[1,2,3,1,2] let list2new Set(arr); console.log(list2); //Set(3) {1, 2, 3} } Set ES6 提供了新的數據結構 Set。它類似于數組,但是成員的值都是唯一的&a…