python爬蟲-->抓取動態內容
上幾篇博文講的都是關於抓取靜態網頁的相關內容,但是現在市面上絕大多數主流網站都在其重要功能中依賴JavaScript,使用JavaScript時,不再是載入後立即下載所有頁面內容,這樣就會造成許多網頁在瀏覽器中展示的內容不會出現在html原始碼中。這時候再用前幾篇博文中介紹的辦法爬取來資料,得到的資料肯定為空。本篇博文將主要介紹對如動態網頁應該如何進行爬取。
這裡我們將介紹兩種辦法來抓取動態網頁資料
① JavaScript逆向工程
② 渲染JavaScript
本篇博文主要思路如下圖:
如右側可以看出谷歌瀏覽器的控制生成了對應結果。那麼我們用前幾篇博文介紹的方法來對countries(國家名稱)資料進行爬取試試。
import lxml.html
from new_chapter3.downloader import Downloader
D=Downloader()
html=D('http://example.webscraping.com/places/default/search')
tree=lxml.html.fromstring(html)
print tree.cssselect('div#result a')
輸出結果
那麼為什麼抓取失敗了呢?開啟網頁原始碼可以幫助我們瞭解失敗的原因。
可以清楚的看出,我們要抓取的內容在原始碼中實際為空!!!可以看出谷歌瀏覽器控制檯顯示的是網頁當前的狀態,也就是使用JavaScript動態載入完搜尋結果之後的網頁。而這個網頁不會出現在html原始碼中。
這裡需要介紹下ajax:
AJAX即“Asynchronous Javascript And XML”(非同步JavaScript和XML),是指一種建立互動式網頁應用的網頁開發技術。
AJAX = 非同步 JavaScript和XML(標準通用標記語言的子集)。
AJAX 是一種用於建立快速動態網頁的技術。
通過在後臺與伺服器進行少量資料交換,AJAX 可以使網頁實現非同步更新。這意味著可以在不重新載入整個網頁的情況下,對網頁的某部分進行更新。
傳統的網頁(不使用 AJAX)如果需要更新內容,必須過載整個網頁頁面。
對動態網頁的逆向工程
某些網頁資料是通過JavaScript動態載入的,要想抓取該資料,我們需要了解網頁是如何載入該資料的,該過程稱為逆向工程。這裡我們使用谷歌瀏覽器,再次開啟
from new_chapter3.downloader import Downloader
import json
D=Downloader()
## 這裡每搜尋後的結果中,每個頁面5個國家資訊,顯示的是第一個頁面,也就是首先的5個國家。
html=D('http://example.webscraping.com/places/ajax/search.json?&search_term=a&page_size=5&page=0')
print json.loads(html)
列印結果:
結果中的records中有page_size個國家資訊。
通過搜尋字母表中的每個字母,可以抓取下所有的國家資訊。然後將其結果儲存到表格中。
#coding:utf-8
import json
import string
import new_chapter3.downloader as Downloader
import new_chapter3.mongo_cache as MongoCache
def main():
template_url = 'http://example.webscraping.com/places/ajax/search.json?&search_term={}&page_size=10&page={}'
countries = set()
cache=MongoCache.MongoCache()
cache.clear()
download = Downloader.Downloader(cache=cache)
for letter in string.lowercase:
page = 0
while True:
html = download(template_url.format(letter,page))
try:
ajax = json.loads(html)
except ValueError as e:
print e
ajax = None
else:
for record in ajax['records']:
countries.add(record['country'])
page += 1
if ajax is None or page >= ajax['num_pages']:
break
open('countries.txt', 'w').write('\n'.join(sorted(countries)))
if __name__ == '__main__':
main()
這裡面需要注意的是,下載這麼多網頁要注意延遲一會,否則會報too many requests。不過這個download函式裡面已經有同一域名內下載不同網頁延遲的功能。
這裡我們依次遍歷了26個字母,這個辦法抓取所有資料顯得比較笨,我們可以用.來代替所有字母,這樣就可以一次性獲取到所有資料。
import json
import csv
import new_chapter3.downloader as downloader
def main():
writer = csv.writer(open('countries2.csv', 'w'))
D = downloader.Downloader()
html = D('http://example.webscraping.com/places/ajax/search.json?&search_term=.&page_size=1000&page=0')
ajax = json.loads(html)
for record in ajax['records']:
writer.writerow([record['country']])
if __name__ == '__main__':
main()
比起上面26個字母慢慢抓取的辦法,這種辦法不僅程式碼量少,而且很快。
渲染動態網頁
有些網站很複雜,不太容易對其進行逆向工程進行解析來獲取資料。我們可以利用瀏覽器渲染引擎來對網頁進行渲染。這種渲染引擎是在瀏覽器在顯示網頁時解析html,應用css樣式並執行JavaScript語句的部分。這裡使用Webkit渲染引擎,通過QT框架可以獲取該引擎的python介面。
我們先嚐試用傳統方法下載原始html來獲取資料:
import lxml.html
from new_chapter3.downloader import Downloader
import json
D=Downloader()
html=D('http://example.webscraping.com/places/default/dynamic')
tree=lxml.html.fromstring(html)
print "結果為: ",tree.cssselect('#result')[0].text_content()
列印結果為:
結果為空,解析其html獲取不了JavaScript內的資料。我們再嘗試使用webkit來獲取資料:
#coding:utf-8
try:
from PySide.QtGui import *
from PySide.QtCore import *
from PySide.QtWebKit import *
except ImportError:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *
import lxml.html
import new_chapter3.downloader as downloader
def direct_download(url):
download = downloader.Downloader()
return download(url)
def webkit_download(url):
##初始化QApplication物件,在其他QT物件完成初始化之前。QT框架需要先建立該物件
app = QApplication([])
##建立QWebView物件,該物件是Web文件的容器
webview = QWebView()
webview.loadFinished.connect(app.quit)
webview.load(url)
app.exec_() # delay here until download finished
return webview.page().mainFrame().toHtml()
def parse(html):
tree = lxml.html.fromstring(html)
print tree.cssselect('#result')[0].text_content()
def main():
url = 'http://example.webscraping.com/places/default/dynamic'
#parse(direct_download(url))
parse(webkit_download(url))
return
print len(r.html)
if __name__ == '__main__':
main()
列印結果:
成功獲取到JavaScript內資料,這種方式就是利用webKit執行JavaScript,然後訪問生成的html,然後再進行解析獲取資料。
上面介紹的瀏覽器引擎還不能抓取搜尋頁面,為此我們還需要對瀏覽器渲染引擎進行擴充套件,使其支援與網站的互動功能。
對與上面的ajax搜尋示例,下面給出另外一個版本,支援互動功能。
#coding:utf-8
try:
from PySide.QtGui import QApplication
from PySide.QtCore import QUrl, QEventLoop, QTimer
from PySide.QtWebKit import QWebView
except ImportError:
from PyQt4.QtGui import QApplication
from PyQt4.QtCore import QUrl, QEventLoop, QTimer
from PyQt4.QtWebKit import QWebView
import lxml.html as lm
def main():
'''
首先設定搜尋引數和模擬動作事件,獲取在此引數和動作下搜尋後得到的網頁
然後在這網頁下,獲取資料
'''
app = QApplication([])
webview = QWebView()
loop = QEventLoop()
webview.loadFinished.connect(loop.quit)
webview.load(QUrl('http://example.webscraping.com/places/default/search'))
loop.exec_()
webview.show()## 顯示渲染視窗,,可以直接在這個窗口裡面輸入引數,執行動作,方便除錯
frame = webview.page().mainFrame()
## 設定搜尋引數
# frame.findAllElements('#search_term') ##尋找所有的search_term框,返回的是列表
# frame.findAllElements('#page_size option:checked')
# ## 表單使用evaluateJavaScript()方法進行提交,模擬點選事件
# frame.findAllElements('#search')
frame.findFirstElement('#search_term').setAttribute('value', '.') ##第一個search_term框
frame.findFirstElement('#page_size option:checked').setPlainText('1000') ##第一個page_size框
## 表單使用evaluateJavaScript()方法進行提交,模擬點選事件
frame.findFirstElement('#search').evaluateJavaScript('this.click()') ##第一個點選框
## 輪詢網頁,等待特定內容出現
## 下面不斷迴圈,直到國家連結出現在results這個div元素中,每次迴圈都會呼叫app.processEvents()
##用於給QT事件執行任務的時間,比如響應事件和更新GUI
elements = None
while not elements:
app.processEvents()
elements = frame.findAllElements('#results a') ##查詢下載網頁內的所有a標籤
countries = [e.toPlainText().strip() for e in elements] ##取出所有a標籤內的文字內容
print countries
if __name__ == '__main__':
main()
為了提高程式碼的易用性,我們把使用到的方法封裝到一個類中:
#coding:utf-8
import re
import csv
import time
try:
from PySide.QtGui import QApplication
from PySide.QtCore import QUrl, QEventLoop, QTimer
from PySide.QtWebKit import QWebView
except ImportError:
from PyQt4.QtGui import QApplication
from PyQt4.QtCore import QUrl, QEventLoop, QTimer
from PyQt4.QtWebKit import QWebView
import lxml.html
class BrowserRender(QWebView):
def __init__(self, display=True):
self.app = QApplication([])
QWebView.__init__(self)
if display:
## 顯示渲染視窗,,可以直接在這個窗口裡面輸入引數,執行動作,方便除錯
self.show() # show the browser
def open(self, url, timeout=60):
"""Wait for download to complete and return result"""
loop = QEventLoop()
timer = QTimer() ## 設定定時器
timer.setSingleShot(True)
timer.timeout.connect(loop.quit)
self.loadFinished.connect(loop.quit)
self.load(QUrl(url))
timer.start(timeout * 1000)
loop.exec_() # delay here until download finished
if timer.isActive(): ##如果定時器還是活躍,那麼說明下載沒有超時
# downloaded successfully
timer.stop()
return self.html() ##網頁下載完成後,將其轉成html,方便在html上進行資料抽取
else:
# timed out
print 'Request timed out:', url
def html(self):
"""Shortcut to return the current HTML"""
return self.page().mainFrame().toHtml()
def find(self, pattern):
"""Find all elements that match the pattern"""
"""因為這個網頁中每個框的名字都不一樣,故findFirstElement和findAllElements沒有什麼區別"""
##只不過findFirstElement返回是一個一個元素
## findFirstElement返回是一個列表,用這個列表時需要遍歷
return self.page().mainFrame().findFirstElement(pattern)
#return self.page().mainFrame().findFirstElement(pattern)
def attr(self, pattern, name, value):
"""Set attribute for matching elements"""
self.find(pattern).setAttribute(name, value)
# for e in self.find(pattern):
# e.setAttribute(name, value)
def text(self, pattern, value):
"""Set attribute for matching elements"""
self.find(pattern).setPlainText(value)
# for e in self.find(pattern):
# e.setPlainText(value)
def click(self, pattern):
"""Click matching elements"""
self.find(pattern).evaluateJavaScript("this.click()")
# for e in self.find(pattern):
# e.evaluateJavaScript("this.click()")
def wait_load(self, pattern, timeout=60):
"""Wait for this pattern to be found in webpage and return matches"""
## 設定定時器,跟蹤等待時間,並在截止事件前取消迴圈,否則當網路出現問題時,事件會無休止的執行下去
deadline = time.time() + timeout
while time.time() < deadline:
self.app.processEvents()
matches = self.page().mainFrame().findAllElements(pattern)
if matches:
return matches
print 'Wait load timed out'
def main():
'''
首先設定搜尋引數和模擬動作事件,獲取在此引數和動作下搜尋後得到的網頁
然後在這網頁下,查詢相關內容
'''
br = BrowserRender()
br.open('http://example.webscraping.com/places/default/search')
br.attr('#search_term', 'value', '.')
br.text('#page_size option:checked', '1000')
br.click('#search')
##設定定時器,跟蹤等待時間,並在截止事件前取消迴圈,否則當網路出現問題時,事件會無休止的執行下去
elements = br.wait_load('#results a')
writer = csv.writer(open('countries1.csv', 'w'))
#for country in [e.toPlainText().strip() for e in elements]:
for country in [e.toPlainText().strip() for e in elements]:
print country,
writer.writerow([country])
if __name__ == '__main__':
main()