0×00 介紹
在互聯(lián)網(wǎng)這個復(fù)雜的環(huán)境中,搜索引擎本身的爬蟲袜漩,出于個人目的的爬蟲雹顺,商業(yè)爬蟲肆意橫行绒净,肆意掠奪網(wǎng)上的或者公共或者私人的資源。顯然數(shù)據(jù)的收集并不是為所欲為,有一些協(xié)議或者原則還是需要每一個人注意肉迫。本文主要介紹關(guān)于爬蟲的一些理論和約定協(xié)議,然后相對完整完成一個爬蟲的基本功能互纯。
本人對于Python學(xué)習(xí)創(chuàng)建了一個小小的學(xué)習(xí)圈子,為各位提供了一個平臺殖卑,大家一起來討論學(xué)習(xí)Python。歡迎各位到來Python學(xué)習(xí)群:960410445一起討論視頻分享學(xué)習(xí)伦乔。Python是未來的發(fā)展方向吏祸,正在挑戰(zhàn)我們的分析能力及對世界的認知方式,因此,我們與時俱進贡翘,迎接變化蹈矮,并不斷的成長,掌握Python核心技術(shù)鸣驱,才是掌握真正的價值所在泛鸟。
0×01 協(xié)議
一般情況下網(wǎng)站的根目錄下存在著一個robots.txt的文件,用于告訴爬蟲那些文件夾或者哪些文件是網(wǎng)站的擁有者或者管理員不希望被搜索引擎和爬蟲瀏覽的踊东,或者是不希望被非人類的東西查看的北滥。但是不僅僅如此,在這個文件中闸翅,有時候還會指明sitemap的位置再芋,爬蟲可以直接尋找sitemap而不用費力去爬取網(wǎng)站,制作自己的sitemap坚冀。那么最好我們看個具體的例子吧济赎,這樣更有助于理解robots協(xié)議:
-----------------------以下時freebuf的robots.txt-------------User-agent: *Disallow:/*?*
Disallow: /trackback
Disallow: /wp-*/Disallow:*/comment-page-*Disallow:/*?replytocom=*
Disallow: */trackbackDisallow:/?randomDisallow:*/feedDisallow:/*.css$
Disallow: /*.js$
Sitemap: http://www.freebuf.com/sitemap.txt
大家可以看到,這里指明了適用的User-agent頭记某,指明了Disallow的目錄司训,也指明了sitemap,然后我們在看一下sitemap中是什么:
我們大致可以發(fā)現(xiàn)這些都是整個網(wǎng)站允許公開的內(nèi)容液南,如果這個爬蟲作者是對freebuf的文章感興趣的話壳猜,大可不必從頭到尾設(shè)計爬蟲算法拿下整個網(wǎng)站的sitemap,這樣直接瀏覽sitemap節(jié)省了大量的時間滑凉。?
0×02 原則
如果協(xié)議不存在的話统扳,我們?nèi)匀徊荒転樗麨椋暇W(wǎng)隨意搜索一下源于爬蟲協(xié)議的官司譬涡,國內(nèi)外都有闪幽。爬蟲的協(xié)議規(guī)則建立在如下的基礎(chǔ)上:
1.????搜索技術(shù)應(yīng)該服務(wù)于人類,尊重信息提供者的意愿涡匀,并維護其隱私權(quán)盯腌;
2.????網(wǎng)站也有義務(wù)保護其使用者的個人信息和隱私不被侵犯。
簡單來說陨瘩,就是構(gòu)建的爬蟲以信息收集為目的是沒錯的腕够,但是不能侵犯別人的隱私,比如你掃描并且進入了網(wǎng)站的robots中的disallow字段舌劳,你就可能涉及侵犯別人隱私的問題帚湘。當然作為一般人來講,我們使用爬蟲技術(shù)無非是學(xué)習(xí)甚淡,或者是搜集想要的信息大诸,并沒有想那么多的侵權(quán),或者是商業(yè)的問題。
0×03 確立目標與分析過程
這里我提供一個爬蟲誕生要經(jīng)歷的一般過程:
1.?確立需求在资柔,sitemap中挑選出需要挖掘的頁面焙贷;
2.?依次分析挑選出的頁面;
3.?存儲分析結(jié)果贿堰。
但是有時候問題就是辙芍,我們的目標網(wǎng)站沒有提供sitemap,那么這就得麻煩我們自己去獲取自己定制的sitemap羹与。
目標:制作一個網(wǎng)站的sitemap:
分析:我們要完成這個過程故硅,但是不存在現(xiàn)成sitemap,筆者建議大家把這個網(wǎng)站想象成一個圖的結(jié)構(gòu)纵搁,網(wǎng)站的url之間縱橫交錯吃衅,我們可以通過主頁面進行深度優(yōu)先或者廣度優(yōu)先搜索從而遍歷整個網(wǎng)站拿到sitemap。顯然我們發(fā)現(xiàn)诡渴,我們首先要做的第一步就是完整的獲取整個頁面的url捐晶。
但是我們首先得想清楚一個問題:我們獲取到的url是應(yīng)該是要限制域名的,如果爬蟲從目標網(wǎng)站跳走了妄辩,也就意味著將無限陷入整個網(wǎng)絡(luò)進行挖掘惑灵。這么龐大的數(shù)據(jù)量,我想不是一般人的電腦硬盤可以承受的吧眼耀!
那么我們的第一步就是編寫代碼去獲取整個頁面的url英支。其實這個例子在上一篇文章中已經(jīng)講到過。
0×04 動手
我們的第一個小目標就是獲取當前頁面所有的url:
首先必須說明的是哮伟,這個腳本并不具有普遍性(只針對freebuf.com)干花,并且效率低下,可以優(yōu)化的地方很多:只是為了方便楞黄,簡單實現(xiàn)了功能池凄,有興趣的朋友可以任意重構(gòu)達到高效優(yōu)雅的目的
importurllibfrombs4importBeautifulSoupimportredefget_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:fortag_aintags_a:???????????urls.append(tag_a['href'])#return urlsexcept:passreturnurls#得到所有freebuf.com下的urldefget_local_urls(url):local_urls = []???urls = get_all_url(url)for_urlinurls:???????ret = _urlif'freebuf.com'inret.replace('//','').split('/')[0]:???????????local_urls.append(_url)returnlocal_urls#得到所有的不是freebuf.com域名的urldefget_remote_urls(url):remote_urls = []???urls = get_all_url(url)for_urlinurls:???????ret = _urlif"freebuf.com"notinret.replace('//','').split('/')[0]:???????????remote_urls.append(_url)returnremote_urlsdef__main__():url ='http://freebuf.com/'rurls = get_remote_urls(url)print"--------------------remote urls-----------------------"forretinrurls:printretprint"---------------------localurls-----------------------"lurls = get_local_urls(url)forretinlurls:printretif__name__ =='__main__':__main__()
這樣我們就得到了該頁面的url,本域名和其他域名下:
當然圖中所示的結(jié)果為部分截圖鬼廓,至少證明了我們上面的代碼比較好的解決的url獲取的問題肿仑,由于這里我們的判斷規(guī)則簡單,還有很多情況沒有考慮到碎税,所以建議大家如果有時間尤慰,可以把上面代碼重構(gòu)成更佳普適,更加完整的腳本雷蹂,再投入真正的實際使用伟端。
0×05 sitemap爬蟲
所謂的sitemap,我們在上面的例子中又講到sitemap記錄了整個網(wǎng)站的網(wǎng)站擁有者允許你爬取內(nèi)容的鏈接匪煌。但是很多情況下责蝠,sitemap沒有寫出党巾,那么問題就來了,我們面對一個陌生的網(wǎng)站如何進行爬人健(在不存在sitemap的情況下).
當然我們首先要獲取sitemap昧港,沒有現(xiàn)成的我們就自己動手獲取整個網(wǎng)站的sitemap。
再開始之前我們需要整理一下思路:我們可以把整站當成一個錯綜復(fù)雜的圖結(jié)構(gòu)支子,有一些算法基礎(chǔ)的讀者都會知道圖的簡單遍歷方法:dfs和bfs(深度優(yōu)先和廣度優(yōu)先)。如果這里讀者有問題的話建議先去學(xué)習(xí)一下這兩種算法达舒。大體的算法結(jié)構(gòu)我們清楚了值朋,但是在實現(xiàn)中我們顯然需要特殊處理url,需要可以區(qū)分當前目標站點域名下的網(wǎng)站和其他域名的網(wǎng)站巩搏,除此之外昨登,在href的值中經(jīng)常會出現(xiàn)相對url,這里也要特別處理贯底。
importurllibfrombs4importBeautifulSoupimporturlparseimporttimeimporturllib2?url ="http://xxxx.xx/"domain ="xxxx.xx"deep =0tmp =""sites = set()visited = set()#local = set()defget_local_pages(url,domain):globaldeepglobalsitesglobaltmp???repeat_time =0pages = set()#防止url讀取卡住whileTrue: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"breakexcept:print"Open Url Failed !!! Repeat"time.sleep(1)???????????repeat_time = repeat_time+1ifrepeat_time ==5:returnprint"Readint the web ..."soup = BeautifulSoup(web.read())print"..."tags = soup.findAll(name='a')fortagintags:#避免參數(shù)傳遞異常try:???????????ret = tag['href']except:print"Maybe not the attr : href"continueo = urlparse.urlparse(ret)"""
???????#Debug I/O
???????for _ret in o:
???????????if _ret == "":
??????????????? pass
???????????else:
??????????????? print _ret
???????"""#處理相對路徑urlifo[0]is""ando[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)#這里不是太完善丰辣,但是可以應(yīng)付一般情況if'../'ino[2]:??????????????? paths = o[2].split('/')fori inrange(len(paths)):ifpaths[i] =='..':??????????????????????? paths[i] =''ifpaths[i-1]:??????????????????????????? paths[i-1] =''tmp_path =''forpathinpaths:ifpath =='':continuetmp_path = tmp_path +'/'+path??????????????? ret =ret.replace(o[2],ret_path)print"FixedPage: "+ ret#協(xié)議處理if'http'notino[0]:print"Bad? Page:"+ ret.encode('ascii')continue#url合理性檢驗ifo[0]is""ando[1]isnot"":print"Bad? Page: "+retcontinue#域名檢驗ifdomainnotino[1]:print"Bad? Page: "+retcontinue#整理,輸出newpage = retifnewpagenotinsites:print"Add New Page: "+ newpage???????????pages.add(newpage)returnpages#dfs算法遍歷全站defdfs(pages):#無法獲取新的url說明便利完成禽捆,即可結(jié)束dfsifpagesisset():returnglobalurlglobaldomainglobalsitesglobalvisited???sites = set.union(sites,pages)forpageinpages:ifpagenotinvisited: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)foriinsites:printi
在這個腳本中笙什,我們采用了dfs(深度優(yōu)先算法),關(guān)于算法的問題我們不做深入討論胚想,實際上在完成過程中我們關(guān)鍵要完成的是對href的處理琐凭,在腳本代碼中我們也可以看到,url的處理占了大部分代碼浊服,但是必須說明的是:很遺憾我們并沒有把所有的情況都解決清楚统屈,但是經(jīng)過測試,上面的代碼可以應(yīng)付很多種情況牙躺。所以大家如果有需要可以隨意修改使用愁憔。
測試:用上面的腳本在網(wǎng)絡(luò)狀況一般的情況下,對我的個人博客(大概100個頁面)進行掃描孽拷,大概用時2分半吨掌,這是單機爬蟲。
0×06 Web元素處理
在接下來的這個例子中乓搬,我們不再關(guān)注url的處理思犁,我們暫且把目光對準web單個頁面的信息處理。按照爬蟲編寫的一般流程进肯,在本例子中激蹲,爭取每一步都是完整的可操作的。
確立目標:獲取freebuf的文章并且生成docx文檔江掩。
過程:偵察目標網(wǎng)頁的結(jié)構(gòu)学辱,針對特定結(jié)構(gòu)設(shè)計方案
腳本實現(xiàn):bs4模塊和docx模塊的使用
預(yù)備知識:
1.Bs4的基本api的使用乘瓤,關(guān)于beautifulSoup的基本使用方法,我這里需要介紹在下面的腳本中我使用到的方法:
Soup= BeautifulSoup(data)#構(gòu)建一個解析器Tags= Soup.findAll(name,attr)
我們重點要講findAll方法的兩個參數(shù):name和attr
Name:? 指的是標簽名策泣,傳入一個標簽名的名稱就可以返回所有固定名稱的標簽名
Attr:???? 是一個字典存儲需要查找的標簽參數(shù)衙傀,返回對應(yīng)的標簽
Tag.children表示獲取tag標簽的所有子標簽Tag.string表示獲取tag標簽內(nèi)的所有字符串,不用一層一層索引下去尋找字符串Tag.attrs[key]表示獲取tag標簽內(nèi)參數(shù)的鍵值對鍵為key的值Tag.img表示獲取tag標簽的標簽名為img的自標簽(一個)
在本例子的使用中萨咕,筆者通過標簽名+id+class來定位標簽统抬,雖然這樣的方法不是絕對的正確,但是一般情況下危队,準確率還是比較高的聪建。
2.docx的使用:在使用這個模塊的時候,要記清楚如果:
pip install python-docx
easy_install python-docx
兩種方式安裝都不能正常使用的話茫陆,就需要下載tar包自己手動安裝
Docx模塊是一個可以直接操作生成docx文檔的python模塊金麸,使用方法極盡簡單:
Demo = Document() #在內(nèi)存中建立一個doc文檔
Demo.add_paragraph(data) #在doc文檔中添加一個段落
Demo.add_picture(“pic.png”) #doc文檔中添加一個圖片
Demo.save(‘demo.docx’) #存儲docx文檔
當然這個模塊還可以操作段落的字體啊格式啊各種強大的功能,大家可以從官方網(wǎng)站找到對應(yīng)的文檔簿盅。鑒于我們以學(xué)習(xí)為目的挥下,我就不作深入的排版介紹了
開始:
觀察html結(jié)構(gòu):
我們大致觀察一下結(jié)構(gòu),定位到文章的具體內(nèi)容需要找到標簽桨醋,然后再遍歷標簽的子標簽即可遍歷到所有的段落棚瘟,配圖資料
這樣定位到圖片,那么我們怎么樣來尋找
代碼
fromdocximportDocumentfrombs4importBeautifulSoupimporturllib?url ="http://freebuf.com/news/94263.html"data = urllib.urlopen(url)?document = Document()?soup = BeautifulSoup(data)article = soup.find(name ="div",attrs={'id':'contenttxt'}).childrenforeinarticle:try:ife.img:???????????pic_name =''printe.img.attrs['src']if'gif'ine.img.attrs['src']:??????????????? pic_name ='temp.gif'elif'png'ine.img.attrs['src']:??????????????? pic_name ='temp.png'elif'jpg'ine.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:passife.string:printe.string.encode('gbk','ignore')???????document.add_paragraph(e.string)???????document.save("freebuf_article.docx")print"success create a document"
當然上面的代碼讨盒,讀者看起來是非常粗糙的解取,其實沒關(guān)系,我們的目的達到了返顺,在使用中學(xué)習(xí)禀苦,這也是python的精神∷烊担可能大家不熟悉beautifulsoup的使用振乏,這里需要大家自行去讀一下beautifulsoup官方的doc,筆者不知道有沒有中文版秉扑,但是讀英文也不是那么麻煩慧邮,基本寫的還是非常簡明易懂。
那么我們可以看一下輸出的結(jié)構(gòu)吧舟陆!
這是文件夾目錄下的文件汉规,打開生成的docx文檔以后:
這里我們發(fā)現(xiàn)排版仍然是有缺陷腐芍,但是所有的內(nèi)容贸毕,圖片都按照原來的順序成功存儲在了.docx文件中盟迟。