为用户提供将连载(网页)导出为 PDF 功能,以便于用户更放心地记录,不用担心数据丢失。并且为 APP 端提供接口,当用户点击导出按钮时,提交导出任务到后台,成功导出后,通过私信告知用户。
实现思路
因为 Java 在转换 PDF 方面效果不够好,加上之前了解了下网页转换为 PDF,发现 phantomjs 是非常棒的一个工具。于是这次就直接选了该 js 进行转换。
主要的思路是这样的:
- 安装
phantomjs
- APP 端传入需要导出的 URL,请求导出接口
- 服务器端接收到导出请求,根据 URL 的参数来判断导出的任务个数(因为有些连载会有几百个阶段,如果一次导出的话,会非常慢且生成的文件巨大),并且这是一个耗时操作,所以异步将任务添加到分布式队列中,同时返回成功状态码给前端,以免前端等待。
- 消息队列的消费者收到任务,开始调用
shell
命令,执行phantomjs
-
phantomjs
转换完毕后,将转成成的 pdf 上传到七牛(我们提供给用户的是一个地址,这样用户下载时就不消耗自己服务器带宽了) - 上传七牛成功后,消息队列手动 ACK,并将导出的结果存入数据库,同时发送私信告知用户,已经成功导出。
这就是主要的实现思路。
安装 phantomjs
-
wget wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
-
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
我们先来简单测试下该 js,看看效果如何。
具体代码为:
'use strict';
var page = require('webpage').create()
, system = require('system')
, args = system.args
, url = args.length > 1 ? args[1] : 'http://www.lianzai.me/toPdf/666666/10192/1.html'
, filename = args.length > 2 ? args[2] : 'tmp';
page.paperSize = {
format: 'A4',
header: {
height: "2cm",
contents: phantom.callback(function(pageNum, numPages) {
return "";
})
},
footer: {
height: "2.5cm",
contents: phantom.callback(function(pageNum, numPages) {
return "<h1 style='position: relative;top:35px;font-size:13px;color:#999;text-align:center;font-weight:400'><span>" + pageNum + " / " + numPages + "</span></h1>";
})
}
}
url = decodeURIComponent(url);
page.open(url, function (status) {
console.log(status);
if (status === 'success') {
page.render('/testpdf/' + filename + '.pdf');
}
phantom.exit();
});
参数中如果没有传递 url 和 fileName,会有默认值。
我们把 phanmotjs
加入到 path
中,以便可以在终端直接使用 phantomjs
。
-
vim /etc/profile
-
export PATH=/usr/software/phantomjs-2.1.1-linux-x86_64/bin:$PATH
-
source /etc/profile
测试效果
phantomjs toPdf.js http://www.lianzai.me/toPdf/123456/14456/1.html 非客观的我
转换成 PDF 的效果如下:
部分截图:
可以看到效果是非常不错的。
定义 API
public static void export(Long uid, @Required String url, String uidSid) {
paramError();
// 判断是否为 vip
Integer vipStatus = userInfoLogic.getVipStatus(uid);
if (!UserInfo.VipStatus.VIP.getIndex().equals(vipStatus)) {
renderJsonFail(ReturnCode.ONLY_VIP_ACCESS.getCode(), ReturnCode.ONLY_VIP_ACCESS.getMsg());
}
Long planId = extractPlanId(url);
long stageCount = planStageLogic.countByUidAndPlanId(uid, planId, 0);
if (stageCount > 0) {
Date now = new Date();
String taskId = Codec.UUID().split("-")[0];
int pageCount = calculatePageCount(stageCount);
Cache.set(taskId, pageCount, "10h");
for (int i = 1; i <= pageCount; i++) {
String url1 = url + "/" + i + ".html";
AsyncOperation.createExportPdfMsg(taskId, uid, planId, url1, now, QueueTypeEnum.EXPORT_PDF);
}
}
renderJsonSuccess();
}
这主要是自己的业务逻辑,主要看
AsyncOperation.createExportPdfMsg(taskId, uid, planId, url1, now, QueueTypeEnum.EXPORT_PDF)
,这里将任务细分为几个任务,然后添加到分布式消息队列中。详细代码就不贴了,都是一些基础的类。
Java 调用 Shell
在上一个步骤中,我们将任务添加到分布式消息队列中,然后现在需要进行消费。这个过程,主要是利用 Java 调用 Shell 命令,让 Java 执行我们的导出脚本。
Java 调用 Shell,关键代码就是
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
该方法会返回一个 int 值,当返回结果为 0 的时候,则表示成功。
在这个问题上,我遇到过 127,255 返回码以及权限不足问题。
以下资料可作为参考
【权限不足】在 command 前面加上 chmod 777
即可解决。
上传到七牛
这个调用七牛 API 即可,上传完毕后,将七牛返回的地址存入数据库,同时向用户发送完成的私信通知。
然后再告诉消息队列,说该条消息已经被成功处理了,可以废弃掉了。
NOTE
-
千万要注意执行的脚本路径,不然会出各种意外的问题,例如权限不足,或者没有报任何错误,但是就是没有效果。
-
中文乱码问题
linux 上可能没有安装可用的中文字体,导出来的pdf中文就为空白了。下载一个字体,进行解压,然后建立软链接
wget http://dlc2.pconline.com.cn/filedown_367689_7048847/f9qOLERr/simsun.zip
unzip simsun.zip
ln -s /usr/share/fonts/truetype/simsun.ttf
以上大体上就完成了我们的需求,还有很多业务逻辑需要自己去考虑,关于这里使用的分布式消息队列,是 RabbitMQ
。有兴趣的可以自己去了解下。