Python爬蟲開發(二):整站爬蟲與Web挖掘

qq_27446553發表於2016-02-29

*原創作者:VillanCh

0×00 介紹

0×01 協議

0×02 原則

0×03 確立目標與分析過程

0×04 動手

0×05 sitemap爬蟲

0×06 web元素處理

0×07 總結與預告

0×00 介紹

在網際網路這個複雜的環境中,搜尋引擎本身的爬蟲,出於個人目的的爬蟲,商業爬蟲肆意橫行,肆意掠奪網上的或者公共或者私人的資源。顯然資料的收集並不是為所欲為,有一些協議或者原則還是需要每一個人注意。本文主要介紹關於爬蟲的一些理論和約定協議,然後相對完整完成一個爬蟲的基本功能。

0×01 協議

一般情況下網站的根目錄下存在著一個robots.txt的檔案,用於告訴爬蟲那些資料夾或者哪些檔案是網站的擁有者或者管理員不希望被搜尋引擎和爬蟲瀏覽的,或者是不希望被非人類的東西檢視的。但是不僅僅如此,在這個檔案中,有時候還會指明sitemap的位置,爬蟲可以直接尋找sitemap而不用費力去爬取網站,製作自己的sitemap。那麼最好我們看個具體的例子吧,這樣更有助於理解robots協議:

-----------------------以下時freebuf的robots.txt-------------
User-agent: *
Disallow: /*?*
Disallow: /trackback
Disallow: /wp-*/
Disallow: */comment-page-*
Disallow: /*?replytocom=*
Disallow: */trackback
Disallow: /?random
Disallow: */feed
Disallow: /*.css$
Disallow: /*.js$
Sitemap: http://www.freebuf.com/sitemap.txt

大家可以看到,這裡指明瞭適用的User-agent頭,指明瞭Disallow的目錄,也指明瞭sitemap,然後我們在看一下sitemap中是什麼:

b1.png

我們大致可以發現這些都是整個網站允許公開的內容,如果這個爬蟲作者是對freebuf的文章感興趣的話,大可不必從頭到尾設計爬蟲演算法拿下整個網站的sitemap,這樣直接瀏覽sitemap節省了大量的時間。 

0×02 原則

如果協議不存在的話,我們仍然不能為所欲為,上網隨意搜尋一下源於爬蟲協議的官司,國內外都有。爬蟲的協議規則建立在如下的基礎上:

1.    搜尋技術應該服務於人類,尊重資訊提供者的意願,並維護其隱私權;

2.    網站也有義務保護其使用者的個人資訊和隱私不被侵犯。

簡單來說,就是構建的爬蟲以資訊收集為目的是沒錯的,但是不能侵犯別人的隱私,比如你掃描並且進入了網站的robots中的disallow欄位,你就可能涉及侵犯別人隱私的問題。當然作為一般人來講,我們使用爬蟲技術無非是學習,或者是蒐集想要的資訊,並沒有想那麼多的侵權,或者是商業的問題。

0×03 確立目標與分析過程

這裡我提供一個爬蟲誕生要經歷的一般過程:

1. 確立需求在,sitemap中挑選出需要挖掘的頁面;

2. 依次分析挑選出的頁面;

3. 儲存分析結果。

但是有時候問題就是,我們的目標網站沒有提供sitemap,那麼這就得麻煩我們自己去獲取自己定製的sitemap。

目標:製作一個網站的sitemap:

分析:我們要完成這個過程,但是不存在現成sitemap,筆者建議大家把這個網站想象成一個圖的結構,網站的url之間縱橫交錯,我們可以通過主頁面進行深度優先或者廣度優先搜尋從而遍歷整個網站拿到sitemap。顯然我們發現,我們首先要做的第一步就是完整的獲取整個頁面的url。

但是我們首先得想清楚一個問題:我們獲取到的url是應該是要限制域名的,如果爬蟲從目標網站跳走了,也就意味著將無限陷入整個網路進行挖掘。這麼龐大的資料量,我想不是一般人的電腦硬碟可以承受的吧!

那麼我們的第一步就是編寫程式碼去獲取整個頁面的url。其實這個例子在上一篇文章中已經講到過。

0×04 動手

我們的第一個小目標就是獲取當前頁面所有的url:

首先必須說明的是,這個指令碼並不具有普遍性(只針對freebuf.com),並且效率低下,可以優化的地方很多:只是為了方便,簡單實現了功能,有興趣的朋友可以任意重構達到高效優雅的目的

import urllib
from bs4 import BeautifulSoup
import re
 
def get_all_url(url):
   urls = []
   web = urllib.urlopen(url)
soup =BeautifulSoup(web.read())
#通過正則過濾合理的url(針對與freebuf.com來講)
   tags_a =soup.findAll(name='a',attrs={'href':re.compile("^https?://")})
    try :
       for tag_a in tags_a:
           urls.append(tag_a['href'])
       #return urls
   except:
       pass
   return  urls
 
#得到所有freebuf.com下的url
def get_local_urls(url):
   local_urls = []
   urls = get_all_url(url)
   for _url in urls:
       ret = _url
       if 'freebuf.com' in ret.replace('//','').split('/')[0]:
           local_urls.append(_url)
   return  local_urls
 
#得到所有的不是freebuf.com域名的url
def get_remote_urls(url):
   remote_urls = []
   urls = get_all_url(url)
   for _url in urls:
       ret = _url
       if "freebuf.com" not in ret.replace('//','').split('/')[0]:
           remote_urls.append(_url)
   return  remote_urls
 
def __main__():
   url = 'http://freebuf.com/'
   rurls = get_remote_urls(url)
   print "--------------------remote urls-----------------------"
   for ret in rurls:
       print ret
   print "---------------------localurls-----------------------"   
   lurls = get_local_urls(url)
   for ret in lurls:
       print ret
   
   
if __name__ == '__main__':
__main__()

這樣我們就得到了該頁面的url,本域名和其他域名下:

b2.png

b3.png

當然圖中所示的結果為部分截圖,至少證明了我們上面的程式碼比較好的解決的url獲取的問題,由於這裡我們的判斷規則簡單,還有很多情況沒有考慮到,所以建議大家如果有時間,可以把上面程式碼重構成更佳普適,更加完整的指令碼,再投入真正的實際使用。

0×05 sitemap爬蟲

所謂的sitemap,我們在上面的例子中又講到sitemap記錄了整個網站的網站擁有者允許你爬取內容的連結。但是很多情況下,sitemap沒有寫出,那麼問題就來了,我們面對一個陌生的網站如何進行爬取(在不存在sitemap的情況下).

當然我們首先要獲取sitemap,沒有現成的我們就自己動手獲取整個網站的sitemap。

再開始之前我們需要整理一下思路:我們可以把整站當成一個錯綜複雜的圖結構,有一些演算法基礎的讀者都會知道圖的簡單遍歷方法:dfs和bfs(深度優先和廣度優先)。如果這裡讀者有問題的話建議先去學習一下這兩種演算法。大體的演算法結構我們清楚了,但是在實現中我們顯然需要特殊處理url,需要可以區分當前目標站點域名下的網站和其他域名的網站,除此之外,在href的值中經常會出現相對url,這裡也要特別處理。

import urllib
from bs4 import BeautifulSoup
import urlparse
import time
import urllib2
 
url = "http://xxxx.xx/"
domain = "xxxx.xx"
deep = 0
tmp = ""
sites = set()
visited = set()
#local = set()
def get_local_pages(url,domain):
   global deep
   global sites
   global tmp
   repeat_time = 0
   pages = set()
   
    #防止url讀取卡住
   while True:
       try:
           print "Ready to Open the web!"
           time.sleep(1)
           print "Opening the web", url
           web = urllib2.urlopen(url=url,timeout=3)
           print "Success to Open the web"
           break
       except:
           print "Open Url Failed !!! Repeat"
           time.sleep(1)
           repeat_time = repeat_time+1
           if repeat_time == 5:
                return
   
   
   print "Readint the web ..."
   soup = BeautifulSoup(web.read())
   print "..."
   tags = soup.findAll(name='a')
   for tag in tags:
       
       #避免引數傳遞異常
       try:
           ret = tag['href']
       except:
           print "Maybe not the attr : href"
           continue
       o = urlparse.urlparse(ret)
       """
       #Debug I/O
       for _ret in o:
           if _ret == "":
                pass
           else:
                print _ret
       """
       #處理相對路徑url
       if o[0] is "" and o[1] is "":
           print "Fix  Page: " +ret
           url_obj = urlparse.urlparse(web.geturl())
           ret = url_obj[0] + "://" + url_obj[1] + url_obj[2] + ret
           #保持url的乾淨
           ret = ret[:8] + ret[8:].replace('//','/')
           o = urlparse.urlparse(ret)
                     #這裡不是太完善,但是可以應付一般情況
           if '../' in o[2]:
                paths = o[2].split('/')
               for i inrange(len(paths)):
                    if paths[i] == '..':
                        paths[i] = ''
                        if paths[i-1]:
                            paths[i-1] = ''
                tmp_path = ''
                for path in paths:
                    if path == '':
                        continue
                    tmp_path = ret_path + '/' +path
                ret =ret.replace(o[2],ret_path)
           print "FixedPage: " + ret
           
           
       #協議處理
       if 'http' not in o[0]:
           print "Bad  Page:" + ret.encode('ascii')
           continue
       
       #url合理性檢驗
       if o[0] is "" and o[1] is not "":
           print "Bad  Page: " +ret
           continue
       
       #域名檢驗
       if domain not in o[1]:
           print "Bad  Page: " +ret
           continue
       
       #整理,輸出
       newpage = ret
       if newpage not in sites:
           print "Add New Page: " + newpage
           pages.add(newpage)
   return pages
 
#dfs演算法遍歷全站
def dfs(pages):
    #無法獲取新的url說明便利完成,即可結束dfs
   if pages is set():
       return
   global url
   global domain
   global sites
   global visited
   sites = set.union(sites,pages)
   for page in pages:
       if page not in visited:
           print "Visiting",page
           visited.add(page)
           url = page
           pages = get_local_pages(url, domain)
           dfs(pages)
   
   print "sucess"
 
 
pages = get_local_pages(url, domain)
dfs(pages)
for i in sites:
print i

在這個指令碼中,我們採用了dfs(深度優先演算法),關於演算法的問題我們不做深入討論,實際上在完成過程中我們關鍵要完成的是對href的處理,在指令碼程式碼中我們也可以看到,url的處理佔了大部分程式碼,但是必須說明的是:很遺憾我們並沒有把所有的情況都解決清楚,但是經過測試,上面的程式碼可以應付很多種情況。所以大家如果有需要可以隨意修改使用。

測試:用上面的指令碼在網路狀況一般的情況下,對我的個人部落格(大概100個頁面)進行掃描,大概用時2分半,這是單機爬蟲。

0×06 Web元素處理

在接下來的這個例子中,我們不再關注url的處理,我們暫且把目光對準web單個頁面的資訊處理。按照爬蟲編寫的一般流程,在本例子中,爭取每一步都是完整的可操作的。

確立目標:獲取freebuf的文章並且生成docx文件。

過程:偵察目標網頁的結構,針對特定結構設計方案

指令碼實現:bs4模組和docx模組的使用

預備知識:

1.Bs4的基本api的使用,關於beautifulSoup的基本使用方法,我這裡需要介紹在下面的指令碼中我使用到的方法:

Soup = BeautifulSoup(data)#構建一個解析器
Tags = Soup.findAll(name,attr)

我們重點要講findAll方法的兩個引數:name和attr

Name:  指的是標籤名,傳入一個標籤名的名稱就可以返回所有固定名稱的標籤名

Attr:     是一個字典儲存需要查詢的標籤引數,返回對應的標籤

Tag.children         表示獲取tag標籤的所有子標籤
Tag.string           表示獲取tag標籤內的所有字串,不用一層一層索引下去尋找字串
Tag.attrs[key]       表示獲取tag標籤內引數的鍵值對鍵為key的值
Tag.img              表示獲取tag標籤的標籤名為img的自標籤(一個)

在本例子的使用中,筆者通過標籤名+id+class來定位標籤,雖然這樣的方法不是絕對的正確,但是一般情況下,準確率還是比較高的。

2.docx的使用:在使用這個模組的時候,要記清楚如果:

pip install python-docx

easy_install python-docx

兩種方式安裝都不能正常使用的話,就需要下載tar包自己手動安裝

Docx模組是一個可以直接操作生成docx文件的python模組,使用方法極盡簡單:

Demo = Document() #在記憶體中建立一個doc文件

Demo.add_paragraph(data) #在doc文件中新增一個段落

Demo.add_picture(“pic.png”) #doc文件中新增一個圖片

Demo.save(‘demo.docx’) #儲存docx文件

當然這個模組還可以操作段落的字型啊格式啊各種強大的功能,大家可以從官方網站找到對應的文件。鑑於我們以學習為目的,我就不作深入的排版介紹了

開始:

觀察html結構:

b4.png

我們大致觀察一下結構,定位到文章的具體內容需要找到標籤,然後再遍歷標籤的子標籤即可遍歷到所有的段落,配圖資料

b5.png

這樣定位到圖片,那麼我們怎麼樣來尋找

程式碼

from docx import Document
from bs4 import BeautifulSoup
import urllib
 
url ="http://freebuf.com/news/94263.html"
data = urllib.urlopen(url)
 
document = Document()
 
soup = BeautifulSoup(data)
article = soup.find(name ="div",attrs={'id':'contenttxt'}).children
for e in article:
   try:
       if e.img:
           pic_name = ''
           print e.img.attrs['src']
           if 'gif' in e.img.attrs['src']:
                pic_name = 'temp.gif'
           elif 'png' in e.img.attrs['src']:
                pic_name = 'temp.png'
           elif 'jpg' in e.img.attrs['src']:
                pic_name = 'temp.jpg'
           else:
                pic_name = 'temp.jpeg'
           urllib.urlretrieve(e.img.attrs['src'], filename=pic_name)
           document.add_picture(pic_name)
   except:
       pass
   if e.string:
       print e.string.encode('gbk','ignore')
       document.add_paragraph(e.string)
       
document.save("freebuf_article.docx")
print "success create a document"

當然上面的程式碼,讀者看起來是非常粗糙的,其實沒關係,我們的目的達到了,在使用中學習,這也是python的精神。可能大家不熟悉beautifulsoup的使用,這裡需要大家自行去讀一下beautifulsoup官方的doc,筆者不知道有沒有中文版,但是讀英文也不是那麼麻煩,基本寫的還是非常簡明易懂。

那麼我們可以看一下輸出的結構吧!

b6.png

這是資料夾目錄下的檔案,開啟生成的docx文件以後:

b7.png

b8.png

這裡我們發現排版仍然是有缺陷,但是所有的內容,圖片都按照原來的順序成功儲存在了.docx檔案中。

0×07 總結與預告

在本文中我們快速學習了簡單單機爬蟲的製作,並且動手對網頁的資訊進行了一定的處理分類。但是這並不是結束,而僅僅是一個開始。還有很多的知識等待我們去探究:比如模擬登陸,抓取登陸以後的web網頁等,其實這些並不困難。

在接下來的文章中我們會陸續講到:

1. 資料儲存

2. 多執行緒

3. 動態網頁抓取(js載入)

*原創作者:VillanCh,本文屬FreeBuf原創獎勵計劃文章,未經作者本人及FreeBuf許可,切勿私自轉載

相關文章