前言
以程式來抓取網頁內容從中取得有用資料的工作早已不是件困難的事,而現代網頁的產生方式也有許多種方式,其中一種是執行過JavaScript才產生的。這類網頁在程式中僅由一般取得靜態網頁的方式來取得是行不通的,必須透過像瀏覽器的運作,執行過其中的JavaScript後,真正的網頁內容才會產生。若要以Python抓取由JavaScript所產生的網頁內容,有一種作法是透過Selenium[1],驅動瀏覽器來進行。Selenuim支援多種瀏覽器,其中包含了Firefox, Google Chrome。但在某些應用的環境中,啟動瀏覽器來抓資料可能不太適合,因為桌面環境就不方便同時再進行其他工作,或是根本沒桌面環境可用。在這種情況下,像PhantomJS這類無需圖形環境的Scriptable Headless Browser[2],無頭(意指沒有圖形畫面顯示)的瀏覽器就相當適合,而Selenuim也支援它。以往PhantomJS在這方面使用的領域佔有重要的一席之地,自己也用了很長一段時間,且得心應手。只是Selenuim後來的版本因某些因素不再支援它,想用Headless瀏覽器,得改用其他像Firefox Headless Mode或是Chrome Headless Mode。
本文簡單記錄Python中以Selenuim驅動Headless Firefox/Chrome的方式、幾點相關建議、提醒以及相關參考。不涉及Selenium與Headless Browser之詳細用途說明,這類請查閱後面所列的參考。內容僅屬粗淺的入門指引,也算不上教學文,文末所列的參考可查到這方面詳細資料。
本文適合的讀者對象是已經會使用Python來抓取一般網頁,但對於以JavaScript或Ajax所產生的網頁資料一籌莫展者。要做這工作,最好熟悉HTML,能懂點JavaScript更好,會使用瀏覽器的開發者工具來檢視HTML元素可方便鎖定想找的東西,以及瀏覽器主控台。
如果想學習如何以Python抓取網頁資料(俗稱網路爬蟲),市面上已有數本這類書籍,像Web Scraping with Python。有Python基礎者可利用Requests, Beautiful Soup, lxml(選項)的組合,或者Scrapy來達成一般的網頁抓取工作,前者的組合可做一般應用也可做爬蟲,較容易入手;Scrapy則專門用於建構爬蟲。在必要時才採用本文所說的方式來抓取由Javascript產生的網頁資料。
準備工作
在進行此工作之前,機器中要先安裝好Python、瀏覽器與以下必要的東西:- Python bindings for Selenium[3]
- 與瀏覽器相對應版號的Driver,供1.來驅動控制瀏覽器。
$ pip install -U selenium
而Driver要與瀏覽器之間的版號必須配合,Driver的下載頁會有這類說明。下載下來的Driver放在PATH環境變數所列的資料夾中。我在Windows使用Portable版的瀏覽器,則是將其放進瀏覽器主執行檔的資料夾中。(若用PortableApps.com平台,瀏覽器版本更新時,此資料夾會被刪除再重建,所以要記得再次把Driver複製進來。)還有記得瀏覽器可別把JavaScript關閉,如裝了NoScript之類的附加元件時要允許JS,否則就沒戲唱了。比較理想的作法是另外弄個Portable版的瀏覽器,不用裝任何附加元件,也不用經常更新版本,就專門做這用途。
Python與Selenium的使用
上述東西裝好後,就可以用Python透過Selenium來驅動控制瀏覽器,以下分別以Firefox與Chrome為例。Selenium with Python[4]雖然不是官方說明文件,但其中提供了相當詳盡與豐富的說明,可詳加查閱;有必要時也可查閱Selenium官方文件[5]。以下簡單的範例僅做展示用途,其中的代碼有幾處是虛構的,別真的拿來就用。
Firefox
Firefox必須是55, 56版及之後的版本才有Headless Mode,記得看一下Driver的下載頁說明,使用合適的版號。以下範例代碼直接取自MDN Web Docs[6],其中的註釋則是額外自行加入的。
from selenium.webdriver import Firefox
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support import expected_conditions as expected
from selenium.webdriver.support.wait import WebDriverWait
if __name__ == "__main__":
options = Options()
options.add_argument('-headless') # 指定以headless mode啟動
driver = Firefox(executable_path='geckodriver', options=options) # 指定所用的driver是Firefox,傳入driver的位置以及Firefox的選項
wait = WebDriverWait(driver, timeout=10) # 以下這4行:指示打開Google的搜尋網頁後,等待直到某個網頁標籤出現,做幾個按鍵與滑鼠的動作
driver.get('http://www.google.com')
wait.until(expected.visibility_of_element_located((By.NAME, 'q'))).send_keys('headless firefox' + Keys.ENTER)
wait.until(expected.visibility_of_element_located((By.CSS_SELECTOR, '#ires a'))).click()
print(driver.page_source) # 印出網頁的HTML源代碼
driver.quit() # 結束並關閉瀏覽器
Google Chrome
Google Chrome自59版起才有Headless Mode。Google提供了一篇不錯的文章[7],雖然其中不含Python的使用,但提到另一篇使用Python的範例[8]。以下範例代碼是從自己的某個專案中取出做修改的:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from bs4 import BeautifulSoup
options = Options()
options.add_argument("--headless")
driver_path = r'D:\ProgramFiles\PA\PortableApps\GoogleChromePortable\App\Chrome-bin\chromedriver.exe' # webdriver執行檔路徑。這最好寫在設置檔中,這裡為方便說明才直接寫在代碼中。
driver = webdriver.Chrome(executable_path=driver_path, chrome_options=options)
driver.get('http://www.example.com/json_api/') # 這個URL是虛構的,原本的URL必須等待數秒鐘之後才會出現所要的JSON資料
WebDriverWait(driver, 18, 6).until_not(lambda x: x.find_element_by_id('x_id').is_displayed()) # 這個x_id也是虛構的
soup = BeautifulSoup(driver.page_source, 'html.parser')
driver.quit()
content = soup.body.get_text() # 取得其中的JSON文字。可與下一行寫成一行,這裡為方便說明,分成兩行。
data = json.loads(content)
# 以下解析JSON並從中取得資料,略
ChromeDriver的網頁上有提供說明文件,包含了Chrome的Options可用的選項[9]。上述WebDriverWait那行,lambda中的x代表的即driver。該網頁在出現所要的資料之前,網頁中某個元素帶有個id,當偵測到這個id不在時,就是所要的資料出現了。若用PyCharm,按Ctrl時滑鼠點代碼中的WebDriverWait,即可看到這函式的原始碼與註解說明,上述.until_not()即是以原始碼註解中的範例所改寫。
再來利用BeautifulSoup[10]取得網頁文件body中的文字,即是所要的JSON資料。
幾點建議與提醒
以下幾點,除了第一點與headless瀏覽有關,其餘與一般做網頁抓取有關:- 寫這類headless瀏覽的代碼時,在開發階段,為了解程式的運作狀況,可以暫時先把代碼中headless參數那行註釋掉,讓瀏覽器畫面出現,以方便觀察運作情形,待開發完成,再移除該行註釋符號。以往用PhantomJS時,自己通常也都是先寫Firefox用的代碼,並觀察Firefox是否如預期般完成了工作,待成功後,再調整成PhantomJS用的代碼,畢竟改用不同瀏覽器,代碼有些不同。而現在不再使用PhantomJS,以Firefox或Chrome而言,要變成headless mode,只是多加個參數而已。
- 如果提供資料下載的網站有提供下載資料的API,請查閱其說明文件,通常直接用API會比自己爬網頁取資料來的省事許多,有的可能也會提供現成的代碼或範例。這類有的會提供付費版,相較於免費版,通常是較少存取限制與較多資料筆數。
- 網路上抓來的資料不見得都是可以任意使用的,有些網站會附上資料的授權協議或是版權宣告,最好注意一下其內容,千萬別做出侵權的行為,如轉賣資料。
- 寫程式來自動抓取網頁資料,這類的程式在某層面上也被稱為網路爬蟲、網路蜘蛛、網路機器人。小規模的只抓取少許特定資料,大規模的如網路搜尋引擎。善用這樣的功能可方便自動達成許多工作,但若程式撰寫不佳或是惡意使然,則可能讓服務器承受過多請求流量甚至癱瘓。我通常會建議人家做這類工作時,別在短時間內送出大量請求,尤其要下載大量資料,最好是中間做個暫停,就算僅0.5秒也好,主要目地在於避免讓服務器負荷增加太多,最好自己先想一下合理的連線頻率。現在有不少提供公開資料服務的服務器其實都會針對使用者單一IP有連線數量的限制,甚至單位時間內的連線次數,如1分鐘6次,若超過會被禁止訪問一段時間,如15分鐘。如果所用的服務器有這類限制,最好查一下說明了解一下。為避免各種不合理的流量,服務器有許多方法或手段來限制這類流量,這多少也會提高取得資料的難度;或者服務端隨意做些改變,可能就夠你花老半天時間來調整或是重寫爬蟲程式,所以別做這種損人又不利己的事。又如果寫的代碼品質不佳,萬一被對方視為網路攻擊,那可誤會大了。據觀察,寫這類教學文的並非都會特別寫像這類的提醒[11]。
- 有的人學了點寫網路爬蟲的皮毛知識就宣稱任何網頁資料都可以弄到手,這只是突顯了自身對於網頁與網路技術認知的膚淺,別天真以為學會這技能就可以為所欲為。除了那種扭曲的連用人眼都難以辨識成功的圖形驗證之外,尚有多種驗證機制或其他手段足以讓爬蟲難以發揮作用或是根本起不了作用。
- 別濫用這項技能,拿他人的網站練功。據觀察,某些人氣高的論壇會遭受俗稱的網路機器人來濫發廣告文,良好的論壇軟體與論壇管理團隊大多會有良好的機制來避免甚至杜絕這類機器人帳號。若使用老舊論壇軟體,最好更換或升級成較好的軟體以避免此項威脅,或是其他安全漏洞。
參考與相關連結
參考
- Selenium (software)
- Headless browser
- Python bindings for Selenium
- Selenium with Python
- Selenium documentation
- Headless mode - Mozilla | MDN
- Getting Started with Headless Chrome
- Running Selenium with Headless Chrome
- Capabilities & ChromeOptions
- Beautiful Soup Documentation
- Python web scraping resource
相關教學文
本文不是詳細的教學文,讀者若想了解進一步的資訊,可從上述參考中找到相關資料。以下也列了幾個他人寫的教學文章,不一定都是headless方面的,本人也沒每篇都細讀過:- webscraping with Selenium - part 1:一系列共5篇文章,這是第1篇,文末會有下一篇的連結。這系列是多年前最初接觸headless crawler這領域時所拜讀的啟蒙文章。
- selenium-webdriver(python) (一)-(九):一系列共九篇,標示為轉載文章,可從其中找到原始文章出處。內容其實與網頁應用的測試(Selenium的原始用途)較相關,本人並未詳讀。
- Python3 爬虫(八):Selenium 和 Chrome Headless:一系列文章之一,內容相當豐富。
- Python 走入現實:selenium+爬蟲:內容較簡短。
- 使用Python控制Headless Chrome:並非使用Selenium,而是Chrome DevTools Protocol。
其他
另外也有人以Qt的WebKit做網頁爬蟲來爬取JavaScript所產生的網頁,像:自己不採這途徑,若僅對網路應用的開發而言,採用Selenuim也不必像Qt般要安裝較多東西。
update: 2020-5-24,8-4
沒有留言:
張貼留言