標籤: 爬蟲 Pythonhtml
博主比較喜歡看書,購物車裏面會放不少書,而後等打折的時候開個大招。node
然而會遇到一個問題,就是不知道什麼書是好書,不知道一本書究竟好很差,因此常常會去豆瓣讀書看看有什麼好書推薦,只是這樣效率比較低。近期學習了爬蟲的基礎知識。有點手癢,故寫一個爬取豆瓣推薦書籍的爬蟲,和你們分享一下。python
咱們給爬蟲設置一個起始url,而後爬取豆瓣在該url推薦的書籍及推薦書籍的推薦書籍……直到達到預設的爬取次數或者某個終止條件。git
由於篇幅有限。不可能解說太多的基礎知識,假設你們認爲理解有困難的話,可以看看慕課網Python開發簡單爬蟲的視頻。這個視頻很是的贊。github
爬蟲一共同擁有5個模塊:調度器,url管理器,html下載器,html解析器和html輸出器。數據庫
爬蟲調度器經過調度其它的模塊完畢任務,上面推薦的視頻中有一張很是棒的圖說明了爬蟲經過調度器執行的流程:
當中的應用模塊相應的是輸出器。解釋一下執行流程:編程
(1) 調度器查詢是否有未爬取的url
(2) 假設「無」則跳轉至(8)。假設「有」則獲取一個url
(3) 下載器依據獲取的url下載html數據
(4) 解析器解析下載的html數據,得到新的url和有價值數據
(5) 調度器將得到的url傳遞給url管理器
(6) 調度器將得到的有價值數據傳遞給輸出器
(7) 跳轉至(1)
(8) 將輸出器中的有價值數據所有輸出設計模式
url管理器對未爬取和已爬取的url進行管理。記錄未爬取的url是爲了對新的網頁進行爬取,記錄已爬取的url是爲了防止爬取已經爬取過的網頁。瀏覽器
url管理器中有2個集合,分別記錄未爬取和已爬取的url。
url管理器中有4種方法,詳見代碼凝視:markdown
#file: url_manager.py
class UrlManager(object):
def __init__(self):
self.new_urls = set() #未爬取url集合
self.old_urls = set() #已爬取url集合
#加入新的單個url。僅僅加入不在新舊集合中的url
def add_new_url(self, url):
if url is None:
return
if url not in self.new_urls and url not in self.old_urls:
self.new_urls.add(url)
#加入新的一堆url。調用add_new_url加入
def add_new_urls(self, urls):
if urls is None or len(urls) == 0:
return
for url in urls:
self.add_new_url(url)
#是否還有未爬取的url
def has_new_url(self):
return len(self.new_urls) != 0
#獲取一個新的url,將該url從未爬取集合刪除,加入到已爬取集合中
def get_new_url(self):
new_url = self.new_urls.pop()
self.old_urls.add(new_url)
return new_url
html下載器依據傳入的url下載網頁的html數據。
下載器需要用到urllib2
庫。這個庫是Python編寫爬蟲時常用的庫。具備依據給定的url獲取html數據。假裝成瀏覽器訪問網頁,設置代理等功能。由於咱們獲取的是豆瓣的推薦書籍,不需要登陸,因此僅僅使用依據url獲取html數據的功能就能夠。
需要注意的是,豆瓣是個很是不錯的站點,因此可能有很是多的爬蟲在爬取豆瓣。所以豆瓣也有很是多的反爬蟲機制。最直接的反爬蟲機制就是禁制程序訪問豆瓣,所以咱們的爬蟲要假裝成瀏覽器進行頁面爬取。
#file: html_downloader.py
import urllib2
class HtmlDownloader(object):
def download(self, url):
if url is None:
return None
try:
request = urllib2.Request(url)
request.add_header('user-agent', 'Mozilla/5.0') #加入頭信息,假裝成Mozilla瀏覽器
response = urllib2.urlopen(request) #訪問這個url
except urllib2.URLError, e: #假設出錯則打印錯誤代碼和信息
if hasattr(e,"code"):
print e.code #錯誤代碼,如403
if hasattr(e,"reason"):
print e.reason #錯誤信息,如Forbidden
if response.getcode() != 200: #200表示訪問成功
return None
return response.read() #返回該url的html數據
解析器解析下載的html數據。得到新的url和有價值數據,該模塊是爬蟲最麻煩的模塊。
解析器需要用到BeautifulSoup
和re
庫。
BeautifulSoup
是用Python寫的一個HTML/XML的解析器。可以很是方便的從HTML/XML字符串中提取信息。
re
是Python默認的正則表達式模塊。提供正則表達式相關的操做。
parser()
方法實現解析器對外部僅僅提供一個方法parser
。該方法調用內部的兩個方法實現解析功能:
#file: html_parser.py
from bs4 import BeautifulSoup
import re
class HtmlParser(object):
def parse(self, page_url, html_cont):
if page_url is None or html_cont is None:
return
soup = BeautifulSoup(html_cont, 'html.parser', from_encoding='utf-8') #建立一個beautifulsoup對象
new_urls = self._get_new_urls(soup) #調用內部方法提取url
new_data = self._get_new_data(page_url, soup) #調用內部方法提取有價值數據
return new_urls, new_data
_get_new_urls()
方法實現內部方法_get_new_urls()
從傳遞的beautifulsoup
對象中提取url信息,那麼究竟提取的哪一個部分的url?咱們以豆瓣《代碼大全》頁面爲樣例進行解說。
打開該頁面。在「喜歡讀‘代碼大全(第2版)’的人也喜歡」處(即1處)點擊鼠標右鍵,審查元素,這時會在瀏覽器下方彈出網頁代碼,只是咱們要的不是這個標題,將鼠標移動到其父結點處(即2處),會發現推薦的書籍都被藍色覆蓋了,即<div id="db-rec-section" class="block5 subject_show knnlike">
包括的url都是咱們要提取的url。
在設計模式處點擊鼠標右鍵,審查元素。可以看到《設計模式》的url爲https://book.douban.com/subject/1052241/
,使用相同的方法可以查看到其它書籍的url,這些url的前綴都是同樣的,不一樣僅僅是最後的數字不同。且這些數字或爲7位,或爲8位,所以推薦書籍url的正則表達式可以寫爲"https://book\.douban\.com/subject/\d+/$"
內部方法_get_new_urls()
的實現代碼例如如下:
#file: html_parser.py
def _get_new_urls(self, soup):
new_urls = set()
#相同喜歡區域:<div id="db-rec-section" class="block5 subject_show knnlike">
recommend = soup.find('div', class_='block5 subject_show knnlike') #先找到推薦書籍的區域
#<a href="https://book.douban.com/subject/11614538/" class="">程序猿的職業素質</a>
links = recommend.find_all('a', href=re.compile(r"https://book\.douban\.com/subject/\d+/$")) #在推薦區域中尋找所有含有豆瓣書籍url的結點
for link in links:
new_url = link['href'] #從結點中提取超連接,即url
new_urls.add(new_url)
return new_urls
一些說明:
find()
與find_all()
查找的是符合其括號裏條件的結點,如上面第4行代碼表示查找標籤爲div
,class
值爲block5 subject_show knnlike
的結點。由於class
是Python中的保留字,因此find()
中加了一個下劃線即class_
find()
是在html中尋找第一個符合條件的結點,這裏的<div id="db-rec-section" class="block5 subject_show knnlike">
是惟一的,因此請放心使用find()
find_all()
是在html中尋找所有的符合條件的結點r
,表示字符串是「原生的」,不需要進行字符串轉義。直接寫正則表達式就能夠了。假設不加字母r
,特殊符號在正則表達式中轉義一次。在字符串中轉義一次。寫起來就十分的麻煩_get_new_data()
方法實現做爲一個讀者。關注的主要信息就是書名,評分,做者,出版社,出版年,頁碼以及價錢,其它的基本就不考慮了。所以咱們就提取以上列舉的信息。
<span property="v:itemreviewed">代碼大全(第2版)</span>
<strong class="ll rating_num " property="v:average"> 9.3 </strong>
<div id="info" class="">
結點的子結點 書名和評分直接使用find()
找到相關結點。而後使用.string
方法提取結點的內容。但是書本的基本信息這樣就不行了,由於做者。出版社等結點的標籤是同樣。怎麼辦?既然咱們想提取的就是做者。出版社等信息。那麼直接依據結點內容搜索。
首先找到<div id="info" class="">
結點,而後在該結點中使用find(text='出版社')
找到內容爲「出版社」的結點,咱們想要的「電子工業出版社」就是該結點的下一個結點。使用next_element
就可以訪問當前結點的下一個結點。got it!
內部方法_get_new_data()
的實現代碼例如如下:
#file: html_parser.py
def _get_new_data(self, page_url, soup):
res_data = {}
#url
res_data['url'] = page_url
# <span property="v:itemreviewed">代碼大全</span>
res_data['bookName'] = soup.find('span', property='v:itemreviewed').string
# <strong class="ll rating_num " property="v:average"> 9.3 </strong>
res_data['score'] = soup.find('strong', class_='ll rating_num ').string
''' <div id="info" class=""> <span> <span class="pl"> 做者</span>: <a class="" href="/search/Steve%20McConnell">Steve McConnell</a> </span><br> <span class="pl">出版社:</span> 電子工業出版社<br> <span class="pl">出版年:</span> 2007-8<br> <span class="pl">頁數:</span> 138<br> <span class="pl">訂價:</span> 15.00元<br> </div> '''
info = soup.find('div', id='info')
try: #有的頁面信息不全
res_data['author'] = info.find(text=' 做者').next_element.next_element.string
res_data['publisher'] = info.find(text='出版社:').next_element
res_data['time'] = info.find(text='出版年:').next_element
res_data['price'] = info.find(text='訂價:').next_element
res_data['intro'] = soup.find('div', class_='intro').find('p').string
except:
return None
if res_data['intro'] == None:
return None
return res_data
一些說明:
輸出器保存已經爬取頁面的有價值信息。而後在腳本結束時將這些信息以較爲友好的html格式輸出。
輸出器將所有的信息保存在一個列表裏面,保存數據方法的代碼例如如下:
#file: html_outputer.py
class HtmlOutputer(object):
def __init__(self):
self.datas = []
def collect_data(self, data):
if data is None:
return
self.datas.append(data)
數據以html格式輸出既簡單又方便。咱們可以先用nodepad++編寫本身想要的html格式,而後使用瀏覽器打開觀察,不斷的改進,終於獲得本身想要的數據展示形式,個人html格式例如如下:
<html>
<head><title>GoodBooks</title></head>
<body>
<h2><a href='https://book.douban.com/subject/1477390/' target=_blank>代碼大全(第2版)</a></h2>
<table border="1">
<tr><td>評分:</td><td><b>9.3</b></td></tr>
<tr><td>做者:</td><td>[美] 史蒂夫·邁克康奈爾</td></tr>
<tr><td>訂價:</td><td>128.00元</td></tr>
<tr><td>出版社:</td><td>電子工業出版社</td></tr>
<tr><td>出版時間:</td><td>2006-3</td></tr>
</table>
<p>
簡單介紹:第2版的《代碼大全》是著名IT暢銷書做者史蒂夫·邁克康奈爾11年前的經典著做的全新演繹:第2版不是初版的簡單修訂增補,而是全然進行了重寫;添加了很是多與時俱進的內容。這也是一本完整的軟件構建手冊。涵蓋了軟件構建過程當中的所有細節。它從軟件質量和編程思想等方面論述了軟件構建的各個問題。並具體論述了緊跟潮流的新技術、高屋建瓴的觀點、通用的概念,還含有豐富而典型的程序演示樣例。這本書中所論述的技術不只填補了0基礎與高級編程技術之間的空白。而且也爲程序猿們提供了一個有關編程技巧的信息來源。
這本書對經驗豐富的程序猿、技術帶頭人、自學的程序猿及差點兒不懂太多編程技巧的學生們都是大有裨益的。
可以說,不論是什麼背景的讀者。閱讀這本書都有助於在更短的時間內、更easy地寫出更好的程序。 </p> <hr> </body> </html>
最後的<hr>
是切割線。瀏覽器中的效果:
點擊查看原圖
把具體的內容使用%s
格式化輸出就能夠,需要注意的是字符變量後面加上.encode('utf-8')
,將字符的編碼格式改成utf-8.
輸出器的輸出代碼例如如下:
#file: html_outputer.py
def output_html(self):
fout = open('GoodBooks.html', 'w')
fout.write('<html>')
fout.write('<meta charset="UTF-8">') #告訴瀏覽器是utf-8編碼
fout.write('<title>GoodBooks_moverzp</title>')
fout.write('<body>')
for data in self.datas:
print data['bookName'], data['score']
fout.write("<h2><a href='%s' target=_blank>%s</a></h2>" % (data['url'].encode('utf-8'), data['bookName'].encode('utf-8')))
fout.write('<table border="1">')
fout.write('<tr><td>評分:</td><td><b>%s</b></td></tr>' % data['score'].encode('utf-8'))
fout.write('<tr><td>做者:</td><td>%s</td></tr>' % data['author'].encode('utf-8'))
fout.write('<tr><td>訂價:</td><td>%s</td></tr>' % data['price'].encode('utf-8'))
fout.write('<tr><td>出版社:</td><td>%s</td></tr>' % data['publisher'].encode('utf-8'))
fout.write('<tr><td>出版時間:</td><td>%s</td></tr>' % data['time'].encode('utf-8'))
fout.write('</table>')
fout.write('<p>%s' % data['intro'].encode('utf-8'))
fout.write('</p><hr>') #加上切割線
fout.write('</body>')
fout.write('</html>')
調度器是爬蟲的「大腦」,進行任務的分配。將第2節爬蟲框架的步驟寫成代碼就實現了調度器。
下面是調度器的代碼實現,以《代碼大全》爲起始url,抓取50個推薦書籍的信息:
#file: spider_main.py
import url_manager, html_downloader, html_parser, html_outputer
import time
class SpiderMain(object):
def __init__(self):
self.urls = url_manager.UrlManager() #url管理器
self.downloader = html_downloader.HtmlDownloader() #html網頁下載器
self.parser = html_parser.HtmlParser() #html分析器
self.outputer = html_outputer.HtmlOutputer() #html輸出器
def craw(self, root_url):
count = 1
self.urls.add_new_url(root_url)
try:
while self.urls.has_new_url():
new_url = self.urls.get_new_url() #從url管理器中獲取一個未爬取的url
print 'craw %d : %s' % (count, new_url)
html_cont = self.downloader.download(new_url) #下載該url的html
new_urls, new_data = self.parser.parse(new_url, html_cont) #分析html。返回urls和data
self.urls.add_new_urls(new_urls) #將獲取的urls加入進未爬取的url集合中,排除已爬取過的url
self.outputer.collect_data(new_data) #數據都在內存中
time.sleep(0.1)
if count == 50:
break
count += 1
except:
print 'craw failed'
self.outputer.output_html()
if __name__ == "__main__":
root_url = "https://book.douban.com/subject/1477390/" #起始地址爲《代碼大全》
obj_spider = SpiderMain()
obj_spider.craw(root_url)
終於爬取的結果例如如下:
點擊查看完整圖
Q1:url管理器中使用set()
保存未爬取的url,獲取新的url時,使用的是pop()
方法,該方法是隨機從集合中取出一個元素並刪除。這可能會致使咱們咱們爬取的書籍與咱們設置的第一個url相去甚遠。最極端的狀況是每次獲得的url都是推薦書籍中相似度最低的書籍。那麼爬不了幾回獲取的信息都是「垃圾信息」。
解決方法:使用隊列保存未爬取的url,這樣爬取的軌跡就是以初始url爲中心均勻擴散。
Q2:不設置抓取頁面的次數。在700次左右會發生403Forbidden
錯誤。
解決方法:八成是豆瓣檢測到了爬蟲,而後把IP封了。可以使用IP代理的方法防止IP被封。
Scrapy
urllib2
, BeautifulSoup
,re