技術還是很弱,我並不知道該從何寫起?因爲學習過程中常常會推翻自己以前的結論(也就是打臉),我不能說現在想的一定是正確的,僅僅只能簡述我現在的理解。如果有不恰當的地方,還望包容和指出,感謝
爬蟲可以快速的自動獲取數據,我覺得它主要可以分爲兩個部分:獲取與分析
獲取數據
有很多方法,可以通過urllib2庫等,下面介紹兩種簡單的
1. requests庫
(1)獲取源碼
import requests # 引入requests庫
HEAD = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54 Safari/537.36'}
result = requests.get('http://www.baidu.com', headers=HEAD) # 獲取百度首頁,HEAD傳入HTTP請求頭
print result.content # 打印源碼
運行上面的代碼,便可將百度的首頁爬取下來。其中HEAD字典用於向服務器傳遞瀏覽器和操作系統的信息,服務器往往會檢查這些信息來進行反爬蟲,所以HTTP請求頭需要攜帶HEAD字典。HEAD裏的User-Agent值可以在右鍵“審查元素”中的“網絡”選項裏,選擇任一文件,在“請求頭”中獲得。
(2)提交數據,模擬登陸杭電ACM網站
import requests
HEAD = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54 Safari/537.36'}
DATA = {'username': '你的用戶名', 'userpass': '你的密碼'} # DATA字典存儲需要提交的數據
requests.post('http://acm.hdu.edu.cn/userloginex.php?action=login', headers=HEAD, data=DATA) # 向該地址提交數據,傳入DATA
運行上面代碼,即完成登錄杭電,通過打印requests.content可知曉登錄結果。其中post方法裏的URL是form表單的action所指地址;如果action爲空,則地址往往隱藏在JS代碼裏,需要仔細查找。DATA字典的key必須與請求頭中的名稱(如下圖紅圈)或是form表單對應標籤的name屬性一樣。1)從返回的源碼可以看出,其並沒有執行JS。但有些數據可能是通過JS生成的,比如杭電ACM題目的URL就是JS生成的,鏈接;
注:右鍵“查看源代碼”是未執行JS的源碼,右鍵“審查元素”中的“Elements”是執行JS後的源碼,比較兩者會發現它們往往不一樣
2)有些數據是通過AJAX傳回的,只有發生某些事件時(點擊,下拉,...),數據纔會源源不斷加載進來,比如螞蜂窩的行程頁面,鏈接(隔段時間後快速下拉,底部數據甚至還沒加載);總之,源碼最好執行JS後再返回,並且能夠模擬人類的下拉和點擊行爲,怎麼辦呢?
顯然應該有個真實的瀏覽器,替我們將代碼先執行一遍後再返回,並提供一些類似JS中dom定位的方法。那麼,下面這款工具就很滿足上述要求。
2. Selenium
Selenium是網頁自動化測試的工具,相較於requests庫,selenium自動對源碼執行JS後再返回。其提供了各種瀏覽器的驅動,通過驅動調用對應瀏覽器(PhantomJS,Chrome,Firefox,...)。其中PhantomJS可以執行JS,但並不提供可視化界面,故相較於其他瀏覽器,PhantomJS更輕,更快。
(1)獲取源碼
from selenium import webdriver
driver = webdriver.Chrome('./chromedriver') # 調用Chrome驅動,打開瀏覽器
driver.get('http://www.baidu.com') # 跳轉到該頁面
print driver.page_source # 打印源碼
driver.close() # 關閉窗口,如果是PhantomJS則不需要
運行代碼,即可打印出百度首頁的源碼;把它與requests庫所打印的源碼作比較,會發現其已經執行過JS,並且忽略了註釋。(2)提交數據,模擬登陸杭電ACM網站
from selenium import webdriver
driver = webdriver.Chrome('./chromedriver')
driver.get('http://acm.hdu.edu.cn/')
driver.find_elements_by_name('username')[0].send_keys('你的用戶名') # 定位元素,並寫入值
driver.find_elements_by_name('userpass')[0].send_keys('你的密碼')
driver.find_elements_by_name('login')[0].click() # 模擬點擊form表單的提交按鈕
driver.close()
使用了selenium驅動Chrome瀏覽器,結果是可視化的,可以看到登陸成功。
1)“driver.find_elements_by_name(text)”:通過name屬性定位一系列HTML標籤,其返回的是一個列表,通過下標可以選出符合的元素對象;
2)“元素對象.send_keys(text)”:向該元素寫入文本text;
3)“元素對象.click()”:點擊該元素,觸發點擊事件;
可以查找selenium的文檔獲得詳細的解釋和更多其他方法。
3. 比較requests和selenium模擬登錄杭電ACM
1)兩者均需要尋找form表單登錄標籤的name屬性,但selenium提供了更多的方法,可以用其他屬性來替代;
2)requests關鍵還要找到form表單提交到的URL,而selenium則不用關心這個,只需要模擬點擊“提交”按鈕;
3)requests的速度遠快於selenium;
因此全部用selenium應該沒問題,但不能說用selenium替代requests,因爲selenium是驅動真實的瀏覽器,所以其所需資源要比requests多,因此能用requests就用requests。其次雖然selenium有很多驅動,但各驅動對同一語句的執行效果可能是不一樣的,所以更換驅動不能簡單只是更換“調用語句”。
解析數據
如果是考慮做搜索引擎,那麼不需要獲取數據,只要考慮如何剔除數據,比如剔除JS和HTML標籤,把剩餘的內容存儲即可,這是一個做減法的過程。
如果是獲取特定的數據,那麼可以通過正則匹配,或是BeautifulSoup庫來獲取,下面使用BeautifulSoup庫來打印杭電ACM每道題的題目,題目鏈接。
< BeautifulSoup文檔:鏈接 >
# coding=utf-8
from selenium import webdriver
from bs4 import BeautifulSoup
class Spider(object): # 定義一個爬蟲類
driver = None
def __init__(self): # 起到構造函數的作用
self.driver = webdriver.Chrome('./chromedriver') # 調用並啓動啓動Chrome瀏覽器
return
def __del__(self): # 起到析構函數的作用
self.driver.close()
return
def get(self, num): # 傳入題目的id
# 訪問題目頁,杭電ACM題目是按照pid從1001到2000+有序排列的
self.driver.get('http://acm.hdu.edu.cn/showproblem.php?pid=%s' % num)
soup = BeautifulSoup(self.driver.page_source, 'lxml') # 通過源碼得到文檔對象
# 判斷是否存在h1標籤,且style屬性爲color:#1A5CC8
if soup.find('h1', style='color:#1A5CC8'):
# 打印題目id和題目名稱
print num, soup.find('h1', style='color:#1A5CC8').get_text()
return
if __name__ == '__main__':
my = Spider() # 創建對象
for i in range(1001, 1501): # 迭代題目id
my.get(i) # 調用get方法
1)“元素對象.get_text()”:獲取該標籤(包括子標籤)的文字內容,剔除HTML代碼,僅保留文本;
2)“BeautifulSoup(source, 'lxml')”:其中lxml爲解釋器類型,鏈接;
更快的速度
當有大量數據需要爬取時,速度的瓶頸會顯現出來,特別是使用selenium的時候,其取決於頁面的加載時間,所以有時會很慢。
因此,爲了加快爬取速度,準備引入進程。這裏之所以用進程而不是線程,主要是考慮當一個進程異常後,不會影響到其他進程。線程之間會共用一些變量,一旦出錯,可能會“帶崩三路”。其次,Python中的線程只能利用單核,而進程卻是可以利用多核資源的。
下面將上個案例(打印杭電ACM每道題的題目)改成多進程版本:# coding=utf-8
import os
from selenium import webdriver
from bs4 import BeautifulSoup
from multiprocessing import Pool
my = [None for item in range(8)] # 列表,存儲爬蟲對象,每個key對應一個獨立的爬蟲對象
PRO = 4 # 進程數
class Spider(object):
driver = None
def __init__(self):
self.driver = webdriver.Chrome('./chromedriver') # 啓動Chrome
# self.driver=webdriver.PhantomJS('./phantomjs') # 啓動PhantomJS,無界面
return
def __del__(self):
self.driver.close()
return
def get(self, num): # 傳入題目id
self.driver.get('http://acm.hdu.edu.cn/showproblem.php?pid=%s' % num) # 訪問題目頁
soup = BeautifulSoup(self.driver.page_source, 'lxml') # 通過源碼得到文檔對象
if soup.find('h1', style='color:#1A5CC8'):
# 打印題目id和題目名稱
print num, soup.find('h1', style='color:#1A5CC8').get_text()
return
def task(num):
global my, PRO # 聲明my與PRO是全局變量
pid = int(os.getpid()) % PRO # 根據進程號爲其分配my[i],參考了循環隊列
try:
my[pid].get(num) # 調用
except Exception, msg:
print num, msg # 打印題目id與異常信息
my[pid] = Spider() # 爲my[i]構建新的對象
my[pid].get(num) # 重新調用
return
if __name__ == '__main__':
for i in range(PRO): # 爲my[i]構建爬蟲對象
my[i] = Spider()
p = Pool(PRO) # 進程池,傳入容量
for i in range(1001, 1501): # 爬取題目id爲1001~1500的題目名稱
p.apply_async(task, args=(i,)) # 異步,爲每個題目id分配其的my[i],並執行
p.close()
p.join()
print '*' * 18 + 'done' + '*' * 18
1)運行後,其速度較上個案例有了很大提升;本例中的進程數爲計算機的核心數,如果進程數遠超核心數,來回切換進程反而會降低爬取速度。
2)突破某速度,遭到封禁:這樣的情況自己還沒有遇到,這可能也與我寫的爬蟲效率要求不是很高有關。我想如果使用selenium之後,對方服務器已經分不清坐在顯示器那頭的到底是人類還是爬蟲。所以服務器可能只能通過速度來區別,並且一般網站應該不會把速度卡的很小,因爲這有可能會誤殺人類瀏覽者。當然如果遇到了封禁,可以通過控制速度或者使用代理來解決,這個我會繼續研究的。
結尾
2. 爬蟲不能生產數據,我們看到什麼,它也只能獲取到這些,僅僅是加快過程而已。
實驗室
我製作了一個記錄博客每天每篇文章訪問量的爬蟲,會自動更新文章與記錄新博客,並提供備份功能。通過Python的Matplotlib庫將爬取到的數據生成了幾張圖片,目前該項目還在持續推進。