1 前言
在進行數據採集時,也即我們說的爬蟲,抓到數據後通常需要進行解析。在PC端通常需要通過寫代碼進行解析。對於抓取業務較簡單的場景,重複寫代碼完成這樣的工作十分耗時。我們希望通過抓取到頁面數據後,在頁面上進行點選目標元素,就可以直接獲得需要的數據,這樣提高工作效率。我們在市面上見到的工具有Portia及八抓魚等工具即通過選擇的形式實現解析。本文將介紹實現的主要原理。只要明白了這些原理,其餘類似Portia及八抓魚的複雜解析功能也就很容易做了。最終效果如下,根據生成的解析代碼,數據採集和解析可一步完成。
2.功能分析
爲了實現點擊選擇解析的效果,需要獲得document對象。而我們直接通過iframe展示可點選解析的頁面會存在跨域請求問題。因此這個工具通過前端與後端兩部分組成。前後兩端需要實現的能力如下。
後端:
- 接收前端http請求。
- 從http請求中解析出訪問的目標網站地址,訪問目標網站,最終返回結果。
前端:
- 點擊元素,並生成css選擇器。
- 元素變色,鼠標移到元素上或從元素上移開後元素的背景色會發生變化。當交替單擊元素時,元素的背景色也會發生變化。
- 生成解析數據,單擊元素後生成要提取的數據。
- 生成解析代碼,單擊元素後生成可解析目標html頁面的代碼。
- 元素名稱及選擇器可編輯,方便自定義修改。
3.實現分析
根據2對功能的分析,爲了快速實現演示,我們會使用到如下技術。
- nodejs,服務端轉發http請求
- vue,實現前端數據雙向綁定及計算屬性的生成。
- medv/finder,用來實現css選擇器的生成。
- browserify,將nodejs模塊轉換爲html頁面可使用的模塊。
3.1 服務端實現
服務端在中功能較簡單,監聽一個端口,轉發http請求。
const request = require('request');
const app = require('express');
const router = app.Router();
const jsdom = require("jsdom");
const {JSDOM} = jsdom;
var iconv = require('iconv-lite');
router.get('/proxy', (req, res) => {
let requestUrl = req.query.urltofetch;
request({
url:requestUrl,
encoding: null
},(error,response,body) => {
if(!error && response && response.statusCode === 200) {
let encoding = response.headers["content-type"].split(";")[1].split("=")[1];
let buf = iconv.decode(body,encoding).toString();
let outHTML = new JSDOM(buf).window.document.documentElement.outerHTML;
res.status(200).end(outHTML);
} else {
res.status(400).end(body);
}
});
3.2 前端實現
3.2.1 css選擇器生成
我們會用到@medv/finder,安裝後通過browserify轉換爲html頁面可使用的模塊。browserify轉換時使用了-r參數。
npm install @medv/finder
browserify -r through > finder.js
隨後在頁面引入即可使用。
<script src="./finder.js"></script>
可以通過下面的代碼看到點擊生成選擇器的效果。
document.addEventListener("click",e => {
const selector = finder(event.target);
console.log(selector);
});
3.3.2 鼠標移動元素變色
鼠標移到元素上或從元素上移開後元素的背景色會發生變化。因此涉及到2個事件的處理onmouseover與onmouseout。
onmouseover與onmouseout事件僅影響背景色變化。
let iframeWindow = window;
let lastTarget = null;
let currentTarget = null;
iframeWindow.onmouseover = function(e) {
currentTarget = e.target;
if (currentTarget !== lastTarget && !currentTarget.clicked) {
currentTarget.style.backgroundColor = '#56121745';
if (lastTarget && !lastTarget.clicked) {
lastTarget.style.backgroundColor = null;
}
}
lastTarget = currentTarget;
};
iframeWindow.onmouseout = function(){
if (lastTarget && !lastTarget.clicked) lastTarget.style.backgroundColor = null;
};
() => {
if (lastTarget && !lastTarget.clicked) lastTarget.style.backgroundColor = null;
}
() => {
if (lastTarget && !lastTarget.clicked) lastTarget.style.backgroundColor = null;
}
3.3.3 鼠標單擊的處理
交替單擊要改變背景色,同時要生成解析後的數據與解析的代碼。我們vue來保存這些數據,並與表單元素相綁定。此外當在輸入框中輸入網頁地址點擊訪問後還要產生請求。
我們vue來保存這些數據,並做表單數據綁定及響應。此外增加了fetch方法,當單擊按鈕時觸發,向後端發起http請求。computed是計算屬性,當我們保存的選擇器信息發生變化時,會生成響應的代碼。
appVm = new Vue({
el: '#app',
data: {
selectedEl: {}
},
methods: {
del: function (value) {
value.el.click();
value.el.style.backgroundColor = null;
},
fetch:function(e) {
let url = document.getElementById("urlToFetch").value;
let req = `http://${location.host}/chrome?urltofetch=${url}`;
axios.get(req).then(response => {
writeHtmltoIframe(response.data,url);
}).catch(error => {
alert(error);
});
}
},
computed:{
selectedElExpansion:function(){
let expansions = [];
for(key in this.selectedEl) {
let value = this.selectedEl[key];
expansions.push(`"${value.name}":document.querySelector("${value.selector}").innerHTML`);
}
return "{" + expansions.join(",") + "}";
}
}
爲了展示單擊產生的數據我們需要一個簡單的html頁面。在html頁面中我們將通過vue的雙向綁定和計算屬性功能,來展示對應的數據。
<head>
<meta charset="UTF-8">
<title>parse</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="./finder.js"></script>
</head>
<body>
<div id="app">
<h1>生成請求</h1>
<p1>{{selectedElExpansion}}</p1>
<hr />
<h1>在線解析</h1>
<input id="urlToFetch"
type="text"
style="width:70%;"
placeholder="URL e.g. https://www.baidu.com" />
<input id="fetch" type="button" value="採集" v-on:click="fetch"/>
<div v-for="(value,key) in selectedEl">
<input v-model="selectedEl[key].name"/>
<input v-model="selectedEl[key].selector" />
{{value.el.innerText}}
<label v-on:click="del(value)">delete</label>
</div>
<iframe id="browser" scrolling="yes" title="onlineParser"
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
style="width: 70%; height: 500px;"></iframe>
</div>
接着處理鼠標單擊元素的事件。當單擊後生成選擇器,並將元素對象與選擇器保存在vue中。爲什麼這裏我會參雜原生Js和Vue的寫法。因此學習開發這個功能時,Vue是後學會的。所以這個demo沒全部用Vue寫。而且現在很晚了,暫時不想改了。
String.prototype.hashCode = function () {
let hash = 0, i, chr;
if (this.length === 0) return hash;
for (i = 0; i < this.length; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
iframeWindow.onclick = function(e) => {
if(appVm.$data.clickPicked !== "parse") {
return;
}
const {target} = e;
e.stopPropagation();
e.preventDefault();
// 記錄當前目標是否被點擊過
const selector = finder(target, {
root: iframeDocument
});
if (target.clicked) {
target.clicked = false;
appVm.$delete(appVm.$data.selectedEl, selector.hashCode());
} else {
target.clicked = true;
let element = iframeDocument.querySelector(selector);
element.selector = selector;
let selectInfo = {
name:selector.hashCode(),
el:iframeDocument.querySelector(selector),
selector:selector
};
appVm.$set(appVm.$data.selectedEl, selector.hashCode(),selectInfo);
}
}
單擊時,爲選擇的文本生成一個默認名稱,隨後使用者可通過文本框對這個名稱進行編輯。
最後就剩下了當向後端發出請求後返回,返回的頁面數據展示在iframe中。當我們接收到數據時,打開新的文檔,將數據寫入即可。Object.freeze是防止某些網站直接就通過location重定向,導致當前頁面不可解析。
function writeHtmltoIframe(html,url) {
const browserIframe = document.getElementById("browser");
const iframeWindow = browserIframe.contentWindow;
const iframeDocument = iframeWindow.document;
iframeDocument.open();
Object.freeze(iframeWindow.location);
iframeDocument.write(`<base href="${url}" target="_self">`);
iframeDocument.write(html);
// 未關閉文檔輸出流的情況下瀏覽器會提示一直處於加載中
iframeDocument.close();
這樣一個簡單可視化解析功能就實現了。其它的複雜功能,如多選元素,分類選擇也是原理也是類似的。
4. 總結
本文介紹了PC數據可視化解析的基本原理,只要瞭解這些,其它更進一步的東西自然也就明白了。當然基於iframe的可視化解析還是存在很多問題。但通過iframe可以簡化問題,使我們學習實現原理。
5.參考
[1]vue api data,https://cn.vuejs.org/v2/api/#data
[2]vue服務端渲染,https://ssr.vuejs.org/zh/
[3]browserify,https://javascript.ruanyifeng.com/tool/browserify.html#toc1
[4]require(’./expample.js).default詳解,https://www.cnblogs.com/cangqinglang/p/10445256.html
[5]express-static.express靜態資源管理中間件
[6]axios,前端https庫,https://www.kancloud.cn/yunye/axios/234845
[7]clipboard設置數據,https://stackoverflow.com/questions/23211018/copy-to-clipboard-with-jquery-js-in-chrome,
[8]clipboard.js,https://clipboardjs.com/
[9]jsdom,https://github.com/jsdom/jsdom