利用PhantomJS做網頁截圖經濟適用,但其API較少,做其他功能就比較吃力了。例如,其自帶的Web Server Mongoose最高只能同時支援10個請求,指望他能獨立成為一個服務是不怎麼實際的。所以這裡需要另一個語言來支撐服務,這裡選用NodeJS來完成。
安裝PhantomJS
首先,到PhantomJS官網下載對應平台的版本,或是下載原始碼自行編譯。再將PhantomJS配置進環境變量,輸入
$ phantomjs
如果有反應,那麼就可以進行下一步了。
利用PhantomJS進行簡單截圖
這裡我們設定了視窗大小為1024 * 800:
page.viewportSize = { width: 1024, height: 800 };
NodeJS與PhantomJS通訊
例如:
phantomjs-node成功將PhantomJS作為NodeJS的一個模組來使用,但我們看看作者的原理解釋:
我會用一個問題來回答這個問題。如何與不支援共享記憶體、套接字、FIFO 或標準輸入的進程通訊?
嗯,PhantomJS 確實支援一件事,那就是打開網頁。事實上,它在打開網頁方面確實很擅長。因此,我們透過啟動 ExpressJS 實例、在子進程中開啟 Phantom,並將其指向一個特殊網頁來與 PhantomJS 進行通信,該網頁將 socket.io 訊息轉換為 alert()
呼叫。這些alert()
電話將由 Phantom 接聽,然後就可以了!
通信本身是透過James Halliday 出色的dnode 庫進行的,幸運的是,與browserify 結合使用時效果很好,可以直接在PhantomJS 的pidgin Javascript 環境中運行。
實際上phantomjs-node使用的也是HTTP或Websocket來進行通訊,不過其依賴龐大,我們不如做一個簡單的東西,暫時還是不考慮這個東東吧。
設計圖
讓我們開始吧
我們在第一版中採用HTTP進行實作。
先利用叢集進行簡單的進程監控(index.js):
if(!fs.existsSync('./snapshot')) {
fs.mkdirSync('./snapshot');
}
if (cluster.isMaster) {
cluster.fork();
cluster.on('exit', function (worker) {
console.log('Worker'worker.id '死了:(');
. > cluster.fork();
});
})
} else {
require('./extract.js));
require('./extract.js));
}
}
然後利用connect做我們的外部API(extract.js):
pkg = JSON.parse(fs.readFileSync('./package.json'));
var app = connect()
.use(connect.logger('dev'))
.use('/snapshot', connect.static(__dirname '/snapshot', { maxAmaxAter pkg. maxAge }))
.use(connect.bodyParser())
.use('/bridge', 橋)
.use('/api', function (req, res, next) {
if (!req.body.urls || !req.body. jobMan.watch(req.body.campaignId, req, res, next);
var marketingId = req.body.campaignId
, imagesPath = './snapshot , url
, imagePath;
function _deal(id, url, imagePath) {
// 直接推入 url 名單
🎜> url: url,
以影像路徑: imagePath
});
}
for (var i = req.body.urls.length; i--;) {
url = req.body.urls[i] ;
imagePath = imagesPath i '.png';
_deal(i, url, imagePath);
_deal(i, url, imagePath);
jobMan.register(campaignId, urls, req, res, next);
console.log('stdout: ' data);
});
(5); console.log('stderr: ' data);
});
snapshot.on('close', function (code) { ;
});
})
.use(connect.static(__dirname '/html', { maxAge: pkg.maxAge }))
: ' 'http://localhost:' pkg.port);
其中bridge是HTTP通訊橋樑,jobMan是工作管理器。
通訊橋樑負責接受或返回作業的相關信息,並聯作業Man(bridge.js):
程式碼如下:
return function (req, res, next) {
if (req.headers.secret !== pkg.secret) return next();
(req.method === "POST") {
var body = JSON.parse(JSON.stringify(req.body));
');
// 快照APP可以取得需要提取的url
} else {
var urls = jobMan.getUrls(req.m.Match(req.m.m. |&|$)/)[1]);
res.writeHead(200, {'Content-Type': 'application/json'});
end(JSON.stringify({ urls: urls }));
}
};
})();
如果請求方法為POST,則認為PhantomJS正在給我們主動job的相關資訊。而為GET時,則認為其要取得job的資訊。
function _send(campaignId){
var job = _jobs[campaignId];
if (!job) return;
if (job.waiting) {
job.waiting = false;
clearTimeout(job.timeout);
var finish = (job.urlsNum === job.finishNum) urls: job.urls,
完成:完成
};
job.urls = []; 刪除_jobs[campaignId]
}
res.writeHead(200, {'Content-類型': 'appli res.writeHead(200, {'Content-Type': 'appli/json'}); 🎜> res.end ( JSON.stringify(data));
}
}
function register(campaignId, urls, req, res, next) { : urls.length,
finishNum: 0,
urls: [],
iting: false,
timeout: null
} ;
watch(campaignId, req, res, next);
}
function watch(campaignId, req, res, next) {
_jobs[campaignId].res = res;
// 20 秒逾時
{
_send(campaignId);
}, 20000);
}
function fire(opts) {
var CampaignId = opts.campaignId
if (job) {
if ( opts.status && fetchObj.title) {
🎜> url: opts.url,
圖片:opts.image,
標題:fetchObj.title, });
}其他{
id: opts.id,
});
}
} else {
console.log('作業
function getUrls(campaignId) {
var job = _jobs[campaignId];
if (job) return job.cacheUrls;
if (job) return job.cacheUrls;
>
return {
註冊:註冊,
觀看:觀看,
火:火,
getUrls:getUrls
})();
這裡我們用fetch對html進行抓取其標題與描述,fetch實作比較簡單(fetch.js):
複製程式碼
程式碼如下:
回傳函數 (html) {
if (!html) return { 標題: false, 說明: false };
var title = html.match(/
if (meta) {
for (var i = meta.length; i--;) {
if(meta[i].indexOf('name="description if(meta[i].indexOf('name="description) | | 元[i].indexOf('name="描述"') > -1){
說明= 元[i].match(/content="(.*?)"/)[1] ;
}
}
}
(標題&& 標題[1] !== '') ? (標題= 標題[1]) : (標題= '無標題');
描述|| (說明= '無描述');
return {
標題:標題,
說明:說明
};
}
};
}
;
;;
function snapshot(id, url, imagePath) {
var page = webpage.create()
, send
, begin
, save
, end;
page.viewportSize = { width: 1024, height: 800 };
page.clipRect = { top: 0, left: 0, width: 1024, height: 800 };
page.settings = {
javascriptEnabled: false,
loadImages: true,
userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko) PhantomJS/1.9.0'
};
page.open(url, function (status) {
var data;
if (status === 'fail') {
data = [
'campaignId=',
campaignId,
'&url=',
encodeURIComponent(url),
'&id=',
id,
'&status=',
].join('');
postPage.open('http://localhost:' + pkg.port + '/bridge', 'POST', data, function () {});
} else {
page.render(imagePath);
var html = page.content;
// callback NodeJS
data = [
'campaignId=',
campaignId,
'&html=',
encodeURIComponent(html),
'&url=',
encodeURIComponent(url),
'&image=',
encodeURIComponent(imagePath),
'&id=',
id,
'&status=',
].join('');
postMan.post(data);
}
// release the memory
page.close();
});
}
var postMan = {
postPage: null,
posting: false,
datas: [],
len: 0,
currentNum: 0,
init: function (snapshot) {
var postPage = webpage.create();
postPage.customHeaders = {
'secret': pkg.secret
};
postPage.open('http://localhost:' + pkg.port + '/bridge?campaignId=' + campaignId, function () {
var urls = JSON.parse(postPage.plainText).urls
, url;
this.len = urls.length;
if (this.len) {
for (var i = this.len; i--;) {
url = urls[i];
snapshot(url.id, url.url , url.imagePath);
}
}
});
this.postPage = postPage;
},
post: function (data) {
this.datas .push(data);
if (!this.posting) {
this.posting = true;
this.fire();
}
},
fire: function () {
if (this.datas.length) {
var data = this.datas.shift()
, that = this;
this.postPage.open('http:// localhost:' pkg.port '/bridge', 'POST', data, function () {
that.fire();
// 子プロセスを強制終了
setTimeout(function () {
if ( this.currentNum === this.len) {
that.postPage.close();
phantom.exit();
}
}, 500);
}) ;
} else {
this.posting = false;
}
}
};
postMan.init(snapshot);