我們在這裡介紹兩種解決動態加載的方法,一種是JavaScript 逆向工程,另一種是渲染 JavaScript。
首先,讓我們看看什麼樣的是動態加載。示例使用網址 http://example.python-scraping.com/search,比如我們查找A開頭的國家,如下:
我們通過開發者工具查看源碼,可以看到我們需要的內容在id=results節點下:
接著,我們編寫提取數據的代碼,代碼如下:
import re
import requests
from bs4 import BeautifulSoup
import lxml.html
import time
from lxml.html import fromstring
#獲取網頁內容
def download(url,user_agent='wswp',proxy=None,num_retries=2):
print ('Downloading:',url)
headers = {
'User-Agent': user_agent}
try:
resp = requests.get(url, headers=headers, proxies=proxy)
html = resp.text
if resp.status_code >= 400:
print('Download error:', resp.text)
html = None
if num_retries and 500 <= resp.status_code < 600:
# recursively retry 5xx HTTP errors
return download(url, num_retries - 1)
except requests.exceptions.RequestException as e:
print('Download error:', e.reason)
html = None
return html
# 提取需要的內容
html = download('http://example.python-scraping.com/search')
tree = fromstring(html)
tree.cssselect('div#results a')
# 輸出結果
Downloading: http://example.python-scraping.com/search
[]
從結果看得出,我們並沒有獲取到我們想要的東西,這是為啥呢,這就是動態加載,打開代碼下載的html源碼,是找不到我們需要的東西的。結果如下:
看到這裡,估計大家對動態渲染有了一定的認識,下面我們介紹如何獲取這種頁面內容。
我們使用之前的抓取方法,無法抓取到動態加載的頁面數據,那想要抓取這部分數據,我們就得了解這種頁面是如何加載數據的,該過程就描述為逆向工程。
繼續前面的例子,我們在浏覽器工具中單擊 Network選項卡,然後執行一次搜索,我們將會看到對於給定頁面的所有請求,大多數請求都是圖片,其中有個search.json的文件,單擊我們發現裡面包含我們需要的所有數據,如下圖:
很容易發現,上圖的結果其實是一個json響應,這個東西不僅可以在浏覽器中訪問,還可以直接下載,響應代碼如下:
import requests
resp = requests.get('http://example.python-scraping.com/places/ajax/search.json?&search_term=A&page_size=10&page=0')
resp.json()
# 結果輸出
{
'records': [{
'pretty_link': '<div><a href="/places/default/view/Afghanistan-1"><img src="/places/static/images/flags/af.png" /> Afghanistan</a></div>',
'country_or_district': 'Afghanistan',
'id': 7969417},
{
'pretty_link': '<div><a href="/places/default/view/Aland-Islands-2"><img src="/places/static/images/flags/ax.png" /> Aland Islands</a></div>',
'country_or_district': 'Aland Islands',
'id': 7969418},
{
'pretty_link': '<div><a href="/places/default/view/Albania-3"><img src="/places/static/images/flags/al.png" /> Albania</a></div>',
'country_or_district': 'Albania',
'id': 7969419},
{
'pretty_link': '<div><a href="/places/default/view/Algeria-4"><img src="/places/static/images/flags/dz.png" /> Algeria</a></div>',
'country_or_district': 'Algeria',
'id': 7969420},
...}
上面的代碼中,我們通過requests 庫的 json 方法訪問了JSON響應,我們還可以下載原始字符串響應,然後使用json.load進行加載。但是上面的代碼有個問題是我們只獲取了包含A字母的內容,那如何獲取全部的呢?當然可以解決,AJAX使用正則表達式進行匹配,所以我們只需要將url中的search_term=A
換成`search_term=.就可以加載全部的數據。
另外,因為url中page_size=10,所以一頁中只有十個數據,這個參數AJAX並不會檢查,所以我們可以給一個很大的數,是的所有數據一次性下載完成。
最終的代碼如下:
from csv import DictWriter
import requests
template_url = 'http://example.python-scraping.com/places/ajax/search.json?&search_term=.&page_size=1000&page=0'
resp = requests.get(template_url)
data = resp.json()
records = data.get('records')
with open('./countries_or_districts.csv', 'w') as countries_or_districts_file:
wrtr = DictWriter(countries_or_districts_file, fieldnames=records[0].keys())
wrtr.writeheader()
wrtr.writerows(records)
運行上述代碼,就可以在響應文件夾下得到如下的數據表:
對於實際中,某一些網站我們能夠快速地對 API 的方法進行逆向工程來了解它如何工作,以及如何使用它在一個請求中獲取結果。但是,一些網站非常復雜,即使使用高級的浏覽器工具也很難理解。這類網站難以實施逆向工程。這就得用到我們這裡要講解到的東西----渲染動態頁面。
盡管經過足夠的努力,任何網站都可以被逆向工程,不過我們可以使用浏覽器渲染引擎避免這些工作,這種渲染引擎是浏覽器在顯示網頁時解析HTML、應用 CSS 樣式並執行 JavaScript 語句的部分。在本節中,我們將使用QWebEngineView渲染引擎,通過 Qt 框架可以獲得該引擎的一個便捷 Python 接口。
我們可以使用位於http://example.python-scraping.com/dynamic 上的這個簡單示例,來演示使用Qt渲染。
常規提取:
import lxml.html
url = 'http://example.python-scraping.com/dynamic'
html = download(url)
tree = lxml.html.fromstring(html)
tree.cssselect('#result')[0].text_content()
# 輸出:
Downloading: http://example.python-scraping.com/dynamic
''
使用Qt框架:
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtWebEngineWidgets import QWebEngineView
import lxml.html
class Render(QWebEngineView): # 子類Render繼承父類QWebEngineView
def __init__(self, url):
self.html = ''
self.app = QApplication(sys.argv)
super().__init__()
self.loadFinished.connect(self._loadFinished)
self.load(QUrl(url))
self.app.exec_()
def _loadFinished(self):
self.page().toHtml(self.callable)
def callable(self, data):
self.html = data
self.app.quit()
if __name__ == '__main__':
url = 'http://example.python-scraping.com/dynamic'
r = Render(url)
result = r.html
tree = lxml.html.fromstring(result)
a = tree.cssselect('#result')[0].text_content()
print(a)
# 結果輸出
Hello World
這裡得說明一下,使用Jupyter Notebook會出現ImportError: QtWebEngineWidgets must be imported before a QCoreApplication instance is created
的錯誤,所以我這裡是使用PyCharm演示的,關於上述代碼,說明如下:
使用前面小節中的 QWebEngineView,我們可以自定義浏覽器渲染引擎,這樣就能完全控制想要執行的行為。如果不需要這麼高的靈活性,那麼還有一個不錯的更容易安裝的替代品 Selenium 可以選擇,它提供的 API 接口可以自動化處理多個常見浏覽器。
Selenium 就是模仿人操作浏覽器,它可操作的浏覽器有多種,比如Firefox (FirefoxDriver)、IE (InternetExplorerDriver)、Opera (OperaDriver) 和 Chrome (ChromeDriver)等,除此之外,它還支持 Android (AndroidDriver)和 iPhone (IPhoneDriver) 的移動應用測試,而且還包括一個基於 HtmlUnit 的無界面實現。本文只講谷歌驅動,其他有需要自行百度。
因為它是操作浏覽器,所以需要下載相應浏覽器的驅動,而且,版本也得相同。比如:我現在使用的谷歌浏覽器版本是 版本 91.0.4472.124,所以我下載的驅動也必須是91版本開頭的驅動,否則無法使用。
谷歌浏覽器驅動下載地址:http://npm.taobao.org/mirrors/chromedriver/
下載好驅動後,解壓,把解壓文件放置在Anaconda\Scripts 目錄下,當然如果你用的不是Anaconda,可以放到相應的位置。說白了,這一步就是把驅動放置在系統環境變量的path路徑下,完全可以直接把驅動所在的地址添加在環境變量path中。
添加環境變量
from selenium import webdriver
driver = webdriver.Chrome()
# 傳入url
driver.get("http://www.baidu.com")
# 傳遞參數,讓百度搜索Selenium2
driver.find_element_by_id("kw").send_keys("Selenium2")
# 找到百度一下按鈕,點擊一下
driver.find_element_by_id("su").click()
# 等待10秒
time.sleep(10)
# 關閉浏覽器
driver.quit()
到這裡算是了解也體驗了selenium的運行機制,下次學習如何定位元素,如何傳遞參數。
既然要模擬人操作浏覽器,那總得知道輸入什麼,點哪裡?這種問題就是它的定位,selenium有多種定位方式,分別是元素定位、Xpath定位和Css定位,每種方式都有自己的特點,實際使用時,可以多種方式混合使用。
篇幅問題,則合理不再細講相應的操作,網上一搜一大堆,感興趣的自行百度。不過下面有個我當時學習的Jupyter 文件,供大家參考。
Selenium 入門 Jupyter演示