Python爬蟲

技術還是很弱,我並不知道該從何寫起?因爲學習過程中常常會推翻自己以前的結論(也就是打臉),我不能說現在想的一定是正確的,僅僅只能簡述我現在的理解。如果有不恰當的地方,還望包容和指出,感謝

爬蟲可以快速的自動獲取數據,我覺得它主要可以分爲兩個部分:獲取與分析



獲取數據


有很多方法,可以通過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更輕,更快。


< selenium文檔:蟲師英文 >


(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之後,對方服務器已經分不清坐在顯示器那頭的到底是人類還是爬蟲。所以服務器可能只能通過速度來區別,並且一般網站應該不會把速度卡的很小,因爲這有可能會誤殺人類瀏覽者。當然如果遇到了封禁,可以通過控制速度或者使用代理來解決,這個我會繼續研究的。



結尾


1. 爬蟲與人類瀏覽者的區別很可能只在於瀏覽速度,只要不過某速度,就不會遭到封禁;

2. 爬蟲不能生產數據,我們看到什麼,它也只能獲取到這些,僅僅是加快過程而已。



實驗室


我製作了一個記錄博客每天每篇文章訪問量的爬蟲,會自動更新文章與記錄新博客,並提供備份功能。通過Python的Matplotlib庫將爬取到的數據生成了幾張圖片,目前該項目還在持續推進。

如果你有興趣,請戳這裏;如果想查看目前的效果,點這裏



發佈了56 篇原創文章 · 獲贊 105 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章