UNIX: 2012年2月アーカイブ

Unicode 文字—十六進表記相互変換

昨夜,漢字の拡大表示の JavaScript 自作プログラム『老眼鏡』について書いた。その後,漢字の入力を,漢字そのものではなく,Unicode コードポイントでも可能とした。つまり,例えば森鷗外の「鷗」という文字を拡大表示したいとき,「鷗」の Unicode コードポイント U+9DD7 の 9dd7 を入力してもよいようにした。Enter キーを押しても実行されるようにも改修した。老眼鏡 Reading Glasses for the Aged からお使いください。もしブックマークに登録するならこのリンクをドラッグしてください。

かな漢字変換で出て来ない文字も,Unicode コードポイントがわかっていれば,これで表示してコピペして入力するという使い方もできる。牛丼の吉野家の「吉」は,商標では異体字の「𠮷」(上部が士ではなく土のキチ,いわゆる土吉。画面に文字が出てますか?)が本来のものだが,「𠮷」(U+20BB7)は Unicode CJK Unified Ideographs Extension B という拡張領域に定義されていて,ちょっと古いエディタ,ワープロでは扱えない文字である。最近のかな漢字変換ソフトウェアでも変換候補に出て来ないものが多いのではないだろうか。このように入力するにやっかいな文字があるとき,『漢字源』で Unicode を調べ『老眼鏡』にこれを入れれば,その文字が表示され,これをコピペして入力することができる。もちろん逆に,漢字を入力して Unicode コードポイントを調べることも可能である。文字は漢字だけでなく 4 オクテットまでの Unicode 文字なら何でも(キリル文字でも,古典ギリシア語複式アクセント文字でも,アラビア文字でも,サンスクリット文字でも,ハングルでも)OKのはずである。

20120224-jiten-yosi.png
『漢字源』で「𠮷」Unicode 値は 20BB7
20120224-tutiyosi.png
20bb7 を入力すると「𠮷」が表示される

さて,『老眼鏡』のために Unicode コードポイント十六進数と文字との間で変換を行う関数を二本書いた。unitohex 関数は引数に文字(「𠮷」)をとって,これを Unicode コードポイント十六進数文字列(「20BB7」)を返す。hextouni 関数はこの逆を行う。この二つの関数コードを以下に掲載しておく。こんなのはネットにごろごろ転がっていそうだが,適当なものがなかなか見当たらず,自分で書くことにした。

// -*- coding: utf-8; mode: js2; -*-
// Unicode 十六進 / 文字 相互変換
// - unitohex('文字'):        文字 -> Unicode 十六進コードポイント
// - hextouni('十六進文字列'): Unicode 十六進コードポイント -> 文字
// 2012 (c) coded by isao yasuda.
/*
UTF-8 符号化方式 (1-4 オクテットのみ)
Code point        octet1    octet2    octet3    octet4
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+0000-U+007F     0xxx xxxx
 (min)U+0000       000 0000
 (max)U+007F       111 1111
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+0080-U+07FF     110y yyyx 10xx xxxx
 (min)U+080          0 0010   00 0000
 (max)U+7FF          1 1111   11 1111
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+0800-U+FFFF     1110 yyyy 10yx xxxx 10xx xxxx
 (min)U+0800           0000   10 0000   00 0000
 (max)U+FFFF           1111   11 1111   11 1111
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+010000-U+1FFFFF 1111 0yyy 10yy xxxx 10xx xxxx 10xx xxxx
 (min)U+010000          000   01 0000   00 0000   00 0000
 (max)U+1FFFFF          111   11 1111   11 1111   11 1111
*/
// 文字 -> Unicode 十六進コードポイント変換
function unitohex(c) {
    // %xx%yy%zz 形式にエスケープする
    var w = encodeURI(c);
    if (w.length > 9) {// 4 オクテット以上の Unicode 文字
        // U+010000 - U+1FFFFF 拡張領域文字
        // UTF-8 デコード
        // 1. 符号化 UTF-8 ビットパターンの取得
        w = w.replace(/%/gi, "");
        var h = "";
        for (var i = 0; i < w.length; i++) {
            // 二進数文字列に変換
            var bstr = parseInt(w.charAt(i), 16).toString(2);
            // 4 bit 前ゼロ付加
            var dig0 = "";
            for (var j = 0; j < (4 - bstr.length); j++) {
                dig0 += "0";
            }
            h += dig0 + bstr;
        }
        // 2. Code Point 部の bit を抽出
        var uar = [];
        uar[0] = parseInt(h.charAt(5),2);
        uar[1] = parseInt(h.substring(6,8) + h.substring(10,12),2);
        uar[2] = parseInt(h.substring(12,16),2);
        uar[3] = parseInt(h.substring(18,22),2);
        uar[4] = parseInt(h.substring(22,24) + h.substring(26,28),2);
        uar[5] = parseInt(h.substr(28,4),2);
        // 3. Code Point digit (4 bit) 毎に十六進数文字列に変換
        w = "";
        if (uar[0] != 0) {// 0, 1 以外ありえない
            w = uar[0].toString(16);
        }
        for (var i = 1; i < uar.length; i++) {
            w += uar[i].toString(16);
        }
    } else {// 3 オクテット (エンコード形式で 9 文字) 以下
        // U+FFFF までの BMP 領域文字: オクテット毎に Latin1 と同じ扱いでよい
        w = (c.charCodeAt(0)).toString(16);
        var dt = 4 - w.length;
        for (var i = 0; i < dt; i++) {
            w = '0' + w;
        }
    }
    return w;
}
// Unicode 十六進コードポイント -> 文字 変換
function hextouni(cp) {
    // 入力チェック [0-9a-fA-F] 以外の入力を弾く]
    if (cp.match(/[^0-9a-fA-F]/)) {
        alert("入力に十六進数以外の文字が含まれています");
        return;
    }
    // 一文字ずつ bit 変換 (4 bit 境界)
    cp = cp.replace(/^0*/, ""); // 前ゼロサプレス
    var bits = "";
    for (var i = 0; i < cp.length; i++) {
        var bs = parseInt(cp.charAt(i), 16).toString(2);
        var dt = 4 - bs.length;
        for (var j = 0; j < dt; j++) {
            bs = '0' + bs;
        }
        bits += bs;
    }
    // 有効桁数で 1-4 オクテット符号の場合分け
    var wb = bits.replace(/^0*/g, "");
    var u8bits = "";
    // 各グループのビットパターンに埋め込み UTF-8 にエンコードする
    if (wb.length < 8) {
        // 1 octet Unicode U+0000-U+007F
        return unescape('%' + cp);
    } else {
        if (wb.length < 12) {
            // 2 octet Unicode U+0080-U+07FF
            bits = regbits(bits, 12);
            // 0123 4567+0123 4567
            // 110y yyyx 10xx xxxx
            u8bits = '110' + bits.substring(1,6) + 
                '10' + bits.substring(6,12);
        } else {
            if (wb.length < 17) {
                // 3 octet Unicode U+0800-U+FFFF
                bits = regbits(bits, 16);
                // 0123 4567+0123 4567+0123 4567
                // 1110 yyyy 10yx xxxx 10xx xxxx
                u8bits = '1110' + bits.substring(0,4) +
                    '10' + bits.substring(4,10) +
                    '10' + bits.substring(10,16);
            } else {
                if (wb.length < 22) {
                    // 4 octet Unicode U+010000-U+1FFFFF
                    bits = regbits(bits, 24);
                    // 0123 4567+0123 4567+0123 4567+0123 4567 
                    // 1111 0yyy 10yy xxxx 10xx xxxx 10xx xxxx
                    u8bits = '11110' + bits.substring(3,6) +
                        '10' + bits.substring(6,12) + '10' +
                        bits.substring(12,18) + '10' +
                        bits.substring(18,24);
                } else {
                    alert("5バイト以上のUnicode文字は未サポート");
                    return;
                }
            }
        }
    }
    // 4 bit 毎に十六進変換
    var u8hex = "";
    for (var i = 0; i < u8bits.length; i+=4) {
        u8hex += parseInt(u8bits.substring(i,i+4), 2).toString(16);
    }
    // 十六進文字 2 個毎に % を付加し,urlencode 形式に変換
    var u8enc = "";
    for (var i = 0; i < u8hex.length; i+=2) {
        u8enc += '%' + u8hex.substring(i,i+2);
    }
    // urldecode で文字に復元し返却
    return decodeURI(u8enc);
}
// bit 正規化 (4 bit 境界にする)
function regbits(bs, bc) {// bit 文字列,正規化数
    var dt = bc - bs.length;
    if (dt < 0) {// 仮に要求よりも短い場合そのまま返却
        return bs;
    }
    // 短い数だけ前0を付加する
    for (var i = 0; i < dt; i++) {
        bs = '0' + bs;
    }
    return bs;
}

UTF-8 の符号化方式は,コードポイントの領域によってオクテット(バイト)数が変化するので,その判定とビット列の取り扱いとがオペレーションの核である。JavaScript コードの先頭コメントに 1 〜 4 オクテットの符号化方式を整理してある。これまで Perl なんかではしょっちゅうこのオペレーションが必要だったので,サブルーチンを書いて使っていたが,JavaScript でははじめてだったので勉強になった。Perl だと pack 関数と ord 関数を用いて UTF-8 Unicode / 十六進数の間をラクに往来できるのだが,JavaScript だと馴れないためか苦労した。もう少し賢いやり方があるかも知れない。それでも encodeURI, decodeURI, unescape などの便利な関数のお世話になった。

久しぶりにトニー・グラハムの書いた『Unicode標準入門』を引っ張り出して UTF-8 符号化方式をおさらいした。最近,技術評論社から文字コードのよい解説書が出た。ともにアマゾンリンクをあげておきます。

Unicode標準入門
トニー・グラハム
関口正裕 監修
乾和志,海老塚徹 訳
翔泳社

老眼鏡 JavaScript

漢詩作成支援のために漢字字典を引きまくっていると,画数が多くてよく読めない字にしばしば遭遇する。おまけに年を取って老眼が進んでいるせいか,虫眼鏡で拡大しないと字のつくりがよく見えないのだ。学研『漢字源』には文字の Unicode コードポイントが収録されており,GNU Emacs テキストエディタ ucs-insert 関数を使って文字の入力は簡単にできる。でも入力で出て来た多画数文字を,いちいちエディタのフォントを大きくして確認するのもやってられない。

そこで Web で簡易に文字を拡大表示する JavaScript を拵えた。自虐も込めて『老眼鏡』という名前にした。入力した文字をただただ大きくするだけの,ホントつまらないツールだけど,Emacs から文字をコピってすぐ確認できるので自分としては大いに役に立っている。文字とサイズを指定すると,そのサイズで文字を表示するだけ。一応 Unicode コードポイントも出力する。こんな感じ。

20120223-rogankyo.png

誰が使うのかわかりませんが, ボタンから試すことが出来る。初期状態では,上の画像のように,このアプリを作るきっかけになった文字「蠶」(かいこ)が設定されている。

ソースコードをあげておく。使い方の説明はコードのはじめのあたりにしるしてある。つまらないアプリの割に 100 行以上のコードになっているのは,拡張領域を含む UTF-8 符号をデコードし Unicode 十六進コードポイントを組立てるのに手間取ったからに過ぎない。

// -*- coding: utf-8; mode: js2; -*-
// 老眼鏡: 文字を拡大する
// - 表示したいブロックに以下を書いておく。
//     <script language="JavaScript">
//       magnifier('字', 100, 400);
//     </script>
//   引数: 初期値文字,文字の大きさ(px),表示枠サイズ幅(px) 
// 2012 (c) isao yasuda.
// 描画領域の作成/初期値表示
function magnifier(chr, size, width) {
    // 引数処理
    // size numeric check
    var msize   = 200;
    if (isNaN(size)) { }// 初期値を使う
    else { msize = size; }
    // width numeric check
    var mwidth  = 500;
    if (isNaN(width)) { }// 初期値を使う
    else { mwidth = width; }
    // 入力/出力エリアの描画
    document.write(
        // 全体枠
        '<div class="mag1" style="' +
            'width: ' + mwidth + 'px;">' +
            // 入力エリア
            '<div>' +
            '<table class="mag2"><tr><td>' +
            '文字:</td><td>' + 
            '<input id="magchr" type="text" size="4" maxlength="4"' +
            'value="' + chr + '" /></td><td>' + 
            'サイズ(px):</td><td>' + 
            '<input id="magsiz" type="text" size="3" maxlength="3"' + 
            'value="' + msize + '" /></td><td>' +
            '<input type="button" value="表 示"' +
            'onclick="magfocus(); magnifychar();" /> ' +
            '<input type="button" value="クリア"' +
            'onclick="magfocus(); magclear();" />' +
            '</td></tr></table></div>' +
            // 表示エリア
            '<div id="magdsp"><span style="font-size: ' + msize + 
            'px; line-height: 120%;">' + chr + '</span>' +
            '</div></div>');
    document.write('<div id="unic">Unicode <span class="code">U+' + 
                   unitohex(chr).toUpperCase() + '</span></div>');
    // 初期値文字の描画
    magnifychar();
    magfocus();
}
// 拡大文字の表示
function magnifychar() {
    var c = document.getElementById("magchr").value;
    if (c == "") {
        alert("文字が入力されていません");
        magfocus();
        return;
    }
    var size = document.getElementById("magsiz").value;
    // size numeric check
    if (isNaN(size)) {
        alert('文字サイズは数値(単位: px)でなければなりません');
        document.getElementById("magsiz").focus();
        return;
    }
    // 文字表示
    document.getElementById("magdsp").innerHTML 
        = '<span style="font-size: ' + size + 
        'px; line-height: 120%;">' + c + '</span>';
    document.getElementById("unic").innerHTML
        = 'Unicode <span class="code">' +
        'U+' + unitohex(c).toUpperCase() + '</span>';
}
// カーソルポイント
function magfocus() {
    document.getElementById("magchr").focus();
}
// 入力文字クリア
function magclear() {
    document.getElementById("magchr").value = ""; 
}
// Unicode 十六進コードポイント変換
function unitohex(c) {
    var w = encodeURI(c);
    if (w.length > 9) {// 4 オクテット以上の Unicode 文字
        // U+010000 - U+1FFFFF 拡張領域文字
        // UTF-8 デコード
        // 1. 符号化 UTF-8 ビットパターンの取得
        w = w.replace(/%/gi, "");
        var h = "";
        for (var i = 0; i < w.length; i++) {
            // 二進数文字列に変換
            var bstr = parseInt(w.charAt(i), 16).toString(2);
            // 4 bit 前ゼロ付加
            var dig0 = "";
            for (var j = 0; j < (4 - bstr.length); j++) {
                dig0 += "0";
            }
            h += dig0 + bstr;
        }
        // 2. Code Point 部の bit を抽出
        var uar = [];
        uar[0] = parseInt(h.charAt(5),2);
        uar[1] = parseInt(h.substring(6,8) + h.substring(10,12),2);
        uar[2] = parseInt(h.substring(12,16),2);
        uar[3] = parseInt(h.substring(18,22),2);
        uar[4] = parseInt(h.substring(22,24) + h.substring(26,28),2);
        uar[5] = parseInt(h.substr(28,4),2);
        // 3. Code Point digit (4 bit) 毎に十六進数文字列に変換
        w = "";
        if (uar[0] != 0) {// 0, 1 以外ありえない
            w = uar[0].toString(16);
        }
        for (var i = 1; i < uar.length; i++) {
            w += uar[i].toString(16);
        }
    } else {
        // U+FFFF までの BMP 領域文字
        w = (c.charCodeAt(0)).toString(16);
        var dt = 4 - w.length;
        for (var i = 0; i < dt; i++) {
            w = '0' + w;
        }
    }
    return w;
}

MovableType5 移行

このお休み,國芳展に出かける以外はうちでずっと MovableType ブログ管理システム(以下 MT)のバージョンアップ作業をやっていた。これまで Ver. 4 を使っていたんだけど,記事数が 1000 を越えてからカテゴリアーカイブが更新されないなどの不具合が出て,記事を追加するたびに再構築するなどの面倒な対処をさせられていた。でも MT 版更新はデザインの移行がしち面倒くさくてならない。それでも,小さい面倒を毎回強いられるより,MT 5.12 へのバージョンアップという本格的な面倒を一回だけ受け容れることにした。

せっかくなのでデザインを新しくすることにした。Photoshop と Illustrator を用いてバナーやロゴなどの素材を自作した。MT4 のウィジェット設定をいちいち画面でコピーしてテキストファイルに格納して,これらを MT5 のデザイン設定に反映した。スタイルシートの調整がもっとも手間を要した。MT では,あるスタイル要素がいろんなスタイルシートで定義されているので,思うような効果が出ないとき悪さをしている定義を探すのがたいへんである。もっとラクな移行方法があるんだろうけど,MT をきちんと勉強するなんて気持ちはまったく起きない。

過去のブログ記事を MT4 環境でエクスポートし,Emacs や sed で必要な編集を施した。6 年も運用していると,引用などの表示様式に統一がなくなってしまったので,少しは改善しようと思ったのである。ついでに,先日インストールした SyntaxHighlighter をプログラム・コードの部分に全面的に適用した。

この過程で SyntaxHighlighter に Emacs-lisp,LaTeX,コマンドライン用のスクリプトがないのが気に入らなくなり,自作した。言語の追加は正規表現の扱い以外は極めて容易である。Emacs-lisp と LaTeX 用を以下に掲載しておく(コマンドライン用は Bash 用のものを少し訂正しただけなので割愛)。lisp 用は funcs 等の変数に指定したものが色づけされる。LaTeX 用はコメント行,コントロールシーケンスのみが色づけされる設定である。lisp 用,LaTeX 用のマークアップは brush に対してそれぞれ lisplatex を指定すればよい。

/**
 * SyntaxHighlighter Emacs-lisp
 */
;(function()
  { 
      typeof(require) != 'undefined' ?
          SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
 
      function Brush()
      {
          var funcs     = 'lambda list progn mapcar car cdr reverse ' +
                          'member append format';
          var keywords  = 'let while unless cond if eq t nil defvar ' +
                          'dotimes setq listp numberp not equal';
          var macros    = 'loop when dolist dotimes defun';
          var operators = '> < + - = * / %';
 
          this.regexList = [
              { regex: SyntaxHighlighter.regexLib.doubleQuotedString, 
                       css: 'string' },
              { regex: new RegExp('&\\w+;', 'g'), css: 'plain' },
              { regex: new RegExp(';.*', 'g'), css: 'comments' },
              { regex: new RegExp('\'(\\w|-)+', 'g'), css: 'variable' },
              { regex: new RegExp(this.getKeywords(keywords), 'gm'), 
                       css: 'keyword' },
              { regex: new RegExp(this.getKeywords(macros), 'gm'),
                       css: 'keyword' },
              { regex: new RegExp(this.getKeywords(funcs), 'gm'), 
                       css: 'functions' }
          ];
      };
   
      Brush.prototype = new SyntaxHighlighter.Highlighter();
      Brush.aliases   = ['lisp'];
      SyntaxHighlighter.brushes.Emacs = Brush;
      typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
  })();
/**
 * SyntaxHighlighter LaTeX
 */
;(function()
  { 
      typeof(require) != 'undefined' ? 
          SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
 
      function Brush()
      {
          this.regexList = [
              { regex: new RegExp('%.*','gm'),  css: 'comments' },
              { regex: new RegExp('\\\\\\w*','gm'), css: 'keyword' }
          ];
      };
   
      Brush.prototype = new SyntaxHighlighter.Highlighter();
      Brush.aliases   = ['latex', 'tex'];
      SyntaxHighlighter.brushes.Latex = Brush;
      typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
  })();

今回のブログ・デザイン変更でいちばん困ったのは,背景色変更に伴う画像の扱いである。背景色はこれまで白(RGB で示すと #ffffff)だったのを明灰色(#e8e5e1)に変えたのだが,すでにゴマンとある画像が白の背景色を前提としているためにドロップシャドウなどが白抜きになっていて,これを明灰色の背景に配置すると白の枠が大いに目立って醜いことこの上ない。例えばこんな感じになってしまう。

sample

この対策のために何百とある画像を作り直すのは現実的でない。背景色になじむ枠画像と重ね合わせる調整をスタイルシートで行うことなどを考えたが,ブラウザによっては配置がずれたりしてさらに醜くなるかも知れず,うまくない。結局,ImageMagick によって色の置換を行うことにした。

ImageMagick には convert ユーティリティが付属していて,各種画像変換が可能である。

convert -fill '#rgb-after' -opaque '#rgb-pre' prefile afterfile

とやると ファイル prefile の RGB 値 rgb-preの色を rgb-after に置き換えて afterfile に出力してくれるのである。しかも -fuzz nn% オプションを付加すれば,指定した度合いでファジーに中間色を置き換えてくれる。いくつかパーセンテージを変えて試したところ,-fuzz 10% くらいがよい感じであった。そこで,tcsh コマンドライン上で以下のようにして画像を一括変換した。もちろん,エクスポートしたブログ記事テキストの画像ファイル名拡張子を .png に置換しておかなければならない。

% foreach i (*.jpg)
foreach? set bn=`basename $i .jpg`
foreach? convert -fuzz 10% -fill '#e8e5e1' -opaque '#ffffff' $bn.jpg $bn.png
foreach? end
%

これで上の画像は以下のとおりとなり,背景色に対してごく自然になじむようになった。UNIX のコマンドライン・ユーティリティの文化は偉大である。ただし,白にごく近い色への変更でないとこの方法は通用しない。また,当然ながら画像の #ffffff の白がすべて #e8e5e1 に置き換わるので純白の画面がおとなしくなってしまうだけでなく,文字を含む画像では少し字が瘠せてしまうので,手放しでよいというわけではない。それでも私の目的には充分であった。Web に掲載する画像にドロップシャドウを付ける場合は,白抜きの部分をアルファチャンネル(透過)にして PNG で作成しておくべき — 今回の教訓。

sample

エクスポート・ブログ記事テキストの編集が終わったら,新 MT5 環境でインポートし,再構築して,過去の記事の取り込みは完了した。MT5 にバージョンアップして,1000 を超過した記事のカテゴリ処理もうまく行くようになっただけでなく,再構築がかなり高速になった。

SyntaxHighlighter 3.0

プログラム・コードを Web ページに掲載する際,コードを pre タグでマークアップするのが一般的ではないだろうか。SyntaxHighlighter を使うと,C/C++ や Java などの予約語をカラー表示したり,行番号を付加したり,重要な行を強調したりできる。これを使用しているプログラマのブログを目にするのは稀ではない。私も使ってみることにした。

いま現在の SyntaxHighlighter 最新バージョンは 3.0.83。ダウンロード・ページから syntaxhighlighter_3.1.0.83.zip を取得して,ローカル計算機で解凍する。解凍してできたディレクトリ下にある compass, scripts, styles, src のディレクトリを Web ドキュメントルート下にコピーすれば,使い始めることができる。ここでは DocumentRoot/sh にインストールしたものとする。

使い方を簡単に説明すると以下のとおりである。SyntaxHighlighter - Installation に書かれているので,きちんとした仕様が知りたい方はそちらを参照のこと。

  1. プログラム・コードを掲載したい HTML(以下「対象ページ」)のヘッダ部に shCore.jsshCore.css を読み込むよう指定する。
  2. 出力様式のタイプがテーマ Theme として管理されており,好みのものを選択して,そのスタイルシートを対象ページに指定する。一覧は SyntaxHighlighter - Themes にある。とくにこだわりがなければ標準 Theme: shThemeDefault.css を指定しておけばよい。私は Eclipse Theme について表示色等カスタマイズしたものを shThemeSite.css として保存して使うことにした。
  3. 対象ページで掲載したいプログラム・コードの言語に応じて SyntaxHighlighter の JavaScript を選択し,それらを対象ページに指定する。JavaScript の一覧は SyntaxHighlighter - Bundled Brushes にある。

上記までで対象ページのヘッダ部は以下のようなものとなる。ここではシェルスクリプト,XML(HTML 含む),Java,C/C++,Perl,JavaScript を扱うものとした。これは SyntaxHighlighter の出力例でもある。

  <link type="text/css" rel="stylesheet" href="/sh/styles/shCore.css" />
  <link type="text/css" rel="stylesheet" href="/sh/styles/shThemeDefault.css" />
  <script type="text/javascript" src="/sh/scripts/shCore.js"></script>
  <script type="text/javascript" src="/sh/scripts/shBrushBash.js"></script>
  <script type="text/javascript" src="/sh/scripts/shBrushXml.js"></script>
  <script type="text/javascript" src="/sh/scripts/shBrushJava.js"></script>
  <script type="text/javascript" src="/sh/scripts/shBrushCpp.js"></script>
  <script type="text/javascript" src="/sh/scripts/shBrushPerl.js"></script>
  <script type="text/javascript" src="/sh/scripts/shBrushJScript.js"></script>

プログラム・コードは pre タグでマークアップする。class オプションに SyntaxHighlighter のコンフィギュレーションを指定する。扱う言語種別,行番号表示や開始番号,タブのサイズ,ハイライトすべき行番号,ツールバー表示有無,等である。SyntaxHighlighter - Configuration に指定可能なオプションの説明がある。以下は,Perl 言語コード,開始行番号 1,タブ・サイズ 4,ハイライト行なし,の指定である。title に題名を設定できる。

<pre class="brush: perl; first-line: 1; tab-size: 4; highlight: null;" 
     title="xlstocsv.pl">

first-line: 1; は行番号の開始を 1 とするということである。行番号を出力したくない場合,gutter: false; を指定する。個別に行をハイライトしたいなら,highlight: [2, 4, 9]; のようにすればよい。プログラム・コードのマークアップが済んだら,最後に以下の JavaScript コードを書いておく。おまじないのようなものである。同一ページ内にいくつも SyntaxHighlighter 使用 pre タグを書いていても,最後に一度だけ発行すればよい。

<script type="text/javascript">SyntaxHighlighter.all()</script>

昨日作成した Excel 一括 CSV 出力の Perl コードは,以下のように表示される。

#!/usr/bin/perl -w
# -*- coding: utf-8; mode: cperl; -*-
# Excel to CSV
use strict;
use utf8;
use Spreadsheet::ParseExcel;
use Spreadsheet::ParseExcel::FmtJapan;
binmode STDOUT, ":utf8"; binmode STDERR, ":utf8";
($#ARGV < 0) && die "Usage: $0 xls-file\n";
my $fnm = $ARGV[0]; # Excel file name
my $ct = 0;         # 出力行数
# Excel object
my $xls = Spreadsheet::ParseExcel->new();
my $fmt = Spreadsheet::ParseExcel::FmtJapan->new(); # 日本語あり
my $bko = $xls->parse($fnm, $fmt); # Excel Book object
my @wsa = $bko->worksheets();      # Worksheet object 配列
utf8::decode($fnm);
# ワークシート毎の処理
foreach my $ws (@wsa) {
    my $wsnm  = $ws->{"Name"};
    my $mxrow = $ws->{"MaxRow"};
    my $mxcol = $ws->{"MaxCol"};
    print STDERR "*** $0 $fnm - sheet: $wsnm start.\n";
    if ((defined $mxrow) && (defined $mxcol)) {
        # 行毎の処理
        for (my $row = 0; $row <= $mxrow; $row++) {
            my $line = "";
            # 列毎の処理
            for (my $col = 0; $col <= $mxcol; $col++) {
                # セル値をセット
                my $cell = $ws->get_cell($row, $col);
                $line .= $cell->value() if (defined $cell);
                $line .= ",";
            }
            print "$line\n";
            $ct++;
        }
    }
}
print STDERR "*** $0 from $fnm wrote $ct lines\n";

ただし,オリジナルの shBrushPerl.js では # 文字があるとそれ以降がすべてコメントとみなされてしまうので,$#ARGV など配列末添字番号変数の表示がおかしくなるので,上掲では少し訂正したものを用いた。53 行目の正規表現の先頭に [^\$] を追加し,以下のとおりに訂正した。

this.regexList = [
	{ regex: new RegExp('[^\$]#[^!].*$', 'gm'),	css: 'comments' },
	{ regex: new RegExp('^\\s*#!.*$', 'gm'),	css: 'preprocessor' },

ちょっと使ってみて制約があった。XML のタグのように < > で囲まれたテキストがあるとき,開始タグに対して終了タグがないと,終了タグが最後に強制的に付加されてしまう仕様のようである。空要素タグ < /> の場合も例外ではない。たとえば C/C++ の #include <stdio.h> の行をそのまま記述すると,終了タグを探しても当然ないので,ソースコード表示の最後に </stdio.h> なるゴミが出てしまうのである。私はまだ SyntaxHighlighter を使い込んでいないのでなんとも判断できないが,< > については,これまでどおり実体参照指定(&lt; &gt;)で記述した方がよさそうである。

Excel ワークシートの一括 CSV 変換

漢詩詩語集を処理するに際して,Excel ファイルを CSV に一括変換するツール『Excel 一括 CSV』だと,JIS X 0212 のいわゆる JIS 第三・第四水準の文字の出力が「?」に化けてしまった。これをなんとかしたいと思い別のツールをいろいろ漁ってみたが,これというものが見当たらなかった。UNIX 系システムでの Excel ファイルのテキスト変換では,xlhtml というプログラムが昔から有名であるが,これとて日本語の扱いに関しては不十分なところがある。この調査の過程で,Windows でなくても動作する Perl Excel ファイルハンドリング・モジュール Spreadsheet::ParseExcel の存在を知り,これを使って一括変換ツールを自作することにした。このモジュールは標準ではないので,コマンドラインから cpan -i Spreadsheet::ParseExcel としてインストールする。

Excel は古いバージョンで作成したものだと,ファイル内の日本語文字コードが Shift_JIS なのか CP932 なのか UCS2 なのかよくわからない。おまけに私の目的として,出力すべきファイルは UTF-8 エンコードでなければならない。Spreadsheet::ParseExcelSpreadsheet::ParseExcel::FmtJapan という日本語処理のための下位モジュールを備えている。これらを併用すれば,日本語文字コードに煩わされずにすみ,JIS X 0212 文字もうまく処理でき,かつ UTF-8 で出力できることがわかった。Spreadsheet::ParseExcel のマニュアル(perldoc Spreadsheet::ParseExcel で閲覧できる)に API 解説とサンプルコードがあるので,参照いただきたい。

今回作成した Excel ワークシート一括 CSV 変換プログラム xlstocsv.pl を以下に掲げる。こんなに短いコードでできてしまうのなら,最初から自分の手を動かすべきであった。同じ課題をもつ方は必ずいらっしゃると思うので,以下のコードをコピって活用いただきたい。もちろん無保証,無サポートである。使い方は,コマンドラインで xlstocsv.pl 対象Excelファイル とする。引数に Excel ファイルを一つ指定する。すべてのワークシートが単一 CSV テキストストリームとして STDOUT に書き出される。ファイルに格納したいのならリダイレクトする。

#!/usr/bin/perl -w
# -*- coding: utf-8; mode: cperl; -*-
# Excel to CSV
use strict;
use utf8;
use Spreadsheet::ParseExcel;
use Spreadsheet::ParseExcel::FmtJapan;
binmode STDOUT, ":utf8"; binmode STDERR, ":utf8";
($#ARGV < 0) && die "Usage: $0 xls-file\n";
my $fnm = $ARGV[0]; # Excel file name
my $ct = 0;         # 出力行数
# Excel object
my $xls = Spreadsheet::ParseExcel->new();
my $fmt = Spreadsheet::ParseExcel::FmtJapan->new(); # 日本語あり
my $bko = $xls->parse($fnm, $fmt); # Excel Book object
my @wsa = $bko->worksheets();      # Worksheet object 配列
utf8::decode($fnm);
# ワークシート毎の処理
foreach my $ws (@wsa) {
    my $wsnm  = $ws->{"Name"};
    my $mxrow = $ws->{"MaxRow"};
    my $mxcol = $ws->{"MaxCol"};
    print STDERR "*** $0 $fnm - sheet: $wsnm start.\n";
    if ((defined $mxrow) && (defined $mxcol)) {
        # 行毎の処理
        for (my $row = 0; $row <= $mxrow; $row++) {
            my $line = "";
            # 列毎の処理
            for (my $col = 0; $col <= $mxcol; $col++) {
                # セル値をセット
                my $cell = $ws->get_cell($row, $col);
                $line .= $cell->value() if (defined $cell);
                $line .= ",";
            }
            print "$line\n";
            $ct++;
        }
    }
}
print STDERR "*** $0 from $fnm wrote $ct lines\n";

もしワークシート毎にファイルをばらしたいのなら,foreach my $ws (@wsa) のブロック(ワークシート単位のループ)部分の始めと終わりに独自ファイル処理(「ファイル名+シート名」でファイルハンドルを割り当てる等)を入れればよい。また,このコードは CSV をカンマ区切り形式とし,かつセルの内容に半角カンマが含まれない前提である。もし半角カンマが含まれてもよいようにするなら,得られたセル値を出力時にダブルクォーテーションマークで括る,等の処置を施していただきたい。

これで,一つの Excel ファイルに夥しいワークシートがあろうが,いちいち Excel を開いて CVS として保存しなくても,コマンドラインから一発で CSV 変換ができるようになった。おまけに FreeBSD でも Linux でも Mac OS X でも動くのである。もちろん Windows でも動くし,Excel がインストールされていなくても Excel ファイルを処理出来る,というメリットがある。

私の課題 — いただいた詩語集の処理に関しては,71 の Excel ファイルそれぞれに何十ものワークシートがあって,Excel をひとつひとつ開いてワークシート毎に CSV エクスポートするのはとてもやっていられないが,xlstocsv.pl を使えば次のコマンド操作だけで二字詩語,三字詩語の CSV(2c.csv, 3c.csv)をまとめることができた。

% foreach i (2c-xls/*.xls)
foreach? set BS=`basename $i .xls`
foreach? (xlstocsv $i > 2c-$BS.csv) >& 2c-$BS.err
foreach? end
% foreach i (3c-xls/*.xls)
foreach? set BS=`basename $i .xls`
foreach? (xlstocsv $i > 3c-$BS.csv) >& 3c-$BS.err
foreach? end
% env LC_ALL=C sort 2c-*.csv | uniq > 2c.csv
% env LC_ALL=C sort 3c-*.csv | uniq > 3c.csv

xlstocsv.pl は引数に Excel ファイルを一つしか指定できないが,複数の Excel ファイルを扱いたいなら,上の tcsh 操作のようにループで回せばよい。次のシェルスクリプトは,カレントディレクトリにあるすべての Excel ファイルを,「xls と同じファイルベース名+拡張子 csv」という名の CSV ファイルに書き出す。

#!/bin/sh
for i in *.xls
do
    BS=`basename $i .xls`
    xlstocsv.pl $BS.xls > $BS.csv
done

misima 漢詩・詩語集拡充

misima 漢詩作成支援の詩語検索機能で参照する詩語データベースを大幅に拡充した。ここのところ,この漢詩プログラム改造に取り組んでいた延長で,詩語のデータベース拡充のための元ネタがないか探していたら,今月 2 日,ある方から個人的に集めた詩語集をいただいた。入手した Microsoft Excel 形式のファイルを CSV に変換して行数を確認すると,なんと 8 万行もある。長年の労苦が偲ばれる素晴らしい成果をいただいた次第である。これは早速データベースにして,提供してくれた方に使ってもらわないとバチが当たる。昨夜と今日のお休み一日かけて,これに取り組んだのである。

いくつものシートに分けられた Excel データを CSV データに変換するのに,『Excel 一括 CSV』を使わせてもらった。妻の Windows 7 で変換したのち,Mac OS X にコピーして,ここからいくつかプログラムを書いて既存のデータベースにマージした。主な作業手順は以下のとおり。

  • 文字コードを UTF-8 に変換(nkf 使用)。
  • CSV データをフォーマット編集(新規作成ツール)
  • 平仄付加(既存ツール)
  • 詩語を旧字体変換して増幅(既存ツール)
  • 新旧両字体で検索してもヒットするよう韻字カラム調整(新規作成ツール)
  • 重複排除(既存ツール)
  • SQLite3 import
  • FreeBSD サーバ Tomcat 環境への辞書・プログラムのデプロイ(配備・インストール)
  • 読み,意味,出典も詩語検索の結果に出力する処理(JavaScript)も追加した。旧字体変換を入れたので,韻字の検索で新旧字体のどちらを指定してもヒットするようデータ構造,クエリを変えた。例えば,韻字が「仏」・平仄「○-●」の検索で,「銅佛」が出て来るようにした。
     

    20120204-sigo.png

    これまでの詩語データベースは,ネットで漁った『唐詩選』の漢詩データ 470 首程度を Perl HTML::TreeBuilder パーサで処理して二字,三字の詩語に分解し,この結果得た約 4000 語程度しか蓄積してなかった。これがいまや二字 33601 語,三字 53657 語,計 87258 語にまで拡張された。まだまだ使い勝手としては疑問が残るけれども,詩語ブラウザとしてはそれなりの規模が備わったのではないかと思う。

    今回作業で使用した『Excel 一括 CSV』ツールでは,残念ながら JIS 第三・第四水準の漢字が文字化けしてしまった。データベースにはこれらは含めなかった。これらを反映するのが次なる課題である。

    Moon Calendar

    Profile

    ISAO YASUDA。システムエンジニア。神奈川県在住。昭和 30 年代を懐かしむオヤジ。ロシアに興味があります。
    [more], [About our site]

    Notice

    この文書はフィクションであり,実在する個人,団体等とは一切関係ありません。

    R-18 指定サイトです。そのうち「18 歳以上ですか」の認証を入れる予定です。

    文書の記述内容は無保証です。不適切な表現があればコメントにてご指摘ください。

    コメント,トラックバックは,現在,運用を停止しています。ご意見等ありましたら isao@yasuda.homeip.net 宛電子メールにてお願いします。

    Links

    Entries

    About this archive

    Entries of Category UNIX after 2012年2月

    Previous: UNIX: 2012年1月

    Next: UNIX: 2012年3月

    Recent Entries in Main Index.
    All Entries in Archive Index.

    March 2012

    Sun Mon Tue Wed Thu Fri Sat
            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
    Powered by Movable Type 5.12 Powered by FreeBSD 8.2-RELEASE
    blog counter