需求描述 由于分享的需要,经常会将一些网站项目搬上站点上进行展示,由于变动频繁,且可能涉及项目较多,需要能自动进行项目整理发布,将网页快照展示在首页列表中。
环境 目前所有项目共用一个域名,项目根目录在域名下一级路径中,项目名即路径名,使用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]); } 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)
测试执行