一、selenium適用場景
二、爬取目標
三、爬取列表頁
(1)初始化
(2)加載列表頁
(3)解析列表頁
(4)main
四、爬取詳情頁
(1)加載詳情頁
(2)解析詳情頁?
(3)修改main()
五、存儲數據
六、完整代碼
????????我們學習了 Selenium 的基本用法,【【14】Selenium的基本使用-CSDN博客】本節我們就來結合一個實際的案例來體會一下 Selenium 的適用場景以及使用方法。
一、selenium適用場景
????????在前面的實戰案例中,有的網頁我們可以直接用 requests 來爬取,有的可以直接通過分析 Ajax 來爬取,不同的網站類型有其適用的爬取方法。
????????Selenium 同樣也有其適用場景:對于那些帶有 JavaScript 渲染的網頁,①有些情況下我們可以直接用 requests 來模擬 Ajax 請求來直接得到數據。②有些情況下 Ajax 的一些請求接口可能帶有一些加密參數,如 token、sign 等等,如果不分析清楚這些參數是怎么生成的話,我們就難以模擬和構造這些參數。怎么辦呢?這時候我們可以直接選擇使用 Selenium 驅動瀏覽器渲染的方式來另辟蹊徑,實現所見即所得的爬取【相當于繞過了 Ajax 請求分析和模擬的階段,直達目標】
????????然而 Selenium 當然也有其局限性,它的爬取效率較低,有些爬取需要模擬瀏覽器的操作,實現相對煩瑣。不過在某些場景下也不失為一種有效的爬取手段。?
二、爬取目標
適用 Selenium 的站點來做案例,其鏈接為?https://spa2.scrape.center/?還是和之前一樣的電影網站,頁面如圖所示。
其 Ajax 請求接口和每部電影的 URL 都包含了加密參數,
比如我們點擊任意一部電影,觀察一下 URL 的變化,如圖所示:
????????這里我們可以看到詳情頁的 URL 和之前就不一樣了,在之前的案例中,URL 的 detail 后面本來直接跟的是 id,如 1、2、3 等數字,但是這里直接變成了一個長字符串,看似是一個 Base64 編碼的內容,所以這里我們無法直接根據規律構造詳情頁的 URL 了。?
????????好,那么接下來我們直接看看 Ajax 的請求,我們從列表頁的第 1 頁到第 10 頁依次點一下,觀察一下 Ajax 請求是怎樣的,如圖所示:
?????????可以看到這里接口的參數比之前多了一個 token,而且每次請求的 token 都是不同的,這個 token 同樣看似是一個 Base64 編碼的字符串。更困難的是,這個接口還是有時效性的,如果我們把 Ajax 接口 URL 直接復制下來,短期內是可以訪問的,但是過段時間之后就無法訪問了,會直接返回 401 狀態碼。
一段時間后:
?????????那現在怎么辦呢?之前我們可以直接用 requests 來構造 Ajax 請求,但現在 Ajax 請求接口帶了這個 token,而且還是可變的,現在我們也不知道 token 的生成邏輯,那就沒法直接通過構造 Ajax 請求的方式來爬取了。這時候我們可以把 token 的生成邏輯分析出來再模擬 Ajax 請求,但這種方式相對較難。所以這里我們可以直接用 Selenium 來繞過這個階段,直接獲取最終 JavaScript 渲染完成的頁面源碼,再提取數據就好了。
所以本課時我們要完成的目標有:
- 通過 Selenium 遍歷列表頁,獲取每部電影的詳情頁 URL。
- 通過 Selenium 根據上一步獲取的詳情頁 URL 爬取每部電影的詳情頁。
- 提取每部電影的名稱、類別、分數、簡介、封面等內容。
三、爬取列表頁
(1)初始化
from selenium import webdriverfrom selenium.common.exceptions import TimeoutExceptionfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.chrome.service import Serviceimport logginglogging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s: %(message)s')INDEX_URL = 'https://spa2.scrape.center/page/{page}'TIME_OUT = 10TOTAL_PAGE = 10
driver_path = "E:/chromedriver-win64/chromedriver.exe"
#
service = Service(driver_path)
browser = webdriver.Chrome(service=service)wait = WebDriverWait(browser, TIME_OUT)
????????首先我們導入了一些必要的 Selenium 模塊,包括 webdriver、WebDriverWait 等等,后面我們會用到它們來實現頁面的爬取和延遲等待等設置。然后接著定義了一些變量和日志配置,和之前幾課時的內容是類似的。接著我們使用 Chrome 類生成了一個 webdriver 對象,賦值為 browser,這里我們可以通過 browser 調用 Selenium 的一些 API 來完成一些瀏覽器的操作,如截圖、點擊、下拉等等。最后我們又聲明了一個 WebDriverWait 對象,利用它我們可以配置頁面加載的最長等待時間。
(2)加載列表頁
????????好,接下來我們就觀察下列表頁,實現列表頁的爬取吧。這里可以觀察到列表頁的 URL 還是有一定規律的,比如第一頁為?https://dynamic2.scrape.center/page/1,頁碼就是 URL 最后的數字,所以這里我們可以直接來構造每一頁的 URL。
?????????那么每個列表頁要怎么判斷是否加載成功了呢?很簡單,當頁面出現了我們想要的內容就代表加載成功了。在這里我們就可以用 Selenium 的隱式判斷條件來判定,比如每部電影的信息區塊的 CSS 選擇器為 #index .item,如圖所示。
????????所以這里我們直接使用 visibility_of_all_elements_located 判斷條件加上 CSS 選擇器的內容即可判定頁面有沒有加載出來,配合 WebDriverWait 的超時配置,我們就可以實現 10 秒的頁面的加載監聽。如果 10 秒之內,我們所配置的條件符合,則代表頁面加載成功,否則則會拋出 TimeoutException 異常。
代碼實現如下:
def?scrape_page(url,?condition,?locator):logging.info('scraping?%s',?url)try:browser.get(url)wait.until(condition(locator))except?TimeoutException:logging.error('error?occurred?while?scraping?%s',?url,?exc_info=True)def?scrape_index(page):url?=?INDEX_URL.format(page=page)scrape_page(url,?condition=EC.visibility_of_all_elements_located,locator=(By.CSS_SELECTOR,?'#index?.item'))
????????第一個方法 scrape_page 依然是一個通用的爬取方法,它可以實現任意 URL 的爬取和狀態監聽以及異常處理,它接收 url、condition、locator 三個參數,其中 url 參數就是要爬取的頁面 URL;condition 就是頁面加載的判定條件,它可以是 expected_conditions 的其中某一項判定條件,如 visibility_of_all_elements_located、visibility_of_element_located 等等;locator 代表定位器,是一個元組,它可以通過配置查詢條件和參數來獲取一個或多個節點,如 (By.CSS_SELECTOR, '#index .item') 則代表通過 CSS 選擇器查找 #index .item 來獲取列表頁所有電影信息節點。另外爬取的過程添加了 TimeoutException 檢測,如果在規定時間(這里為 10 秒)沒有加載出來對應的節點,那就拋出 TimeoutException 異常并輸出錯誤日志。
????????第二個方法 scrape_index 則是爬取列表頁的方法,它接收一個參數 page,通過調用 scrape_page 方法并傳入 condition 和 locator 對象,完成頁面的爬取。這里 condition 我們用的是 visibility_of_all_elements_located,代表所有的節點都加載出來才算成功。
注意,這里爬取頁面我們不需要返回任何結果,因為執行完 scrape_index 后,頁面正好處在對應的頁面加載完成的狀態,我們利用 browser 對象可以進一步進行信息的提取。
(3)解析列表頁
好,現在我們已經可以加載出來列表頁了,下一步當然就是進行列表頁的解析,提取出詳情頁 URL ,我們定義一個如下的解析列表頁的方法:
from urllib.parse import urljoindef parse_index():elements = browser.find_elements(By.CSS_SELECTOR, '#index .item .name')for element in elements:href = element.get_attribute('href')yield urljoin(INDEX_URL, href)
????????這里我們通過 find_elements_by_css_selector 方法直接提取了所有電影的名稱,接著遍歷結果,通過 get_attribute 方法提取了詳情頁的 href,再用 urljoin 方法合并成一個完整的 URL。?
(4)main
def?main():try:for?page?in?range(1,?TOTAL_PAGE?+?1):scrape_index(page)detail_urls?=?parse_index()logging.info('details?urls?%s',?list(detail_urls))finally:browser.close()
遍歷了所有頁碼,依次爬取了每一頁的列表頁并提取出來了詳情頁的 URL。?
觀察結果我們可以發現,詳情頁那一個個不規則的 URL 就成功被我們提取到了!
四、爬取詳情頁
????????好了,既然現在我們已經可以成功拿到詳情頁的 URL 了,接下來我們就進一步完成詳情頁的爬取并提取對應的信息吧。
(1)加載詳情頁
????????同樣的邏輯,詳情頁我們也可以加一個判定條件,如判斷電影名稱加載出來了就代表詳情頁加載成功,同樣調用 scrape_page 方法即可,代碼實現如下:
def?scrape_detail(url):scrape_page(url,?condition=EC.visibility_of_element_located,locator=(By.TAG_NAME,?'h2'))
????????這里的判定條件 condition 我們使用的是 visibility_of_element_located,即判斷單個元素出現即可,locator 我們傳入的是 (By.TAG_NAME, 'h2'),即 h2 這個節點,也就是電影的名稱對應的節點,如圖所示。
(2)解析詳情頁?
????????如果執行了 scrape_detail 方法,沒有出現 TimeoutException 的話,頁面就加載成功了,接著我們再定義一個解析詳情頁的方法,來提取出我們想要的信息就可以了,實現如下:
def parse_detail():url = browser.current_url# 使用新的定位方法name = browser.find_element(By.TAG_NAME, 'h2').text# 選擇所有類別并提取文本categories = [element.text for element in browser.find_elements(By.CSS_SELECTOR, '.categories button span')]# 獲取封面圖的 src 屬性cover = browser.find_element(By.CSS_SELECTOR, '.cover').get_attribute('src')# 獲取評分score = browser.find_element(By.CLASS_NAME, 'score').text# 獲取劇情簡介drama = browser.find_element(By.CSS_SELECTOR, '.drama p').textreturn {'url': url,'name': name,'categories': categories,'cover': cover,'score': score,'drama': drama}
?這里我們定義了一個 parse_detail 方法,提取了 URL、名稱、類別、封面、分數、簡介等內容,提取方式如下:
URL:直接調用 browser 對象的 current_url 屬性即可獲取當前頁面的 URL。
名稱:通過提取 h2 節點內部的文本即可獲取,這里使用了 find_element_by_tag_name 方法并傳入 h2,提取到了名稱的節點,然后調用 text 屬性即提取了節點內部的文本,即電影名稱。
類別:為了方便,類別我們可以通過 CSS 選擇器來提取,其對應的 CSS 選擇器為 .categories button span,可以選中多個類別節點,這里我們通過 find_elements_by_css_selector 即可提取 CSS 選擇器對應的多個類別節點,然后依次遍歷這個結果,調用它的 text 屬性獲取節點內部文本即可。
封面:同樣可以使用 CSS 選擇器 .cover 直接獲取封面對應的節點,但是由于其封面的 URL 對應的是 src 這個屬性,所以這里用 get_attribute 方法并傳入 src 來提取。
分數:分數對應的 CSS 選擇器為 .score ,我們可以用上面同樣的方式來提取,但是這里我們換了一個方法,叫作 find_element_by_class_name,它可以使用 class 的名稱來提取節點,能達到同樣的效果,不過這里傳入的參數就是 class 的名稱 score 而不是 .score 了。提取節點之后,我們再調用 text 屬性提取節點文本即可。
簡介:同樣可以使用 CSS 選擇器 .drama p 直接獲取簡介對應的節點,然后調用 text 屬性提取文本即可。
最后,我們把結果構造成一個字典返回即可。
(3)修改main()
?在 main 方法中再添加這兩個方法的調用,實現如下:
def?main():try:for?page?in?range(1,?TOTAL_PAGE?+?1):scrape_index(page)detail_urls?=?parse_index()for?detail_url?in?list(detail_urls):logging.info('get?detail?url?%s',?detail_url)scrape_detail(detail_url)detail_data?=?parse_detail()logging.info('detail?data?%s',?detail_data)finally:browser.close()
這樣,爬取完列表頁之后,我們就可以依次爬取詳情頁,來提取每部電影的具體信息了。
2020-03-29?12:24:10,723?-?INFO:?scraping?https://dynamic2.scrape.center/page/12020-03-29?12:24:16,997?-?INFO:?get?detail?url?https://dynamic2.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx2020-03-29?12:24:16,997?-?INFO:?scraping?https://dynamic2.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx2020-03-29?12:24:19,289?-?INFO:?detail?data?{'url':?'https://dynamic2.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx',?'name':?'霸王別姬?-?Farewell?My?Concubine',?'categories':?['劇情',?'愛情'],?'cover':?'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c',?'score':?'9.5',?'drama':?'影片借一出《霸王別姬》的京戲,牽扯出三個人之間一段隨時代風云變幻的愛恨情仇。段小樓(張豐毅?飾)與程蝶衣(張國榮?飾)是一對打小一起長大的師兄弟,兩人一個演生,一個飾旦,一向配合天衣無縫,尤其一出《霸王別姬》,更是譽滿京城,為此,兩人約定合演一輩子《霸王別姬》。但兩人對戲劇與人生關系的理解有本質不同,段小樓深知戲非人生,程蝶衣則是人戲不分。段小樓在認為該成家立業之時迎娶了名妓菊仙(鞏俐?飾),致使程蝶衣認定菊仙是可恥的第三者,使段小樓做了叛徒,自此,三人圍繞一出《霸王別姬》生出的愛恨情仇戰開始隨著時代風云的變遷不斷升級,終釀成悲劇。'}2020-03-29?12:24:19,291?-?INFO:?get?detail?url?https://dynamic2.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIy2020-03-29?12:24:19,291?-?INFO:?scraping?https://dynamic2.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIy2020-03-29?12:24:21,524?-?INFO:?detail?data?{'url':?'https://dynamic2.scrape.center/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIy',?'name':?'這個殺手不太冷?-?Léon',?'categories':?['劇情',?'動作',?'犯罪'],?'cover':?'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@464w_644h_1e_1c',?'score':?'9.5',?'drama':?'里昂(讓·雷諾?飾)是名孤獨的職業殺手,受人雇傭。一天,鄰居家小姑娘馬蒂爾德(納塔麗·波特曼?飾)敲開他的房門,要求在他那里暫避殺身之禍。原來鄰居家的主人是警方緝毒組的眼線,只因貪污了一小包毒品而遭惡警(加里·奧德曼?飾)殺害全家的懲罰。馬蒂爾德?得到里昂的留救,幸免于難,并留在里昂那里。里昂教小女孩使槍,她教里昂法文,兩人關系日趨親密,相處融洽。?女孩想著去報仇,反倒被抓,里昂及時趕到,將女孩救回。混雜著哀怨情仇的正邪之戰漸次升級,更大的沖突在所難免……'}...
五、存儲數據
保存為 JSON 文本文件,實現如下:
from?os?import?makedirs
import jsonfrom?os.path?import?existsRESULTS_DIR?=?'results'exists(RESULTS_DIR)?or?makedirs(RESULTS_DIR)def?save_data(data):name?=?data.get('name')data_path?=?f'{RESULTS_DIR}/{name}.json'json.dump(data,?open(data_path,?'w',?encoding='utf-8'),?ensure_ascii=False,?indent=2)
如果覺得爬取過程中彈出瀏覽器有所干擾,我們可以開啟 Chrome 的 Headless 模式,這樣爬取過程中便不會再彈出瀏覽器了,同時爬取速度還有進一步的提升。
只需要做如下修改即可:
options?=?webdriver.ChromeOptions()options.add_argument('--headless')browser?=?webdriver.Chrome(options=options)
?最后添加上 save_data 的調用,完整看下運行效果:
六、完整代碼
from selenium import webdriverfrom selenium.common.exceptions import TimeoutExceptionfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.chrome.service import Serviceimport logginglogging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s: %(message)s')INDEX_URL = 'https://spa2.scrape.center/page/{page}'TIME_OUT = 10TOTAL_PAGE = 10
driver_path = "E:/chromedriver-win64/chromedriver.exe"
#
service = Service(driver_path)
browser = webdriver.Chrome(service=service)wait = WebDriverWait(browser, TIME_OUT)def scrape_page(url, condition, locator):logging.info('scraping %s', url)try:browser.get(url)wait.until(condition(locator))except TimeoutException:logging.error('error occurred while scraping %s', url, exc_info=True)def scrape_index(page):url = INDEX_URL.format(page=page)scrape_page(url, condition=EC.visibility_of_all_elements_located,locator=(By.CSS_SELECTOR, '#index .item'))
from urllib.parse import urljoindef parse_index():elements = browser.find_elements(By.CSS_SELECTOR, '#index .item .name')for element in elements:href = element.get_attribute('href')yield urljoin(INDEX_URL, href)def scrape_detail(url):scrape_page(url, condition=EC.visibility_of_element_located,locator=(By.TAG_NAME, 'h2'))
from selenium.webdriver.common.by import Bydef parse_detail():url = browser.current_url# 使用新的定位方法name = browser.find_element(By.TAG_NAME, 'h2').text# 選擇所有類別并提取文本categories = [element.text for element in browser.find_elements(By.CSS_SELECTOR, '.categories button span')]# 獲取封面圖的 src 屬性cover = browser.find_element(By.CSS_SELECTOR, '.cover').get_attribute('src')# 獲取評分score = browser.find_element(By.CLASS_NAME, 'score').text# 獲取劇情簡介drama = browser.find_element(By.CSS_SELECTOR, '.drama p').textreturn {'url': url,'name': name,'categories': categories,'cover': cover,'score': score,'drama': drama}
from os import makedirsfrom os.path import exists
import jsonRESULTS_DIR = 'results'exists(RESULTS_DIR) or makedirs(RESULTS_DIR)def save_data(data):name = data.get('name')data_path = f'{RESULTS_DIR}/{name}.json'# 保存數據到JSON文件with open(data_path, 'w', encoding='utf-8') as file:json.dump(data, file, ensure_ascii=False, indent=2)def main():try:# 遍歷所有頁面for page in range(1, TOTAL_PAGE + 1):# 獲取并解析每頁的URLscrape_index(page)detail_urls = parse_index()# 遍歷詳情頁鏈接并解析詳情數據for detail_url in list(detail_urls):logging.info('get detail url %s', detail_url)# 獲取詳情頁面內容scrape_detail(detail_url)detail_data = parse_detail()# 日志打印詳情數據logging.info('detail data %s', detail_data)# 保存解析到的詳情數據save_data(detail_data)finally:# 關閉瀏覽器browser.close()if __name__ == "__main__":main()