【编程】Chrome extension扩展开发实战

怎样从众多网页上快速提取信息?比如说:

  • 从数百个宝贝页面提取价格和属性
  • 从数千个招聘职位页面中提取招聘信息

你可能需要kSpider这样一个工具。我的开源项目地址 http://10knet.com/zhyuzh/kspider

强烈建议你仔细阅读下面的内容然后再使用。

问题分析

由于现代Web开发方式(React,Vue,AngularJS等)的流行,很多页面都采用先加载页面再用js加载数据的模式,这就导致我们在页面上看到的内容和网页源码不一致,简单说就是Ctrl+S保存下来的页面文件并不包含你想要的信息。——这对于数据采集来说非常不利。

当然,可以使用爬虫技术直接从页面的Request接口中直接获取数据,但很多网站越来越多的使用反爬虫机制来阻止这种方式,爬虫技术和反爬虫技术之争就是魔道之争,总体来说,爬虫技术是很被动的,人家服务端换个花样,就让爬虫工程师折腾几天。

解决思路

对于普通用户来说,终极解决方案是浏览器自动化,模拟人类的浏览行为进行数据获取。换句话说,就是人能看到的数据就能够抓取下来。这样才能让爬虫工程师以不变应反爬虫服务器的万变。

PhantomJS和Selenium等爬虫工具都是类似的思路。但这些编程框架对于普通办公人员来说都太难使用。其实我们日常最多的工作就是抓取数十数百个数据,没必要兴师动众的写程序。用几个小时写爬虫,测爬虫,好容易代码成功运行,又用不了几天就被服务器升级搞得不能用...真不如每天花十几分钟手工保存来的简单,反正每天的新数据也就几十几百条而已。

最直接的简单思路就是:

  1. 手工打开这些页面(能脚本自动化打开更好);
  2. 执行一些必要的点击操作(或者根本不需要操作);
  3. 把看到的页面内容保存下来(注意是页面内容而不是页面源码);
  4. 用Python读取这些页面,BeautifulSoup解析,提取需要的信息,做数据处理。

解决方案

恐怕没有什么比直接写个Chrome浏览器插件来的更简单。写一个插件,实现保存单个页面内容,批量保存多个页面内容的方法,将会是个非常好的起步。

下面先从几个步骤简单介绍这个kSpider插件的开发思路。

插件本质

Chrome插件就是一个文件夹里面的几个web网页和js代码。和常规网页不同的是这些js代码可以使用Chrome的各种接口,简单说就是可以用js控制Chrome浏览器甚至进一步控制操作系统。比如说用js来打开和关闭tab页面,甚至捕获操作系统桌面。

调试插件

怎么运行下载的项目或者自己编写的项目?

从右上角菜单【更多工具-扩展程序】打开页面。

勾选右上角的【开发者模式】,然后就可以加载你的项目文件夹,修改后只要点这个刷新按钮即可重新加载运行。

manifest.json

项目文件夹必须要有一个manifest.json文件,它设置了使用哪些网页和js代码文件。官方说明看这里:https://developer.chrome.com/extensions/manifest

下面这个是我的配置,仅供参考。

{
  "name": "kSpider",
  "version": "1.0",
  "description": "Data spider!",
  "manifest_version": 2,
  "icons": {
    "16": "public/img/icon.png",
    "48": "public/img/icon.png",
    "128": "public/img/icon.png"
  },
  "background": {
    "page": "public/index.html",
    "_scripts": [
      "public/js/background.js"
    ],
    "persistent": true
  },
  "content_scripts": [
    {
      "matches": [
        ""
      ],
      "js": [
        "public/js/docstart.js"
      ],
      "run_at": "document_start"
    },
    {
      "matches": [
        ""
      ],
      "js": [
        "public/js/docend.js"
      ],
      "run_at": "document_end"
    }
  ],
  "permissions": [
    "activeTab",
    "activeTab",
    "background",
    "downloads",
    "declarativeContent",
    "history",
    "notifications",
    "pageCapture",
    "tabCapture",
    "unlimitedStorage",
    "storage",
    "webNavigation",
    "webRequest",
    "",
    "alarms"
  ],
  "page_action": {
    "default_popup": "public/popup.html",
    "default_icon": {
      "16": "public/img/icon.png",
      "48": "public/img/icon.png",
      "128": "public/img/icon.png"
    }
  },
  "options_page": "public/options.html",
  "commands": {
    "saveThisPage": {
      "suggested_key": {
        "default": "Ctrl+Shift+S",
        "mac": "Command+Shift+S"
      },
      "description": "快速保存当前页面内容"
    }
  }
}

需要特别注意的是下面几点:

  • background。必须。至关重要!它不是可有可无的背景,而是指实际运行在后台的js代码!几乎就是各种编程语言里面的main函数的意思。它有两种设置方法,html或者js文件,我这里是用的html,index.html是整个插件的入口。_script去掉下划线后也可以。background指定的脚本可以直接控制浏览器甚至操作系统。加载扩展程序的页面里,点击插件卡片上那个不显眼的【查看视图:背景页】或者【查看视图:public/index.html】打开的就是这个页面的控制台。你也可以使用下面这个代码直接打开它的页面。
chrome.windows.create({
    url: chrome.extension.getURL("public/index.html"),
    type: "popup",
    height: 480,
  });
  • content_scripts。可选。注入到每个网页的脚本,这个脚本可以在网页内运行!就相当于网页里面的自己的js代码,它可以访问网页的全部元素!你可以让这个脚本提前页面运行start或者等页面都加载后再运行end。不需要额外设置,只要这里设置之后脚本就会自动随每个页面自动执行。但是这种脚本并不能使用Chrome的API功能,换而言之,它不可以控制浏览器或操作系统。如果你希望它能控制浏览器,那么只能让它通过chrome.runtime.sendMessage向background发送message,然后background通过chrome.runtime.onMessage.addListener接收到message之后替它执行。

  • page_action。必须。插件图标一般都出现在浏览器右上角地址栏右侧,点击图标会弹出一个菜单。这个菜单实际是个网页!就是那个popup.html页面,它也可以自带js代码。这里的脚本和background的脚本一样,可以直接控制浏览器,也可以通过chrome.extension.getBackgroundPage()获取background内的函数代码使用。

  • permissions。可选。插件可能要用到的权限。

  • options_page。可选。在那个加载扩展程序的页面,点击那个【详细信息】然后找到【扩展程序选项】,点击右侧箭头,就会打开你这里设置的options.html。可以用来为你的插件提供更多设置界面。但实际上藏得这么深根本没啥用。

  • commands。可选。这个是注册快捷键。可以在page_action或者background的代码中添加下面的代码来绑定到这个快捷键。
chrome.commands.onCommand.addListener(function (cmd) {
    if (cmd == 'saveThisPage') { //ctrl+shif+s 保存当前页面
        try {
            saveThisPage()
        } catch (e) {
            console.log('kSpider:saveThisPage failed:', e)
        }
    }
})

代码实现

下图是我的项目文件目录。


其中真正有用的就是index.html-index.js、popup.html-popup.js、docend.js,其他的都可以忽略。
下面是这几个文件的代码,最核心的部分都在index.js里面,这里并没有用到网上提到较多的message通信方法,而是使用更巧妙的办法实现了页面内容的获取和存储,你可能需要仔细看一下代码注释,这里不啰嗦解释了。

index.js

console.log('kSpider:Hello from kspiders index.js.')

var urlList = [] //所有待处理的地址列表
var pageCode = 'console.log("kSpider:Runcode for ervery page.")' //每个页面要执行的代码

//确保扩展能够对每个页面都有效
chrome.runtime.onInstalled.addListener(function () {
    //初始化后背景页面控制台输出
    chrome.storage.sync.set({ color: '#3aa757' }, function () {
        console.log('Hello from kspiders onInstalled background.js.');
    });

    //所有页面都激活扩展
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
        chrome.declarativeContent.onPageChanged.addRules([{
            conditions: [new chrome.declarativeContent.PageStateMatcher({
                pageUrl: { hostContains: '' },
            })],
            actions: [new chrome.declarativeContent.ShowPageAction()]
        }]);
    });
});


let docHtmlStr = 'document.documentElement.innerHTML' //获取当前页面内容的命令
let curActTabInfo = { active: true, currentWindow: true, url: '' } //过滤当前激活窗口的设置

/**
 * 保存页面特定内容到html
 * 为index.js中的pageRun函数的querystr提供支援
 * 存储的文件名,kSpiderSavedPage.html,路径使用用户默认设置,自动避免重名
 * 不同于保存源代码,这是将整个DOM实时的内容进行保存,需要关闭浏览器【设置-高级-下载内容-下之前询问...】
 * @param {*} content   字符串,文字内容
 */
function saveContent(content) {
    if (!content) content = document.documentElement.innerHTML
    let blob = new Blob([content], { type: 'text/html' });
    let objectURL = URL.createObjectURL(blob);
    let filename = 'kSpiderSavedPage.html'
    let conf = { url: objectURL, filename: filename, conflictAction: "uniquify" }
    chrome.downloads.download(conf, function (downloadId) {
        console.log("kSpider:SavePage filename:", filename, downloadId);
    });
}

/**
 * 在页面上执行动作,可以是tabsinfo过滤到的多个页面
 * 只能根据tabinfo对象进行筛选页面,不能直接指定页面
 * @param {*} [callback=(r) => { console.log(r) }]  要执行的函数,r参数是querystr返回的结果,比如一个页面元素
 * @param {*} querystr  字符串,可以使用JQuery语法,$('.someclass').click()
 * @param {boolean} [tabsinfo={ active: true, currentWindow: true }]    选择目标页面,默认是当前页
 */
function pageRun(callback = (r) => { console.log(r) }, querystr = docHtmlStr, tabsinfo = curActTabInfo) {
    chrome.tabs.query(tabsinfo, function (tabs) {
        for (var i = 0; i < tabs.length; i++) {
            console.log(tabs[i].url)
            chrome.tabs.executeScript(tabs[i].id, { code: querystr }, function (result) {
                callback(result)
            })
        }
    })
}

/**
 * 保存当前激活的页面内容
 */
function saveThisPage() {
    pageRun(saveContent, 'document.documentElement.innerHTML', { active: true, currentWindow: true, url: '' })
}

/**
 * 保存当前窗口所有页面内容
 */
function saveAllPage() {
    pageRun(saveContent, 'document.documentElement.innerHTML', { currentWindow: true, url: ['http://*/*', 'https://*/*'] })
}

//快捷键监听
chrome.commands.onCommand.addListener(function (cmd) {
    if (cmd == 'saveThisPage') { //ctrl+shif+s 保存当前页面
        try {
            saveThisPage()
        } catch (e) {
            console.log('kSpider:saveThisPage failed:', e)
        }
    }
})

popup.js

console.log('kSpider:Hello from popup.js.')

let kbg = chrome.extension.getBackgroundPage() //调用index的方法

//一键保存当前页面到预先设定的路径
let saveThisBtn = document.getElementById('saveThis');
saveThisBtn.onclick = (e) => {
  try {
    kbg.saveThisPage()
  } catch (e) {
    console.log('kSpider:saveThisPage failed:', e)
  }
}

//一键保存所有页面到预先设定的路径
let saveAllBtn = document.getElementById('saveAll');
saveAllBtn.onclick = (e) => {
  try {
    kbg.saveAllPage()
  } catch (e) {
    console.log('kSpider:saveAllPage failed:', e)
  }
}

//打开kSpider控制台
let openConsoleBtn = document.getElementById('openConsole');
openConsoleBtn.onclick = function (element) {
  chrome.windows.create({
    url: chrome.extension.getURL("public/index.html"),
    type: "popup",
    height: 480,
  });
};

docend.js

console.log('kSpider:Hello from docend.js.')


/**
 * 用来载入外部函数库
 * 为index.js中的pageRun函数的querystr提供支援
 * @param {*} url   js文件地址,字符串
 * @param {*} onDone    载入后执行的函数onDone() 
 * @param {*} onError   载入失败执行的函数onError(e)
 */
function loadJS(url, onDone, onError) {
    if (!onDone) onDone = function () { };
    if (!onError) onError = function () { };
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200 || xhr.status == 0) {
                try {
                    eval(xhr.responseText);
                } catch (e) {
                    onError(e);
                    return;
                }
                onDone();
            } else {
                onError(xhr.status);
            }
        }
    }.bind(this);
    try {
        xhr.open("GET", url, true);
        xhr.send();
    } catch (e) {
        onError(e);
    }
}

//载入JQuery
loadJS('https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js', function () {
    console.log('juquery ok')
    console.log($("p"))
})

index.html,实际上并不需要这么复杂,只要能实现三个按钮就可以了。




  


popup.html






  
  
  
  kSpider





  


项目地址

项目已经放在我的网站上开源了,大家可以直接下载到本地,然后浏览器加载这个项目运行使用。

http://10knet.com/zhyuzh/kspider


欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】


每个人的智能新时代

如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~


END

你可能感兴趣的:(【编程】Chrome extension扩展开发实战)