Python爬蟲入門(urllib+Beautifulsoup)
本文包括:
1、爬蟲簡單介紹
2、爬蟲架構三大模塊
3、urllib
4、BeautifulSoup
5、實戰演練:爬取百度百科1000個頁面
1、爬蟲簡單介紹
爬蟲:一段自動抓取互聯網信息的程序
從一個url出發,然后訪問和這個url相關的各種url,并提取相關的價值數據。
URL:Uniform Resource Location的縮寫,譯為“統一資源定位符”
-
URL的格式由下列三部分組成:
- 第一部分是協議(或稱為服務方式);
- 第二部分是存有該資源的主機IP地址(有時也包括端口號);
- 第三部分是主機資源的具體地址,如目錄和文件名等。
2、爬蟲架構三大模塊
-
URL 管理器
管理待抓取URL集合和已抓取URL集合
防止重復抓取、防止循環抓取
-
邏輯:
1.判斷待添加URL是否在容器中
2.添加新URL到待爬取集合
3.判斷是否有待爬取URL
4.獲取待爬取URL
5.將URL從待爬取移動至已爬取
-
URL管理器的實現方式有三種:
1、適合個人的:內存
2、小型企業或個人:關系數據庫(永久存儲或內存不夠用,如 MySQL)
3、大型互聯網公司:緩存數據庫(高性能,如支持 set 的 redis)
-
網絡下載器
將給定的URL網頁內容下載到本地,以便后續操作
-
常見網絡下載器:
urllib2:Python 官方基礎模塊
-
requests:第三方
注意:python 3.x中 urllib 庫和 urilib2 庫合并成了urllib 庫。其中 urllib2.urlopen() 變成了urllib.request.urlopen()。urllib2.Request() 變成了 urllib.request.Request()
-
特殊情境處理(4種 handler):
1.需要用戶登錄才能訪問(HTTPCookieProcessor)
2.需要代理才能訪問(ProxyHandler)
3.協議使用HTTPS加密訪問(HTTPSHandler)
4.URL自動跳轉(HTTPRedirectHandler)
-
4種方法下載網頁的實例(基于 Python3.6)
見下一節:urllib庫
-
網絡解析器
- 通過解析得到想要的內容,解析出新的 url 交給 URL 管理器,形成循環
- 正則表達式:模糊匹配
- beautifulsoup:第三方,可使用 html.parser 和 lxml 作為解析器,結構化解析(DOM 樹)
- html.parser
- lxml
3、urllib
-
4種方法下載網頁的實例(基于 Python3.6)
import urllib.request import http.cookiejar url = 'https://baidu.com' print('urllib下載網頁方法1:最簡潔方法') # 直接請求 res = urllib.request.urlopen(url) # 獲取狀態碼,如果是200則獲取成功 print(res.getcode()) # 讀取內容 #cont是很長的字符串就不輸出了 cont = res.read().decode('utf-8') print('urllib下載網頁方法2:添加data、http header') # 創建Request對象 request = urllib.request.Request(url) # 添加數據 request.data = 'a' # 添加http的header #將爬蟲偽裝成Mozilla瀏覽器 request.add_header('User-Agent', 'Mozilla/5.0') # 添加http的header #指定源網頁,防止反爬 request.add_header('Origin', 'https://xxxx.com') # 發送請求獲取結果 response = urllib.request.urlopen(request) print('urllib下載網頁方法3:添加特殊情景的處理器') # 創建cookie容器 cj = http.cookiejar.CookieJar() # 創建一個opener opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) # 給urllib安裝opener urllib.request.install_opener(opener) # 使用帶有cookie的urllib訪問網頁 response = urllib.request.urlopen(url) # 使用 post 提交數據 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語法:
根據一個HTML網頁字符串創建BeautifulSoup對象,創建的同時就將整個文檔字符串下載成一個DOM樹,后根據這個DOM樹搜索節點。find_all方法搜索出所有滿足的節點,find方法只會搜索出第一個滿足的節點,兩方法參數一致。搜索出節點后就可以訪問節點的名稱、屬性、文字。因此在搜索時也可以按照以上三項搜索。
-
實例:
from bs4 import BeautifulSoup # 第一步:根據HTML網頁字符串創建BeautifulSoup對象 soup = BeautifulSoup( 'XX.html', # HTML文檔字符串 'html.parser' # HTML解析器 from_encoding='utf8' # HTML文檔的編碼 ) # 第二步:搜索節點(find_all,find) # 方法:find_all(name,attrs,string) # 名稱,屬性,文字 # 查找所有標簽為a的標簽 soup.find_all(‘a’) # 查找第一個標簽為a的標簽 soup.find(‘a’) # 查找所有標簽為a,鏈接符合'/view/123.html'形式的節點 soup.find_all('a',href='/view/123.html') # 查找所有標簽為div,class為abc,文字為python的節點 soup.find_all('div',class_='abc',string='python') # class 是 Python 保留關建字,所以為了區別加了下劃線 # 以下三種方式等價 # soup.h1 = soup.html.body.h1 = soup.html.h1 # 最強大的是利用正則表達式 import re soup.find('a', href=re.compile(r"view")) soup.find_all("img", {"src":re.compile("xxx")}) # 第三步:訪問節點信息 # 得到節點:<a href=‘1.html’>Python</a> node = soup.find(‘a’) # 獲取查找到的節點的標簽名稱 print(node.name) # 獲取查找的a節點的href屬性 print(node['href']) # 獲取查找到的a節點的文本字符串 print(node.get_text())
findAll() 、find()函數詳解:
findAll(tag, attributes, recursive, text, limit, keywords)。
find(tag, attributes, recursive, text, keywords)tag 可以傳入一個標簽的名稱或多個標簽名組成的Python列表做標簽參數
attributes是用一個Python字典封裝一個標簽的若干屬性和對應的屬性值
recursive是布爾變量,默認為True,如果為False,findAll就只查找文檔的一級標簽
text用標簽的文本內容去匹配,而不是標簽的屬性
limit 只能用于findAll,find其實就是findAll的limit=1的特殊情況
keyword 有點冗余,不管了
95%的時間都只用到了tag、attributes。
-
導航樹
-
子標簽children與后代標簽descendants:
# 匹配標簽的的下一級標簽 bsobj.find("table", {"id":"giftlist"}).children # 匹配標簽的所有后代標簽,包括一大堆亂七八糟的img,span等等 bsobj.find("table", {"id":"giftlist"}).descendants
-
兄弟標簽next_siblings/previous_siblings:
# 匹配標簽的后一個標簽 bsobj.find("table", {"id":"giftlist"}).tr.next_siblings # 匹配標簽的前一個標簽(如果同級標簽的最后一個標簽容易定位,那么就用這個) bsobj.find("table", {"id":"giftlist"}).tr.previous_siblings
-
父標簽parent、parents:
# 選取table標簽本身(這個操作多此一舉,只是為了舉例層級關系) bsobj.find("table", {"id":"giftlist"}).tr.parent
-
-
獲取屬性
-
如果我們得到了一個標簽對象,可以用下面的代碼獲得它的全部標簽屬性:
mytag.attrs
-
注意:這行代碼返回的是一個字典對象,所以可以獲取任意一個屬性值,例如獲取src屬性值就這樣寫代碼:
mytag.attrs["src"]
-
-
編寫可靠的代碼(捕捉異常)
-
讓我們看看爬蟲import語句后面的第一行代碼,如何處理那里可能出現的異常:
html = urlopen("http://www.pythonscraping.com/pages/page1.html")
這有可能會報404錯誤,所以應該捕捉異常:
try: html = urlopen("http://www.pythonscraping.com/pages/page1.html") except HTTPError as e: print(e) # 返回空值,中斷程序,或者執行另一個方案 else: # 程序繼續。注意:如果你已經在上面異常捕捉那一段代碼里返回或中斷(break), # 那么就不需要使用else語句了,這段代碼也不會執行
-
下面這行代碼(nonExistentTag是虛擬的標簽,BeautifulSoup對象里實際沒有)
print(bsObj.nonExistentTag)
會返回一個None對象。處理和檢查這個對象是十分必要的。如果你不檢查,直接調用這個None對象的子標簽,麻煩就來了。如下所示。
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、實戰演練:爬取百度百科1000個頁面
-
步驟
- 確定目標:確定抓取哪個網站的哪些網頁的哪部分數據。本實例確定抓取百度百科python詞條頁面以及它相關的詞條頁面的標題和簡介。
- 分析目標:確定抓取數據的策略。一是分析要抓取的目標頁面的URL格式,用來限定要抓取的頁面的范圍;二是分析要抓取的數據的格式,在本實例中就是要分析每一個詞條頁面中標題和簡介所在的標簽的格式;三是分析頁面的編碼,在網頁解析器中指定網頁編碼,才能正確解析。
- 編寫代碼:在解析器中會使用到分析目標步驟所得到的抓取策略的結果。
- 執行爬蟲。
-
確定框架
20171031-baike- spider_main.py 是爬蟲主體
- url_manager.py 維護了兩個集合,用來記錄要爬取的 url 和已爬取的 url
- html_downloader.py 調用了 urllib 庫來下載 html 文檔
- html_parser.py 調用了 BeautifulSoup 來解析 html 文檔
- html_outputer.py 把解析后的數據存儲起來,寫入 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: # 防止重復爬取 self.new_urls.add(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) 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)
解釋:主程序將會從 “Python” 的詞條頁面進入,然后開始爬取數據。注意到,每爬取一個頁面,都有可能有新的 url 被解析出來,所以要交給 url_manager 管理,然后將 new_data 收集起來,當跳出 while 循環時,將數據輸出(因數據量不大,直接存放在內存中)。
-
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
本篇內容來自慕課網視頻教程:http://www.imooc.com/learn/563
爬取百度百科的源碼地址:https://github.com/edisonleolhl/imooc