Splash是一個JavaScript渲染服務,是一個帶有HTTP API的輕量級瀏覽器,同時它對接了Python中的Twisted和QT庫。
利用它,我們同樣可以實現動態渲染頁面的抓取。
1. 功能介紹和基本實例
### Splash的使用 ''' Splash是一個JavaScript渲染服務,是一個帶有HTTP API的輕量級瀏覽器,同時它對接了Python中的Twisted和QT庫。 利用它,我們同樣可以實現動態渲染頁面的抓取。 ''' ## 功能介紹 # 1.異步方式處理多個網頁渲染過程 # 2.獲取渲染后頁面的源代碼或截圖 # 3.通過關閉圖片渲染或者使用Adblock規則來加快頁面渲染速度 # 4.可執行特定的JavaScript腳本 # 5.可通過Lua腳本來控制頁面渲染過程 # 6.獲取渲染的詳細過程並通過HAR(HTTP Archive)格式呈現 ## 基本實例 function main(splash, args) splash:go("http://www.baidu.com") splash:wait(0.5) local title = splash:evaljs("document.title") return { title = title } end
2. Splash用lua腳本爬取網頁的基本使用介紹
2.1 異步處理
## 異步處理 # ipairs,為集合元素進行編號(編號從1開始),類似於python的enumerate # lua腳本語言中字符串拼接用 .. # splash:wait()類似於python中的time.sleep() # 當Splash執行wait方法時,它會轉而去處理其他任務,等到指定時間結束后再回來進行繼續處理 function main(splash, args) local example_urls = {"www.baidu.com", "www.taobao.com", "www.zhihu.com"} local urls = args.urls or example_urls local results = {} for index, url in ipairs(urls) do local ok, reason = splash:go("http://" .. url) if ok then splash:wait(2) results[url] = splash:png() end end return results end
2.2 Splash的對象屬性
2.2.1 args,main方法中的第二個args屬性即加載到splash中,即 args.url == splash.args.url
2.2.2 js_enabled,頁面JavaScript的執行開關,默認為true
## js_enabled,頁面JavaScript的執行開關,默認為True function main(splash, args) splash:go("http://www.baidu.com") splash.js_enabled = False local title = splash:evaljs("document.title") return { title = title } end ''' 執行結果: { "error": 400, "type": "ScriptError", "description": "Error happened while executing Lua script", "info": { "type": "SPLASH_LUA_ERROR", "message": "[string \"function main(splash, args)\r...\"]:3: setAttribute(self, QWebSettings.WebAttribute, bool): argument 2 has unexpected type 'NoneType'", "source": "[string \"function main(splash, args)\r...\"]", "line_number": 3, "error": "setAttribute(self, QWebSettings.WebAttribute, bool): argument 2 has unexpected type 'NoneType'" } } '''
2.2.3 resource_timeout,頁面加載超時時間,單位是秒
## resource_timeout,頁面加載超時時間,單位是秒 function main(splash, args) splash.resource_timeout = 0.01 assert(splash:go("http://www.baidu.com")) return splash:png() end ''' { "error": 400, "type": "ScriptError", "description": "Error happened while executing Lua script", "info": { "source": "[string \"function main(splash, args)\r...\"]", "line_number": 3, "error": "network5", "type": "LUA_ERROR", "message": "Lua error: [string \"function main(splash, args)\r...\"]:3: network5" } } '''
2.2.4 images_enabled,頁面圖片是否加載,默認為true
## images_enabled,頁面圖片是否加載,默認為True ## 禁用圖片可以節省網絡流量以及加快網頁加載速度,當加載頁面可能會影響JavaScript的渲染 ## Splash使用了緩存,即訪問過一次頁面的圖片,即使禁用頁面圖片加載后,依舊可以訪問到緩存中的圖片 ## 如下面示例,返回的phg頁面就不會帶有圖片 function main(splash, args) splash.images_enabled = false assert(splash:go("https://www.bilibili.com/")) return { png = splash:png() } end
2.2.4 plugins_enabled,控制瀏覽器插件(如flash等)是否開啟,默認為false
2.2.5 scroll_position,控制頁面的上下左右滾動,用x定位左右,y定位上下;如下示例,定位到y=800的位置
## scroll_position,控制頁面的上下左右滾動,用x定位左右,y定位上下;如下示例,定位到y=800的位置 function main(splash, args) assert(splash:go("https://www.mi.com")) splash.scroll_position = {y = 800} return {png = splash:png()} end
2.3 Splash對象的方法
2.3.1 go方法,用來請求某個鏈接,可以模擬get和post請求,同時傳入請求頭、表單等數據,賦值傳入變量時用{}
## go方法,用來請求某個鏈接,可以模擬get和post請求,同時傳入請求頭、表單等數據,賦值傳入變量時用{} ## go方法有2個返回值,ok和reason,ok為空代表網頁加載出現錯誤,此時reason變量中包含錯誤原因 # url:請求的URL # baseurl:可選參數,默認為空,表示自願加載的相對路徑 # headers:可選參數,默認為空,請求頭 # http_method:可選參數,默認為get,可以支持post # body:可選參數,默認為空,發post請求時的表單數據,傳入的數據內容類型為json # formdata:可選參數,默認為空,發post請求時的表單數據,傳入的數據內容類型為x-www-form-urlencoded function main(splash, args) local ok, reason = splash:go{url="http://httpbin.org/post", http_method="POST", body="name=dmr"} if ok then return splash:html() end end
2.3.2 wait方法,控制頁面的等待時間,可以傳入多值,賦值傳入變量時用{}
## wait方法,控制頁面的等待時間,可以傳入多值,賦值傳入變量時用{} ## 同樣有2個返回值,ok和reason,ok為空代表網頁加載出現錯誤,此時reason變量中包含錯誤原因 ## ok, reason = splash:go{time, cancel_on_redirect=false, cancel_on_error=false} # time:等待的秒數 # cancel_on_redirect:可選參數,默認為false,表示如果發生了重定向則停止等待並返回重定向結果 # cancel_on_error:可選參數,默認為false,表示如果發生了錯誤就停止等待 function main(splash, args) local ok, reason = splash:go("http://www.mi.com") splash:wait(2) if ok then return splash:html() end end
2.3.3 JavaScript等的操作方法
## jsfunc方法,可以直接調用JavaScript定義的方法,所調用的方法要用雙括號包圍 ## 如下示例,通過構造JavaScript方法來獲取訪問頁面的title和div數量並返回 function main(splash, args) local get_div_count = splash:jsfunc([[ function(){ var title = document.title; var body = document.body; var divs = body.getElementsByTagName('div'); var div_count = divs.length; return {div_count, title}; } ]]) ok, reason = splash:go("https://www.mi.com") result = get_div_count() return ("This page'title is %s, there are %s divs"):format(result.title, result.div_count) end ## evaljs方法,可以執行JavaScript代碼並返回最后一條JavaScript的返回結果 ## 如下示例,只返回最后一條JavaScript語句的執行結果 function main(splash, args) splash:go("https://www.mi.com") result = splash:evaljs("document.title;document.body.getElementsByTagName('div').length;") return result end ## runjs方法,可以執行JavaScript代碼,與evaljs類似,但是更偏向於執行某些動作或聲明某些方法 ## 如下示例,用runjs方法構造了一個JavaScript定義的方法,然后用evaljs執行此方法獲取返回結果 function main(splash, args) splash:go("https://www.mi.com") splash:runjs("fofo = function(){return 'dmr'}") result = splash:evaljs('fofo()') return result end ## autoload方法,可以設置每個頁面訪問時自動加載的對象,在Lua語言中nil相當於python的None ## ok, reason = splash:autoload{source_or_url, source=nil, url=nil} # source_or_url:JavaScript代碼或JavaScript庫鏈接 # source:JavaScript代碼 # url:JavaScript庫鏈接 # 示例1,通過構造一個get_path_title對象方法,用evaljs調用執行獲取返回結果,在這里與runjs類似,不過構造方法需要用[]中括號 function main(splash, args) splash:autoload([[ function get_path_title(){ return document.title; } ]]) splash:go("https://www.mi.com") result = splash:evaljs('get_path_title()') return result end # 示例2,用autoload加載jquery方法庫 function main(splash, args) assert(splash:autoload("https://code.jquery.com/jquery-2.2.4.min.js")) assert(splash:go("https://www.baidu.com")) local version = splash:evaljs("$.fn.jquery") return 'Version is '..version end
2.3.4 call_later方法,設置定時任務進行延時執行,並且可以在執行前通過cancel()方法重新執行定時任務
## call_later方法,設置定時任務進行延時執行,並且可以在執行前通過cancel()方法重新執行定時任務 # 如下示例,構造一個timer定時任務,當訪問頁面時,等待0.2秒獲取頁面的截圖,再等待1秒后獲取頁面的截圖 # 第一次獲取頁面的截圖頁面還沒加載出來,所以獲取到的是空白頁 function main(splash, args) local pngs = {} local timer = splash:call_later(function() pngs['a'] = splash:png() splash:wait(1) pngs['b'] = splash:png() end, 0.2) splash:go("https://www.mi.com") return pngs end
2.3.5 http_get方法,模擬發送http的get請求
## http_get方法,模擬發送http的get請求 ## response = splash:http_get{url, headers=nil, follow_redirects=true} # url:請求URL # headers:可選參數,默認為空,請求頭 # follwo_redirects:可選參數,表示是否啟動自動重定向,默認為true function main(splash, args) local t = require("treat") local response = splash:http_get("https://www.taobao.com") if response.status == 200 then return { b_html = response.body, html = t.as_string(response.body), url = response.url, status = response.status, } end end
2.3.6 http_post方法,模擬發送http的post請求,與http_get方法類似,不過多個body表單參數
## http_post方法,模擬發送http的post請求,與http_get方法類似,不過多個body表單參數 ## response = splash:http_get{url, headers=nil, follow_redirects=true, body=nil} # url:請求URL # headers:可選參數,默認為空,請求頭 # follwo_redirects:可選參數,表示是否啟動自動重定向,默認為true # body:可選參數,默認為空,表單數據 # 如下示例,將表單數據提交到了json中 function main(splash, args) local t = require("treat") local json = require("json") local response = splash:http_post{ "http://httpbin.org/post", body=json.encode({name="dmr"}), headers={["content-type"]="application/json"} } if response.status == 200 then return { 'response', b_html = response.body, html = t.as_string(response.body), url = response.url, status = response.status, } end end
2.3.7 set_content方法,用來設置頁面的內容
## set_content方法,用來設置頁面的內容 ## 如下示例,可以看到頁面中有dmr的內容 function main(splash, args) assert(splash:set_content("<h1>dmr</h1>")) return splash:png() end
2.3.8 html方法,用來獲取網頁的源代碼
## html方法,用來獲取網頁的源代碼 function main(splash, args) assert(splash:go("https://www.baidu.com")) return splash:html() end
2.3.9 png方法和jpeg方法,用來獲取網頁頁面png格式或jpeg格式的截圖
## png方法和jpeg方法,用來獲取網頁頁面png格式或jpeg格式的截圖 function main(splash, args) assert(splash:go("https://www.baidu.com")) return { png = splash:png(), jpeg = splash:jpeg() } end
2.3.10 har方法,用來獲取頁面加載過程描述
## har方法,用來獲取頁面加載過程描述 function main(splash, args) assert(splash:go("https://www.baidu.com")) return { har = splash:har() } end
2.3.11 url方法,用來獲取當前正在訪問頁面的url
## url方法,用來獲取當前正在訪問頁面的url function main(splash, args) assert(splash:go("https://www.baidu.com")) return {url = splash:url()} end
2.3.12 Cookies操作
## get_cookies方法,用來獲取當前頁面的cookies function main(splash, args) assert(splash:go("https://www.baidu.com")) return {cookies = splash:get_cookies()} end ## add_cookie方法,為當前頁面添加cookie ## splash:add_cookie{name, value, path=nil, domain=nil, expires=nil, httpOnly=nil, secure=nil} function main(splash, args) splash:add_cookie{'name', 'dmr'} assert(splash:go("https://www.baidu.com")) return {cookies = splash:get_cookies()} end ## clear_cookies方法,清楚所有的cookies function main(splash, args) splash:add_cookie{'name', 'dmr'} assert(splash:go("https://www.baidu.com")) splash:clear_cookies() return {cookies = splash:get_cookies()} end
2.3.13 瀏覽器視圖操作
## get_viewport_size方法,用來獲取當前頁面的大小,即寬高 function main(splash, args) assert(splash:go("https://www.taobao.com")) return splash:get_viewport_size() end ## set_viewport_size方法,設置當前瀏覽器頁面的大小,即寬高 function main(splash, args) splash:set_viewport_size(400, 400) assert(splash:go("https://www.taobao.com")) return splash:png() end ## set_viewport_full方法,用來設置瀏覽器全屏顯示 function main(splash, args) splash:set_viewport_full() assert(splash:go("https://www.taobao.com")) return splash:png() end
2.3.14 set_user_agent方法,用來設置瀏覽器的User-Agent
## set_user_agent方法,用來設置瀏覽器的User-Agent function main(splash, args) splash:set_user_agent('Splash') assert(splash:go("https://httpbin.org/get")) return splash:html() end
2.3.15 set_custom_headers方法,可以用來設置請求頭
## set_custom_headers方法,可以用來設置請求頭 function main(splash, args) splash:set_custom_headers({ ["User-Agent"] = "Splash", ["Host"] = "Splash.org" }) assert(splash:go("https://httpbin.org/get")) return splash:html() end
2.3.16 select方法,查找符合條件的第一個節點,用的是CSS選擇器
## select方法,查找符合條件的第一個節點,用的是CSS選擇器 function main(splash, args) assert(splash:go("https://www.taobao.com")) input = splash:select("#q") input:send_text("數碼") splash:wait(2) return splash:jpeg() end
2.3.17 select_all方法,查找符合條件的所有節點,用的是CSS選擇器
## select_all方法,查找符合條件的所有節點,用的是CSS選擇器 function main(splash, args) local treat = require("treat") assert(splash:go("https://movie.douban.com/top250")) assert(splash:wait(1)) local items = splash:select_all(".inq") local sum = {} for index, item in ipairs(items) do sum[index] = item.node.innerHTML end return { obj1 = sum, obj2 = treat.as_array(sum) } end
2.3.18 mouse_click方法,模擬鼠標點擊操作
## mouse_click方法,模擬鼠標點擊操作 ## 1.傳入x和y進行點擊操作 ## 2.查找到相關節點,調用此方法進行點擊操作 function main(splash, args) splash:go("https://www.baidu.com") input = splash:select("#kw") input:send_text("Python") splash:wait(1) search = splash:select('#su') search:mouse_click() splash:wait(2) return splash:png() end
3. Splash API的調用,以Python為例
官方API文檔:https://splash.readthedocs.io/en/stable/api.html
3.1 render.html頁面,此接口用於獲取JavaScript渲染的頁面的HTML代碼
## render.html頁面,此接口用於獲取JavaScript渲染的頁面的HTML代碼 ## 獲取百度頁面源代碼url示例:http://localhost:8050/render.html?url=https://www.baidu.com # 示例,通過調用render.html頁面獲取百度的源代碼並且設置等待時間為4秒 import requests url = 'http://10.0.0.100:8050/render.html?url=https://www.baidu.com&wait=4' response = requests.get(url) print(response.text)
3.2 render.png和render.jpeg,此接口獲取網頁截圖, 返回的是二進制數據
## render.png和render.jpeg,此接口獲取網頁截圖,返回的是二進制數據 ## 配置參數:url,wait,width,height ## render.jpeg多了個參數quality,用來調整圖片的質量,取值1-100,默認值為75,應盡量避免取95以上的數值 ## url示例:http://localhost:8050/render.png?url=https://www.baidu.com&wait=2&width=400&height=400 # 示例,通過render.png接口獲取頁面寬400高400的頁面截圖並保存到文件中 import requests url = 'http://10.0.0.100:8050/render.png?url=https://www.taobao.com&wait=5&width=1000&height=700' url2 = 'http://10.0.0.100:8050/render.jpeg?url=https://www.taobao.com&wait=5&width=1000&height=700&quality=90' response = requests.get(url) with open('taobao.png', 'wb') as f: f.write(response.content) response2 = requests.get(url2) with open('taobao.jpeg', 'wb') as f: f.write(response2.content)
3.3 render.har,此接口用來獲取頁面加載的HAR數據,返回的是json格式的數據
## render.har,此接口用來獲取頁面加載的HAR數據,返回的是json格式的數據 ## url示例:http://localhost:8050/render.har?url=https://www.baidu.com&wait=2 import requests, json url = 'http://10.0.0.100:8050/render.har?url=https://www.baidu.com&wait=2' response = requests.get(url) print(response.content) with open('har.text', 'w') as f: f.write(json.dumps(json.loads(response.content), indent=2))
3.4 render.json,此接口包含了前面接口的所有功能,返回結果是json格式
## render.json,此接口包含了前面接口的所有功能,返回結果是json格式 ## url示例:http://localhost:8050/render.json?url=https://www.baidu.com&html=1&png=1&jpeg=1&har=1 ## 默認返回:{'url': 'https://www.baidu.com/', 'requestedUrl': 'https://www.baidu.com/', 'geometry': [0, 0, 1024, 768], 'title': '百度一下,你就知道'} ## 通過將html、png、jpeg、har參數置為1獲取相關的頁面數據 import requests, json url = 'http://10.0.0.100:8050/render.json?url=https://www.baidu.com&html=1&png=1&jpeg=1&har=1' response = requests.get(url) data = json.loads(response.content) print(data) print(data.get('html')) print(data.get('png')) print(data.get('jpeg')) print(data.get('har'))
3.5 execute,功能強大,此接口可實現與Lua腳本的對接,實現交互性操作
## execute,此接口可實現與Lua腳本的對接,可實現交互性操作 ## url示例:http://localhost:8050/execute?lua_source= # 示例1,簡單示例,返回lua的執行結果 from urllib.parse import quote import requests lua = ''' function main(splash) return 'hello' end ''' url = 'http://10.0.0.100:8050/execute?lua_source=%s' % quote(lua) response = requests.get(url) print(response.text) # 示例2,通過execute執行lua腳本獲取頁面的url,png,html from urllib.parse import quote import requests, json lua = ''' function main(splash) splash:go('https://www.baidu.com') splash:wait(2) return { html = splash:html(), png = splash:png(), url = splash:url() } end ''' url = 'http://10.0.0.100:8050/execute?lua_source=%s' % quote(lua) response = requests.get(url) print(type(response.text), response.text) dic = json.loads(response.text) print(type(dic), len(dic), dic.keys())
4. Splash通過Nginx配置負載均衡基本思路

1. 配置多台splash服務器 2. 選中其中一台或者另起一台服務器安裝nginx服務 3. 配置nginx.conf配置文件,內容大致為: http { upstream splash { least_conn; #最少連接負載均衡,不配置這進行輪詢,ip_hash配置ip散列負載均衡 server 10.0.0.100:8050; server 10.0.0.99:8050; server 10.0.0.98:8050; server 10.0.0.97:8050; } server { listen 8050; location / { proxy_pass http://splash; # 指定域名 auth_basic "Restricted"; # 配置認證 auth_basic_user_file /etc/nginx/conf.d/.htpasswd; # 指定認證的用戶密碼文件 } } } 4. 如不配置認證,直接重載nginx:sudo nginx -s reload;配置了認證則需要構建密碼文件,建議用htpasswd命令構建,在重載nginx 5. 進行相關測試,測試腳本 from urllib.parse import quote import requests, re lua = ''' function main(splash) local treat = require('treat') response = splash:http_get('https://www.baidu.com') return treat.as_string(response.body) end ''' url = 'http://splash/execute?lua_source=%s' % quote(lua) response = requests.get(url) ip = re.search('(\d+\.\d+\.\d+\.\d+)', response.text).group(1) print(ip)