2016年12月31日 星期六

Node.js 小密技:以 Readline 核心模組一行行讀取檔案內容

Standard

最近參與了一些關於資料處理的專案,處理了很多各式各樣的原始資料(Raw Data)或各種不同格式的資料,於是使用到了 Node.js 上的一些小技巧。像是一行行讀取檔案內容這件事,就隱藏了一些技巧。

對很多人來說,處理的檔案內容都不大,如果用 Node.js 來一行行讀取檔案內容,不外乎就是將整個檔案讀出後再進行切割,做法大致上如下:
var fs = require('fs');

fs.readFile('example.txt', function(err, data) {

    // 以換行字元作為切割點,將內容切成一個大陣列
    var lines = data.split('\n');

    lines.forEach(function(line) {
        // 一行行處理
    });
});
但有些時候,由於檔案並不小,若又牽涉到運算,不可能整個檔案都讀出到記憶體上才進行切割,這時就得用到 Stream(資料流)機制,將檔案一段段讀出來進行處理。然後,為了進行一行行的切割,我們會自己做這樣的機制,先將一段段讀取出來的檔案內容放到緩衝區(Buffer),然後找到換行字元進行切斷取出,然後再繼續讀取檔案,重複這樣的過程直到檔案結尾。

的確,實做這樣的機制有點麻煩,所以其實能利用 Node.js 現成內建的核心模組 Readline 來做到切割資料流中一行字串的工作。因為常見的 Readline 用法都是拿來做終端機字元模式下的命令列操作,所以許多人沒有想到可以這樣使用 Readline。作法其實很簡單,就把 Readline 的 input 從標準輸入(Standard Input)換成我們的檔案讀取資料流就可以。

完整做法如下:
var fs = require('fs');
var readline = require('readline');

// 建立檔案讀取資料流
var inputStream = fs.createReadStream('example.txt');

// 將讀取資料流導入 Readline 進行處理 
var lineReader = readline.createInterface({ input: inputStream });
lineReader.on('line', function(line) {

    // 取得一行行結果
    console.log('NEW LINE', line);
});

後記

其實這樣的 Readline 用法,在 Node.js 官方 API 文件上可以看到,只不過是不久前才被加進去的,在文件的最後面。:-P

參考連結:https://nodejs.org/api/readline.html

2016年12月28日 星期三

產品開發玩技術很過癮!實作 QML 動畫背景!

Standard

由於最近在開發自己的產品,又開始重操舊業,開發起 Linux 系統的相關應用和嵌入式技術。為了這個產品,精心開發了一個使用者介面,除了動手把驅動程式搞定、圖形化介面搞定,也調教效能、改善系統架構。

開發自己的產品很過癮,愛怎麼搞就怎麼搞!於是,看到死板的背景覺得很不舒服,就在思考是否可以跑個動畫背景呢?

因為使用的是 QML 技術來開發 UI,最直接的想法,就是用 QtMultimedia 的 MediaPlayer 無限循環播放一個影片,當作動畫背景:
MediaPlayer {
    id: bg;
    source: 'bg.mov';
    loops: MediaPlayer.Infinite;
    autoPlay: true;
}

VideoOutput {
    anchors.fill: parent;
    source: bg;
}

當然,我們選擇的背景影片,是一個開頭跟結尾一樣的影片,如果正確循環播放,會無縫接軌的變成一個順暢的動畫背景。

然而,結果不如預期,碰到了一個問題,那就是每當背景影片播放到最後時,會畫面變成全黑,然後才再一次重新開始播放,沒辦法「無縫接軌」。仔細暸解以後,發現 MediaPlayer 元件是 QMediaPlayer 的 QML Type 實作,所有秘密都藏在 QMediaPlayer 之中。因為 QMediaPlayer 預設所有的通知事件,都是固定以 1000ms(1秒)的頻率來觸發,這代表,當 QML 元件發現影片播完時,通常已經是播完以後的事了,所以畫面一定會因為影片結束而變黑,然後 QML 元件才發現影片結束,重新進行播放。

知道緣由後,我們可以從事件更新頻率下手,讓 QML 元件發現影片播完的時間更接近實際影片結束的時間,但這必須動用到 C/C++ 的實作,因為 QMediaPlayer 的事件更新頻率無法以純 QML 的方法修改。

C/C++ 完整應用程式的實作如下,我們把更新頻率調高為每 100ms 一次:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
#include <QMediaObject>
#include <QMediaPlayer>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/resources/App.qml")));

    // Getting background component by using background objectName
    QObject *obj = static_cast<QObject *>(engine.rootObjects().first());
    QObject *background = obj->findChild<QObject *>("background");

    // Set NotifyInterval to 100ms
    QMediaPlayer *player = qvariant_cast<QMediaPlayer *>(background->property("mediaObject"));
    player->setNotifyInterval(100);

    return app.exec();
}

除此之外,為了可以順利取得 QML 中的 MediaPlayer 元件,我們需要幫其設定一個「objectName」作為識別,讓 C/C++ 這的原生程式可以搜尋的到該元件:
MediaPlayer {
    id: bg;
    objectName: 'background';
    source: 'bg.mov';
    loops: MediaPlayer.Infinite;
    autoPlay: true;
}

雖然我們縮短了每次更新的間隔時間,調到了 100ms,已經非常接近了影片結束時間,但仍然可能會發生問題。所以保險起見,我們可以多做些檢查工作,在影片結束前 100ms 左右時,就讓他重頭開始播放一次影片。
MediaPlayer {
    id: bg;
    objectName: 'background';
    source: 'bg.mov';
    loops: MediaPlayer.Infinite;
    autoPlay: true;

    onPositionChanged: {
        if (position >= duration - 100) {
            bg.seek(0);
        }
    }
}

理論上,這樣做會損失不到 100ms 長度的動畫,但人通常感覺不出來這麼短的損失,而且是前方還有 UI 介面的情況之下。但如果你仍然有感,可以考慮把頻率改為 50ms 或更少。

最後就可以享受會動的背景啦!