基于PhantomJS的网站截图服务API设计与开发

原文链接:http://www.mostclan.com/post-329.html
作者:Veris
Blog:最族 [ http://www.mostclan.com ]

为公司某业务实现“服务端对网站截图”功能,搜罗了很多技术最终采用了PhantomJS无头浏览器技术。

什么是PhantomJS?

PhantomJS是一个基于webkit的javaScript API。它使用QtWebKit作为它核心浏览器的功能,使用webkit来编译解释执行javaScript代码。任何你可以基于在webkit浏览器做的事情,它都能做到。它不仅是个隐性的浏览器,提供了诸如css选择器、支持wen标准、DOM操作、json、HTML5等,同时也提供了处理文件I/O的操作,从而使你可以向操作系统读写文件等。phantomJS的用处可谓非常广泛诸如网络监测、网页截屏、无需浏览器的wen测试、页面访问自动化等。

网上流传PhantomJS已暂停维护,转而投入selenium的开发,所以用户更倾向于目前流行的浏览器自动化测试框架“selenium”,这里作者选择使用PhantomJS来做服务,因为部署比较简单,且完全能满足业务需求。

1、实现截图

var webpage = require('webpage');
var page = webpage.create();
page.settings.javascriptEnabled = false; // 禁用JavaScript代码
page.open("http://www.mostclan.com", function (status) {
    if (status === "success") {
        console.log(page.title);
        console.log("截图成功");
        page.render("screenshot.png");
    } else {
        console.log("截图失败");
    }
});

将文件保存为webpage.js,使用如下命令执行程序

phantomjs webpage.js

(这里需要安装PhantomJS,详细步骤不做阐述,请自行百度)

如果返回截图成功,那么恭喜你截图服务已经写好了,就是这么简单,你可以查看目录下的“screenshot.png”有一张我的博客截图_

webpage 是 PhantomJS 的核心模块,上面的代码中,open() 方法有两个参数。

  • 第一个参数是请求地址(不要忘记协议头),默认使用 GET 方式

  • 第二个参数是回调函数,回调参数status表示网页状态主要有success和fail两种。

值的注意的是,只要请求有返回结果,status参数就是success,即使服务器返回的状态是404或500错误。

如果需要使用POST请求或其他请求要求,可以使用如下方式:

var webpage = require('webpage');
var page = webpage.create();
var settings = {
    operation: "POST",
    encoding: "utf8",
    headers: {
        "Content-Type": "application/json"
    },
    data: JSON.stringify({
        params: "data",
        array: ["1", "2"]
    })
};
page.settings.javascriptEnabled = false; // 禁用JavaScript代码
page.open("http://www.mostclan.com", settings, function (status) {
    if (status === "success") {
        console.log(page.title);
        console.log("截图成功");
        page.render("screenshot.png");
    } else {
        console.log("截图失败");
    }
});

这里第二个参数变成一个对象类型的配置信息,可以配置请求方式、编码、头信息、数据等。

另外代码中有一句 page.settings.javascriptEnabled = false;,如果不需要页面渲染时执行JS代码,可以禁用此选项来加速截图,因为一般网站都是由HTML+CSS来渲染页面的,其他基本上对渲染结果影响不大。

page.render()可以将打开的网页截图并保存成本地图片,可以将指定的图片文件名作为参数传入,render 方法可以根据文件名的后缀将图片保存成对应的格式。

目前支持PNG、GIF、JPEG、PDF四种图片格式。

page.render('temp.jpeg', {format: 'jpeg', quality: '100'});

其他参数详细细节可参考官方文档

2、搭建WebServer
这里用到一个基于mongoose的WebServer模块,因为该模块目前仅允许10个并发请求,所以在大流量并发环境下请选择其他Web服务模块,或并发量不高的话可以做个多服务的负载集群,后文会介绍操作方法。

var webserver = require('webserver').create();
webserver.listen(8080, function (request, response) {
    response.statusCode = 200;
    response.write('<html><h1>Hello World!</h1></html>');
    response.close();
});

将文件保存为webserver.js,使用如下命令执行程序

phantomjs webserver.js

然后访问 http://127.0.0.1:8080 便可看到刚刚搭建的Web服务

listen方法的第一个参数可以为一个端口号,也可以是ip:port这种形式
第二个参数是回调方法,主要有两个回调参数:requestresponse

request参数的几个常用属性:

  • method,请求方式(get、post等)
  • url,请求的URL,包含请求的get参数
  • post,POST请求数据
  • postRaw,POST数据原始信息,就是application/x-www-form-urlencoded编码提交的数据
  • headers,请求头信息

response参数的几个常用方法:

  • statusCode(code),设置HTTP状态码
  • write(data),向response中写入数据
  • close(),关闭HTTP连接(这个比较重要,如果未关闭会一直占用连接,后面会讲这个坑)

3、API服务网关设计

Web服务搭建好后,我们就可以做服务网关了,这样通过调用网关便能获取网站截图。
首先规定交互协议和返回数据格式,我们协定如下:

请求参数:(POST请求服务网关)

  • url: 需要截图的网站地址
  • out_order_id: 外部订单ID,如果是异步回调通知形式,可以加这个参数来识别图片
  • method: 请求方式
  • headers: 请求头
  • data: 请求参数(json格式)
    • screen_width: 截图屏幕宽度(如果有要求的话)
    • screen_height: 截图屏幕高度(如果有要求的话)
  • sign: 签名(这里如果要暴露服务给外网使用,又想限定用户的话可以设置签名参数来授权,详细设计方法不再阐述)

响应参数:

  • code: 状态值 0-失败 1-成功
  • msg: 回馈消息
  • data: 响应数据
    • screen_image_data: 截屏图片数据(base64加密)
    • out_order_id: 外部订单ID
  • timestamp: 时间戳

设计好数据格式就可以运行服务了,完整代码我分享在github上(见文末),可运行src/server.js

我这在本地使用postman请求服务网关,可以看到时间响应还是挺快的,部署到服务器会更快点。

渲染效果如下:(我这里图片采用了jpg压缩了,追求质量可以改成png,不过响应渲染的速度会下降一些)

4、服务化部署、异常情况及解决方案

高可用问题:为了使服务正常可控,一定要使用try()catch(e){}形式捕获异常!部署到Linux需要注意的是——如何在后台保持稳定运行,这里推荐大家使用Supervisor来守护进程,可以在后台运行的同时记录日志,当进程异常奔溃还可以自动重载服务。(详情可自行查阅资料)

超时问题:因为截图比较耗时,可以改为异步的形式,即服务处理完成后回调通知结果,图片则可使用本地存储/OSS存储图片数据,返回图片链接形式加快响应速度。

字体问题:如果部署在linux上,很容易出现中文乱码或显示不出的问题,这里需要在系统上安装字体,可参考https://blog.csdn.net/weiguang1017/article/details/80229133

另外请求服务的生命周期也至关重要:
我们可以试着将response.close();注释掉,然后启动服务再请求,会发现一直在等待响应中,当我如此反复请求10次发现无法再请求了

这是因为WebServer模块只支持10个并发,而且可以response完没有close连接,就导致一直占用tcp连接,造成服务不可用,我们可以通过linux命令来查看情况

ps aux | grep server.js

拿到进程的PID后使用lsof命令查看进程详情

lsof -nPp 39207

情况如下图,可以看到有10个TCP连接,其中有几个是CLOSE_WAIT状态。

出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接(我这里postman取消了请求),但是服务方处于忙线状态,没有关闭连接。

所以response.close();这一句千万不能漏,无论是请求成功还是异常情况都要正常响应用户并关闭连接。

5、服务扩展与展望

如若想要实现高并发大流量的服务,则需增加服务网关(可分布式部署),使用nginx反向代理来负载均衡(反向到各个服务网关的WebServer地址),这里和一般网站的高并发架构方式类似,设计如下图:

如果服务网关数量不够,扛不住大量并发访问,则可以通过限流的形式缓解压力

至此一个简单的网站截图服务就做好了,以如今的设计应该能满足日常需要,有更好设计方案或想法欢迎留言和issue。

本项目源代码公开在 https://github.com/VerisFung/WebScreenshotService ,欢迎Star!

转载请注明出处:
作者:Veris
最族 [ http://www.mostclan.com ]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,521评论 0 6
  • phantomjs实现了一个无界面的webkit浏览器。虽然没有界面,但dom渲染、js运行、网络访问、canva...
    卍卍_卐卐阅读 38,482评论 1 13
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,128评论 0 3
  • 8. 方法定义(Method Definitions) 通用的HTTP/1.0的方法集将在下面定义,虽然该方法集可...
    Palomar阅读 3,151评论 0 2
  • 谈论WEB编程的时候常说天天在写CGI,那么CGI是什么呢?可能很多时候并不会去深究这些基础概念,再比如除了CGI...
    __七把刀__阅读 2,191评论 2 11