基於PhantomJs的Java後臺網頁截圖技術

ontologyFhcj發表於2019-01-29

基於PhantomJs的Java後臺網頁截圖技術

公司之前做的某一手機應用,裡面有一需求是一鍵出圖(有一統計資訊類的網頁,需要在不開啟網頁的情況下實時對網頁進行截圖然後儲存到伺服器上),手機上便可以檢視該圖片了。剛開始拿到需求發現比較棘手,參考了很多文章解決方案大楷有以下幾種:

  • 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的Java後臺網頁截圖技術

下載

直接進入官網下載http://phantomjs.org/download.html,目前官方支援三種作業系統,包括windowsMac OSLinux,
而博主伺服器基於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=" + 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後臺網頁截圖技術

相關文章