0%

制作一个自动更新的项目首页

需求描述

由于分享的需要,经常会将一些网站项目搬上站点上进行展示,由于变动频繁,且可能涉及项目较多,需要能自动进行项目整理发布,将网页快照展示在首页列表中。

环境

目前所有项目共用一个域名,项目根目录在域名下一级路径中,项目名即路径名,使用Nginx做分发,项目存放在同一个目录下,且都配置有默认主页。

实现方案

使用脚本定期扫描项目目录,访问近期修改及添加的项目的主页,使用phantomjs生成网页快照,剔除被删掉的项目,更新到首页项目列表中。

实现过程

环境中的项目安装略过

安装phantomjs

phantomjs脚本

phantomjs脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
var fs = require("fs");
var args = require('system').args;
var webpage = require("webpage");

var programDir = "../static/";
var programs = fs.list(programDir);

var interval = parseInt(args[1]);
var pageW = 1920;
var pageH = 1080;

var shotDir = "shot/";
var tempDir = "temp/";

fs.makeDirectory(tempDir);
var shots = fs.list(shotDir);
for(var index=2; index < shots.length; index ++){
fs.move(shotDir + shots[index], tempDir + shots[index]);
}

// var全局变量污染,使用函数解决
function task(page, program, url) {
page.viewportSize = {
width: pageW,
height: pageH
};
console.log("loading " + program);
page.onLoadFinished = function(status){
console.log("load " + status);
if (status != "success"){
console.log("fail to load " + url);
}else{
page.clipRect = { left: 0, top: 0, width: pageW, height: pageH };
page.render(shotDir + program + ".jpg");
console.log('Gen:', program);
}
};

page.onResourceError = function(resourceError) {
console.log('= onResourceError()');
console.log(' - unable to load url: "' + resourceError.url + '"');
console.log(' - error code: ' + resourceError.errorCode + ', description: ' + resourceError.errorString );
};

page.onError = function(msg, trace) {
system.stderr.writeLine('= onError()');
var msgStack = [' ERROR: ' + msg];
if (trace) {
msgStack.push(' TRACE:');
trace.forEach(function(t) {
msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function + '")' : ''));
});
}
system.stderr.writeLine(msgStack.join('\n'));
};
page.open(url, function (status) {
});
}

// 第一二个元素为.和..
for(var index2=2; index2 < programs.length; index2++){
var program = programs[index2];
if(fs.lastModified(programDir + program).getTime() > Date.now() - interval * 1000 || !fs.exists(tempDir + program)){
var page = webpage.create();
page.viewportSize = {
width: pageW,
height: pageH
};
console.log("loading " + program);
task(page, program, "https://domain.com/" + program)
}else{
console.log("copying " + program);
fs.move(tempDir + program, shotDir + program);
}
}

setTimeout(function(){
var temps = fs.list(tempDir);
for(var index=2; index < temps.length; index ++){
console.log("removing " + temps[index]);
fs.remove(tempDir + temps[index]);
}
fs.removeDirectory(tempDir);
phantom.exit();
}, 1000 * 2 * programs.length);

脚本用于生成参数指定项目在1920*1080像素大小浏览器中渲染出来的页面截屏,如果出现页面乱码,需要安装字体模块。

1
$ yum install bitmap-fonts bitmap-fonts-cjk

使用一些工具对图片进行缩放,过程略。

生成页面

根据生成的图片文档,渲染主页模版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>项目站</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="main" style="width:80%;margin:auto;">
<div class="panel panel-default" style="margin-top:10%;">
<div class="panel-heading">只是一些例子</div>
<div class="panel-body">
{% for line in range(0, (len(programs)-1) / 4 + 1) %}
<div class="row">
{% for program in programs[line*4: (line+1)*4] %}
<div class="col-xs-6 col-md-3">
<a href="https://domain.com/{{ program }}" class="thumbnail">
<img src="{{ program }}.jpg" alt="{{ program }}">
</a>
</div>
{% end %}
</div>
{% end %}
</div>
</div>
</div>
</body>
</html>

渲染模版,这里使用tornado.template

渲染脚本文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
import tornado.template

def gen(programs):
loader = tornado.template.Loader("./")
return loader.load("template.html").generate(programs=programs)

def detect():
programs = os.listdir("shot")
if programs:
with open("index.html", "w") as html:
html.write(gen([program.split(".")[0] for program in programs]))

if __name__ == "__main__":
detect()

新的实现(2018.03.07更新)

发现一个性能更好的浏览器渲染引擎,加上phantomjs项目已停止更新,更改使用新的方案实现。

Chrome Headless + puppeteer

Chrome59版本发布的时候,携带了一个无图形界面的浏览器环境,即Chrome Headless,可以用于构建web测试、爬虫程序,相对于此前常用的phantomjs,性能上有成倍的提升。puppeteer是node中chrome headless的驱动环境,可将多个过程整合到node脚本中。

centos7工具安装

参阅 centos安装puppeteer爬坑

安装puppeteer

puppeteer中会依赖安装chrome工程版Chromium,使用npm进行安装的时候会自动下载依赖,国内服务器由于屏蔽无法下载可暂略过下载脚本(国外服务器可直接忽略)。

1
npm install puppeteer --ignore-scripts --save

安装配置chromium

在node本地仓库的puppeteer目录package.json文件中找到依赖的chromium版本号,下载对应的chromnium文件:

1
2
3
4
linux: 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
mac: 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
win32: 'https://storage.googleapis.com/chromium-browser-snapshots/Win/%d/chrome-win32.zip',
win64: 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',

文件解压后放到puppeteer目录.local-chromium/linux-%d目录下。

由于安全性的要求,默认将chromium运行在沙箱环境中,需配置CHROME_DEVEL_SANDBOX环境,指向沙箱的运行程序,即chromium的解压目录下的chrome-devel-sandbox文件,但是文件必须属于root用户,具有4755文件权限,调整如下:

1
2
3
4
cp node_modules/puppeteer/.local-chromium/linux-536395/chrome-linux/chrome_sandbox /usr/local/sbin/chrome-devel-sandbox
sudo chown root:root /usr/local/sbin/chrome-devel-sandbox
sudo chmod 4755 /usr/local/sbin/chrome-devel-sandbox
export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

当然,也可以选择忽略安全性问题,在启动chromium时,使用–no-sandbox参数。

其他依赖

puppeteer启动必须依赖项

1
sudo yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 nss.x86_64 -y

由于缺失的字体,可能导致乱码

1
sudo yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

脚本

node脚本

script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const fs = require('fs');
const puppeteer = require('puppeteer');
const jade = require('jade');

const shotDir = "shot/";
const programDir = "../static/";
const pageW = 1920;
const pageH = 1080;

let programs = fs.readdirSync(programDir);

async function genScreenShot (program, screenshot) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({width: pageW, height: pageH});
await page.goto('https://domain.com/' + program, {waitUntil: 'networkidle2', timeout: 60000});
await page.screenshot({path: screenshot});
await browser.close();
}

for (let program of programs) {
let screenshot = shotDir + program + '.png';
if (!fs.existsSync(screenshot) || fs.statSync(programDir + program).mtimeMs > fs.statSync(screenshot).mtimeMs) {
genScreenShot(program, screenshot);
console.log('gen screenshot of ' + program);
} else {
console.log('Keep screenshot of ' + program);
}
}

let html = jade.renderFile('template.jade', {programs: programs});
fs.writeFileSync('index.html', html);
console.log('gen home over.');

模版文件

template.jade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
doctype html
html(lang='en')
head
meta(charset="UTF-8")
title 项目站
link(rel="stylesheet", href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css", integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u", crossorigin="anonymous")
body
div.wrapper
div.main.panel.panel-default
div.panel-heading 只是一些例子
div.panel-body
- for (var line=0; line<(programs.length-1) / 4 + 1; line++)
- var subs = programs.slice(line * 4, line * 4 + 4)
div.row
each program in subs
- var link = 'https://domain.com/' + program
- var img = program + '.png'
div.col-xs-6.col-md-3
a.thumbnail(href=link)
img(src=img, alt=program)

测试执行

1
node script.js