瀏覽器爬蟲數據採集可視化解析實現之單頁面的可視化解析

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章