Python爬蟲入門(urllib+Beautifulsoup)

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

  • 文檔:https://www.crummy.com/software/BeautifulSoup/bs4/doc/

  • 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個頁面

  • 步驟

    1. 確定目標:確定抓取哪個網站的哪些網頁的哪部分數據。本實例確定抓取百度百科python詞條頁面以及它相關的詞條頁面的標題和簡介。
    2. 分析目標:確定抓取數據的策略。一是分析要抓取的目標頁面的URL格式,用來限定要抓取的頁面的范圍;二是分析要抓取的數據的格式,在本實例中就是要分析每一個詞條頁面中標題和簡介所在的標簽的格式;三是分析頁面的編碼,在網頁解析器中指定網頁編碼,才能正確解析。
    3. 編寫代碼:在解析器中會使用到分析目標步驟所得到的抓取策略的結果。
    4. 執行爬蟲。
  • 確定框架

    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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容