爬蟲初探-Scrapy

爬蟲初探-Scrapy

Scrapy 資料

爬取步驟

  • 我們準(zhǔn)備爬取宅男女神排行榜的所有女神相冊,首先看看入口是怎么樣的。

    20171024-goddessrank
  • 可以看到這里有5頁,每頁有20個女神,所以我們的爬蟲邏輯應(yīng)該是:

    • 遍歷這20個女神,進(jìn)入到各自的主頁,獲取它們主頁的個人信息。

      • 從女神主頁進(jìn)入她的相冊寫真集頁面(如果某女神寫真集較少,則直接在主頁進(jìn)入相冊),把各相冊中圖片下載下來,這里要注意某個寫真相冊有很多頁,每頁有好幾張圖片,在這里同樣需要遍歷每一頁。
    • 遍歷這5頁,重復(fù)上述動作。

  • 進(jìn)入第一個女神:夏美醬的主頁,可以看到有她的一些個人信息,以及寫真集。

    20171024-xiameijiang
  • 好的,大致信息已經(jīng)知道了,我們從簡單的個人信息爬取開始。

爬取個人信息

  • 首先從簡單的做起,爬取排行榜所有女神的個人信息,如姓名、生日、年齡、三圍、出生,在女神的主頁,通過谷歌瀏覽器的開發(fā)者工具,可以看到這樣的代碼:

    20171024-xiameijianginfo
  • 于是 spider 中爬取女神個人信息的代碼是這樣的:

      import scrapy
      import re
    
      class GoddessSpider(scrapy.Spider):
          name = "goddess"
          start_urls = ['https://www.nvshens.com/rank/sum/']
    
          def parse(self, response):
              # follow links to goddess pages
              for href in response.css('div.rankli_imgdiv a::attr(href)'):
                  yield response.follow(href, self.parse_goddess)
    
              # follow pagination links
              # ...
    
          def parse_goddess(self, response):
              def util(self, l):
                  if l is not None and len(l) != 0:
                      return l[0]
                  else:
                      return None
              dic = dict(zip(response.css('div.infodiv td::text').extract()[0::2], response.css('div.infodiv td::text').extract()[1::2]))
              dic['姓名'] = response.css('div.div_h1 h1::text').extract()[0]
              yield dic
    

    解釋:用 'div.infodiv td::text' 找到的既包含了“年齡”又包含了“20(屬牛)”,而且是按順序存儲的,所以調(diào)用 extract() 方法把 Selector 對象變成列表后,把這個列表的奇數(shù)項作為 key (如:年齡、生日、星座...),偶數(shù)項作為 value (如:20(屬牛)、1997-09-22、處女座)。然后再用 zip 函數(shù),就可以做到兩個列表轉(zhuǎn)為字典。很實用的功能。
    姓名在女神主頁的 h1 中可以找到,最后加進(jìn)字典中即可。

  • 輸出類似下面這樣:

      2017-10-24 13:43:16 [scrapy.core.scraper] DEBUG: Scraped from <200 https://www.nvshens.com/girl/24410/>
      {'年 齡:': '22 (屬豬)', '生 日:': '1995-10-01', '星 座:': '天秤座', '身 高:': '165', '三 圍:': 'B88 W60 H86', '出 生:': '中國 上海徐匯區(qū)', '職 業(yè):': '平面模特、主播', '興 趣:': '旅游、時尚、文藝、美食', '姓名': '周于希dummy(Dummy Zhou)'}
      2017-10-24 13:43:16 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.nvshens.com/girl/19705/> (referer: https://www.nvshens.com/rank/sum/)
      2017-10-24 13:43:16 [scrapy.core.scraper] DEBUG: Scraped from <200 https://www.nvshens.com/girl/20440/>
      {'年 齡:': '22 (屬狗)', '生 日:': '1994-12-24', '星 座:': '魔羯座', '身 高:': '165', '三 圍:': 'B90(F75) W60 H88', '出 生:': '中國 浙江杭州', '職 業(yè):': '鋼管舞老師、模特', '興 趣:': '舞蹈', '姓名': '于姬(Una)'}
    
  • 剛才只爬取了第一頁的女神主頁,還有4頁需要爬取,查看入口的分頁器代碼,并沒有像 Scrapy 官方教程那么簡單,在官方教程中,“下一頁”的按鈕有明確的 class 或者 id 唯一標(biāo)識,但在這里沒有,如下:

    20171024-goddesspages
  • 可以看到,“”的按鈕并沒有定義 class 或者 id,這就和其他的頁數(shù)按鈕混在一起了,那要怎么判斷下一頁呢?可以看到,當(dāng)前頁(圖中也就是第1頁)是比較特別的,因為它被 class='cur' 唯一標(biāo)識了,而跳轉(zhuǎn)第2頁看看,class='cur' 就變成第二頁的標(biāo)識了,顯然,這里就是突破口。

  • 進(jìn)入 Scrapy 的 shell 窗口,調(diào)試一下:

      ## 讀取分頁條的所有按鈕(也就是 a 鏈接)
      >>> response.css('div.pagesYY a::attr(href)')
      [<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-self::*/a/@href" data='1.ht
      ml'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-self::*/a/@href" data=
      '2.html'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-self::*/a/@href"
      data='3.html'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-self::*/a/@h
      ref" data='4.html'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-self::*
      /a/@href" data='5.html'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-se
      lf::*/a/@href" data='2.html'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-
      or-self::*/a/@href" data='5.html'>]
    
      ## 讀取分頁條的當(dāng)前頁數(shù)按鈕
    
      >>> response.css('div.pagesYY a.cur::attr(href)')
      [<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' pagesYY ')]/descendant-or-self::*/a[@class and contai
      ns(concat(' ', normalize-space(@class), ' '), ' cur ')]/@href" data='1.html'>]
    
  • 可以看到,當(dāng)前頁數(shù)的 Selector 對象并不與上面相同(因為選擇器不同),即不能簡單用 in 關(guān)鍵字判斷(if 'a' in 'abc'),利用正則表達(dá)式來尋找 X.html,比較一下就行了,下面是代碼:

      def parse(self, response):
          # follow links to goddess pages
          for href in response.css('div.rankli_imgdiv a::attr(href)'):
              yield response.follow(href, self.parse_goddess)
    
          # follow pagination links
          next_page = None
          L = len(response.css('div.pagesYY a::attr(href)'))
          for i in range(L):
              tmp_page = re.findall(r"[1-5].html", str(response.css('div.pagesYY a::attr(href)')[i]))
              print("tmp_page=", tmp_page)
              cur_page = re.findall(r"[1-5].html", str(response.css('div.pagesYY a.cur::attr(href)')))
              print("cur_page=", cur_page)
              if tmp_page == cur_page and cur_page != ['5.html']:
                  next_page = response.css('div.pagesYY a::attr(href)')[i+1] # Attention: next_page = cur_page + 1
                  print("next_page=", next_page)
                  break
          if next_page is not None:
              print("--------------------------------------------------------------------")
              yield response.follow(next_page, self.parse)
    

爬取圖片

  • 好了,前面把整個框架搭好了,現(xiàn)在要進(jìn)入女神主頁的相冊,有點小興奮呢:)

  • 這里有個問題了,有些女神的相冊很多,主頁只能顯示最近6個相冊,在相冊 div 的右下角有個按鈕用來進(jìn)入相冊集頁面(例如夏美醬的相冊集 url :'https://www.nvshens.com/girl/21501/album/'):

      <span class='archive_more'><a style='text-decoration: none' href='/girl/21501/album/' title='全部圖片' class='title'>共50冊</a></span>
    
  • 然而有些女神只有少數(shù)相冊,甚至只有一個相冊,右下角也沒有上述按鈕,如果在地址欄手動輸入:XXXX/album/,那么會出現(xiàn)404錯誤,我們的爬蟲當(dāng)然要“智能”判斷這兩種情況,實現(xiàn)全部爬取。

  • 用簡單的 if 判斷一下即可,在其中一個分支中要再開一個函數(shù)處理:

      def parse_goddess(self, response):
          # get goddess info, like name, age, birthday ...
          def util(self, l):
              if l is not None and len(l) != 0:
                  return l[0]
              else:
                  return None
          dic = dict(zip(response.css('div.infodiv td::text').extract()[0::2], response.css('div.infodiv td::text').extract()[1::2]))
          dic['姓名'] = response.css('div.div_h1 h1::text').extract()[0]
          yield dic
    
          # get to the album page (before photo page) or photo page directly
          if response.css('span.archive_more a::attr(href)') is not None:
              for archive_more in response.css('span.archive_more a::attr(href)'):
                  yield response.follow(archive_more, self.parse_goddess_album)
          else:
              for album_link in response.css('a.igalleryli_link::attr(href)'):
                  yield response.follow(album_link, self.parse_goddess_photo)
    
      def parse_goddess_album(self, response):
          for album_link in response.css('a.igalleryli_link::attr(href)'):
              yield response.follow(album_link, self.parse_goddess_photo)
    
  • 好的,現(xiàn)在就要開始編寫 parse_goddess_photo 函數(shù)了,我們隨便打開一個女神相冊進(jìn)入,再調(diào)用檢查工具,看一看從哪里突破。

    20171025-goddessphotohtml
  • 圖片的 url 地址一目了然,可以用選擇器找到外部的 ul#hgallery 標(biāo)簽,然后加個 for 循環(huán)即可,注意到爬取圖片下載到本地時有兩點要注意:

    • 路徑:在工程目錄下創(chuàng)建一個文件夾,名字就是當(dāng)前爬取的相冊,里面儲存該相冊的所有圖片,同時還可以爬取該相冊的介紹信息,保存到相冊文件夾的 txt 文件中。而且,要為每個本地圖片指定名字,在這里用了正則表達(dá)式,把 url 最后的 http://../..//XX.jpg 中的 XX.jpg 作為本地圖片的名字。

    • urllib 當(dāng)前版本下載圖片到本地要這樣操作:

      import urllib.request
      with open(path + "".join(re.findall(r"..jpg", img_src)), 'wb+') as f_img:
          conn = urllib.request.urlopen(img_src)
          f_img.write(conn.read())
      
  • 這個頁面的“下一頁”按鈕是有 class 標(biāo)識的,雖然“上一頁”和“下一頁”按鈕的 class 都是 a1,但是無論當(dāng)前打開哪一頁,這兩個按鈕一直都存在,比如在第一頁按上一頁,還是第一頁的地址,在最后一頁按下一頁,還是最后一頁的地址,又因為 Scrapy 默認(rèn)不會爬取重復(fù)的頁面,所以這里很好編寫代碼。

  • 結(jié)合下載圖片的操作,新創(chuàng)建的 parse_goddess_photo 函數(shù)可以按葫蘆畫瓢寫出:

      def parse_goddess_photo(self, response):
          # NOW U ARE IN PHOTO PAGE!
          # download photo
          album_title = response.css('h1#htilte::text').extract_first()
          album_desc = response.css('div#ddesc::text').extract_first()
          album_info = response.css('div#dinfo span::text').extract_first() + response.css('div#dinfo::text').extract()[1]
          path = 'goddess_photo/' + album_title + '/'
          if not os.path.exists(path):
              os.makedirs(path)
          with open(path + 'album_info.txt', 'a+') as f:
              f.write(album_desc)
              f.write(album_info)
          for img_src in response.css('ul#hgallery img::attr(src)').extract():
              with open(path + "".join(re.findall(r"[0-9]{1,4}.jpg", img_src)), 'wb+') as f_img:
                  f_img.write(urllib.request.urlopen(img_src).read())
                  print("DOWALOADING img_src:" + img_src)
    
          # follow pagination links
          next_page = response.css('a.a1::attr(href)')[1]
          if next_page is not None:
              print("".join(re.findall(r"..html", str(next_page))) + '--> next_page:' + album_title)
              yield response.follow(next_page, self.parse_goddess_photo)
    

最終效果

  • 第一次寫爬蟲,也沒考慮到效率問題,大概花了5個小時才爬取完,在下載圖片時,不同的圖片就放在不同的文件夾里,這樣很好管理。

    20171025-xiaoguo
  • 所有圖片加起來總大小超過7G。

    20171025-goddessspace
  • 其實這篇筆記是我邊擼代碼邊寫的,寫在這里的時候爬蟲還在運(yùn)行,因為之前我沒注意要把 setting.py 中的 ROBOTSTXT_OBEY 的值改成 FALSE,所以在爬取到快結(jié)束時發(fā)現(xiàn)卡住了,后來重新運(yùn)行,查了下原因才改過來的。

  • 重新開始爬取,我是選擇從排行榜第四頁進(jìn)入的,運(yùn)行了挺久的了,還沒爬到新的女神,一直在爬之前爬過的,所以下載的圖片自然沒有增長,因為 Scrapy 默認(rèn)開啟10個線程,所以那些沒有爬過的女神并不是按順序的。

  • 總之,這篇筆記主要是記錄了第一次爬蟲的經(jīng)歷,挺好玩的,也有挺多需要注意的地方,下次想想怎么改進(jìn)爬蟲速度,再學(xué)習(xí)一下應(yīng)對網(wǎng)站反爬蟲的方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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