開發chrome插件實現爬蟲

你們好!我是一個喜歡前端的~菜雞Hjavascript

需求

  • 完成安裝插件後、對整個瀏覽器的頁面請求進行攔截、而且能夠經過插件配置指定攔截接口進行展現、包括下載和導出攔截的數據內容、這裏面能夠配合後端去作不少不少事情

問題

  • 攔截全部請求組裝請求信息和結果
  • 插件與頁面的互相通訊、作對應操做

效果圖

微信圖片_20210414171235.png

微信圖片_20210414171240.png

微信圖片_20210414171244.png

微信圖片_20210414171505.png

首先咱們先認識一個文件叫manifest.json、他是一個配置文件、chrome插件先讀取這個配置文件去作初始化工做、好比一些插件的圖標配置、插件點擊顯示的頁面、插件須要的權限受權、等等....php

第一個插件

  • 建立配置文件manifest.json
  • 建立展現圖標
  • 建立展現頁面
//chromePlugin文件結構
    - icon 
        - logo.png  //圖片尺寸不能大於129...好像是吧...若是不顯示的話就弄小一些
    - background
        - index.html
        - index.js
    - browser_action
        - index.html
        - index.js
        - index.css
    - manifest.json
    
複製代碼

manifest.jsoncss

{
    //你的插件名稱
    "name": "chrome",
    //描述
    "description": "chrome插件",
    //版本
    "version": "1.0",
    //必填項、並且填2
    "manifest_version": 2,
    //你能夠理解爲你的插件注入在瀏覽器的一個後臺服務器
    "background": {
        "page": "/background/index.html"
    },
    //插件點擊後顯示的頁面
    "browser_action": {
        "default_icon": "/icon/logo.png",
        "default_title": "chrome插件",
        "default_popup": "/browser_action/index.html"
   },
   //icons
   "icons": {
        "16": "/icon/logo.png",
        "32": "/icon/logo.png",
        "48": "/icon/logo.png",
        "128": "/icon/logo.png"
    }
}

複製代碼

browser_action/index.html && index.csshtml

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    /*注意一下這裏引入css的路徑規則 */
    <link rel="stylesheet" type="text/css" href="/browser_action/index.css">
</head>
<body>
    <div class="app">
        個人第一個chrome插件
    </div>
</body>
</html>

//index.css
.app{
    width: 200px;height: 100px;background: yellow;
}

複製代碼

簡單描述下配置文件流程、background你能夠理解爲你的插件一直駐留在瀏覽器的代碼塊、裏面能夠放一些共享給插件頁面的數據等等... browser_action就是指插件點擊後顯示的頁面內容、你能夠嘗試去寫一些你本身想要展現的內容部分、而後打開瀏覽器、更多工具-> 擴展程序、右上角開發模式打開 而後把你的項目直接拖入進去、會自動識別、不出意外的話你的插件就安裝好了、而後點擊插件、那麼恭喜你、你的第一個chrome插件已經完成!😊前端

問題1:攔截全部請求組裝請求信息和結果

思路是:重寫XMLHttpRequest和fetch、重寫後經過chrome提供的配置文件去往每一個頁面把重寫的代碼注入進去,到攔截效果、先認識一個 content_scripts 配置、他是一個告訴chrome插件我須要在當前網頁去加載個人js 的一個配置 往manifest.json添加下面代碼java

//注入到頁面js的規則配置
"content_scripts": [
    {
    // 定義哪些頁面須要注入content script "<all_urls>"全部頁面 
        "matches": ["<all_urls>"],
        //css文件地址
        "css": [],
        //注入的js文件地址
        "js": ["/contentScript/install.js"],
        //控制content script注入的時機。能夠是document_start, document_end或者document_idle。默認document_idle。
        "run_at":"document_start"
       }
 ],
 //經過chrome.extension.getURL來獲取包內資源的路徑。須要在manifest.json文件中設置訪問權限web_accessible_resources
 "web_accessible_resources": [
     "/contentScript/network.js"
  ]
複製代碼

ok 這樣我就添加了注入js的腳本命令、如今咱們須要在相對應的路徑下建立好文件夾和文件/contentScript/install.js 而後在contentScript文件夾下在建立一個network.js 和 install.jsnode

install.js git

setTimeout(() => {
    const script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    //經過chrome.extension.getURL來獲取包內資源的路徑。須要在manifest.json文件中設置訪問權限web_accessible_resources
    script.setAttribute('src', chrome.extension.getURL('/contentScript/network.js'));
    document.head.appendChild(script);
});
複製代碼

重寫請求攔截方法 network.js github

const tool = {
    isString(value) {
        return Object.prototype.toString.call(value) == '[object String]';
    },
    isPlainObject(obj) {
        let hasOwn = Object.prototype.hasOwnProperty;
        // Must be an Object.
        if (!obj || typeof obj !== 'object' || obj.nodeType || isWindow(obj)) {
            return false;
        }
        try {
            if (obj.constructor && !hasOwn.call(obj, 'constructor') && !hasOwn.call(obj.constructor.prototype, 'isPrototypeOf')) {
                return false;
            }
        } catch (e) {
            return false;
        }
        let key;
        for (key in obj) {}
        return key === undefined || hasOwn.call(obj, key);
    }
}

//這個類是基於騰訊開源vconsole(https://github.com/Tencent/vConsole)、寫的適用本插件的一個類
class RewriteNetwork {
    constructor() {
        this.reqList = {}; // URL as key, request item as value
        this._open = undefined; // the origin function
        this._send = undefined;
        this._setRequestHeader = undefined;
        this.status = false;
        this.mockAjax();
        this.mockFetch();
    }
    onRemove() {
        if (window.XMLHttpRequest) {
            window.XMLHttpRequest.prototype.open = this._open;
            window.XMLHttpRequest.prototype.send = this._send;
            window.XMLHttpRequest.prototype.setRequestHeader = this._setRequestHeader;
            this._open = undefined;
            this._send = undefined;
            this._setRequestHeader = undefined
        }
    }
  /** * mock ajax request * @private */
  mockAjax() {
    let _XMLHttpRequest = window.XMLHttpRequest;
    if (!_XMLHttpRequest) { return; }
    const that = this;
    //保存原生_XMLHttpRequest方法、用於下方重寫
    const _open = window.XMLHttpRequest.prototype.open,
          _send = window.XMLHttpRequest.prototype.send,
          _setRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
    that._open = _open;
    that._send = _send;
    that._setRequestHeader = _setRequestHeader;
    //重寫設置請求頭open
    window.XMLHttpRequest.prototype.open = function() {
        let XMLReq = this;
        let args = [].slice.call(arguments),
            method = args[0],
            url = args[1],
            id = that.getUniqueID();
        let timer = null;

        // may be used by other functions
        XMLReq._requestID = id;
        XMLReq._method = method;
        XMLReq._url = url;

        // mock onreadystatechange
        let _onreadystatechange = XMLReq.onreadystatechange || function() {};
        //定時輪詢去查看狀態 每次 readyState 屬性改變的時候調用的事件句柄函數。當 readyState 爲 3 時,它也可能調用屢次。
        let onreadystatechange = function() {
            let item = that.reqList[id] || {};

            //恢復初始化
            item.readyState = XMLReq.readyState;
            item.status = 0;
            //同步XMLReq狀態
            if (XMLReq.readyState > 1) {
                item.status = XMLReq.status;
            }
            item.responseType = XMLReq.responseType;
            //初始化狀態。XMLHttpRequest 對象已建立或已被 abort() 方法重置。
            if (XMLReq.readyState == 0) {
                if (!item.startTime) {
                    item.startTime = (+new Date());
                }
                //open() 方法已調用,可是 send() 方法未調用。請求尚未被髮送
            } else if (XMLReq.readyState == 1) {
                if (!item.startTime) {
                    item.startTime = (+new Date());
                }
            //Send() 方法已調用,HTTP 請求已發送到 Web 服務器。未接收到響應。
            } else if (XMLReq.readyState == 2) {
                // HEADERS_RECEIVED
                item.header = {};
                let header = XMLReq.getAllResponseHeaders() || '',
                    headerArr = header.split("\n");
                // extract plain text to key-value format
                for (let i=0; i<headerArr.length; i++) {
                    let line = headerArr[i];
                    if (!line) { continue; }
                    let arr = line.split(': ');
                    let key = arr[0],
                        value = arr.slice(1).join(': ');
                    item.header[key] = value;
                }
            //全部響應頭部都已經接收到。響應體開始接收但未完成
            } else if (XMLReq.readyState == 3) {
                 //HTTP 響應已經徹底接收。
            } else if (XMLReq.readyState == 4) {
                clearInterval(timer);
                item.endTime = +new Date(),
                item.costTime = item.endTime - (item.startTime || item.endTime);
                item.response = XMLReq.response;
                item.method = XMLReq._method;
                item.url = XMLReq._url;
                item.req_type = 'xml';
                item.getData = XMLReq.getData;
                item.postData = XMLReq.postData;
                that.filterData(item)
            } else {
                clearInterval(timer);
            }
        return _onreadystatechange.apply(XMLReq, arguments);
      };
      XMLReq.onreadystatechange = onreadystatechange;

      //輪詢查詢狀態
      let preState = -1;
      timer = setInterval(function() {
        if (preState != XMLReq.readyState) {
          preState = XMLReq.readyState;
          onreadystatechange.call(XMLReq);
        }
      }, 10);

      return _open.apply(XMLReq, args);
    };

    // 重寫設置請求頭setRequestHeader
    window.XMLHttpRequest.prototype.setRequestHeader = function() {
        const XMLReq = this;
        const args = [].slice.call(arguments);

        const item = that.reqList[XMLReq._requestID];
        if (item) {
            if (!item.requestHeader) { item.requestHeader = {}; }
            item.requestHeader[args[0]] = args[1];
        }
        return _setRequestHeader.apply(XMLReq, args);
    };

    // 重寫send
    window.XMLHttpRequest.prototype.send = function() {
        let XMLReq = this;
        let args = [].slice.call(arguments),
            data = args[0];

        let item = that.reqList[XMLReq._requestID] || {};
        item.method = XMLReq._method ? XMLReq._method.toUpperCase() : 'GET';

        let query = XMLReq._url ? XMLReq._url.split('?') : []; // a.php?b=c&d=?e => ['a.php', 'b=c&d=', 'e']
        item.url = XMLReq._url || '';
        item.name = query.shift() || ''; // => ['b=c&d=', 'e']
        item.name = item.name.replace(new RegExp('[/]*$'), '').split('/').pop() || '';
        
        if (query.length > 0) {
            item.name += '?' + query;
            item.getData = {};
            query = query.join('?'); // => 'b=c&d=?e'
            query = query.split('&'); // => ['b=c', 'd=?e']
            for (let q of query) {
            q = q.split('=');
            item.getData[ q[0] ] = decodeURIComponent(q[1]);
            }
        }
        if (item.method == 'POST') {
            // save POST data
            if (tool.isString(data)) {
                let arr = data.split('&');
                item.postData = {};
                for (let q of arr) {
                    q = q.split('=');
                    item.postData[ q[0] ] = q[1];
                }
            } else if (tool.isPlainObject(data)) {
                item.postData = data;
            } else {
                item.postData = '[object Object]';
            }

        }
        XMLReq.getData = item.getData || "";
        XMLReq.postData = item.postData || "";
        return _send.apply(XMLReq, args);
    };

  };
  
  /** * mock fetch request * @private */
  mockFetch() {
    const _fetch = window.fetch;
    if (!_fetch) { return ""; }
    const that = this;

    const prevFetch = function(input, init){
      let id = that.getUniqueID();
      that.reqList[id] = {};
      let item = that.reqList[id] || {};
      let query = [],
          url = '',
          method = 'GET',
          requestHeader = null;
      // handle `input` content
      if (tool.isString(input)) { // when `input` is a string
        method = init.method ? init.method : 'GET';
        url = input;
        requestHeader = init.headers ? init.headers : null
      } else { // when `input` is a `Request` object
        method = input.method || 'GET';
        url = input.url;
        requestHeader = input.headers;
      }
      
      query = url.split('?');

      item.id = id;
      item.method = method;
      item.requestHeader = requestHeader;
      item.url = url;
      item.name = query.shift() || '';
      item.name = item.name.replace(new RegExp('[/]*$'), '').split('/').pop() || '';

      if (query.length > 0) {
        item.name += '?' + query;
        item.getData = {};
        query = query.join('?'); // => 'b=c&d=?e'
        query = query.split('&'); // => ['b=c', 'd=?e']
        for (let q of query) {
          q = q.split('=');
          item.getData[ q[0] ] = q[1];
        }
      }
      
      if (item.method === "post") { 
        if (tool.isString(input)) {
          if (tool.isString(init.body && init.body)) {
            let arr = init.body.split('&');
            item.postData = {};
            for (let q of arr) {
              q = q.split('=');
              item.postData[ q[0] ] = q[1];
            }
          } else if (tool.isPlainObject(init.body && init.body)) {
            item.postData = init.body && init.body;
          } else {
            item.postData = '[object Object]';
          }
        } else { 
          item.postData = '[object Object]';
        }
      }
    // UNSENT
        if (!item.startTime) {  item.startTime = (+new Date()); }
        return _fetch(url, init).then((response) => {
            response.clone().json().then((json) => {
                item.endTime = +new Date(),
                item.costTime = item.endTime - (item.startTime || item.endTime);
                item.status = response.status;
                item.header = {};
                for (let pair of response.headers.entries()) {
                    item.header[pair[0]] = pair[1];
                }
                item.response = json;
                item.readyState = 4;
                const contentType = response.headers.get('content-type');
                item.responseType  = contentType.includes('application/json') ? 'json' : contentType.includes('text/html') ? 'text' : '';
                item.req_type = 'fetch';
                that.filterData(item)
                return json;
            })
            return response;
        })
    }
    window.fetch = prevFetch;
  }
  
    filterData({ url,method,req_type,response,getData,postData}){
        if(!url) return;
        const req_data = {
            url,
            method,
            req_type,
            response,
            getData, //query參數
            postData
        }
        console.log('攔截的結果',req_data)
    }

  /** * generate an unique id string (32) * @private * @return string */
  getUniqueID() {
    let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
    return id;
  }
}

const network = new RewriteNetwork();

複製代碼

隨便打開一個測試網站、f12打開控制檯而後裏面就會有攔截的結果輸出、這樣咱們就完成了頁面接口攔截了、接下來咱們就須要完成、插件與頁面的互相通訊、作對應操做web

問題2:插件與頁面的互相通訊

  • inject_js(實際插入到頁面的js) 與 content_script通訊
//inject_js利用postMessage方法和content_script進行通訊、在攔截請求的方法裏面去發送數據給content_script
//network.js
const senMes = (data) =>{
    window.postMessage(data, '*');
}
....
console.log('攔截的結果',req_data)
senMes(req_data)

//install.js
//接收inject頁面消息
....
window.addEventListener("message", function(e){
    const { data } = e;
    console.log('接收networkJS數據',data)
}, false);


複製代碼
  • content_script 與 background(後臺永久注入服務)通訊
//content_script/install.js
const sendBgMessage = (data) =>{
    chrome.runtime.sendMessage({ type:'page_request',data}, function(response) {
        console.log('後臺的回覆:' + response);
    });
}
//background(後臺永久注入服務)接收
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
    console.log('background接收數據',request)
    //答覆
    sendResponse('bg後臺收到消息')
});

複製代碼
  • browser_action頁面與 background.js通訊、基本差很少
//browser_action頁面js
const sendMes = (data) =>{
    return new Promise( resolve =>{
        chrome.runtime.sendMessage( data, (res)=> {  resolve(res) });
    })
}

//background(後臺永久注入服務)接收
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
    console.log('background接收數據',request)
    //答覆
    sendResponse('bg後臺收到消息')
});
    
複製代碼

完結

基本上一些核心的內容都在這裏、接下來就是根據本身的實際業務場景去配置去完成他 、下面我把開發文檔貼出來

chrome中文文檔

chrome英文文檔

相關文章
相關標籤/搜索