Python爬蟲入門(urllib+Beautifulsoup)

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

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

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

  • 步驟

    1. 確定目標(biāo):確定抓取哪個網(wǎng)站的哪些網(wǎng)頁的哪部分?jǐn)?shù)據(jù)侦香。本實例確定抓取百度百科python詞條頁面以及它相關(guān)的詞條頁面的標(biāo)題和簡介。
    2. 分析目標(biāo):確定抓取數(shù)據(jù)的策略纽疟。一是分析要抓取的目標(biāo)頁面的URL格式罐韩,用來限定要抓取的頁面的范圍;二是分析要抓取的數(shù)據(jù)的格式污朽,在本實例中就是要分析每一個詞條頁面中標(biāo)題和簡介所在的標(biāo)簽的格式散吵;三是分析頁面的編碼,在網(wǎng)頁解析器中指定網(wǎng)頁編碼蟆肆,才能正確解析矾睦。
    3. 編寫代碼:在解析器中會使用到分析目標(biāo)步驟所得到的抓取策略的結(jié)果。
    4. 執(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市矗漾,隨后出現(xiàn)的幾起案子锈候,更是在濱河造成了極大的恐慌,老刑警劉巖敞贡,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泵琳,死亡現(xiàn)場離奇詭異,居然都是意外死亡誊役,警方通過查閱死者的電腦和手機获列,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛔垢,“玉大人击孩,你說我怎么就攤上這事∨羝幔” “怎么了巩梢?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長艺玲。 經(jīng)常有香客問我且改,道長,這世上最難降的妖魔是什么板驳? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任又跛,我火速辦了婚禮,結(jié)果婚禮上若治,老公的妹妹穿的比我還像新娘慨蓝。我一直安慰自己感混,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布礼烈。 她就那樣靜靜地躺著弧满,像睡著了一般。 火紅的嫁衣襯著肌膚如雪此熬。 梳的紋絲不亂的頭發(fā)上庭呜,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音犀忱,去河邊找鬼募谎。 笑死,一個胖子當(dāng)著我的面吹牛阴汇,可吹牛的內(nèi)容都是我干的数冬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼搀庶,長吁一口氣:“原來是場噩夢啊……” “哼拐纱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起哥倔,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤秸架,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后咆蒿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡蜡秽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年府阀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芽突。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡试浙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寞蚌,到底是詐尸還是另有隱情田巴,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布挟秤,位于F島的核電站壹哺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏艘刚。R本人自食惡果不足惜管宵,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箩朴,春花似錦岗喉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至埠居,卻和暖如春查牌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滥壕。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工纸颜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捏浊。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像撞叨,于是被迫代替她去往敵國和親金踪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容