整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          「Python」教你編寫網絡爬蟲

          .網絡爬蟲何時有用

          假設我有一個鞋店,并且想要及時了解競爭對手的價格。我可以每天訪問他們的網站,與我店鋪中鞋子的價格進行對比。但是,如果我店鋪中的鞋類品種繁多,或是希望能夠更加頻繁地查看價格變化的話,就需要花費大量的時間,甚至難以實現。再舉一個例子,我看中了一雙鞋,想等它促銷時再購買。我可能需要每天訪問這家鞋店的網站來查看這雙鞋是否降價,也許需要等待幾個月的時間,我才能如愿盼到這雙鞋促銷。上述這 兩個重復性的手工流程,都可以利用網絡爬蟲技術實現自動化處理。

          理想狀態下,網絡爬蟲并不是必須品,每個網站都應該提供API,以結構化的格式共享它們的數據。然而現實情況中,雖然一些網站已經提供了這種API,但是它們通常會限制可以抓取的數據,以及訪問這些數據的頻率。另外,對于網站的開發者而言,維護前端界面比維護后端API接口優先級更高??傊覀儾荒軆H僅依賴于API去訪問我們所需的在線數據,而是應該學習一些網絡爬蟲技術的相關知識。

          2. 網絡爬蟲是否合法

          網絡爬蟲目前還處于早期的蠻荒階段,“允許哪些行為”這種基本秩序還處于建設之中。從目前的實踐來看,如果抓取數據的行為用于個人使用,則不存在問題;而如果數據用于轉載,那么抓取的數據類型就非常關鍵了。

          世界各地法院的一些案件可以幫助我們確定哪些網絡爬蟲行為是允許的。在Feist Publications, Inc.起訴Rural Telephone Service Co.的案件中,美國聯邦最高法院裁定抓取并轉載真實數據(比如,電話清單)是允許的。而在澳大利亞,Telstra Corporation Limited起訴Phone Directories Company Pty Ltd這一類似案件中,則裁定只有擁有明確作者的數據,才可以獲得版權。此外,在歐盟的ofir.dk起訴home.dk一案中,最終裁定定期抓取和深度鏈接是允許的。

          這些案件告訴我們,當抓取的數據是現實生活中的真實數據(比如,營業地址、電話清單)時,是允許轉載的。但是,如果是原創數據(比如,意見和評論),通常就會受到版權限制,而不能轉載。

          無論如何,當你抓取某個網站的數據時,請記住自己是該網站的訪客,應當約束自己的抓取行為,否則他們可能會封禁你的IP,甚至采取更進一步的法律行動。這就要求下載請求的速度需要限定在一個合理值之內,并且還需要設定一個專屬的用戶代理來標識自己。在下面的小節中我們將會對這些實踐進行具體介紹。

          關于上述幾個法律案件的更多信息可以參考下述地址:

          • http://caselaw.lp.findlaw.com/scripts/getcase. pl?court=US&vol=499&invol=340
          • http://www.austlii.edu.au/au/cases/cth/FCA/2010/44.html
          • http://www.bvhd.dk/uploads/tx_mocarticles/S_og_Handelsrettens_afg_relse_i_Ofir-sagen.pdf

          3. 背景調研

          在深入討論爬取一個網站之前,我們首先需要對目標站點的規模和結構進行一定程度的了解。網站自身的robots.txt和Sitemap文件都可以為我們提供一定的幫助,此外還有一些能提供更詳細信息的外部工具,比如Google搜索和WHOIS。

          3.1 檢查robots.txt

          大多數網站都會定義robots.txt文件,這樣可以讓爬蟲了解爬取該網站時存在哪些限制。這些限制雖然僅僅作為建議給出,但是良好的網絡公民都應當遵守這些限制。在爬取之前,檢查robots.txt文件這一寶貴資源可以最小化爬蟲被封禁的可能,而且還能發現和網站結構相關的線索。關于robots.txt協議的更多信息可以參見http://www.robotstxt.org。下面的代碼是我們的示例文件robots.txt中的內容,可以訪問http://example.webscraping.com/robots.txt獲取。

              # section 1
              User-agent: BadCrawler
              Disallow: /
          
              # section 2
              User-agent: *
              Crawl-delay: 5
              Disallow: /trap
          
              # section 3
              Sitemap: http://example.webscraping.com/sitemap.xml

          在section 1中,robots.txt文件禁止用戶代理為BadCrawler的爬蟲爬取該網站,不過這種寫法可能無法起到應有的作用,因為惡意爬蟲根本不會遵從robots.txt的要求。本章后面的一個例子將會展示如何讓爬蟲自動遵守robots.txt的要求。

          section 2規定,無論使用哪種用戶代理,都應該在兩次下載請求之間給出5秒的抓取延遲,我們需要遵從該建議以避免服務器過載。這里還有一個/trap鏈接,用于封禁那些爬取了不允許鏈接的惡意爬蟲。如果你訪問了這個鏈接,服務器就會封禁你的IP一分鐘!一個真實的網站可能會對你的IP封禁更長時間,甚至是永久封禁。不過如果這樣設置的話,我們就無法繼續這個例子了。

          section 3定義了一個Sitemap文件,我們將在下一節中了解如何檢查該文件。

          3.2 檢查網站地圖

          網站提供的Sitemap文件(即網站地圖)可以幫助爬蟲定位網站最新的內容,而無須爬取每一個網頁。如果想要了解更多信息,可以從http://www.sitemaps.org/protocol.html獲取網站地圖標準的定義。下面是在robots.txt文件中發現的Sitemap文件的內容。

              <?xml version="1.0" encoding="UTF-8"?>
              <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
                <url><loc>http://example.webscraping.com/view/Afghanistan-1
                  </loc></url>
                <url><loc>http://example.webscraping.com/view/Aland-Islands-2
                 </loc></url>
                <url><loc>http://example.webscraping.com/view/Albania-3</loc>
                  </url>
                ...
              </urlset>

          網站地圖提供了所有網頁的鏈接,我們會在后面的小節中使用這些信息,用于創建我們的第一個爬蟲。雖然Sitemap文件提供了一種爬取網站的有效方式,但是我們仍需對其謹慎處理,因為該文件經常存在缺失、過期或不完整的問題。

          3.3 估算網站大小

          目標網站的大小會影響我們如何進行爬取。如果是像我們的示例站點這樣只有幾百個URL的網站,效率并沒有那么重要;但如果是擁有數百萬個網頁的站點,使用串行下載可能需要持續數月才能完成,這時就需要使用第4章中介紹的分布式下載來解決了。

          估算網站大小的一個簡便方法是檢查Google爬蟲的結果,因為Google很可能已經爬取過我們感興趣的網站。我們可以通過Google搜索的site關鍵詞過濾域名結果,從而獲取該信息。我們可以從http://www.google.com/advanced_search了解到該接口及其他高級搜索參數的用法。

          圖1所示為使用site關鍵詞對我們的示例網站進行搜索的結果,即在Google中搜索site:example.webscraping.com。

          從圖1中可以看出,此時Google估算該網站擁有202個網頁,這和實際情況差不多。不過對于更大型的網站,我們會發現Google的估算并不十分準確。

          在域名后面添加URL路徑,可以對結果進行過濾,僅顯示網站的某些部分。圖2所示為搜索site:example.webscraping.com/view的結果。該搜索條件會限制Google只搜索國家頁面。

          圖1

          圖2

          這種附加的過濾條件非常有用,因為在理想情況下,你只希望爬取網站中包含有用數據的部分,而不是爬取網站的每個頁面。

          3.4 識別網站所用技術

          構建網站所使用的技術類型也會對我們如何爬取產生影響。有一個十分有用的工具可以檢查網站構建的技術類型——builtwith模塊。該模塊的安裝方法如下。

              pip install builtwith

          該模塊將URL作為參數,下載該URL并對其進行分析,然后返回該網站使用的技術。下面是使用該模塊的一個例子。

              >>> import builtwith
              >>> builtwith.parse('http://example.webscraping.com')
              {u'javascript-frameworks': [u'jQuery', u'Modernizr', u'jQuery UI'],
               u'programming-languages': [u'Python'],
               u'web-frameworks': [u'Web2py', u'Twitter Bootstrap'],
               u'web-servers': [u'Nginx']}

          從上面的返回結果中可以看出,示例網站使用了Python的Web2py框架,另外還使用了一些通用的JavaScript庫,因此該網站的內容很有可能是嵌入在HTML中的,相對而言比較容易抓取。而如果改用AngularJS構建該網站,此時的網站內容就很可能是動態加載的。另外,如果網站使用了ASP.NET,那么在爬取網頁時,就必須要用到會話管理和表單提交了。

          3.5 尋找網站所有者

          對于一些網站,我們可能會關心其所有者是誰。比如,我們已知網站的所有者會封禁網絡爬蟲,那么我們最好把下載速度控制得更加保守一些。為了找到網站的所有者,我們可以使用WHOIS協議查詢域名的注冊者是誰。Python中有一個針對該協議的封裝庫,其文檔地址為https://pypi.python.org/pypi/python-whois,我們可以通過pip進行安裝。

              pip install python-whois

          下面是使用該模塊對appspot.com這個域名進行WHOIS查詢時的返回結果。

              >>> import whois
              >>> print whois.whois('appspot.com')
              {
                ...
                "name_servers": [
                  "NS1.GOOGLE.COM",
                  "NS2.GOOGLE.COM",
                  "NS3.GOOGLE.COM",
                  "NS4.GOOGLE.COM",
                  "ns4.google.com",
                  "ns2.google.com",
                  "ns1.google.com",
                  "ns3.google.com"
                ],
              "org": "Google Inc.",
              "emails": [
                  "abusecomplaints@markmonitor.com",
                  "dns-admin@google.com"
                ]
              }

          從結果中可以看出該域名歸屬于Google,實際上也確實如此。該域名是用于Google App Engine服務的。當我們爬取該域名時就需要十分小心,因為Google經常會阻斷網絡爬蟲,盡管實際上其自身就是一個網絡爬蟲業務。

          4. 編寫第一個網絡爬蟲

          為了抓取網站,我們首先需要下載包含有感興趣數據的網頁,該過程一般被稱為爬取(crawling)。爬取一個網站有很多種方法,而選用哪種方法更加合適,則取決于目標網站的結構。我們首先會探討如何安全地下載網頁,然后會介紹如下3種爬取網站的常見方法:

          • 爬取網站地圖;
          • 遍歷每個網頁的數據庫ID;
          • 跟蹤網頁鏈接。

          4.1 下載網頁

          要想爬取網頁,我們首先需要將其下載下來。下面的示例腳本使用Python的urllib2模塊下載URL。

              import urllib2
              def download(url):
                  return urllib2.urlopen(url).read()

          當傳入URL參數時,該函數將會下載網頁并返回其HTML。不過,這個代碼片段存在一個問題,即當下載網頁時,我們可能會遇到一些無法控制的錯誤,比如請求的頁面可能不存在。此時,urllib2會拋出異常,然后退出腳本。安全起見,下面再給出一個更健壯的版本,可以捕獲這些異常。

              import urllib2
          
              def download(url):
                  print 'Downloading:', url
                  try:
                      html = urllib2.urlopen(url).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                  return html

          現在,當出現下載錯誤時,該函數能夠捕獲到異常,然后返回None。

          1.重試下載

          下載時遇到的錯誤經常是臨時性的,比如服務器過載時返回的503 Service Unavailable錯誤。對于此類錯誤,我們可以嘗試重新下載,因為這個服務器問題現在可能已解決。不過,我們不需要對所有錯誤都嘗試重新下載。如果服務器返回的是404 Not Found這種錯誤,則說明該網頁目前并不存在,再次嘗試同樣的請求一般也不會出現不同的結果。

          互聯網工程任務組(Internet Engineering Task Force)定義了HTTP錯誤的完整列表,詳情可參考https://tools.ietf.org/html/rfc7231#section-6。從該文檔中,我們可以了解到4xx錯誤發生在請求存在問題時,而5xx錯誤則發生在服務端存在問題時。所以,我們只需要確保download函數在發生5xx錯誤時重試下載即可。下面是支持重試下載功能的新版本 代碼。

              def download(url, num_retries=2):
                  print 'Downloading:', url
                  try:
                      html = urllib2.urlopen(url).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                      if num_retries > 0:
                          if hasattr(e, 'code') and 500 <= e.code < 600:
                              # recursively retry 5xx HTTP errors
                              return download(url, num_retries-1)
                  return html

          現在,當download函數遇到5xx錯誤碼時,將會遞歸調用函數自身進行重試。此外,該函數還增加了一個參數,用于設定重試下載的次數,其默認值為兩次。我們在這里限制網頁下載的嘗試次數,是因為服務器錯誤可能暫時還沒有解決。想要測試該函數,可以嘗試下載http://httpstat.us/500,該網址會始終返回500錯誤碼。

          >>> download('http://httpstat.us/500')
          Downloading: http://httpstat.us/500
          Download error: Internal Server Error
          Downloading: http://httpstat.us/500
          Download error: Internal Server Error
          Downloading: http://httpstat.us/500
          Download error: Internal Server Error

          從上面的返回結果可以看出,download函數的行為和預期一致,先嘗試下載網頁,在接收到500錯誤后,又進行了兩次重試才放棄。

          2.設置用戶代理

          默認情況下,urllib2使用Python-urllib/2.7作為用戶代理下載網頁內容,其中2.7是Python的版本號。如果能使用可辨識的用戶代理則更好,這樣可以避免我們的網絡爬蟲碰到一些問題。此外,也許是因為曾經歷過質量不佳的Python網絡爬蟲造成的服務器過載,一些網站還會封禁這個默認的用戶代理。比如,在使用Python默認用戶代理的情況下,訪問http://www.meetup.com/,目前會返回如圖3所示的訪問拒絕提示。


          圖3

          因此,為了下載更加可靠,我們需要控制用戶代理的設定。下面的代碼對download函數進行了修改,設定了一個默認的用戶代理“wswp”(即Web Scraping with Python的首字母縮寫)。

              def download(url, user_agent='wswp', num_retries=2):
                  print 'Downloading:', url
                  headers = {'User-agent': user_agent}
                  request = urllib2.Request(url, headers=headers)
                  try:
                      html = urllib2.urlopen(request).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                      if num_retries > 0:
                          if hasattr(e, 'code') and 500 <= e.code < 600:
                              # retry 5XX HTTP errors
                              return download(url, user_agent, num_retries-1)
                  return html

          現在,我們擁有了一個靈活的下載函數,可以在后續示例中得到復用。該函數能夠捕獲異常、重試下載并設置用戶代理。

          4.2 網站地圖爬蟲

          在第一個簡單的爬蟲中,我們將使用示例網站robots.txt文件中發現的網站地圖來下載所有網頁。為了解析網站地圖,我們將會使用一個簡單的正則表達式,從<loc>標簽中提取出URL。下面是該示例爬蟲的代碼。

              def crawl_sitemap(url):
                  # download the sitemap file
                  sitemap = download(url)
                  # extract the sitemap links
                  links = re.findall('<loc>(.*?)</loc>', sitemap)
                  # download each link
                  for link in links:
                      html = download(link)
                      # scrape html here
                      # ...

          現在,運行網站地圖爬蟲,從示例網站中下載所有國家頁面。

          >>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')
          Downloading: http://example.webscraping.com/sitemap.xml
          Downloading: http://example.webscraping.com/view/Afghanistan-1
          Downloading: http://example.webscraping.com/view/Aland-Islands-2
          Downloading: http://example.webscraping.com/view/Albania-3
          ...

          可以看出,上述運行結果和我們的預期一致,不過正如前文所述,我們無法依靠Sitemap文件提供每個網頁的鏈接。下面我們將會介紹另一個簡單的爬蟲,該爬蟲不再依賴于Sitemap文件。

          4.3 ID遍歷爬蟲

          本節中,我們將利用網站結構的弱點,更加輕松地訪問所有內容。下面是一些示例國家的URL。

          • http://example.webscraping.com/view/Afghanistan-1
          • http://example.webscraping.com/view/Australia-2
          • http://example.webscraping.com/view/Brazil-3

          可以看出,這些URL只在結尾處有所區別,包括國家名(作為頁面別名)和ID。在URL中包含頁面別名是非常普遍的做法,可以對搜索引擎優化起到幫助作用。一般情況下,Web服務器會忽略這個字符串,只使用ID來匹配數據庫中的相關記錄。下面我們將其移除,加載http://example.webscraping.com/view/1,測試示例網站中的鏈接是否仍然可用。測試結果如圖4所示。

          圖4

          從圖4中可以看出,網頁依然可以加載成功,也就是說該方法是有用的?,F在,我們就可以忽略頁面別名,只遍歷ID來下載所有國家的頁面。下面是使用了該技巧的代碼片段。

              import itertools
              for page in itertools.count(1):
                  url = 'http://example.webscraping.com/view/-%d' % page
                  html = download(url)
                  if html is None:
                      break
                  else:
                      # success - can scrape the result
                      pass

          在這段代碼中,我們對ID進行遍歷,直到出現下載錯誤時停止,我們假設此時已到達最后一個國家的頁面。不過,這種實現方式存在一個缺陷,那就是某些記錄可能已被刪除,數據庫ID之間并不是連續的。此時,只要訪問到某個間隔點,爬蟲就會立即退出。下面是這段代碼的改進版本,在該版本中連續發生多次下載錯誤后才會退出程序。

              # maximum number of consecutive download errors allowed
              max_errors = 5
              # current number of consecutive download errors
              num_errors = 0
              for page in itertools.count(1):
                  url = 'http://example.webscraping.com/view/-%d' % page
                  html = download(url)
                  if html is None:
                      # received an error trying to download this webpage
                      num_errors += 1
                      if num_errors == max_errors:
                          # reached maximum number of
                          # consecutive errors so exit
                          break
                  else:
                      # success - can scrape the result
                      # ...
                      num_errors = 0

          上面代碼中實現的爬蟲需要連續5次下載錯誤才會停止遍歷,這樣就很大程度上降低了遇到被刪除記錄時過早停止遍歷的風險。

          在爬取網站時,遍歷ID是一個很便捷的方法,但是和網站地圖爬蟲一樣,這種方法也無法保證始終可用。比如,一些網站會檢查頁面別名是否滿足預期,如果不是,則會返回404 Not Found錯誤。而另一些網站則會使用非連續大數作為ID,或是不使用數值作為ID,此時遍歷就難以發揮其作用了。例如,Amazon使用ISBN作為圖書ID,這種編碼包含至少10位數字。使用ID對Amazon的圖書進行遍歷需要測試數十億次,因此這種方法肯定不是抓取該站內容最高效的方法。

          4.4 鏈接爬蟲

          到目前為止,我們已經利用示例網站的結構特點實現了兩個簡單爬蟲,用于下載所有的國家頁面。只要這兩種技術可用,就應當使用其進行爬取,因為這兩種方法最小化了需要下載的網頁數量。不過,對于另一些網站,我們需要讓爬蟲表現得更像普通用戶,跟蹤鏈接,訪問感興趣的內容。

          通過跟蹤所有鏈接的方式,我們可以很容易地下載整個網站的頁面。但是,這種方法會下載大量我們并不需要的網頁。例如,我們想要從一個在線論壇中抓取用戶賬號詳情頁,那么此時我們只需要下載賬號頁,而不需要下載討論貼的頁面。本文中的鏈接爬蟲將使用正則表達式來確定需要下載哪些頁面。下面是這段代碼的初始版本。

              import re
          
              def link_crawler(seed_url, link_regex):
                  """Crawl from the given seed URL following links matched by link_regex
                  """
                  crawl_queue = [seed_url]
                  while crawl_queue:
                      url = crawl_queue.pop()
                      html = download(url)
                      # filter for links matching our regular expression
                      for link in get_links(html):
                          if re.match(link_regex, link):
                              crawl_queue.append(link)
          
              def get_links(html):
                  """Return a list of links from html
                  """
                  # a regular expression to extract all links from the webpage
                  webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']',
                      re.IGNORECASE)
                  # list of all links from the webpage
                  return webpage_regex.findall(html)

          要運行這段代碼,只需要調用link_crawler函數,并傳入兩個參數:要爬取的網站URL和用于跟蹤鏈接的正則表達式。對于示例網站,我們想要爬取的是國家列表索引頁和國家頁面。其中,索引頁鏈接格式如下。

          • http://example.webscraping.com/index/1
          • http://example.webscraping.com/index/2

          國家頁鏈接格式如下。

          • http://example.webscraping.com/view/Afghanistan-1
          • http://example.webscraping.com/view/Aland-Islands-2

          因此,我們可以用/(index|view)/這個簡單的正則表達式來匹配這兩類網頁。當爬蟲使用這些輸入參數運行時會發生什么呢?你會發現我們得到了如下的下載錯誤。

              >>> link_crawler('http://example.webscraping.com',
                  '/(index|view)')
              Downloading: http://example.webscraping.com
              Downloading: /index/1
              Traceback (most recent call last):
                  ...
              ValueError: unknown url type: /index/1

          可以看出,問題出在下載/index/1時,該鏈接只有網頁的路徑部分,而沒有協議和服務器部分,也就是說這是一個相對鏈接。由于瀏覽器知道你正在瀏覽哪個網頁,所以在瀏覽器瀏覽時,相對鏈接是能夠正常工作的。但是,urllib2是無法獲知上下文的。為了讓urllib2能夠定位網頁,我們需要將鏈接轉換為絕對鏈接的形式,以便包含定位網頁的所有細節。如你所愿,Python中確實有用來實現這一功能的模塊,該模塊稱為urlparse。下面是link_crawler的改進版本,使用了urlparse模塊來創建絕對路徑。

              import urlparse
              def link_crawler(seed_url, link_regex):
                  """Crawl from the given seed URL following links matched by link_regex
                  """
                  crawl_queue = [seed_url]
                  while crawl_queue:
                      url = crawl_queue.pop()
                      html = download(url)
                      for link in get_links(html):
                          if re.match(link_regex, link):
                              link = urlparse.urljoin(seed_url, link)
                              crawl_queue.append(link)

          當你運行這段代碼時,會發現雖然網頁下載沒有出現錯誤,但是同樣的地點總是會被不斷下載到。這是因為這些地點相互之間存在鏈接。比如,澳大利亞鏈接到了南極洲,而南極洲也存在到澳大利亞的鏈接,此時爬蟲就會在它們之間不斷循環下去。要想避免重復爬取相同的鏈接,我們需要記錄哪些鏈接已經被爬取過。下面是修改后的link_crawler函數,已具備存儲已發現URL的功能,可以避免重復下載。

              def link_crawler(seed_url, link_regex):
                  crawl_queue = [seed_url]
                  # keep track which URL's have seen before
                  seen = set(crawl_queue)
                  while crawl_queue:
                      url = crawl_queue.pop()
                      html = download(url)
                      for link in get_links(html):
                          # check if link matches expected regex
                          if re.match(link_regex, link):
                              # form absolute link
                              link = urlparse.urljoin(seed_url, link)
                              # check if have already seen this link
                              if link not in seen:
                                  seen.add(link)
                                  crawl_queue.append(link)

          當運行該腳本時,它會爬取所有地點,并且能夠如期停止。最終,我們得到了一個可用的爬蟲!

          高級功能

          現在,讓我們為鏈接爬蟲添加一些功能,使其在爬取其他網站時更加有用。

          解析robots.txt

          首先,我們需要解析robots.txt文件,以避免下載禁止爬取的URL。使用Python自帶的robotparser模塊,就可以輕松完成這項工作,如下面的代碼所示。

          >>> import robotparser
          >>> rp = robotparser.RobotFileParser()
          >>> rp.set_url('http://example.webscraping.com/robots.txt')
          >>> rp.read()
          >>> url = 'http://example.webscraping.com'
          >>> user_agent = 'BadCrawler'
          >>> rp.can_fetch(user_agent, url)
          False
          >>> user_agent = 'GoodCrawler'
          >>> rp.can_fetch(user_agent, url)
          True

          robotparser模塊首先加載robots.txt文件,然后通過can_fetch()函數確定指定的用戶代理是否允許訪問網頁。在本例中,當用戶代理設置為 BadCrawler 時,robotparser模塊會返回結果表明無法獲取網頁,這和示例網站robots.txt的定義一樣。

          為了將該功能集成到爬蟲中,我們需要在crawl循環中添加該檢查。

              ...
              while crawl_queue:
                  url = crawl_queue.pop()
                  # check url passes robots.txt restrictions
                  if rp.can_fetch(user_agent, url):
                      ...
                  else:
                      print 'Blocked by robots.txt:', url
          支持代理

          有時我們需要使用代理訪問某個網站。比如,Netflix屏蔽了美國以外的大多數國家。使用urllib2支持代理并沒有想象中那么容易(可以嘗試使用更友好的Python HTTP模塊requests來實現該功能,其文檔地址為http://docs.python-requests.org/)。下面是使用urllib2支持代理的代碼。

              proxy = ...
              opener = urllib2.build_opener()
              proxy_params = {urlparse.urlparse(url).scheme: proxy}
              opener.add_handler(urllib2.ProxyHandler(proxy_params))
              response = opener.open(request)

          下面是集成了該功能的新版本download函數。

              def download(url, user_agent='wswp', proxy=None, num_retries=2):
                  print 'Downloading:', url
                  headers = {'User-agent': user_agent}
                  request = urllib2.Request(url, headers=headers)
          
                  opener = urllib2.build_opener()
                  if proxy:
                      proxy_params = {urlparse.urlparse(url).scheme: proxy}
                      opener.add_handler(urllib2.ProxyHandler(proxy_params))
                  try:
                      html = opener.open(request).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                      if num_retries > 0:
                          if hasattr(e, 'code') and 500 <= e.code < 600:
                          # retry 5XX HTTP errors
                          html = download(url, user_agent, proxy,
                              num_retries-1)
                  return html
          下載限速

          如果我們爬取網站的速度過快,就會面臨被封禁或是造成服務器過載的風險。為了降低這些風險,我們可以在兩次下載之間添加延時,從而對爬蟲限速。下面是實現了該功能的類的代碼。

              class Throttle:
                  """Add a delay between downloads to the same domain
                  """
                  def __init__(self, delay):
                      # amount of delay between downloads for each domain
                      self.delay = delay
                      # timestamp of when a domain was last accessed
                      self.domains = {}
          
                  def wait(self, url):
                      domain = urlparse.urlparse(url).netloc
                      last_accessed = self.domains.get(domain)
          
                      if self.delay > 0 and last_accessed is not None:
                          sleep_secs = self.delay - (datetime.datetime.now() -
                              last_accessed).seconds
                          if sleep_secs > 0:
                              # domain has been accessed recently
                              # so need to sleep
                              time.sleep(sleep_secs)
                      # update the last accessed time
                      self.domains[domain] = datetime.datetime.now()

          Throttle類記錄了每個域名上次訪問的時間,如果當前時間距離上次訪問時間小于指定延時,則執行睡眠操作。我們可以在每次下載之前調用Throttle對爬蟲進行限速。

              throttle = Throttle(delay)
              ...
              throttle.wait(url)
              result = download(url, headers, proxy=proxy,
                  num_retries=num_retries)
          避免爬蟲陷阱

          目前,我們的爬蟲會跟蹤所有之前沒有訪問過的鏈接。但是,一些網站會動態生成頁面內容,這樣就會出現無限多的網頁。比如,網站有一個在線日歷功能,提供了可以訪問下個月和下一年的鏈接,那么下個月的頁面中同樣會包含訪問再下個月的鏈接,這樣頁面就會無止境地鏈接下去。這種情況被稱為爬蟲陷阱。

          想要避免陷入爬蟲陷阱,一個簡單的方法是記錄到達當前網頁經過了多少個鏈接,也就是深度。當到達最大深度時,爬蟲就不再向隊列中添加該網頁中的鏈接了。要實現這一功能,我們需要修改seen變量。該變量原先只記錄訪問過的網頁鏈接,現在修改為一個字典,增加了頁面深度的記錄。

              def link_crawler(..., max_depth=2):
                  max_depth = 2
                  seen = {}
                  ...
                  depth = seen[url]
                  if depth != max_depth:
                      for link in links:
                          if link not in seen:
                              seen[link] = depth + 1
                              crawl_queue.append(link)

          現在有了這一功能,我們就有信心爬蟲最終一定能夠完成。如果想要禁用該功能,只需將max_depth設為一個負數即可,此時當前深度永遠不會與之相等。

          最終版本

          這個高級鏈接爬蟲的完整源代碼可以在https://bitbucket.org/ wswp/code/src/tip/chapter01/link_crawler3.py下載得到。要測試這段代碼,我們可以將用戶代理設置為BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那個用戶代理。從下面的運行結果中可以看出,爬蟲果然被屏蔽了,代碼啟動后馬上就會結束。

          >>> seed_url = 'http://example.webscraping.com/index'
          >>> link_regex = '/(index|view)'
          >>> link_crawler(seed_url, link_regex, user_agent='BadCrawler')
          Blocked by robots.txt: http://example.webscraping.com/

          現在,讓我們使用默認的用戶代理,并將最大深度設置為1,這樣只有主頁上的鏈接才會被下載。

          >>> link_crawler(seed_url, link_regex, max_depth=1)
          Downloading: http://example.webscraping.com//index
          Downloading: http://example.webscraping.com/index/1
          Downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10
          Downloading: http://example.webscraping.com/view/Antarctica-9
          Downloading: http://example.webscraping.com/view/Anguilla-8
          Downloading: http://example.webscraping.com/view/Angola-7
          Downloading: http://example.webscraping.com/view/Andorra-6
          Downloading: http://example.webscraping.com/view/American-Samoa-5
          Downloading: http://example.webscraping.com/view/Algeria-4
          Downloading: http://example.webscraping.com/view/Albania-3
          Downloading: http://example.webscraping.com/view/Aland-Islands-2
          Downloading: http://example.webscraping.com/view/Afghanistan-1

          和預期一樣,爬蟲在下載完國家列表的第一頁之后就停止了。

          本文節選自《用Python寫網絡爬蟲》

          本書講解了如何使用Python來編寫網絡爬蟲程序,內容包括網絡爬蟲簡介,從頁面中抓取數據的三種方法,提取緩存中的數據,使用多個線程和進程來進行并發抓取,如何抓取動態頁面中的內容,與表單進行交互,處理頁面中的驗證碼問題,以及使用Scarpy和Portia來進行數據抓取,并在最后使用本書介紹的數據抓取技術對幾個真實的網站進行了抓取,旨在幫助讀者活學活用書中介紹的技術。

          本書適合有一定Python編程經驗,而且對爬蟲技術感興趣的讀者閱讀。

          讀:本文主要分為兩個部分:一部分是網絡爬蟲的概述,幫助大家詳細了解網絡爬蟲;另一部分是HTTP請求的Python實現,幫助大家了解Python中實現HTTP請求的各種方式,以便具備編寫HTTP網絡程序的能力。

          作者:范傳輝

          如需轉載請聯系華章科技

          01 網絡爬蟲概述

          接下來從網絡爬蟲的概念、用處與價值和結構等三個方面,讓大家對網絡爬蟲有一個基本的了解。

          1. 網絡爬蟲及其應用

          隨著網絡的迅速發展,萬維網成為大量信息的載體,如何有效地提取并利用這些信息成為一個巨大的挑戰,網絡爬蟲應運而生。網絡爬蟲(又被稱為網頁蜘蛛、網絡機器人),是一種按照一定的規則,自動地抓取萬維網信息的程序或者腳本。下面通過圖3-1展示一下網絡爬蟲在互聯網中起到的作用:

          ▲圖3-1 網絡爬蟲

          網絡爬蟲按照系統結構和實現技術,大致可以分為以下幾種類型:通用網絡爬蟲、聚焦網絡爬蟲、增量式網絡爬蟲、深層網絡爬蟲。實際的網絡爬蟲系統通常是幾種爬蟲技術相結合實現的。

          搜索引擎(Search Engine),例如傳統的通用搜索引擎baidu、Yahoo和Google等,是一種大型復雜的網絡爬蟲,屬于通用性網絡爬蟲的范疇。但是通用性搜索引擎存在著一定的局限性:

          1. 不同領域、不同背景的用戶往往具有不同的檢索目的和需求,通用搜索引擎所返回的結果包含大量用戶不關心的網頁。
          2. 通用搜索引擎的目標是盡可能大的網絡覆蓋率,有限的搜索引擎服務器資源與無限的網絡數據資源之間的矛盾將進一步加深。
          3. 萬維網數據形式的豐富和網絡技術的不斷發展,圖片、數據庫、音頻、視頻多媒體等不同數據大量出現,通用搜索引擎往往對這些信息含量密集且具有一定結構的數據無能為力,不能很好地發現和獲取。
          4. 通用搜索引擎大多提供基于關鍵字的檢索,難以支持根據語義信息提出的查詢。

          為了解決上述問題,定向抓取相關網頁資源的聚焦爬蟲應運而生。

          聚焦爬蟲是一個自動下載網頁的程序,它根據既定的抓取目標,有選擇地訪問萬維網上的網頁與相關的鏈接,獲取所需要的信息。與通用爬蟲不同,聚焦爬蟲并不追求大的覆蓋,而將目標定為抓取與某一特定主題內容相關的網頁,為面向主題的用戶查詢準備數據資源。

          說完了聚焦爬蟲,接下來再說一下增量式網絡爬蟲。增量式網絡爬蟲是指對已下載網頁采取增量式更新和只爬行新產生的或者已經發生變化網頁的爬蟲,它能夠在一定程度上保證所爬行的頁面是盡可能新的頁面。

          和周期性爬行和刷新頁面的網絡爬蟲相比,增量式爬蟲只會在需要的時候爬行新產生或發生更新的頁面,并不重新下載沒有發生變化的頁面,可有效減少數據下載量,及時更新已爬行的網頁,減小時間和空間上的耗費,但是增加了爬行算法的復雜度和實現難度。

          例如:想獲取趕集網的招聘信息,以前爬取過的數據沒有必要重復爬取,只需要獲取更新的招聘數據,這時候就要用到增量式爬蟲。

          最后說一下深層網絡爬蟲。Web頁面按存在方式可以分為表層網頁和深層網頁。表層網頁是指傳統搜索引擎可以索引的頁面,以超鏈接可以到達的靜態網頁為主構成的Web頁面。深層網絡是那些大部分內容不能通過靜態鏈接獲取的、隱藏在搜索表單后的,只有用戶提交一些關鍵詞才能獲得的Web頁面。

          例如用戶登錄或者注冊才能訪問的頁面??梢韵胂筮@樣一個場景:爬取貼吧或者論壇中的數據,必須在用戶登錄后,有權限的情況下才能獲取完整的數據。

          2. 網絡爬蟲結構

          下面用一個通用的網絡爬蟲結構來說明網絡爬蟲的基本工作流程,如圖3-4所示。

          ▲圖3-4 網絡爬蟲結構

          網絡爬蟲的基本工作流程如下:

          1. 首先選取一部分精心挑選的種子URL。
          2. 將這些URL放入待抓取URL隊列。
          3. 從待抓取URL隊列中讀取待抓取隊列的URL,解析DNS,并且得到主機的IP,并將URL對應的網頁下載下來,存儲進已下載網頁庫中。此外,將這些URL放進已抓取URL隊列。
          4. 分析已抓取URL隊列中的URL,從已下載的網頁數據中分析出其他URL,并和已抓取的URL進行比較去重,最后將去重過的URL放入待抓取URL隊列,從而進入下一個循環。

          02 HTTP請求的Python實現

          通過上面的網絡爬蟲結構,我們可以看到讀取URL、下載網頁是每一個爬蟲必備而且關鍵的功能,這就需要和HTTP請求打交道。接下來講解Python中實現HTTP請求的三種方式:urllib2/urllib、httplib/urllib以及Requests。

          1. urllib2/urllib實現

          urllib2和urllib是Python中的兩個內置模塊,要實現HTTP功能,實現方式是以urllib2為主,urllib為輔。

          1.1 首先實現一個完整的請求與響應模型

          urllib2提供一個基礎函數urlopen,通過向指定的URL發出請求來獲取數據。最簡單的形式是:

          import urllib2
          response=urllib2.urlopen('http://www.zhihu.com')
          html=response.read()
          print html
          

          其實可以將上面對http://www.zhihu.com的請求響應分為兩步,一步是請求,一步是響應,形式如下:

          import urllib2
          # 請求
          request=urllib2.Request('http://www.zhihu.com')
          # 響應
          response = urllib2.urlopen(request)
          html=response.read()
          print html
          

          上面這兩種形式都是GET請求,接下來演示一下POST請求,其實大同小異,只是增加了請求數據,這時候用到了urllib。示例如下:

          import urllib
          import urllib2
          url = 'http://www.xxxxxx.com/login'
          postdata = {'username' : 'qiye',
           'password' : 'qiye_pass'}
          # info 需要被編碼為urllib2能理解的格式,這里用到的是urllib
          data = urllib.urlencode(postdata)
          req = urllib2.Request(url, data)
          response = urllib2.urlopen(req)
          html = response.read()
          

          但是有時會出現這種情況:即使POST請求的數據是對的,但是服務器拒絕你的訪問。這是為什么呢?問題出在請求中的頭信息,服務器會檢驗請求頭,來判斷是否是來自瀏覽器的訪問,這也是反爬蟲的常用手段。

          1.2 請求頭headers處理

          將上面的例子改寫一下,加上請求頭信息,設置一下請求頭中的User-Agent域和Referer域信息。

          import urllib
          import urllib2
          url = 'http://www.xxxxxx.com/login'
          user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
          referer='http://www.xxxxxx.com/'
          postdata = {'username' : 'qiye',
           'password' : 'qiye_pass'}
          # 將user_agent,referer寫入頭信息
          headers={'User-Agent':user_agent,'Referer':referer}
          data = urllib.urlencode(postdata)
          req = urllib2.Request(url, data,headers)
          response = urllib2.urlopen(req)
          html = response.read()
          

          也可以這樣寫,使用add_header來添加請求頭信息,修改如下:

          import urllib
          import urllib2
          url = 'http://www.xxxxxx.com/login'
          user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
          referer='http://www.xxxxxx.com/'
          postdata = {'username' : 'qiye',
           'password' : 'qiye_pass'}
          data = urllib.urlencode(postdata)
          req = urllib2.Request(url)
          # 將user_agent,referer寫入頭信息
          req.add_header('User-Agent',user_agent)
          req.add_header('Referer',referer)
          req.add_data(data)
          response = urllib2.urlopen(req)
          html = response.read()
          

          對有些header要特別留意,服務器會針對這些header做檢查,例如:

          • User-Agent:有些服務器或Proxy會通過該值來判斷是否是瀏覽器發出的請求。
          • Content-Type:在使用REST接口時,服務器會檢查該值,用來確定HTTP Body中的內容該怎樣解析。在使用服務器提供的RESTful或SOAP服務時,Content-Type設置錯誤會導致服務器拒絕服務。常見的取值有:application/xml(在XML RPC,如RESTful/SOAP調用時使用)、application/json(在JSON RPC調用時使用)、application/x-www-form-urlencoded(瀏覽器提交Web表單時使用)。
          • Referer:服務器有時候會檢查防盜鏈。

          1.3 Cookie處理

          urllib2對Cookie的處理也是自動的,使用CookieJar函數進行Cookie的管理。如果需要得到某個Cookie項的值,可以這么做:

          import urllib2
          import cookielib
          cookie = cookielib.CookieJar()
          opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
          response = opener.open('http://www.zhihu.com')
          for item in cookie:
           print item.name+':'+item.value
          

          但是有時候會遇到這種情況,我們不想讓urllib2自動處理,我們想自己添加Cookie的內容,可以通過設置請求頭中的Cookie域來做:

          import urllib2
          opener = urllib2.build_opener()
          opener.addheaders.append( ( 'Cookie', 'email=' + "xxxxxxx@163.com" ) )
          req = urllib2.Request( "http://www.zhihu.com/" )
          response = opener.open(req)
          print response.headers
          retdata = response.read()
          

          1.4 Timeout設置超時

          在Python2.6之前的版本,urllib2的API并沒有暴露Timeout的設置,要設置Timeout值,只能更改Socket的全局Timeout值。示例如下:

          import urllib2
          import socket
          socket.setdefaulttimeout(10) # 10 秒鐘后超時
          urllib2.socket.setdefaulttimeout(10) # 另一種方式
          

          在Python2.6及新的版本中,urlopen函數提供了對Timeout的設置,示例如下:

          import urllib2
          request=urllib2.Request('http://www.zhihu.com')
          response = urllib2.urlopen(request,timeout=2)
          html=response.read()
          print html
          

          1.5 獲取HTTP響應碼

          對于200 OK來說,只要使用urlopen返回的response對象的getcode()方法就可以得到HTTP的返回碼。但對其他返回碼來說,urlopen會拋出異常。這時候,就要檢查異常對象的code屬性了,示例如下:

          import urllib2
          try:
           response = urllib2.urlopen('http://www.google.com')
           print response
          except urllib2.HTTPError as e:
           if hasattr(e, 'code'):
           print 'Error code:',e.code
          

          1.6 重定向

          urllib2默認情況下會針對HTTP 3XX返回碼自動進行重定向動作。要檢測是否發生了重定向動作,只要檢查一下Response的URL和Request的URL是否一致就可以了,示例如下:

          import urllib2
          response = urllib2.urlopen('http://www.zhihu.cn')
          isRedirected = response.geturl() == 'http://www.zhihu.cn'
          

          如果不想自動重定向,可以自定義HTTPRedirectHandler類,示例如下:

          import urllib2
          class RedirectHandler(urllib2.HTTPRedirectHandler):
           def http_error_301(self, req, fp, code, msg, headers):
           pass
           def http_error_302(self, req, fp, code, msg, headers):
           result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, 
           msg, headers)
           result.status = code
           result.newurl = result.geturl()
           return result
          opener = urllib2.build_opener(RedirectHandler)
          opener.open('http://www.zhihu.cn')
          

          1.7 Proxy的設置

          在做爬蟲開發中,必不可少地會用到代理。urllib2默認會使用環境變量http_proxy來設置HTTP Proxy。但是我們一般不采用這種方式,而是使用ProxyHandler在程序中動態設置代理,示例代碼如下:

          import urllib2
          proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
          opener = urllib2.build_opener([proxy,])
          urllib2.install_opener(opener)
          response = urllib2.urlopen('http://www.zhihu.com/')
          print response.read()
          

          這里要注意的一個細節,使用urllib2.install_opener()會設置urllib2的全局opener,之后所有的HTTP訪問都會使用這個代理。這樣使用會很方便,但不能做更細粒度的控制,比如想在程序中使用兩個不同的Proxy設置,這種場景在爬蟲中很常見。比較好的做法是不使用install_opener去更改全局的設置,而只是直接調用opener的open方法代替全局的urlopen方法,修改如下:

          import urllib2
          proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
          opener = urllib2.build_opener(proxy,)
          response = opener.open("http://www.zhihu.com/")
          print response.read()
          

          2. httplib/urllib實現

          httplib模塊是一個底層基礎模塊,可以看到建立HTTP請求的每一步,但是實現的功能比較少,正常情況下比較少用到。在Python爬蟲開發中基本上用不到,所以在此只是進行一下知識普及。下面介紹一下常用的對象和函數:

          • 創建HTTPConnection對象:
          • class httplib.HTTPConnection(host[, port[, strict[, timeout[, source_address]]]])。
          • 發送請求:
          • HTTPConnection.request(method, url[, body[, headers]])。
          • 獲得響應:
          • HTTPConnection.getresponse()。
          • 讀取響應信息:
          • HTTPResponse.read([amt])。
          • 獲得指定頭信息:
          • HTTPResponse.getheader(name[, default])。
          • 獲得響應頭(header, value)元組的列表:
          • HTTPResponse.getheaders()。
          • 獲得底層socket文件描述符:
          • HTTPResponse.fileno()。
          • 獲得頭內容:
          • HTTPResponse.msg。
          • 獲得頭http版本:
          • HTTPResponse.version。
          • 獲得返回狀態碼:
          • HTTPResponse.status。
          • 獲得返回說明:
          • HTTPResponse.reason。

          接下來演示一下GET請求和POST請求的發送,首先是GET請求的示例,如下所示:

          import httplib
          conn =None
          try:
           conn = httplib.HTTPConnection("www.zhihu.com")
           conn.request("GET", "/")
           response = conn.getresponse()
           print response.status, response.reason
           print '-' * 40
           headers = response.getheaders()
           for h in headers:
           print h
           print '-' * 40
           print response.msg
          except Exception,e:
           print e
          finally:
           if conn:
           conn.close()
          

          POST請求的示例如下:

          import httplib, urllib
          conn = None
          try:
           params = urllib.urlencode({'name': 'qiye', 'age': 22})
           headers = {"Content-type": "application/x-www-form-urlencoded"
           , "Accept": "text/plain"}
           conn = httplib.HTTPConnection("www.zhihu.com", 80, timeout=3)
           conn.request("POST", "/login", params, headers)
           response = conn.getresponse()
           print response.getheaders() # 獲取頭信息
           print response.status
           print response.read()
          except Exception, e:
           print e
           finally:
           if conn:
           conn.close()
          

          3. 更人性化的Requests

          Python中Requests實現HTTP請求的方式,是本人極力推薦的,也是在Python爬蟲開發中最為常用的方式。Requests實現HTTP請求非常簡單,操作更加人性化。

          Requests庫是第三方模塊,需要額外進行安裝。Requests是一個開源庫,源碼位于:

          GitHub: https://github.com/kennethreitz/requests

          希望大家多多支持作者。

          使用Requests庫需要先進行安裝,一般有兩種安裝方式:

          • 使用pip進行安裝,安裝命令為:pip install requests,不過可能不是最新版。
          • 直接到GitHub上下載Requests的源代碼,下載鏈接為:
          • https://github.com/kennethreitz/requests/releases
          • 將源代碼壓縮包進行解壓,然后進入解壓后的文件夾,運行setup.py文件即可。

          如何驗證Requests模塊安裝是否成功呢?在Python的shell中輸入import requests,如果不報錯,則是安裝成功。如圖3-5所示。

          ▲圖3-5 驗證Requests安裝

          3.1 首先還是實現一個完整的請求與響應模型

          以GET請求為例,最簡單的形式如下:

          import requests
          r = requests.get('http://www.baidu.com')
          print r.content
          

          大家可以看到比urllib2實現方式的代碼量少。接下來演示一下POST請求,同樣是非常簡短,更加具有Python風格。示例如下:

          import requests
          postdata={'key':'value'}
          r = requests.post('http://www.xxxxxx.com/login',data=postdata)
          print r.content
          

          HTTP中的其他請求方式也可以用Requests來實現,示例如下:

          r = requests.put('http://www.xxxxxx.com/put', data = {'key':'value'})
          r = requests.delete('http://www.xxxxxx.com/delete')
          r = requests.head('http://www.xxxxxx.com/get')
          r = requests.options('http://www.xxxxxx.com/get')
          

          接著講解一下稍微復雜的方式,大家肯定見過類似這樣的URL:

          http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1

          就是在網址后面緊跟著“?”,“?”后面還有參數。那么這樣的GET請求該如何發送呢?肯定有人會說,直接將完整的URL帶入即可,不過Requests還提供了其他方式,示例如下:

          import requests
           payload = {'Keywords': 'blog:qiyeboy','pageindex':1}
          r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload)
          print r.url
          

          通過打印結果,我們看到最終的URL變成了:

          http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1

          3.2 響應與編碼

          還是從代碼入手,示例如下:

          import requests
          r = requests.get('http://www.baidu.com')
          print 'content-->'+r.content
          print 'text-->'+r.text
          print 'encoding-->'+r.encoding
          r.encoding='utf-8'
          print 'new text-->'+r.text
          

          其中r.content返回的是字節形式,r.text返回的是文本形式,r.encoding返回的是根據HTTP頭猜測的網頁編碼格式。

          輸出結果中:“text-->”之后的內容在控制臺看到的是亂碼,“encoding-->”之后的內容是ISO-8859-1(實際上的編碼格式是UTF-8),由于Requests猜測編碼錯誤,導致解析文本出現了亂碼。Requests提供了解決方案,可以自行設置編碼格式,r.encoding='utf-8'設置成UTF-8之后,“new text-->”的內容就不會出現亂碼。

          但是這種手動的方式略顯笨拙,下面提供一種更加簡便的方式:chardet,這是一個非常優秀的字符串/文件編碼檢測模塊。安裝方式如下:

          pip install chardet
          

          安裝完成后,使用chardet.detect()返回字典,其中confidence是檢測精確度,encoding是編碼形式。示例如下:

          import requests
          r = requests.get('http://www.baidu.com')
          print chardet.detect(r.content)
          r.encoding = chardet.detect(r.content)['encoding']
          print r.text
          

          直接將chardet探測到的編碼,賦給r.encoding實現解碼,r.text輸出就不會有亂碼了。

          除了上面那種直接獲取全部響應的方式,還有一種流模式,示例如下:

          import requests
          r = requests.get('http://www.baidu.com',stream=True)
          print r.raw.read(10)
          

          設置stream=True標志位,使響應以字節流方式進行讀取,r.raw.read函數指定讀取的字節數。

          3.3 請求頭headers處理

          Requests對headers的處理和urllib2非常相似,在Requests的get函數中添加headers參數即可。示例如下:

          import requests
          user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
          headers={'User-Agent':user_agent}
          r = requests.get('http://www.baidu.com',headers=headers)
          print r.content
          

          3.4 響應碼code和響應頭headers處理

          獲取響應碼是使用Requests中的status_code字段,獲取響應頭使用Requests中的headers字段。示例如下:

          import requests
          r = requests.get('http://www.baidu.com')
          if r.status_code == requests.codes.ok:
           print r.status_code# 響應碼
           print r.headers# 響應頭
           print r.headers.get('content-type')# 推薦使用這種獲取方式,獲取其中的某個字段
           print r.headers['content-type']# 不推薦使用這種獲取方式
          else:
           r.raise_for_status()
          

          上述程序中,r.headers包含所有的響應頭信息,可以通過get函數獲取其中的某一個字段,也可以通過字典引用的方式獲取字典值,但是不推薦,因為如果字段中沒有這個字段,第二種方式會拋出異常,第一種方式會返回None。

          r.raise_for_status()是用來主動地產生一個異常,當響應碼是4XX或5XX時,raise_for_status()函數會拋出異常,而響應碼為200時,raise_for_status()函數返回None。

          3.5 Cookie處理

          如果響應中包含Cookie的值,可以如下方式獲取Cookie字段的值,示例如下:

          import requests
          user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
          headers={'User-Agent':user_agent}
          r = requests.get('http://www.baidu.com',headers=headers)
          # 遍歷出所有的cookie字段的值
          for cookie in r.cookies.keys():
           print cookie+':'+r.cookies.get(cookie)
          

          如果想自定義Cookie值發送出去,可以使用以下方式,示例如下:

          import requests
          user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
          headers={'User-Agent':user_agent}
          cookies = dict(name='qiye',age='10')
          r = requests.get('http://www.baidu.com',headers=headers,cookies=cookies)
          print r.text
          

          還有一種更加高級,且能自動處理Cookie的方式,有時候我們不需要關心Cookie值是多少,只是希望每次訪問的時候,程序自動把Cookie的值帶上,像瀏覽器一樣。Requests提供了一個session的概念,在連續訪問網頁,處理登錄跳轉時特別方便,不需要關注具體細節。使用方法示例如下:

          import Requests
          oginUrl = 'http://www.xxxxxxx.com/login'
          s = requests.Session()
          #首先訪問登錄界面,作為游客,服務器會先分配一個cookie
          r = s.get(loginUrl,allow_redirects=True)
          datas={'name':'qiye','passwd':'qiye'}
          #向登錄鏈接發送post請求,驗證成功,游客權限轉為會員權限
          r = s.post(loginUrl, data=datas,allow_redirects= True)
          print r.text
          

          上面的這段程序,其實是正式做Python開發中遇到的問題,如果沒有第一步訪問登錄的頁面,而是直接向登錄鏈接發送Post請求,系統會把你當做非法用戶,因為訪問登錄界面時會分配一個Cookie,需要將這個Cookie在發送Post請求時帶上,這種使用Session函數處理Cookie的方式之后會很常用。

          3.6 重定向與歷史信息

          處理重定向只是需要設置一下allow_redirects字段即可,例如:

          r=requests.get('http://www.baidu.com',allow_redirects=True)

          將allow_redirects設置為True,則是允許重定向;設置為False,則是禁止重定向。如果是允許重定向,可以通過r.history字段查看歷史信息,即訪問成功之前的所有請求跳轉信息。示例如下:

          import requests
          r = requests.get('http://github.com')
          print r.url
          print r.status_code
          print r.history
          

          打印結果如下:

          https://github.com/
          200
          (<Response [301]>,)
          

          上面的示例代碼顯示的效果是訪問GitHub網址時,會將所有的HTTP請求全部重定向為HTTPS。

          3.7 超時設置

          超時選項是通過參數timeout來進行設置的,示例如下:

          requests.get('http://github.com', timeout=2)
          

          3.8 代理設置

          使用代理Proxy,你可以為任意請求方法通過設置proxies參數來配置單個請求:

          import requests
          proxies = {
           "http": "http://0.10.1.10:3128",
           "https": "http://10.10.1.10:1080",
          }
          requests.get("http://example.org", proxies=proxies)
          

          也可以通過環境變量HTTP_PROXY和HTTPS_PROXY?來配置代理,但是在爬蟲開發中不常用。你的代理需要使用HTTP Basic Auth,可以使用http://user:password@host/語法:

          proxies = {
           "http": "http://user:pass@10.10.1.10:3128/",
          }
          

          03 小結

          本文主要講解了網絡爬蟲的結構和應用,以及Python實現HTTP請求的幾種方法。希望大家對本文中的網絡爬蟲工作流程和Requests實現HTTP請求的方式重點吸收消化。

          關于作者:范傳輝,資深網蟲,Python開發者,參與開發了多項網絡應用,在實際開發中積累了豐富的實戰經驗,并善于總結,貢獻了多篇技術文章廣受好評。研究興趣是網絡安全、爬蟲技術、數據分析、驅動開發等技術。

          本文摘編自《Python爬蟲開發與項目實戰》,經出版方授權發布。

          延伸閱讀《Python爬蟲開發與項目實戰》

          推薦語:零基礎學習爬蟲技術,從Python和Web前端基礎開始講起,由淺入深,包含大量案例,實用性強。

          多程序員新人想了解爬蟲的實現,然而,網絡上的許多信息是教大家如何爬蟲工具。工具的使用對于快速完成頁面爬取工作是有幫助的,但不利于大家掌握爬蟲的原理

          本文將帶大家從最基本和最本質的途徑去編寫一個爬蟲,讓大家真正了解爬蟲的工作原理。并且,能夠在此基礎上根據自身需求改造出需要的爬蟲。

          網頁的組成


          在開始爬蟲工作之前,我們先了解下,什么是網頁。

          以某網頁為例,其展示效果如下:、

          我們在網頁上點擊右鍵,“查看源代碼”可以看到網頁的代碼信息,如下:

          其中有很多文本,也有很多鏈接。這些鏈接有的指向另一個頁面,有的指向css文件、js文件、圖片文件等。

          分類一下,網頁一共包含以下幾個部分,如圖所示。

          各個部分的含義如下:

          • 其中HTML是網頁的主要部分,存儲了網頁的主要內容
          • CSS則是對網頁中的內容進行修飾
          • Js則是對網頁增加一些動態的功能
          • 圖片、視頻則是一些輔助的材料

          爬蟲就是主要爬取HTML部分。因為HTML中包含了主要的信息。

          爬蟲的基本原理


          而爬蟲的一個重要特點就是順藤摸瓜——根據鏈接,從一個網頁跳轉到另一個網頁,不斷進行。從而獲取眾多網頁的信息。

          那實現爬蟲,要完成的基本功能是:

          • 爬取(下載)某個網頁
          • 查找當前網頁中的鏈接,繼續爬取

          基于此,我們可以寫出一個爬蟲的核心偽代碼。

          爬蟲(網頁地址)
            爬取某個頁面
            分析頁面中的鏈接
            使用分析得到的鏈接再次調用方法   爬蟲(網頁地址)

          對的,你沒看錯,就是這么簡單。

          其中有兩個功能需要實現,即爬取某個頁面、分析頁面中的鏈接。

          接下來我們分別介紹這兩個功能的實現。


          核心功能的實現

          1 爬取某個頁面

          這個功能使用Python實現起來比較簡單,只要打開一個文本,然后將網絡某地址的信息寫入文本就算是爬取完成了。

          實現代碼如下。

          htmlFile=open('./output/'+(str(pageId)+'.txt'),'w')
          htmlFile.write(urllib.urlopen(url).read())
          htmlFile.close()

          2 分析頁面中的鏈接

          該工作需要正則表達式的幫助,'href="[^(javascript)]\S*[^(#)(css)(js)(ico)]\"'可以幫助我們匹配出網頁中的鏈接。

          pattern=re.compile('href="[^(javascript)]\S*[^(#)(css)(js)(ico)]\"')
          htmlFile=open('./output/'+(str(pageId)+'.txt'),'r')
                  for line in htmlFile:
                      ans=re.findall(pattern,line)


          爬蟲的實現


          有了偽代碼和兩個核心功能的實現代碼后,我們可以直接寫出爬蟲的主要代碼:

          htmlFile=open('./output/'+(str(pageId)+'.txt'),'w')
          htmlFile.write(urllib.urlopen(url).read())
          htmlFile.close()
          
          htmlFile=open('./output/'+(str(pageId)+'.txt'),'r')
          for line in htmlFile:
              ans=re.findall(pattern,line)
              for one in ans :
                  urlTail=one.split('"')[1]
                  url=urlparse.urljoin(url,urlTail)
                  if urlMap.has_key(url):
                      print 'skip---'+url
                  else:
                      print 'download---'+url
                      pageId += 1
                      urlMap[url]=pageId
                      idMap[pageId]=url
                      catchFile=open('./output/'+(str(urlMap[url])+'.txt'),'w')
                      try:
                          catchFile.write(urllib.urlopen(url).read())
                      except:
                          pass
                      finally:
                          catchFile.close()
          htmlFile.close()

          即爬取某個頁面、分析頁面中的鏈接、繼續下載。

          當然,在這個過程中,有幾點要注意:

          1、遇到死鏈要跳過,不要一直卡在那里

          2、凡是下載過的頁面不要重復下載,否則可能形成環路,永無止境

          只要注意了以上兩點,就可以寫出爬蟲。

          我直接給出爬蟲的代碼,放在下面的開源地址上。

          https://github.com/yeecode/EasyCrawler


          現有功能與展望

          該爬蟲十分基礎、簡單、容易理解,就是上面偽代碼的直接實現。

          該爬蟲的基本功能如下:

          • 輸入一個入口地址后,會爬取該地址網頁中`href=`指向的頁面,并將內容下載下來,依次保存
          • 對于不能訪問的壞鏈接,將會忽略
          • 該爬蟲只能爬取入口地址的鏈接,不再向更深處爬取
          • 會自動給頁面編ID,并跳過已爬取的頁面

          整個示例極少依賴外部項目,十分簡單、易懂、純粹。因此該項目不僅便于學習,也便于在此基礎上擴展新功能。

          基于以上功能,我們可以修改實現眾多其他功能,包括但不限于:

          • 根據頁面不斷爬取,而不是只爬取一層鏈接
          • 設置爬取范圍,例如只爬取某個域名下的鏈接
          • 定時爬取某個地址的數據,并對比其變化
          • 只爬取網頁中的圖片信息
          • 等等……

          好了,希望能夠幫到題主。也希望大家能在此基礎上實現更酷炫的功能。



          往期精彩文章:

          • 高效日志系統搭建秘技!架構師必讀
          • 程序員最有成就感的那一刻是什么時候?
          • 遠程過程調用RPC的實現原理:動態代理


          歡迎關注我們,不錯過軟件架構編程方面的干貨知識。


          主站蜘蛛池模板: 精品无人区一区二区三区在线| 武侠古典一区二区三区中文| 成人免费一区二区无码视频| 动漫精品一区二区三区3d| 久久婷婷色综合一区二区| 亚洲AV成人精品一区二区三区| 中文字幕VA一区二区三区| 色屁屁一区二区三区视频国产| 色窝窝无码一区二区三区| 国产精品无码不卡一区二区三区 | 国产精久久一区二区三区| 伦理一区二区三区| 亚洲av永久无码一区二区三区 | 久久99久久无码毛片一区二区| 精品人妻少妇一区二区| 久久精品无码一区二区三区不卡 | 精品国产一区二区三区AV性色| 一本一道波多野结衣一区| 高清国产AV一区二区三区| 国产精品一区12p| 亚洲AV无码一区二区三区鸳鸯影院| 无码国产精品一区二区免费16 | 亚洲日韩精品一区二区三区无码| 天堂不卡一区二区视频在线观看| 久久精品国产一区| 2022年亚洲午夜一区二区福利| 日韩一区二区视频在线观看| 理论亚洲区美一区二区三区 | 一区二区三区四区在线播放| 亚洲综合激情五月色一区| 国产一区在线视频| 国产福利酱国产一区二区| 久久久久久免费一区二区三区| 精品国产日韩亚洲一区在线| 国产福利一区二区在线视频 | bt7086福利一区国产| 亚洲欧美日韩一区二区三区| 国产成人一区二区三区高清 | 国产一区二区三区精品视频| 国产午夜精品一区二区三区极品| 国产麻豆精品一区二区三区|