Python3網絡爬蟲(九):使用Selenium爬取百度文庫word文章

轉載請註明作者和出處: http://blog.csdn.net/c406495762
運行平臺: Windows
Python版本: Python3.x
IDE: Sublime text3

1 前言

    大家都應該有過從百度文庫下載東西的經歷,對於下載需要下載券的文章,我們可以辦理文庫VIP(土豪的選擇):

    有的人也會在某寶購買一定的下載券,然後進行下載。而另一些勤勤懇懇的人,則會選擇上傳文章,慢慢攢下載券。任勞任怨的人,則會自己一點一點的複製粘貼,複製到word裏文字太大,那就複製到txt文件裏。而既不想花錢又不想攢下載券,也不想一點一點複製粘貼的人,會選擇“冰點文庫”這樣的下載軟件,不過貌似現在“冰點文庫”已經不能使用了。當然,還有一些其他破解方法,比如放到手機的百度文庫APP裏,另存爲文章,不需要下載券就可以下載文章。諸如此類的方法,可謂五花八門。而對於學習爬蟲的人來說,面對怎樣免費下載一個付費的word文章的問題,第一個想到的應該就是:自己寫個程序搞下來。

2 問題分析

    我們以如何下載下面這篇文章爲例,分析問題:

    URL : https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html

    我想,看到這樣的一個文章,如果爬取當前頁面的內容還是很好爬的吧。感覺so easy!至少我當時是這麼想的,但是當把文章翻到最下方的時候,我看到了如下內容:

    呃….需要點擊“繼續閱讀”才能顯示後續的內容,我單爬這一頁內容,是爬不到後續的內容的。第一個想到的方法是,抓包分析下,然後我又一次蒙逼了:

    Request URL這麼長!!最後的expire時間信息好解決,其他的信息呢?不想做無謂的掙扎,因此,我果斷地放棄這個方法。

    問題:獲取當前頁的內容好辦,怎麼獲取接下來頁面的內容?

    帶着這個思考,Selenium神器走入了我的視線。

3 預備知識

3.1 Selenium

3.1.1 簡介

    Selenium 是什麼?一句話,自動化測試工具。它支持各種瀏覽器,包括 Chrome,Safari,Firefox 等主流界面式瀏覽器,如果你在這些瀏覽器裏面安裝一個 Selenium 的插件,那麼便可以方便地實現Web界面的測試。換句話說叫 Selenium 支持這些瀏覽器驅動。Selenium支持多種語言開發,比如 Java,C,Ruby等等,而對於Python,當然也是支持的!

3.1.2 安裝

pip3 install selenium

3.1.3 基礎知識

    詳細內容可查看官網文檔:http://selenium-python.readthedocs.io/index.html

3.1.3.1 小試牛刀

    我們先來一個小例子感受一下 Selenium,這裏我們用 Chrome 瀏覽器來測試。

from selenium import webdriver

browser = webdriver.Chrome()
browser.get('http://www.baidu.com/')

    運行這段代碼,會自動打開瀏覽器,然後訪問百度。

    如果程序執行錯誤,瀏覽器沒有打開,那麼應該是沒有裝 Chrome 瀏覽器或者 Chrome 驅動沒有配置在環境變量裏。下載驅動,然後將驅動文件路徑配置在環境變量即可。

    驅動下載地址:https://sites.google.com/a/chromium.org/chromedriver/downloads

    windows下設置環境變量的方法:

    win+r,輸入sysdm.cpl,點擊確定,出現如下對話框:

    選擇高級->環境變量。在系統變量的Path變量中,添加驅動文件路徑即可(注意:分號)。

    Linux的環境變量也好設置,在~/.bashrc文件中export即可,記得source ~/.bashrc

    當然,你不設置環境變量也是可以的,程序可以這樣寫:

from selenium import webdriver

browser = webdriver.Chrome('path\to\your\chromedriver.exe')
browser.get('http://www.baidu.com/')

    上面的path\to\your\chromedriver.exe 是你的chrome驅動文件位置,可以使用絕對路徑。我們通過驅動的位置傳遞參數,也可以調用驅動,結果如下圖所示:

3.1.3.2 模擬提交

    下面的代碼實現了模擬提交提交搜索的功能,首先等頁面加載完成,然後輸入到搜索框文本,點擊提交,然後使用page_source打印提交後的頁面的信息。

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome()
driver.get("http://www.python.org")
assert "Python" in driver.title
elem = driver.find_element_by_name("q")
elem.send_keys("pycon")
elem.send_keys(Keys.RETURN)
print(driver.page_source)

    全自動的哦,程序操控!是不是很酷炫?

    其中 driver.get 方法會打開請求的URL,WebDriver 會等待頁面完全加載完成之後纔會返回,即程序會等待頁面的所有內容加載完成,JS渲染完畢之後才繼續往下執行。注意:如果這裏用到了特別多的 Ajax 的話,程序可能不知道是否已經完全加載完畢。

    WebDriver 提供了許多尋找網頁元素的方法,譬如 find_element_by_* 的方法。例如一個輸入框可以通過 find_element_by_name 方法尋找 name 屬性來確定。

    然後我們輸入來文本然後模擬點擊了回車,就像我們敲擊鍵盤一樣。我們可以利用 Keys 這個類來模擬鍵盤輸入。

    最後最重要的一點是可以獲取網頁渲染後的源代碼。通過,輸出 page_source 屬性即可。這樣,我們就可以做到網頁的動態爬取了。

3.1.3.3 元素選取

    關於元素的選取,有如下API:

    單個元素選取:

find_element_by_id
find_element_by_name
find_element_by_xpath
find_element_by_link_text
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_class_name
find_element_by_css_selector

    多個元素選取:

find_elements_by_name
find_elements_by_xpath
find_elements_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elements_by_css_selector

    另外還可以利用 By 類來確定哪種選擇方式:

from selenium.webdriver.common.by import By

driver.find_element(By.XPATH, '//button[text()="Some text"]')
driver.find_elements(By.XPATH, '//button')

    By類的一些屬性如下:

ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"

    這些方法跟JavaScript的一些方法有相似之處,find_element_by_id,就是根據標籤的id屬性查找元素,find_element_by_name,就是根據標籤的name屬性查找元素。舉個簡單的例子,比如我想找到下面這個元素:

<input type="text" name="passwd" id="passwd-id" />

    我們可以這樣獲取它:

element = driver.find_element_by_id("passwd-id")
element = driver.find_element_by_name("passwd")
element = driver.find_elements_by_tag_name("input")
element = driver.find_element_by_xpath("//input[@id='passwd-id']")

    前三個都很好理解,最後一個xpath什麼意思?這個無需着急,xpath是非常強大的元素查找方式,使用這種方法幾乎可以定位到頁面上的任意元素,在後面我會進行單獨講解。

3.1.3.4 界面交互

    通過元素選取,我們能夠找到元素的位置,我們可以根據這個元素的位置進行相應的事件操作,例如輸入文本框內容、鼠標單擊、填充表單、元素拖拽等等。由於篇幅原因,我就不一一講解了,主要講解本次實戰用到的鼠標單擊,更詳細的內容,可以查看官方文檔。

elem = driver.find_element_by_xpath("//a[@data-fun='next']")
elem.click()

    比如上面這句話,我使用find_element_by_xpath()找到元素位置,暫且不用理會這句話什麼意思,暫且理解爲找到了一個按鍵的位置。然後我們使用click()方法,就可以觸發鼠標左鍵單擊事件。是不是很簡單?但是有一點需要注意,就是在點擊的時候,元素不能有遮擋。什麼意思?就是說我在點擊這個按鍵之前,窗口最好移動到那裏,因爲如果這個按鍵被其他元素遮擋,click()就觸發異常。因此穩妥起見,在觸發鼠標左鍵單擊事件之前,滑動窗口,移動到按鍵上方的一個元素位置:

page = driver.find_elements_by_xpath("//div[@class='page']")
driver.execute_script('arguments[0].scrollIntoView();', page[-1]) #拖動到可見的元素去

    上面的代碼,就是將窗口滑動到page這個位置,在這個位置,我們能夠看到我們需要點擊的按鍵。

3.1.3.5 添加User-Agent

    使用webdriver,是可以更改User-Agent的,代碼如下:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('user-agent="Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"')
driver = webdriver.Chrome(chrome_options=options)
driver.get('https://www.baidu.com/')

    使用Android的User-Agent打開瀏覽器,畫風是這樣的(第二條新聞的圖片略勁爆):

    Selenium就先介紹這麼多,對於本次實戰內容,已經足夠。那麼接下來,讓我們聊聊xpath。

3.2 Xpath

    這個方法是非常強大的元素查找方式,使用這種方法幾乎可以定位到頁面上的任意元素。在正式開始使用XPath進行定位前,我們先了解下什麼是XPath。XPath是XML Path的簡稱,由於HTML文檔本身就是一個標準的XML頁面,所以我們可以使用XPath的語法來定位頁面元素。

    假設我們現在以圖所示HTML代碼爲例,要引用對應的對象,XPath語法如下:

    絕對路徑寫法(只有一種),寫法如下:

    引用頁面上的form元素(即源碼中的第3行):

/html/body/form[1]

    注意:

  • 元素的xpath絕對路徑可通過firebug直接查詢。

  • 一般不推薦使用絕對路徑的寫法,因爲一旦頁面結構發生變化,該路徑也隨之失效,必須重新寫。

  • 絕對路徑以單/號表示,而下面要講的相對路徑則以//表示,這個區別非常重要。另外需要多說一句的是,當xpath的路徑以/開頭時,表示讓Xpath解析引擎從文檔的根節點開始解析。當xpath路徑以//開頭時,則表示讓xpath引擎從文檔的任意符合的元素節點開始進行解析。而當/出現在xpath路徑中時,則表示尋找父節點的直接子節點,當//出現在xpath路徑中時,表示尋找父節點下任意符合條件的子節點,不管嵌套了多少層級(這些下面都有例子,大家可以參照來試驗)。弄清這個原則,就可以理解其實xpath的路徑可以絕對路徑和相對路徑混合在一起來進行表示,想怎麼玩就怎麼玩。

    下面是相對路徑的引用寫法:

  • 查找頁面根元素://

  • 查找頁面上所有的input元素://input

  • 查找頁面上第一個form元素內的直接子input元素(即只包括form元素的下一級input元素,使用絕對路徑表示,單/號)://form[1]/input

  • 查找頁面上第一個form元素內的所有子input元素(只要在form元素內的input都算,不管還嵌套了多少個其他標籤,使用相對路徑表示,雙//號)://form[1]//input

  • 查找頁面上第一個form元素://form[1]

  • 查找頁面上id爲loginForm的form元素://form[@id='loginForm']

  • 查找頁面上具有name屬性爲username的input元素://input[@name='username']

  • 查找頁面上id爲loginForm的form元素下的第一個input元素://form[@id='loginForm']/input[1]

  • 查找頁面具有name屬性爲contiune並且type屬性爲button的input元素://input[@name='continue'][@type='button']

  • 查找頁面上id爲loginForm的form元素下第4個input元素://form[@id='loginForm']/input[4]

    Xpath功能很強大,所以也可以寫得更加複雜一些,如下面圖所示的HTML源碼。

    如果我們現在要引用id爲“J_password”的input元素,該怎麼寫呢?我們可以像下面這樣寫:
//*[@id='J_login_form']/dl/dt/input[@id='J_password']

    也可以寫成:

//*[@id='J_login_form']/*/*/input[@id='J_password']

    這裏解釋一下,其中//*[@id=’ J_login_form’]這一段是指在根元素下查找任意id爲J_login_form的元素,此時相當於引用到了form元素。後面的路徑必須按照源碼的層級依次往下寫。按照圖(3)所示代碼中,我們要找的input元素包含在一個dt標籤內,而dt又包含在dl標籤內,所以中間必須寫上dl和dt兩層,纔到input這層。當然我們也可以用*號省略具體的標籤名稱,但元素的層級關係必須體現出來,比如我們不能寫成//*[@id='J_login_form']/input[@id='J_password'],這樣肯定會報錯的。

    前面講的都是xpath中基於準確元素屬性的定位,其實xpath作爲定位神器也可以用於模糊匹配。本次實戰,可以進行準確元素定位,因此就不講模糊匹配了。如果有興趣,可以自行了解。

4 動手實戰

    以上面提到的文章爲例,進行爬取講解。URL : https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html

4.1 頁面切換

    由於網頁的百度文庫頁面複雜,可能抓取內容不全,因此使用User-Agent,模擬手機登錄,然後打印文章標題,文章頁數,並進行翻頁。先看下這個網站。

    我們需要找到兩個元素的位置,一個是頁碼元素的位置,我們根據這個元素的位置,將瀏覽器的滑動窗口移動到這個位置,這樣就可以避免click()下一頁元素的時候,有元素遮擋。然後找到下一頁元素的位置,然後根據下一頁元素的位置,觸發鼠標左鍵單擊事件。

    我們審查元素看一下,這兩個元素:

    我們根據這兩個元素,就可以通過xpath查找元素位置,代碼分別如下:

page = driver.find_elements_by_xpath("//div[@class='page']")
nextpage = driver.find_element_by_xpath("//a[@data-fun='next']")

    由於page元素有很多,所以我們使用find_elements_by_xpath()方法查找,然後使用page[-1],也就是鏈表中的最後一個元素的信息進行瀏覽器窗口滑動,代碼如下:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('user-agent="Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"')
driver = webdriver.Chrome(chrome_options = options)
driver.get('https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html')
page = driver.find_elements_by_xpath("//div[@class='page']")
driver.execute_script('arguments[0].scrollIntoView();', page[-1]) #拖動到可見的元素去
nextpage = driver.find_element_by_xpath("//a[@data-fun='next']")
nextpage.click()

    運行效果,自動翻頁了有木有!

4.2 內容爬取

    爬取內容這裏,使用之前重點講過的BeautifulSoup就可以。這裏不再細獎,審查元素,自己分析下就有了。代碼如下:

from selenium import webdriver
from bs4 import BeautifulSoup

options = webdriver.ChromeOptions()
options.add_argument('user-agent="Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"')
driver = webdriver.Chrome(chrome_options=options)
driver.get('https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html')

html = driver.page_source
bf1 = BeautifulSoup(html, 'lxml')
result = bf1.find_all(class_='rtcspage')
for each_result in result:
     bf2 = BeautifulSoup(str(each_result), 'lxml')
     texts = bf2.find_all('p')
     for each_text in texts:
          main_body = BeautifulSoup(str(each_text), 'lxml')
          for each in main_body.find_all(True):
               if each.name == 'span':
                    print(each.string.replace('\xa0',''),end='')
                elif each.name == 'br':
                    print('')

    爬取結果如下:

    爬取的內容還是蠻規整的,對吧?

4.3 整體代碼

    我們能夠翻頁,也能夠爬取當前頁面內容,代碼稍作整合,就可以爬取所有頁面的內容了!找下網頁的規律就會發現,5頁文章放在一個網頁裏。思路:爬取正文內容,再根據爬取到的文章頁數,計算頁數/5.0,得到一個分數,如果這個分數大於1,則翻頁繼續爬,如果小於或等於1,代表到最後一頁了。停止翻頁。有一點注意一下,翻頁之後,等待延時一下,等待頁面加載之後在爬取內容,這裏,我們使用最簡單的辦法,用sleep()進行延時。因此總體代碼如下:

# -*- coding:UTF-8 -*-
from selenium import webdriver
from bs4 import BeautifulSoup
import re

if __name__ == '__main__':

    options = webdriver.ChromeOptions()
    options.add_argument('user-agent="Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"')
    driver = webdriver.Chrome(chrome_options=options)
    driver.get('https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html')

    html = driver.page_source
    bf1 = BeautifulSoup(html, 'lxml')
    result = bf1.find_all(class_='rtcspage')
    bf2 = BeautifulSoup(str(result[0]), 'lxml')
    title = bf2.div.div.h1.string
    pagenum = bf2.find_all(class_='size')
    pagenum = BeautifulSoup(str(pagenum), 'lxml').span.string
    pagepattern = re.compile('頁數:(\d+)頁')
    num = int(pagepattern.findall(pagenum)[0])
    print('文章標題:%s' % title)
    print('文章頁數:%d' % num)

    while True:
        num = num / 5.0
        html = driver.page_source
        bf1 = BeautifulSoup(html, 'lxml')
        result = bf1.find_all(class_='rtcspage')
        for each_result in result:
            bf2 = BeautifulSoup(str(each_result), 'lxml')
            texts = bf2.find_all('p')
            for each_text in texts:
                main_body = BeautifulSoup(str(each_text), 'lxml')
                for each in main_body.find_all(True):
                    if each.name == 'span':
                        print(each.string.replace('\xa0',''),end='')
                    elif each.name == 'br':
                        print('')
            print('\n')
        if num > 1:
            page = driver.find_elements_by_xpath("//div[@class='page']")
            driver.execute_script('arguments[0].scrollIntoView();', page[-1]) #拖動到可見的元素去
            nextpage = driver.find_element_by_xpath("//a[@data-fun='next']")
            nextpage.click()
            time.sleep(3)
        else:
            break

    運行結果:

    瞧,最後一頁的內容也爬取下來了,接下來的工作就簡單了,把這個結果寫到txt文件中,我這裏就不再進行講解了。

    至此,整篇的內容,我們都爬取下來了。是不是很酷?那就開始動手實踐吧!

5 總結

    這樣爬取是可以爬取到內容,但是缺點也很明顯:

  • 沒有處理圖片內容,可以後續完善;

  • 代碼通用性不強,有的文章結構不是這樣,需要對代碼進行略微修改,才能爬取到內容;

  • 對於上百頁的內容爬取有些問題,翻頁方式變了,需要換種方法處理,有興趣的可以自己看下;

  • 等待頁面切換方法太out,可以使用顯示等待的方式,等待頁面加載;

  • selenium雖好,但是有些耗時,可以使用PhantomJS對這部分代碼進行替換;

  • 最後,我感覺我的方法可能有些low,如果有更好的方法,歡迎交流。

    PS:如果覺得本篇本章對您有所幫助,歡迎關注、評論、頂!

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