析動態內容
根據權威機構發布的全球互聯網可訪問性審計報告,全球約有四分之三的網站其內容或部分內容是通過JavaScript動態生成的,這就意味著在瀏覽器窗口中“查看網頁源代碼”時無法在HTML代碼中找到這些內容,也就是說我們之前用的抓取數據的方式無法正常運轉了。解決這樣的問題基本上有兩種方案,一是JavaScript逆向工程;另一種是渲染JavaScript獲得渲染后的內容。
JavaScript逆向工程
下面我們以“360圖片”網站為例,說明什么是JavaScript逆向工程。其實所謂的JavaScript逆向工程就是找到通過Ajax技術動態獲取數據的接口。
但是當我們在瀏覽器中通過右鍵菜單“顯示網頁源代碼”的時候,居然驚奇的發現頁面的HTML代碼中連一個<img>標簽都沒有,那么我們看到的圖片是怎么顯示出來的呢?原來所有的圖片都是通過JavaScript動態加載的,而在瀏覽器的“開發人員工具”的“網絡”中可以找到獲取這些圖片數據的網絡API接口,
那么結論就很簡單了,只要我們找到了這些網絡API接口,那么就能通過這些接口獲取到數據,當然實際開發的時候可能還要對這些接口的參數以及接口返回的數據進行分析,了解每個參數的意義以及返回的JSON數據的格式,這樣才能在我們的爬蟲中使用這些數據。
關于如何從網絡API中獲取JSON格式的數據并提取出我們需要的內容,在之前的《文件和異常》一文中已經講解過了,這里不再進行贅述。
使用Selenium
盡管很多網站對自己的網絡API接口進行了保護,增加了獲取數據的難度,但是只要經過足夠的努力,絕大多數還是可以被逆向工程的,但是在實際開發中,我們可以通過瀏覽器渲染引擎來避免這些繁瑣的工作,WebKit就是一個利用的渲染引擎。
WebKit的代碼始于1998年的KHTML項目,當時它是Konqueror瀏覽器的渲染引擎。2001年,蘋果公司從這個項目的代碼中衍生出了WebKit并應用于Safari瀏覽器,早期的Chrome瀏覽器也使用了該內核。在Python中,我們可以通過Qt框架獲得WebKit引擎并使用它來渲染頁面獲得動態內容,關于這個內容請大家自行閱讀《爬蟲技術:動態頁面抓取超級指南》一文。
如果沒有打算用上面所說的方式來渲染頁面并獲得動態內容,其實還有一種替代方案就是使用自動化測試工具Selenium,它提供了瀏覽器自動化的API接口,這樣就可以通過操控瀏覽器來獲取動態內容。首先可以使用pip來安裝Selenium。
pip3 install selenium
下面以“阿里V任務”的“直播服務”為例,來演示如何使用Selenium獲取到動態內容并抓取主播圖片。
import requests from bs4 import BeautifulSoup def main(): resp = requests.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang') soup = BeautifulSoup(resp.text, 'lxml') for img_tag in soup.select('img[src]'): print(img_tag.attrs['src']) if __name__ == '__main__': main()
運行上面的程序會發現沒有任何的輸出,因為頁面的HTML代碼上根本找不到<img>標簽。接下來我們使用Selenium來獲取到頁面上的動態內容,再提取主播圖片。
from bs4 import BeautifulSoup from selenium import webdriver from selenium.webdriver.common.keys import Keys def main(): driver = webdriver.Chrome() driver.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang') soup = BeautifulSoup(driver.page_source, 'lxml') for img_tag in soup.body.select('img[src]'): print(img_tag.attrs['src']) if __name__ == '__main__': main()
在上面的程序中,我們通過Selenium實現對Chrome瀏覽器的操控,如果要操控其他的瀏覽器,可以創對應的瀏覽器對象,例如Firefox、IE等。運行上面的程序,如果看到如下所示的錯誤提示,那是說明我們還沒有將Chrome瀏覽器的驅動添加到PATH環境變量中,也沒有在程序中指定Chrome瀏覽器驅動所在的位置。
selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home
為了解決上面的問題,可以到Selenium的官方網站找到瀏覽器驅動的下載鏈接并下載需要的驅動,在Linux或macOS系統下可以通過下面的命令來設置PATH環境變量,Windows下配置環境變量也非常簡單,不清楚的可以自行了解。
export PATH=$PATH:/Users/Hao/Downloads/Tools/chromedriver/
其中/Users/Hao/Downloads/Tools/chromedriver/就是chromedriver所在的路徑。
用 Windows 自帶的 MSAA 發現瀏覽器窗口,自動執行 JavaScript 很簡單!
aardio 代碼示例:
import winex;
import winex.accObject;
import winex.key;
//遍歷瀏覽器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") {
//獲取 MSAA 接口對象
var accObject = winex.accObject.fromWindow(hwnd);
//查找文本框
var edit = accObject.find(
role = 0x2A;
name = "<Address and search bar>|<地址和搜索欄>";
)
if(edit){
//獲取瀏覽器地址欄內容
var url = edit.value();
//修改瀏覽器地址欄內容
edit.setValue("javascript:alert(document.location.href)")
edit.takeFocus();
//后臺發送按鍵消息
winex.key.click(hwnd,"ENTER");
thread.delay(1000);
}
}
運行一下看看效果:
所有 Windows 系統都自帶 MSAA,接口簡單,易于使用,生成的 EXE 程序體積也會很小。aardio 標準庫 winex.accObject 則對 MSAA 做了進一步封裝,用法就更簡單了。
1、什么是窗口
「窗口」是應用程序在屏幕上創建的一個顯示區域,通常用于接收并處理用戶操作,并顯示要輸出的內容。窗口上的文本框、按鈕、菜單這些也都是窗口。
我們一般將頂層獨立窗口稱為「窗體」,而窗體上的子窗口稱為「控件」。
2、什么是窗口句柄
窗口句柄是一個用于唯一標準窗口的整數值。
其實很多系統資源,例如位圖、進程、線程都有唯一標準資源的句柄。
在 aardio 中所有句柄都存為指針類型,唯有窗口句柄是普通的數值類型。
3、無句柄窗口
無句柄窗口是指該窗口上的控件沒有創建子窗口,典型的例如網頁上的按鈕、文本框都沒有窗口句柄。MSAA 可用于操作無句柄窗口。
請在 aardio 中打開 『工具 > 探測器 > 窗口探測器』:
拖動『窗口探測器』左下角的十字圖標到目標窗口上,就會顯示窗口信息。
使用窗口探測器我們可以發現 Chrome, Edge 等瀏覽器的網頁窗口類名都是 "Chrome_WidgetWin_1", 所以我們可以用下面的 aadio 代碼獲取所有打開的瀏覽器窗口:
import winex;
for hwnd,title in winex.each("Chrome_WidgetWin_1") {
}
aardio 中 winex 名字空間的所有庫、函數都是用于操作外部程序窗口的。
winex.each() 用于遍歷所有符合條件的窗口, winex.each() 的第一個參數可以指定窗口類名,這個類名支持模式匹配語法( 類正則表達式,但更簡單,用法請參考語法文檔 )。
自窗口句柄獲取 MSAA 對象,代碼很簡單:
import winex;
import winex.accObject;
//遍歷瀏覽器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") {
//獲取 MSAA 接口對象
var accObject = winex.accObject.fromWindow(hwnd);
}
請在 aardio 中打開 winex.accObject 的文檔或源碼,搜索“ACC對象瀏覽工具” 并下載該工具( inspect.exe )。
運行 inspect.exe ,點選下圖的『 Watch Cursor 』圖標:
也就是允許探測鼠標指向的窗口。
然后將鼠標移向瀏覽器的地址欄,Inspect 找到了地址欄所在的 ACC 對象,并顯示了一堆信息,我們重點關注這幾行:
Name: "Address and search bar"
Role: editable text (0x2A)
Name 是 ACC 對象的名稱。
Role 是 ACC 對象的角色,其實就是控件類型。
根據上面的信息,我們修改代碼獲取瀏覽器地址欄:
import winex;
import winex.accObject;
import console;
//遍歷瀏覽器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") {
//獲取 MSAA 接口對象
var accObject = winex.accObject.fromWindow(hwnd);
//查找地址欄
var edit = accObject.find(
role = 0x2A;
name = "<Address and search bar>|<地址和搜索欄>";
)
//顯示地址欄的內容
if(edit) console.log( edit.value() )
}
console.pause();
在 aardio 中運行上面的代碼,我們干凈利索地拿到了瀏覽器地址欄的網址。
拿到一個 accObject 對象以后,可以調用 accObject.find() 函數繼續查找符合條件的子節點。而查找參數就是我們用 Inspect.exe 探測到的參數。
查找參數中,role, state 可以是文本,也可以是數值,一般建議用數值( 速度更快 )。
上面的 name 參數用到了模式匹配:
name = "<Address and search bar>|<地址和搜索欄>";
這個模式表達式中的 | 線是 “或” 的意思,而 < > 括號用于包含子串。如果目標 ACC 對象的 name 包含 "Address and search bar" 或者 "地址和搜索欄" 都符合條件。
aardio 標準庫 key,mouse 用于對前臺窗口模擬按鍵鼠標。
例如:
key.press("ENTER")
作用就是模擬按下回車鍵。
如果我們改用 winex.key, winex.mouse 就可以直接向后臺窗口發送按鍵或鼠標消息。這樣的好處是不會干擾用戶操作。
例如向瀏覽器窗口發送回車鍵消息:
import winex;
import winex.key;
//遍歷瀏覽器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") {
//后臺發送按鍵消息
winex.key.click(hwnd,"ENTER");
thread.delay(1000);
}
操作按鍵的函數都需要用到鍵名。
我們還可以直接運行 aardio 『 工具 > 鼠標按鍵 > 按鍵指令生成器』
在『按鍵指令生成器』窗口內我們任意按鍵,就可以顯示對應的鍵名了。
有時候先進也是一種負擔,飛機比自行車先進,這不等于任何時候都要用飛機替代自行車。
當然,在 aardio 中調用 UIA 也是很簡單的,示例( 可獨立運行 ):
import process;
process.executeWaitInput("notepad.exe",io.getSpecial(0x25/*_CSIDL_SYSTEM*/,"drivers\etc\HOSTS"));
//導入 .Net 類
import System.Windows.Automation;
TreeScope = ::UIAutomationTypes.import("System.Windows.Automation.TreeScope");
//訪問 .Net 類的靜態成員
Automation = System.Windows.Automation;
AutomationElement = Automation.AutomationElement;
RootElement = AutomationElement.RootElement;
//查找記事本窗口
var condNotepadClass = Automation.PropertyCondition(AutomationElement.ClassNameProperty,"Notepad")
var notepad = RootElement.FindFirst( TreeScope.Children, condNotepadClass)
//查找記事本的編輯框
var condEditClass = Automation.PropertyCondition(AutomationElement.ClassNameProperty,"Edit");
var editBox = notepad.FindFirst( TreeScope.Descendants, condEditClass);
if(!editBox){
//Windows 11
condEditClass = Automation.PropertyCondition(AutomationElement.ClassNameProperty,"RichEditD2DPT");
editBox = notepad.FindFirst( TreeScope.Descendants, condEditClass);
}
//獲取記事本內的文本
var textPattern = editBox.GetCurrentPattern(Automation.TextPattern.Pattern);
var text = textPattern.DocumentRange.GetText(50)
import win.dlg.message;
win.dlg.message().info(text + " ……")
aardio 調用 WebDriver 就更簡單了,示例:
我們看一下通過Python Selenium WebDriver執行JavaScript語句的幾種不同方法。
在本教程中,讓我們分析Selenium WebDriver中使用最少但功能最強大的功能。是的,我將討論JavaScript執行器,并向您展示通過Python Selenium WebDriver執行JavaScript語句的幾種不同方法。
可能會發生這種情況,在某些實時項目中,Selenium WebDriver無法對特定的Web元素執行操作。例如,由于WebDriver模擬最終用戶交互,因此很自然地會拒絕單擊最終用戶看不到的元素(有時即使Web元素在頁面上可見,也會發生這種情況)。可能有其他幾個類似的原因或情況。
在這些情況下,我們可以依靠JavaScript來單擊或對該Web元素執行操作,并且可以通過WebDriver執行這些JavaScript語句。
您可以使用JavaScript執行WebElement界面所做的所有操作。
什么是JavaScript?
JavaScript是一種腳本語言,它在客戶端運行,即在瀏覽器上運行,并且當您瀏覽網頁時會做一些神奇的事情。有關更多詳細信息,請在DZone上搜索關鍵字“JavaScript”。
我們如何在WebDriver中使用JavaScript?
Python Selenium WebDriver提供了一個內置方法:
driver.execute_script("some javascript code here");
我們可以通過兩種方式在瀏覽器中執行JavaScript。
方法1:在文檔根級別執行JavaScript
在這種情況下,我們使用JavaScript提供的方法捕獲我們想要使用的元素,然后在其上聲明一些操作并使用WebDriver執行此JavaScript。
例:
javaScript = "document.getElementsByName('username')[0].click();"
driver.execute_script(javaScript)
我們在這里做什么?
第1步:我們正在使用JavaScript檢查并通過屬性“名稱”獲取元素。(另外,可以使用'id'和'class'屬性。)
第2步:使用JavaScript聲明并對元素執行單擊操作。
第3步:調用execute_script()方法并將我們創建的JavaScript作為字符串值傳遞。
請注意 上面[0] 的 getElementsByName('username')[0] 聲明。JavaScript函數 getElementsByName , getElementsByClassName 等返回所有匹配的元件的陣列。在我們的例子中,我們需要對可以通過的第一個匹配元素進行操作 index [0] 。如果您知道自己在做什么,即,如果您知道要操作的元素的索引,則可以直接使用索引,例如 getElementsByName('username')[2] 。
但是,如果您使用的是JavaScript函數' getElementById ',則不需要使用任何索引,因為它只返回一個元素('id'應該是唯一的)。
執行時,WebDriver會將JavaScript語句注入瀏覽器,腳本將執行該任務。在我們的示例中,它對目標元素執行單擊操作。此JavaScript具有自己的命名空間,不會干擾實際網頁中的JavaScript。
方法2:在元素級別執行JavaScript
在這種情況下,我們使用WebDriver捕獲我們想要使用的元素,然后使用JavaScript在其上聲明一些操作,并通過將web元素作為參數傳遞給JavaScript來使用WebDriver執行此JavaScript。
這令人困惑嗎?讓我們分解吧。
例如:
userName = driver.find_element_by_xpath("http://button[@name='username']")
driver.execute_script("arguments[0].click();", userName)
我們在這里做什么?
第1步:使用WebDriver提供的方法檢查和捕獲元素,例如'find_element_by_xpath ':
userName = driver.find_element_by_xpath("http://button[@name='username']")
第2步:使用JavaScript聲明并對元素執行單擊操作:
arguments[0].click()
第3步:execute_script() 使用我們創建的JavaScript語句作為字符串值調用 方法,并使用WebDriver作為參數捕獲Web元素:
driver.execute_script("arguments[0].click();", userName)
上面兩行代碼可以縮短為下面的格式,我們使用WebDriver找到一個元素,聲明一些JavaScript函數,并使用WebDriver執行JavaScript。
driver.execute_script("arguments[0].click();",
driver.find_element_by_xpath("http://button[@name='username']"))
更頻繁面臨的另一個問題是需要滾動到網頁的底部。您可以在一行代碼中執行此操作:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
此外,您的語句中可以有多個JavaScript操作。
例如:
userName = driver.find_element_by_xpath("http://button[@name='username']")
password = driver.find_element_by_xpath("http://button[@name='password']")
driver.execute_script("arguments[0].click();arguments[1].click();", userName, password)
在這種情況下,web元素的順序的使用很重要。訪問 index 與 [0] 一個JavaScript語句中的任何位置將檢索傳遞的第一個網頁元素。
driver.execute_script("arguments[1].click();arguments[0].click();", userName, password)
如何返回值
JavaScript執行程序的另一個重要方面是它可用于從Web元素中獲取值。這意味著該 execute_script() 方法可以返回值。
例如:
print driver.execute_script('return document.getElementById("fsr").innerText')
請注意,如果您想要JavaScript代碼返回的內容,則需要使用return。此外,可以使用Selenium定位元素并將其傳遞到腳本中。
什么元素找不到會發生什么?
當JavaScript找不到要操作的元素時,它會拋出帶有相應錯誤消息的WebDriver異常。
場景1:我們正在嘗試使用' print driver.execute_script('return document.getElementById("fsr").innerText') ' 來讀取屬性, 但網頁中沒有這樣的元素。我們在異常跟蹤中收到以下消息:
selenium.common.exceptions.WebDriverException: Message: unknown error: Cannot read property 'innerText' of null
場景2:我們試圖在JavaScript中使用無效的操作或錯誤函數名稱,例如' print driver.execute_script('document.getElementById("fsr").clic();') '。(注意click() 方法名稱中的拼寫錯誤 。)
selenium.common.exceptions.WebDriverException: Message: unknown error: document.getElementById(...).clic is not a function
摘要
以下是可以使用JavaScript的一些潛在操作的摘要。
使用Selenium處理DOM時,JavaScript的基本知識有很大幫助。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。