谷歌浏览器插件开发

简介

Chrome扩展主要用于对浏览器功能的增强,它更强调与浏览器相结合。比如Chrome扩展可以在浏览器的工具栏和地址栏中显示图标,它可以更改用户当前浏览的网页中的内容,直接操作浏览页面的DOM树等。这里用它来采集数据,类似于爬虫,然后将处理的数据发送到指定接口,导入数据库。
还有一种Chrome应用,但与浏览器内容相对独立,这里不介绍。

开发环境

开发浏览器插件不需要特别的工具,只需要装上谷歌浏览器, 一个记事本足矣。调试什么都可以在浏览器进行。

目录结构

这里写的是简单的插件,基本目录结构如下:

python chrome 爬虫 chrome浏览器爬虫插件开发_浏览器

谷歌浏览器简单的插件实际上就是一个拓展的网页,所以和普通的HTML的结构没什么特殊之处,上面的js、images完全可以是你喜欢的名称,引用时路径写对就行,需要注意的是,插件必须包含一个manifest.json文件(必须是这个名字),此文件描述了插件的一些基本信息,安装插件时会读取插件相关信息。
插件的html页面可以引用外部的js文件,但是注意,不能直接在页面里面直接写JavaScript脚本,是不会执行,必须是引用的脚本,切记

manifest.json详解

manifest.json是插件最重要的配置和描述文件,下面用我的例子来说说

{
    "manifest_version": 2,
    "name": "采集插件",
    "version": "1.0",
    "description": "获取网页数据,保存直接到OA系统",
    "content_scripts": [
        {
            "matches": ["https://abc.xxxx.com/*"],
            "run_at": "document_end",
            "js": ["js/getdata.js"]
        }
    ],
    "browser_action": {
        "default_icon": {
            "16": "images/icon16.png",
            "32": "images/icon32.png",
            "38": "images/icon38.png",
            "48": "images/icon48.png",
            "64": "images/icon64.png",
            "128": "images/icon128.png"
        },
        "default_title": "状态",
        "default_popup": "popup.html"
    },
    "options_page": "options.html",
    "permissions": [
        "storage",
        "https://abc.xxxx.com/*",
        "http://127.0.0.1:8080/*"
    ]
}

name定义了扩展的名称,
version定义了扩展的版本,
description定义了扩展的描述,
icons定义了扩展相关图标文件的位置,谷歌会根据需要选择合适大小的图标
version的值最多可以是由三个圆点分为四段的版本号,每段只能是数字,每段数字不能大于65535且不能以0开头(可以是0,但不可以是0123),版本号段左侧为高位,比如1.0.2.0版本比1.0.0.1版本更高。每次更新扩展时,新的版本号必须比之前的版本号高。
browser_action指定扩展的图标放在Chrome的工具栏中,
browser_action中的default_icon属性定义了相应图标文件的位置,
default_title定义了当用户鼠标悬停于扩展图标上所显示的文字,default_popup则定义了当用户单击扩展图标时所显示页面的文件位置,
content_scripts属性可以指定将哪些脚本何时注入到哪些页面中,当用户访问这些页面后,相应脚本即可自动运行,从而对页面DOM进行操作。
Manifest的content_scripts属性值为数组类型,数组的每个元素可以包含matches、exclude_matches、css、js、run_at、all_frames、include_globs和exclude_globs等属性。
其中matches属性定义了哪些页面会被注入脚本,exclude_matches则定义了哪些页面不会被注入脚本,css和js对应要注入的样式表和JavaScript;
run_at定义了何时进行注入,
另外,all_frames可以定义脚本是否会注入到嵌入式框架中,
include_globs和exclude_globs则是全局URL匹配,最终脚本是否会被注入由matches、exclude_matches、include_globs和exclude_globs的值共同决定。
简单的说,如果URL匹配mathces值的同时也匹配include_globs的值,会被注入;如果URL匹配exclude_matches的值或者匹配exclude_globs的值,则不会被注入。
content_scripts中的脚本只是共享页面的DOM树,而并不共享页面内嵌JavaScript的命名空间。
也就是说,如果当前页面中的JavaScript有一个全局变量a,content_scripts中注入的脚本也可以有一个全局变量a,两者不会相互干扰。当然你也无法通过content_scripts访问到页面本身内嵌JavaScript的变量和函数。
Manifest的permissions属性中声明需要谷歌拓展API的storage权限和跨域的权限。

项目业务代码等

业务部分

以下是get_data.js代码,主要业务操作在这里

timeStart="";
timeEnd="";
timeEnd_collect=""; //采集结束时间,程序到这时间终止

//encode要发送到OA系统的数据
function encodeFormData(data){
    if(!data) return '';
    var pairs = [];
    for(var name in data){
        if(!data.hasOwnProperty(name)) continue;
        if(typeof data[name] === 'function') continue;
        var value = data[name].toString();
        name = encodeURIComponent(name.replace('%20','+'));
        value = encodeURIComponent(value.replace('%20','+'));
        pairs.push(name+'='+value);
    }
    return pairs.join('&');
}
//获取,处理页面数据
function  get_data(){
    var trs = window.frames["MainIframe"].document.getElementById("table_data_tbody").children;
    var datas=[];
    for(var i=0;i<trs.length;i++){
        var tds =trs[i].childNodes;    
        var data_tmp= {
            STARTTIME:tds[1].innerText,
            ENDTIME:tds[2].innerText,
            ZUOXI:tds[3].innerText,
            ZUOXI_ID:tds[4].innerText,
            FROM_NUM:tds[5].innerText,
            TO_NUM:tds[6].innerText,
            CALL_TYPE:tds[7].innerText,
            DURATION:tds[8].innerText,
            SATISFACTION:tds[9].innerText,
            ACID:tds[10].innerText
        };        
        datas[i]=data_tmp;
    }
    var s = JSON.stringify(datas);
    var data2sent={
        data:s
    }
    httpRequest('http://127.0.0.1:8080/invCloudOA/appuser/calllog2db',encodeFormData(data2sent),function(result){
        html = result;
        console.log(html);
    });
}

//模拟用户操作,发送请求给acc
function sent_req(){
    chrome.storage.local.set({"log":"模拟用户操作,修改时间参数"});

    var timestamp_end = Date.parse(new Date(timeEnd));
    window.frames["MainIframe"].document.getElementById("type_duration").click();
    window.frames["MainIframe"].document.getElementById("timeStart").value=timeStart;
    window.frames["MainIframe"].document.getElementById("timeEnd").value=timeEnd;
    window.frames["MainIframe"].document.getElementById("btnOk").click();

    chrome.storage.local.set({"log":"发送请求,延时处理返回数据"});
    //延时数据处理
    setTimeout(get_data,12000);
    //设置新的时间
    chrome.storage.local.set({"log":"时间参数修改"});
    timeStart=timeEnd;
    var end_timestamp = Date.parse(new Date(timeEnd));
    var new_end_date = new Date();
    new_end_date.setTime(end_timestamp+60*60*1000); 
    timeEnd=new_end_date.format('yyyy-MM-dd hh:mm:ss');
    var p = {
        timeStart:timeStart,
        timeEnd:timeEnd,
    }
    chrome.storage.local.set(p,function(){});
    chrome.storage.local.set({"log":"数据处理完成,准备发给OA"});
    setTimeout(sent_req, 10000);
}

//构造请求,发给OA
function httpRequest(url,data, callback){
    var xhr = new XMLHttpRequest();
    xhr.open('post',url);
    xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    chrome.storage.local.set({"log":"发送数据给OA,OA处理中"});
    xhr.send(data);
}

//时间格式化工具
Date.prototype.format = function(format) {
    var date = {
           "M+": this.getMonth() + 1,
           "d+": this.getDate(),
           "h+": this.getHours(),
           "m+": this.getMinutes(),
           "s+": this.getSeconds(),
           "q+": Math.floor((this.getMonth() + 3) / 3),
           "S+": this.getMilliseconds()
    };
    if (/(y+)/i.test(format)) {
           format = format.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    for (var k in date) {
           if (new RegExp("(" + k + ")").test(format)) {
                  format = format.replace(RegExp.$1, RegExp.$1.length == 1
                         ? date[k] : ("00" + date[k]).substr(("" + date[k]).length));
           }
    }
    return format;
}

//程序入口
chrome.storage.local.get("isenable", function(obj) {

    chrome.storage.local.get("timeStart", function(obj) {
        timeStart=obj.timeStart;
    });
    chrome.storage.local.get("timeEnd", function(obj) {
        timeEnd=obj.timeEnd;
    });

    if(obj.isenable){
        setTimeout(sent_req,6000);
        chrome.storage.local.set({"log":"插件已经正常开启!"});
    }
});

以上是点击插件图标弹出的页面,预想是用来显示运行时的日志的,效果不好,懒得改了。

<html>
    <head>
        <title>参数设定</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    </head>
    <body>

        <div style="width: 300px;height:60px;text-align: center">
                <br/>
            <p id="log">
                插件日志
            </p>

        </div>

        <script src="js/popup.js"></script>
    </body>
</html>
插件的配置部分

配置页面,没追求,自己随便写一个简陋的页面

<html>
    <head>
        <title>参数设定</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    </head>
    <style>
    .big{
        font-size: 30px;
    }
    .biginput{
        width: 300px;
        height: 50px;
        font-size: 30px;
    }
    </style>
    <body>

        <div style="width: 80%;height:auto;text-align: center">
                <br/>
                <span class="big">开始时间:</span><input class="biginput" type="text" id="timeStart" value="2017-11-01 00:00:00"/><br/><br/>
                <span class="big">结束时间:</span><input class="biginput" type="text" id="timeEnd" value="2017-11-01 00:30:00"/><br/><br/>
                <span class="big">是否启用:</span><input  type="checkbox" id="isenable" /><br/><br/>
                <input type="button" class="biginput" id="save" value="保存" /><br/>
        </div>
        <script src="js/options.js"></script>
    </body>
</html>

配置页面引用的脚本,一看就懂

//加载数据,显示目前的配置
window.onload=function(){
    chrome.storage.local.get("isenable", function(obj) {
        document.getElementById('isenable').checked=obj.isenable
    });
    chrome.storage.local.get("timeStart", function(obj) {
        document.getElementById('timeStart').value=obj.timeStart
    });
    chrome.storage.local.get("timeEnd", function(obj) {
        document.getElementById('timeEnd').value=obj.timeEnd
    });
}

//保存
document.getElementById('save').onclick = function(){
    var timeStart = document.getElementById('timeStart').value;
    var timeEnd = document.getElementById('timeEnd').value;
    var isenable = document.getElementById('isenable').checked;
    var p = {
        timeStart:timeStart,
        timeEnd:timeEnd,
        isenable:isenable
    }
    chrome.storage.local.set(p,function(){
        alert('设置已保存');
        }
    );
}

运行

打开插件管理

python chrome 爬虫 chrome浏览器爬虫插件开发_谷歌_02

加载已经解压的应用,选择对应文件夹

python chrome 爬虫 chrome浏览器爬虫插件开发_浏览器插件_03

发布

直接点击上图的打包拓展程序,选择文件夹,密钥可选,如果是升级,可以选择首次自动生成的密钥

python chrome 爬虫 chrome浏览器爬虫插件开发_chrome_04


生成插件文件和证书

python chrome 爬虫 chrome浏览器爬虫插件开发_chrome_05

安装

打开拓展页面,直接拉过去,确定即可

python chrome 爬虫 chrome浏览器爬虫插件开发_浏览器插件_06

这里特别说一下,谷歌插件支持三种方法中的一种来储存数据:
第一种是使用HTML5的localStorage;
第二种是使用Chrome提供的存储API;
第三种是使用Web SQL Database。
localStorage就是h5自带的
Chrome提供的存储API和localStorage相似,拓展一些功能
如果储存区域指定为sync,数据可以自动同步;
content_scripts可以直接读取数据,而不必通过background页面;
在隐身模式下仍然可以读出之前存储的数据;
读写速度更快;
用户数据可以以对象的类型保存。

localStorage是基于域名的,而content_scripts是注入到用户当前浏览页面中的,如果content_scripts直接读取localStorage,所读取到的数据是用户当前浏览页面所在域中的。所以通常的解决办法是content_scripts通过runtime.sendMessage和background通信,由background读写扩展所在域(通常是chrome-extension://extension-id/)的localStorage,然后再传递给content_scripts。

Chrome提供的存储API就没有这些问题,可以跨页面读取,666