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/vue@2.6.11"></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