基于PhantomJs的Java后台网页截图技术

栏目: Java · 发布时间: 5年前

内容简介:公司之前做的某一手机应用,里面有一需求是一键出图(有一统计信息类的网页,需要在不打开网页的情况下实时对网页进行截图然后保存到服务器上),手机上便可以查看该图片了。刚开始拿到需求发现比较棘手,参考了很多文章解决方案大楷有以下几种:参考文章:经过试验Robot失败,DJNativeSwing组件截图成功,但由于网页css的复杂性导致图片失真严重而达不到预期效果。然后继续寻找解决方案,PlantomJs是最完美的解决方案。

公司之前做的某一手机应用,里面有一需求是一键出图(有一统计信息类的网页,需要在不打开网页的情况下实时对网页进行截图然后保存到服务器上),手机上便可以查看该图片了。刚开始拿到需求发现比较棘手,参考了很多文章解决方案大楷有以下几种:

  • Robot
  • 利用JNI,调用第三方C/C++组件
  • DJNativeSwing组件

参考文章: blog.csdn.net/cping1982/a…

经过试验Robot失败,DJNativeSwing组件截图成功,但由于网页css的复杂性导致图片失真严重而达不到预期效果。然后继续寻找解决方案,PlantomJs是最完美的解决方案。

PlantomJs是一个基于javascript的webkit内核无头浏览器 也就是没有显示界面的浏览器,你可以在基于 webkit 浏览器做的事情,它都能做到。PlantomJs提供了如 CSS 选择器、DOM操作、JSON、HTML5、Canvas、SVG 等。PhantomJS 的用处很广泛,如网络监控、网页截屏、页面访问自动化、无需浏览器的 Web 测试等,而博主只需要一很小的功能就是网页截屏。

实现思路

手机发送请求到服务器,服务器截取网页为图片保存到硬盘,生成可访问的URL返回手机上,示意图如下:

基于PhantomJs的 <a href='https://www.codercto.com/topics/22013.html'>Java</a> 后台网页截图技术

下载

直接进入官网下载http://phantomjs.org/download.html,目前官方支持三种操作系统,包括windows\Mac OS\Linux, 而博主服务器基于windows,所以下载https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-windows.zip,解压后得到以下目录:

基于PhantomJs的Java后台网页截图技术

编写截图JavaScript

参考文章http://www.cnblogs.com/jasondan/p/4108263.html

负责截图脚本 screenshot.js 如下:

/**
 * phantomJs 脚本
 */
var page = require('webpage').create(), system = require('system'), address, output, size;

if (system.args.length < 3 || system.args.length > 5) {
	phantom.exit(1);
} else {
	address = system.args[1];
	output = system.args[2];
	//定义宽高
	page.viewportSize = {
		width : 800,
		height : 600
	};
	page.open(address, function(status) {
		var bb = page.evaluate(function() {
			return document.getElementsByTagName('html')[0].getBoundingClientRect();
		});
		page.clipRect = {
			top : bb.top,
			left : bb.left,
			width : bb.width,
			height : bb.height
		};
		window.setTimeout(function() {
			page.render(output);
			page.close();
			console.log('渲染成功...');
		}, 1000);
	});
}
复制代码
address = system.args[1];//传入的URL地址
output = system.args[2];//保存的图片路径
复制代码

以上是 screenshot.js 的脚本内容

编写服务器Java代码

public static void main(String[] args) throws IOException {
		String BLANK = "  ";
		Process process = Runtime.getRuntime().exec(
				"D:/develop_software/phantomjs/bin/phantomjs.exe" + BLANK //你的phantomjs.exe路径
				+ "D:/screenshot.js" + BLANK //就是上文中那段javascript脚本的存放路径
				+ "http://www.baidu.com" + BLANK //你的目标url地址
				+ "D:/baidu.png");//你的图片输出路径

		InputStream inputStream = process.getInputStream();
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		String tmp = "";
		while ((tmp = reader.readLine()) != null) {
			if (reader != null) {
				reader.close();
			}
			if (process != null) {
				process.destroy();
				process = null;
			}
			System.out.println("渲染成功...");
		}
	}
复制代码

运行以上java代码,会在d盘下生成baidu.png的图片截图成功如下图:

基于PhantomJs的Java后台网页截图技术

至此一个demo完成!

代码封装(实际项目)

1、screenshot.js处理

实际应用中类似于 screenshot.js 一般不放在固定目录,一般放在应用根目录下

基于PhantomJs的Java后台网页截图技术

在tomcat启动时就把 screenshot.js 路径缓存起来

/**
  * 获取【网页快照截图脚本】文件的路径
  * 
  * @return
  */
private String getFullJsPath() {
	return AppContext.getAbsPath() + "/apicture/js/screenshot.js";
}
复制代码

2、phantomjs.exe处理

把phantomjs.exe的路径配置化,不直接像demo中那样写死到程序中,在web应用中一般都有一个总的applicationConfig.xml来存放诸如这种东西,于是在applicationConfig.xml中加入如下xml节点:

...
<phantomJs>
	<bin>D:/develop_software/phantomjs/bin/phantomjs.exe</bin>
	<imagePath>apicture/pub</imagePath><!--图片生成路径-->
</phantomJs>
...
复制代码

通过jaxb工具包将配置转化到对象中

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * phantomJs 配置
 * 
 * @author Fhcj
 *         2016年8月26日
 * @since
 * @version
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "phantomJs")
public class PhantomJsConifg {

	@XmlElement(name = "bin")
	private String bin;
	@XmlElement(name = "imagePath")
	private String imagePath;

	public String getBin() {
		return bin;
	}

	public void setBin(String bin) {
		this.bin = bin;
	}

	public String getImagePath() {
		return imagePath;
	}

	public void setImagePath(String imagePath) {
		this.imagePath = imagePath;
	}

}
复制代码

3、编写action

博主用的是nutz mvc作为action层,同理可用servlet或者spring mvc、struts2等

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.rbp.rt.web.result.ObjectResult;
import com.*.eqma.AppContext;
import com.*.eqma.config.PhantomJsConifg;
import com.*.eqma.web.servlet.YingJiJspServlet;
import com.*.eqma.web.servlet.ZaiQingJspServlet;
import com.*.eqma.web.servlet.ZhenQingJspServlet;
import com.*.utils.DateUtils;
import com.*.utils.JsonUtils;
import com.*.utils.StringUtils;

/**
 * 
 * 一张图访问服务
 * 
 * @author Fhcj
 *         2016年9月2日
 * @since
 * @version
 */
@Ok("json")
public class APictureAction {

	private static final Logger LOG = LoggerFactory.getLogger(APictureAction.class);
	private static final String BLANK = " ";
	private static PhantomJsConifg pjc;

	private String BIN_PATH;
	private String IMAGE_PUB_PATH;

	/**
	 * 应急一张图
	 * 
	 * @param evt_id
	 *            事件id
	 * @return
	 */
	@SuppressWarnings("unused")
	@At("/apicture/yingJi")
	public String yingJiPicture(@Param("evt_id") String evt_id) {
		ObjectResult responseResult = new ObjectResult();

		if (StringUtils.isEmpty(evt_id)) {
			responseResult.setNote("地震事件Id为空,无法渲染图片");
			responseResult.setStatus(-1);
			return JsonUtils.obj2Json(responseResult);
		}

		
		String pictureName = evt_id + "_yingJi.png";
		try {
			String imgageFullPath = getFullImagePath(pictureName);// 得到图片完整路径
			// 如果该事件的一张图存在则不用渲染
			if (new File(imgageFullPath).exists()) {
				LOG.info("事件ID为【{}】的【应急一张图】已经存在,将不会重新渲染:{}", evt_id, imgageFullPath);
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}

			String url = YingJiJspServlet.getURL() + "?id=" + evt_id;// 应急一张图访问接口URL

			Process process = Runtime.getRuntime().exec(cmd(imgageFullPath, url));

			InputStream inputStream = process.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
			String tmp = "";
			while ((tmp = reader.readLine()) != null) {
				close(process, reader);

				LOG.info("事件ID为【{}】的【应急一张图】渲染成功:{}", evt_id, imgageFullPath);
				LOG.info("事件ID为【{}】的【应急一张图】访问路径为:{}", evt_id, getPictureVisitURL(pictureName));
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}
		} catch (Exception e) {
			responseResult.setStatus(-1);
			responseResult.setNote("事件ID为【{}】的【应急一张图】渲染失败");
			LOG.error("事件ID为【{}】的【应急一张图】渲染失败:", e);
		}

		return JsonUtils.obj2Json(responseResult);
	}

	/**
	 * 灾情一张图
	 * 
	 * @param evt_id
	 *            事件id
	 * @return
	 */
	@SuppressWarnings("unused")
	@At("/apicture/zaiQing")
	public String zaiQingPicture(@Param("evt_id") String evt_id) {
		ObjectResult responseResult = new ObjectResult();

		if (StringUtils.isEmpty(evt_id)) {
			responseResult.setNote("地震事件Id为空,无法渲染图片");
			responseResult.setStatus(-1);
			return JsonUtils.obj2Json(responseResult);
		}

		String pictureName = evt_id + "_zaiQing.png";
		try {
			String imgageFullPath = getFullImagePath(pictureName);

			// 如果该事件的一张图存在则不用渲染
			if (new File(imgageFullPath).exists()) {
				LOG.info("事件ID为【{}】的【灾情一张图】已经存在,将不会重新渲染:{}", evt_id, imgageFullPath);
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}

			String url = ZaiQingJspServlet.getURL() + "?id=" + evt_id;// 灾情一张图访问接口URL
			Process process = Runtime.getRuntime().exec(cmd(imgageFullPath, url));

			InputStream inputStream = process.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
			String tmp = "";
			while ((tmp = reader.readLine()) != null) {
				close(process, reader);

				LOG.info("事件ID为【{}】的【灾情一张图】渲染成功:{}", evt_id, imgageFullPath);
				LOG.info("事件ID为【{}】的【灾情一张图】访问路径为:{}", evt_id, getPictureVisitURL(pictureName));
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}
		} catch (Exception e) {
			responseResult.setStatus(-1);
			responseResult.setNote("事件ID为【{}】的【灾情一张图】渲染失败");
			LOG.error("事件ID为【{}】的【灾情一张图】渲染失败:", e);
		}

		return JsonUtils.obj2Json(responseResult);
	}
	/**
	 * 震情一张图
	 * 
	 * @param lng
	 *            经度
	 * @param lat
	 *            纬度
	 * @return
	 */
	@SuppressWarnings("unused")
	@At("/apicture/zhenQing")
	public String zhenQingPicture(@Param("lng") String lng, @Param("lat") String lat) {
		ObjectResult responseResult = new ObjectResult();
		String pictureName = DateUtils.formatCurrentDate("yyyyMMddHHmmssSSS") + "_zhenQing.png";
		try {

			String imgageFullPath = getFullImagePath(pictureName);

			String url = ZhenQingJspServlet.getURL() + "?lng=" + lng + "⪫=" + lat;// 震情一张图访问接口URL

			Process process = Runtime.getRuntime().exec(cmd(imgageFullPath, url));

			InputStream inputStream = process.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
			String tmp = "";
			while ((tmp = reader.readLine()) != null) {
				close(process, reader);

				LOG.info("【震情一张图】渲染成功:{}", imgageFullPath);
				LOG.info("【震情一张图】访问路径为:{}", getPictureVisitURL(pictureName));
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}
		} catch (Exception e) {
			responseResult.setStatus(-1);
			responseResult.setNote("【震情一张图】渲染失败");
			LOG.error("【震情一张图】渲染失败:", e);
		}

		return JsonUtils.obj2Json(responseResult);
	}

	/**
	 * 获取执行JS脚本的window cmd 命令
	 * 
	 * @param imgageFullPath
	 *            图片完整路径
	 * @param url
	 *            截图网页的URL
	 * @return
	 */
	private String cmd(String imgageFullPath, String url) {
		return getBinPath() + BLANK + getFullJsPath() + BLANK + url + BLANK + imgageFullPath;
	}

	/**
	 * 关闭进程
	 * 
	 * @param process
	 * @param bufferedReader
	 * @throws IOException
	 */
	private void close(Process process, BufferedReader bufferedReader) throws IOException {
		if (bufferedReader != null) {
			bufferedReader.close();
		}
		if (process != null) {
			process.destroy();
			process = null;
		}
	}

	/**
	 * 通过图片名获取最终【客户端】访问的URL
	 * 
	 * @param pictureName
	 * @return
	 */
	private String getPictureVisitURL(String pictureName) {
		return AppContext.getDomain() + "/" + pjc.getImagePath() + "/" + pictureName;
	}

	/**
	 * 通过图片名获取最终完整路径
	 * 
	 * @param pictureName
	 * @return
	 */
	private String getFullImagePath(String pictureName) {
		return getPictureRootPath() + "/" + pictureName;
	}

	/**
	 * 获取【网页快照截图脚本】文件的路径
	 * 
	 * @return
	 */
	private String getFullJsPath() {
		return AppContext.getAbsPath() + "/apicture/js/screenshot.js";
	}

	/**
	 * 获取图片生成的根路径
	 * 
	 * @return
	 */
	private String getPictureRootPath() {
		ensurePhantomJsConfig();
		IMAGE_PUB_PATH = AppContext.getAbsPath() + "/" + pjc.getImagePath();
		return IMAGE_PUB_PATH;
	}

	/**
	 * 获取phantomjs.exe所在路径
	 * 
	 * @return
	 */
	private String getBinPath() {
		ensurePhantomJsConfig();
		BIN_PATH = pjc.getBin();
		return BIN_PATH;
	}

	/**
	 * 确保配置存在
	 */
	private void ensurePhantomJsConfig() {
		if (pjc == null) {
			pjc = AppContext.getApplicationConfig().getPhantomJsConifg();
		}
	}
}
复制代码

于是访问http://localhost:8080/xxx/apicture/zhenQing便会返回图片的URL,手机端便可查看展示如下:

基于PhantomJs的Java后台网页截图技术

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大转换

大转换

尼古拉斯·卡尔 / 闫鲜宁、张付国 / 中信 / 2016-2 / 49

1、我们这个时代最清醒的思考者之一尼古拉斯·卡尔继《浅薄》《玻璃笼子》之后又一重磅力作。 2、在这部跨越历史、经济和技术领域的著作中,作者从廉价的电力运营方式对社会变革的深刻影响延伸到互联网对我们生活的这个世界的重构性影响。 3、《快公司》《金融时报》《华尔街日报》联袂推荐 简介 早在2003年,尼古拉斯·卡尔先生发表在《哈佛商业评论》上的一篇文章——IT Doesn't ......一起来看看 《大转换》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具