大資料時代,各行各業對資料採集的需求日益增多,網路爬蟲的運用也更為廣泛,越來越多的人開始學習網路爬蟲這項技術,K哥爬蟲此前已經推出不少爬蟲進階、逆向相關文章,為實現從易到難全方位覆蓋,特設【0基礎學爬蟲】專欄,幫助小白快速入門爬蟲,本期為自動化工具 Selenium 的使用。
概述
目前,很多網站都採用 Ajax 等技術進行動態載入資料,想要採集這類網站的資料,需要透過抓包對網站的資料介面進行分析,去尋找想要採集的資料由哪個介面傳輸。而且,就算找到了資料介面,這些介面可能也是被加密過的,想要透過介面獲取資料,需要對加密引數進行逆向分析,這個過程對於初學者來說非常複雜。
為了解決這些問題,能夠更加簡單的進行爬取資料,我們可以使用到一些自動化工具,如 Selenium、playwright、pyppeteer 等,這些工具可以模擬瀏覽器執行,直接獲取到資料載入完成後的網頁原始碼,這樣我們就可以省去複雜的抓包、逆向流程,直接拿到資料。
Selenium 的使用
介紹
Selenium 是一個流行的自動化測試框架,可用於測試 Web 應用程式的使用者介面。它支援多種程式語言,如Java、Python、Ruby等,並提供了一系列 API,可以直接操作瀏覽器進行測試。
安裝
使用 selenium 首先需要下載瀏覽器驅動檔案,這裡以谷歌瀏覽器為例。在驅動下載頁面找到與自己瀏覽器版本最為接近的檔案,如我的谷歌瀏覽器版本為 112.0.5615.86
,最接近的檔案為 112.0.5615.49
,選擇此檔案,下載對應系統版本的壓縮包,將壓縮包中的chromedriver.exe程式放到python目錄中。因為正常情況下Python在安裝時就會被新增到系統環境變數之中,將chromedriver.exe放到Python目錄下它就可以在任意位置被執行。
新增好驅動檔案後需要安裝 Python 的第三方庫 selenium。
pip install selenium
使用
Selenium 支援多種瀏覽器,如谷歌、火狐、Edge、Safari等,這裡我們以谷歌瀏覽器為例。
from selenium import webdriver
# 初始化瀏覽器物件
driver = webdriver.Chrome()
# 驅動瀏覽器開啟目標網址
driver.get('https://www.baidu.com/')
# 列印當前頁面的原始碼
print(driver.page_source)
# 關閉瀏覽器
driver.quit()
執行程式碼後我們會發現自動開啟了一個瀏覽器,訪問了目標網址,在控制檯輸出了頁面的原始碼,然後自動關閉。
Selenium 提供了一系列實用的 Api,透過它我們可以實現更多操作。
元素查詢
在之前的文章《解析庫的使用》中,我們已經講到了 Xpath、bs4 這兩個庫的使用方法,講到了 Xpath 的路徑表示式和 CSS 選擇器,因此這裡主要講解定位方法,路徑表示式與 CSS 選擇器的使用可以去前文中瞭解。
以京東首頁為例,想要獲取秒殺欄目的商品資訊,我們可以透過多種方法來進行定位。
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.jd.com/')
# 根據 Xpath 定位
goods_xpath = driver.find_elements(By.XPATH, '//div[@class="slider_list"]/div/a[@class="slider_item seckill-item slider_active"]')
# 根據 Css 選擇器定位
goods_css = driver.find_elements(By.CSS_SELECTOR, 'a[class="slider_item seckill-item slider_active"]')
# 根據類名定位
goods_class_name = driver.find_elements(By.CLASS_NAME,'seckill-item')
print(goods_xpath)
for goods in goods_xpath:
# 輸出節點的文字資訊
print(goods.text)
driver.quit()
# [<selenium.webdriver.remote.webelement.WebElement(session="f49c1906753e404ca0a017...]
# 歐臻廷保溼修護亮顏銀霜面霜70ml護膚品化妝品乳液滋潤送女友禮物禮盒款
# ¥1380.00
# Redmi K50Pro 天璣9000 AMOLED 2K柔性直屏 OIS光學防抖 120W快充 幻鏡 8GB+256GB 5G智慧手機 小米紅米
# ¥2619.00
# 卡詩(KERASTASE)黑鑽鑰源魚子醬洗髮水250ml 改善毛躁呵護受損
# ¥219.00
除了示例程式碼中的,還有其它定位方法:
driver.find_elements(By.ID,'ID')
driver.find_elements(By.LINK_TEXT,'LINK_TEXT')
driver.find_elements(By.PARTIAL_LINK_TEXT,'PARTIAL_LINK_TEXT')
driver.find_elements(By.TAG_NAME,'TAG_NAME')
元素互動
Selenium 可以實現對頁面中元素的點選、輸入等操作。
想要採集京東的指定商品資訊,首先需要在輸入框輸入商品名稱,然後點選搜尋按鈕,網頁就會跳轉到搜尋頁面,展示我們搜尋的商品資訊。這個流程我們也可以透過 Selenium 來模擬實現。
driver.get('https://www.jd.com/')
# 獲取搜尋框
search = driver.find_element(By.XPATH,'//div[@role="serachbox"]/input')
# 獲取查詢按鈕
button = driver.find_element(By.XPATH,'//div[@role="serachbox"]/button')
# 在搜尋框中輸入 Python
search.send_keys('Python')
# 點選查詢按鈕
button.click()
等待
在我們使用 Selenium 時會遇到以下兩種情況:
- 頁面未載入完畢,但是我們需要的元素已經載入完畢
- 頁面載入完畢,但是我們需要的元素為載入完畢
Selenium 的 get 方法是預設等待頁面載入完畢後再執行下面的操作。在遇到第一種情況時,要採集的資料已經生成了,但是可能由於某個資源載入緩慢導致頁面一直在載入中狀態,這樣 Selenium 就會一直等待頁面完全載入,造成採集速度緩慢等問題。而情況二,頁面已經載入完成了,但是要採集的資料依舊沒有渲染出來,這就使 Selenium 定位元素失敗導致程式異常。為了避免解決這兩種情況,我們可以設定不等待頁面完全載入,只等待目標元素載入完畢。
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
caps = DesiredCapabilities().CHROME
#不等待頁面載入
caps["pageLoadStrategy"] = "none"
driver = webdriver.Chrome(desired_capabilities=caps)
強制等待
使用 time.sleep() 實現強制等待。不推薦使用。
driver.get('https://www.jd.com/')
# 強制休眠6秒
time.sleep(6)
隱式等待
等待頁面載入的時間,當頁面載入完成後執行下一步,如果載入時間超過設定的時間時直接執行下一步。不推薦使用。
# 隱式等待10秒
driver.implicitly_wait(10)
driver.get('https://www.jd.com/')
顯式等待
等待條件滿足後執行下一步,條件不滿足則一直等待,當超過設定的時間時丟擲異常。推薦使用。
from selenium import webdriver
import selenium.common.exceptions
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get('https://www.jd.com/')
try:
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'a[class="slider_item seckill-item slider_active"]')
)
)
except selenium.common.exceptions.TimeoutException:
print('元素載入超時')
當 CSS 選擇器指向的元素存在時則執行下一部,不存在則繼續等待,直到超過設定的10秒,丟擲超時異常。
Actions
上文中講到了元素互動,其中點選、輸入行為都是屬於 Selenium 的動作 Api 之中的,除此之外,Selenium還提供了非常豐富的動作 Api,這裡只介紹常用的方法。
滑鼠操作
from selenium.webdriver import ActionChains
# 單擊元素並按住
clickable = driver.find_element(By.ID, "clickable")
ActionChains(driver).click_and_hold(clickable).perform()
# 雙擊,將滑鼠移動到元素中心並雙擊
clickable = driver.find_element(By.ID, "clickable")
ActionChains(driver).double_click(clickable).perform()
# 按偏移量移動滑鼠
mouse_tracker = driver.find_element(By.ID, "mouse-tracker")
ActionChains(driver).move_to_element_with_offset(mouse_tracker, 8, 0).perform()
# 按當前指標位置進行偏移,如之前沒有移動滑鼠,則預設在視窗的左上角。(13, 15)為橫縱座標的偏移值,13為向右移動13,15為向下移動15,負數則反之。
ActionChains(driver).move_by_offset( 13, 15).perform()
# 按偏移拖放。點選元素並按鈕,移動指定偏移量,然後釋放滑鼠
draggable = driver.find_element(By.ID, "draggable")
start = draggable.location
finish = driver.find_element(By.ID, "droppable").location
ActionChains(driver).drag_and_drop_by_offset(draggable, finish['x'] - start['x'], finish['y'] - start['y']).perform()
滾輪
# 滾動到指定元素
iframe = driver.find_element(By.TAG_NAME, "iframe")
ActionChains(driver).scroll_to_element(iframe).perform()
# 按給定值滾動,(0, delta_y) 為向右和向下滾動的量,負值則反之。
footer = driver.find_element(By.TAG_NAME, "footer")
delta_y = footer.rect['y']
ActionChains(driver).scroll_by_amount(0, delta_y).perform()
反檢測
Selenium 有著非常明顯的缺陷,就是容易被網站檢測到。我們透過 Selenium 開啟網頁時會發現,視窗上方會顯示瀏覽器正受到自動測試軟體的控制,這就說明 Selenium 驅動瀏覽器與使用者正常開啟瀏覽器是不同的,它存在著許多 WebDriver 的特徵,網站可以透過檢測這些特徵來禁止 Selenium 訪問。
我們可以透過一些特徵值檢測的網站來對比正常訪問與 Selenium 訪問的區別。
上面是正常訪問,下面是 Selenium 訪問,可以很清晰的看到 WebDriver 一欄標紅了,這就說明 Selenium 被檢測到了。網站的檢測原理主要是透過檢查 window.navigator 物件中是否存在 webdriver 屬性。我們瞭解到這一點後,可以透過一些操作來修改window.navigator 物件,在頁面未載入時將它的 webdriver 屬性設定為 false,這樣或許就能避開網站的檢測機制。
from selenium import webdriver
from selenium.webdriver import ChromeOptions
options = ChromeOptions()
# 以最高許可權執行
options.add_argument('--no-sandbox')
# navigator.webdriver 設定為 false
options.add_argument("--disable-blink-features=AutomationControlled")
# 隱藏"Chrome正在受到自動軟體的控制"提示
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
driver = webdriver.Chrome(options=options)
with open('./stealth.min.js', 'r') as f:
js = f.read()
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': js})
可以看到,我們進行了一些隱藏特徵的操作,但在最後我們讀取一個檔案,然後將這個檔案資訊傳入到了execute_cdp_cmd()方法中,這個操作其實也是在隱藏特徵。
stealth.min.js 來自於 puppeteer 的一個外掛,puppeteer 是一個控制 headless Chrome 的 Node.js API ,puppeteer 有一個外掛名為 puppeteer-extra-plugin-stealth,它的開發目的就是為了防止 puppeteer 被檢測,它可以隱藏許多自動化特徵。puppeteer-extra 的作者也編寫了一個指令碼,用於將最新的特徵隱藏方法puppeteer-extra-stealth 提取到 JS 檔案之中,生成的 JS 檔案可以用於純 CDP 實現,也可以用於測試開發工具中的檢測規避。而 Selenium 正好支援 CDP 的呼叫,CDP 全稱(Chrome DevTools Protocol),利用它可以在瀏覽器載入之前執行 JS 語句。
如果你已經安裝了 node.js ,npx extract-stealth-evasions
執行此命令就可以生成 stealth.min.js 檔案。下圖就隱藏特徵後訪問結果。
無頭模式
無頭模式下網站執行不會彈出視窗,可以減少一些資源消耗,也避免了瀏覽器視窗執行時對裝置正常使用帶來的影響,在伺服器上執行需要用到。但是無頭模式下被網站檢測的特徵點非常多,因此需要根據自己的應用場景來使用。
options = ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
總結
使用 Selenium 來進行資料的爬取是一種優勢與劣勢都非常明顯的選擇。它的優勢就是簡單,不需要對網站進行除錯,不需要關注資料的來源,大大減少了爬蟲程式的開發時間。它的劣勢有多種:採集效率低,資源佔用大,不穩定,容易被檢測,且需要依賴於 WebDriver,當瀏覽器更新後就需要更新對應的 WebDriver。因此 Selenium 適用於那些逆向難度較大,且對採集效率要求不高的場景。