Python爬蟲入門(urllib+Beautifulsoup)
本文包括:
1恒水、爬蟲簡單介紹
2倔幼、爬蟲架構(gòu)三大模塊
3继薛、urllib
4修壕、BeautifulSoup
5、實戰(zhàn)演練:爬取百度百科1000個頁面
1遏考、爬蟲簡單介紹
爬蟲:一段自動抓取互聯(lián)網(wǎng)信息的程序
從一個url出發(fā)慈鸠,然后訪問和這個url相關(guān)的各種url,并提取相關(guān)的價值數(shù)據(jù)灌具。
URL:Uniform Resource Location的縮寫青团,譯為“統(tǒng)一資源定位符”
-
URL的格式由下列三部分組成:
- 第一部分是協(xié)議(或稱為服務(wù)方式);
- 第二部分是存有該資源的主機IP地址(有時也包括端口號)咖楣;
- 第三部分是主機資源的具體地址督笆,如目錄和文件名等。
2诱贿、爬蟲架構(gòu)三大模塊
-
URL 管理器
管理待抓取URL集合和已抓取URL集合
防止重復(fù)抓取娃肿、防止循環(huán)抓取
-
邏輯:
1.判斷待添加URL是否在容器中
2.添加新URL到待爬取集合
3.判斷是否有待爬取URL
4.獲取待爬取URL
5.將URL從待爬取移動至已爬取
-
URL管理器的實現(xiàn)方式有三種:
1、適合個人的:內(nèi)存
2珠十、小型企業(yè)或個人:關(guān)系數(shù)據(jù)庫(永久存儲或內(nèi)存不夠用料扰,如 MySQL)
3、大型互聯(lián)網(wǎng)公司:緩存數(shù)據(jù)庫(高性能焙蹭,如支持 set 的 redis)
-
網(wǎng)絡(luò)下載器
將給定的URL網(wǎng)頁內(nèi)容下載到本地晒杈,以便后續(xù)操作
-
常見網(wǎng)絡(luò)下載器:
urllib2:Python 官方基礎(chǔ)模塊
-
requests:第三方
注意:python 3.x中 urllib 庫和 urilib2 庫合并成了urllib 庫。其中 urllib2.urlopen() 變成了urllib.request.urlopen()壳嚎。urllib2.Request() 變成了 urllib.request.Request()
-
特殊情境處理(4種 handler):
1.需要用戶登錄才能訪問(HTTPCookieProcessor)
2.需要代理才能訪問(ProxyHandler)
3.協(xié)議使用HTTPS加密訪問(HTTPSHandler)
4.URL自動跳轉(zhuǎn)(HTTPRedirectHandler)
-
4種方法下載網(wǎng)頁的實例(基于 Python3.6)
見下一節(jié):urllib庫
-
網(wǎng)絡(luò)解析器
- 通過解析得到想要的內(nèi)容桐智,解析出新的 url 交給 URL 管理器,形成循環(huán)
- 正則表達(dá)式:模糊匹配
- beautifulsoup:第三方烟馅,可使用 html.parser 和 lxml 作為解析器说庭,結(jié)構(gòu)化解析(DOM 樹)
- html.parser
- lxml
3、urllib
-
4種方法下載網(wǎng)頁的實例(基于 Python3.6)
import urllib.request import http.cookiejar url = 'https://baidu.com' print('urllib下載網(wǎng)頁方法1:最簡潔方法') # 直接請求 res = urllib.request.urlopen(url) # 獲取狀態(tài)碼郑趁,如果是200則獲取成功 print(res.getcode()) # 讀取內(nèi)容 #cont是很長的字符串就不輸出了 cont = res.read().decode('utf-8') print('urllib下載網(wǎng)頁方法2:添加data刊驴、http header') # 創(chuàng)建Request對象 request = urllib.request.Request(url) # 添加數(shù)據(jù) request.data = 'a' # 添加http的header #將爬蟲偽裝成Mozilla瀏覽器 request.add_header('User-Agent', 'Mozilla/5.0') # 添加http的header #指定源網(wǎng)頁,防止反爬 request.add_header('Origin', 'https://xxxx.com') # 發(fā)送請求獲取結(jié)果 response = urllib.request.urlopen(request) print('urllib下載網(wǎng)頁方法3:添加特殊情景的處理器') # 創(chuàng)建cookie容器 cj = http.cookiejar.CookieJar() # 創(chuàng)建一個opener opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) # 給urllib安裝opener urllib.request.install_opener(opener) # 使用帶有cookie的urllib訪問網(wǎng)頁 response = urllib.request.urlopen(url) # 使用 post 提交數(shù)據(jù) from urllib import parse from urllib.request import Request from urllib.request import urlopen req = Request(url) postData = parse.urlencode([ (key1, value1), (key2, value2), ... ]) urlopen(req, data=postData.encode('utf-8'))
4寡润、BeautifulSoup
-
BeautifulSoup語法:
根據(jù)一個HTML網(wǎng)頁字符串創(chuàng)建BeautifulSoup對象捆憎,創(chuàng)建的同時就將整個文檔字符串下載成一個DOM樹,后根據(jù)這個DOM樹搜索節(jié)點梭纹。find_all方法搜索出所有滿足的節(jié)點躲惰,find方法只會搜索出第一個滿足的節(jié)點,兩方法參數(shù)一致变抽。搜索出節(jié)點后就可以訪問節(jié)點的名稱础拨、屬性、文字绍载。因此在搜索時也可以按照以上三項搜索诡宗。
-
實例:
from bs4 import BeautifulSoup # 第一步:根據(jù)HTML網(wǎng)頁字符串創(chuàng)建BeautifulSoup對象 soup = BeautifulSoup( 'XX.html', # HTML文檔字符串 'html.parser' # HTML解析器 from_encoding='utf8' # HTML文檔的編碼 ) # 第二步:搜索節(jié)點(find_all,find) # 方法:find_all(name,attrs,string) # 名稱,屬性,文字 # 查找所有標(biāo)簽為a的標(biāo)簽 soup.find_all(‘a(chǎn)’) # 查找第一個標(biāo)簽為a的標(biāo)簽 soup.find(‘a(chǎn)’) # 查找所有標(biāo)簽為a,鏈接符合'/view/123.html'形式的節(jié)點 soup.find_all('a',href='/view/123.html') # 查找所有標(biāo)簽為div,class為abc,文字為python的節(jié)點 soup.find_all('div',class_='abc',string='python') # class 是 Python 保留關(guān)建字击儡,所以為了區(qū)別加了下劃線 # 以下三種方式等價 # soup.h1 = soup.html.body.h1 = soup.html.h1 # 最強大的是利用正則表達(dá)式 import re soup.find('a', href=re.compile(r"view")) soup.find_all("img", {"src":re.compile("xxx")}) # 第三步:訪問節(jié)點信息 # 得到節(jié)點:<a href=‘1.html’>Python</a> node = soup.find(‘a(chǎn)’) # 獲取查找到的節(jié)點的標(biāo)簽名稱 print(node.name) # 獲取查找的a節(jié)點的href屬性 print(node['href']) # 獲取查找到的a節(jié)點的文本字符串 print(node.get_text())
findAll() 塔沃、find()函數(shù)詳解:
findAll(tag, attributes, recursive, text, limit, keywords)。
find(tag, attributes, recursive, text, keywords)tag 可以傳入一個標(biāo)簽的名稱或多個標(biāo)簽名組成的Python列表做標(biāo)簽參數(shù)
attributes是用一個Python字典封裝一個標(biāo)簽的若干屬性和對應(yīng)的屬性值
recursive是布爾變量阳谍,默認(rèn)為True蛀柴,如果為False,findAll就只查找文檔的一級標(biāo)簽
text用標(biāo)簽的文本內(nèi)容去匹配边坤,而不是標(biāo)簽的屬性
limit 只能用于findAll名扛,find其實就是findAll的limit=1的特殊情況
keyword 有點冗余,不管了
95%的時間都只用到了tag茧痒、attributes肮韧。
-
導(dǎo)航樹
-
子標(biāo)簽children與后代標(biāo)簽descendants:
# 匹配標(biāo)簽的的下一級標(biāo)簽 bsobj.find("table", {"id":"giftlist"}).children # 匹配標(biāo)簽的所有后代標(biāo)簽,包括一大堆亂七八糟的img旺订,span等等 bsobj.find("table", {"id":"giftlist"}).descendants
-
兄弟標(biāo)簽next_siblings/previous_siblings:
# 匹配標(biāo)簽的后一個標(biāo)簽 bsobj.find("table", {"id":"giftlist"}).tr.next_siblings # 匹配標(biāo)簽的前一個標(biāo)簽(如果同級標(biāo)簽的最后一個標(biāo)簽容易定位弄企,那么就用這個) bsobj.find("table", {"id":"giftlist"}).tr.previous_siblings
-
父標(biāo)簽parent、parents:
# 選取table標(biāo)簽本身(這個操作多此一舉区拳,只是為了舉例層級關(guān)系) bsobj.find("table", {"id":"giftlist"}).tr.parent
-
-
獲取屬性
-
如果我們得到了一個標(biāo)簽對象拘领,可以用下面的代碼獲得它的全部標(biāo)簽屬性:
mytag.attrs
-
注意:這行代碼返回的是一個字典對象,所以可以獲取任意一個屬性值樱调,例如獲取src屬性值就這樣寫代碼:
mytag.attrs["src"]
-
-
編寫可靠的代碼(捕捉異常)
-
讓我們看看爬蟲import語句后面的第一行代碼约素,如何處理那里可能出現(xiàn)的異常:
html = urlopen("http://www.pythonscraping.com/pages/page1.html")
這有可能會報404錯誤届良,所以應(yīng)該捕捉異常:
try: html = urlopen("http://www.pythonscraping.com/pages/page1.html") except HTTPError as e: print(e) # 返回空值,中斷程序圣猎,或者執(zhí)行另一個方案 else: # 程序繼續(xù)士葫。注意:如果你已經(jīng)在上面異常捕捉那一段代碼里返回或中斷(break), # 那么就不需要使用else語句了送悔,這段代碼也不會執(zhí)行
-
下面這行代碼(nonExistentTag是虛擬的標(biāo)簽慢显,BeautifulSoup對象里實際沒有)
print(bsObj.nonExistentTag)
會返回一個None對象。處理和檢查這個對象是十分必要的欠啤。如果你不檢查荚藻,直接調(diào)用這個None對象的子標(biāo)簽,麻煩就來了洁段。如下所示应狱。
print(bsObj.nonExistentTag.someTag)
這時就會返回一個異常:
AttributeError: 'NoneType' object has no attribute 'someTag'
那么我們怎么才能避免這兩種情形的異常呢?最簡單的方式就是對兩種情形進行檢查:
try: badContent = bsObj.nonExistingTag.anotherTag exceptAttributeError as e: print("Tag was not found") else: if badContent == None: print ("Tag was not found") else: print(badContent)
-
5祠丝、實戰(zhàn)演練:爬取百度百科1000個頁面
-
步驟
- 確定目標(biāo):確定抓取哪個網(wǎng)站的哪些網(wǎng)頁的哪部分?jǐn)?shù)據(jù)侦香。本實例確定抓取百度百科python詞條頁面以及它相關(guān)的詞條頁面的標(biāo)題和簡介。
- 分析目標(biāo):確定抓取數(shù)據(jù)的策略纽疟。一是分析要抓取的目標(biāo)頁面的URL格式罐韩,用來限定要抓取的頁面的范圍;二是分析要抓取的數(shù)據(jù)的格式污朽,在本實例中就是要分析每一個詞條頁面中標(biāo)題和簡介所在的標(biāo)簽的格式散吵;三是分析頁面的編碼,在網(wǎng)頁解析器中指定網(wǎng)頁編碼蟆肆,才能正確解析矾睦。
- 編寫代碼:在解析器中會使用到分析目標(biāo)步驟所得到的抓取策略的結(jié)果。
- 執(zhí)行爬蟲炎功。
-
確定框架
20171031-baike- spider_main.py 是爬蟲主體
- url_manager.py 維護了兩個集合枚冗,用來記錄要爬取的 url 和已爬取的 url
- html_downloader.py 調(diào)用了 urllib 庫來下載 html 文檔
- html_parser.py 調(diào)用了 BeautifulSoup 來解析 html 文檔
- html_outputer.py 把解析后的數(shù)據(jù)存儲起來,寫入 output.html 文檔中
-
url_manager
class UrlManager(object): def __init__(self): # 初始化兩個集合 self.new_urls = set() self.old_urls = set() def add_new_url(self, url): if url is None: return if url not in self.new_urls or self.old_urls: # 防止重復(fù)爬取 self.new_urls.add(url) def add_new_urls(self, urls): if urls is None or len(urls) == 0: return for url in urls: # 調(diào)用子程序 self.add_new_url(url) def has_new_url(self): return len(self.new_urls) != 0 def get_new_url(self): new_url = self.new_urls.pop() self.old_urls.add(new_url) return new_url
解釋:管理器維護了兩個集合(new_urls蛇损、old_urls)赁温,分別記錄要爬和已爬 url,注意到前兩個 add 方法淤齐,一個是針對單個 url股囊,一個是針對 url 集合,不要忘記去重操作更啄。
-
html_downloader
# coding:utf-8 import urllib.request class HtmlDownloader(object): def download(self, url): if url is None: return None response = urllib.request.urlopen(url) if response.getcode() != 200: # 判斷是否請求成功 return None return response.read()
解釋:很直觀的下載稚疹,這是最簡單的做法
-
html_parser
from bs4 import BeautifulSoup import urllib.parse import re class HtmlParser(object): def _get_new_urls(self, page_url, soup): new_urls = set() links = soup.find_all('a', href = re.compile(r"/item/")) for link in links: new_url = link['href'] new_full_url = urllib.parse.urljoin(page_url, new_url) new_urls.add(new_full_url) return new_urls def _get_new_data(self, page_url, soup): res_data = {} res_data['url'] = page_url title_node = soup.find('dd', class_="lemmaWgt-lemmaTitle-title").find("h1") res_data['title'] = title_node.get_text() summary_node = soup.find('div', class_="lemma-summary") res_data['summary'] = summary_node.get_text() return res_data 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') new_urls = self._get_new_urls(page_url, soup) new_data = self._get_new_data(page_url, soup) return new_urls, new_data
解釋:在解析器中,注意到 parse 方法祭务,它從 html 文檔中找到所有詞條鏈接内狗,并將它們包裝到 new_urls 集合中怪嫌,最后返回,同時柳沙,它還會解析出 new_data 集合喇勋,這個集合存放了詞條的名字(title)以及摘要(summary)。
-
spider_main
# coding:utf-8 from baike_spider import url_manager, html_downloader, html_parser, html_outputer import logging class SpiderMain(object): def __init__(self): self.urls = url_manager.UrlManager() self.downloader = html_downloader.HtmlDownloader() self.parser = html_parser.HtmlParser() self.outputer = html_outputer.HtmlOutputer() def crawl(self, root_url): count = 1 # record the current number url self.urls.add_new_url(root_url) while self.urls.has_new_url(): try: new_url = self.urls.get_new_url() print('crawl No.%d: %s'%(count, new_url)) html_cont = self.downloader.download(new_url) new_urls, new_data = self.parser.parse(new_url, html_cont) self.urls.add_new_urls(new_urls) self.outputer.collect_data(new_data) if count == 1000: break count += 1 except: logging.warning('crawl failed') self.outputer.output_html() if __name__ == "__main__": root_url = "https://baike.baidu.com/item/Python/407313" obj_spider = SpiderMain() obj_spider.crawl(root_url)
解釋:主程序?qū)?“Python” 的詞條頁面進入偎行,然后開始爬取數(shù)據(jù)。注意到贰拿,每爬取一個頁面蛤袒,都有可能有新的 url 被解析出來,所以要交給 url_manager 管理膨更,然后將 new_data 收集起來妙真,當(dāng)跳出 while 循環(huán)時,將數(shù)據(jù)輸出(因數(shù)據(jù)量不大荚守,直接存放在內(nèi)存中)珍德。
-
html_outputer
class HtmlOutputer(object): def __init__(self): self.datas = [] # 列表 def collect_data(self, data): if data is None: return self.datas.append(data) def output_html(self): with open('output.html', 'w', encoding='utf-8') as fout: fout.write("<html>") fout.write("<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"></head>") fout.write("<body>") fout.write("<table>") for data in self.datas: fout.write("<tr>") fout.write("<td>%s</td>" % data["url"]) fout.write("<td>%s</td>" % data["title"]) fout.write("<td>%s</td>" % data["summary"]) fout.write("</tr>") fout.write("</table>") fout.write("</body>") fout.write("</html>")
解釋:注意編碼問題就好
-
輸出:
"C:\Program Files\Python36\python.exe" D:/PythonProject/immoc/baike_spider/spider_main.py crawl No.1: https://baike.baidu.com/item/Python/407313 crawl No.2: https://baike.baidu.com/item/Zope crawl No.3: https://baike.baidu.com/item/OpenCV crawl No.4: https://baike.baidu.com/item/%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F crawl No.5: https://baike.baidu.com/item/JIT crawl No.6: https://baike.baidu.com/item/%E9%9C%80%E6%B1%82%E9%87%8F crawl No.7: https://baike.baidu.com/item/Linux crawl No.8: https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B crawl No.9: https://baike.baidu.com/item/Pylons crawl No.10: https://baike.baidu.com/item/%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1 Process finished with exit code 0
-
output.html
20171031-baikeout
本篇內(nèi)容來自慕課網(wǎng)視頻教程:http://www.imooc.com/learn/563
爬取百度百科的源碼地址:https://github.com/edisonleolhl/imooc