c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

新字を舊字に (ほぼ) 自動で変換する

嘗て俗字⇒正字変換機といふものを書いた。Web UIで、新字を舊字に変換するツールだ。今回は此れをWEB UIではなく、PHPで自動にやるやつをやる。
cf. 俗字⇒正字変換機 http://c4se.hatenablog.com/entry/20090519/1242715608
c4seのコンテンツを書いてゐて、編集方針を、漢字はできる限り舊字を使ふ編集方針にした。日本語入力には正字正かなIMEプロジェクトのGoogle日本語入力用正字正かな辭書を使ってゐる。しかし舊かなはともかく、わたしは舊字が書けないので、正確に舊字に変換することができずに間違ふ。新かなや新字が変換候補にでなくするといふ選択肢は無い。それが必要な機會は多い。
そこで自動変換することをかんがへる。
1. 新字を舊字に強制変換する。
2. 新字を舊字にする候補を提示し、選択に從ひ変換する。
の二段階に分ける。

新字を舊字に強制変換する

新字を舊字に自動で変換したいのだが、さうもいかない場合がある。ほとんどの漢字では、舊字は単に形を省略され新字になってゐる。この場合新字は舊字に一対一対応し、自動で変換できる。ところが一部の漢字では、舊字を、既にある別の字と同一を見なし同じ形にしてしまふといふ方法がとられた。この場合新字は舊字に一体多対応する。自動の変換は多対応する各舊字の用例を網羅せねばならず、大変難しいことになる。
たとへば餘/余がある。餘の新字は余である。餘とは「あまり」といふ意味の字だ。ところが余といふ字はもともとあり、これは「わたし」といふ意味だった。「わたし」といふ意味の余の字と、「あまり」といふ意味の餘の字が新字では一緒になってゐる。これを舊字にするには、文中の字の意味を把握しなければならない。これは「ちょっとしたスクリプト」のできる範囲ではない。
そこで、大部分の一対一対応する新字は強制変換し、一体多対応する新字は、舊字の候補を提示し選んでもらう形にする。

まず対応表を用意する。昔の俗字⇒正字変換機での表を基に、橘榛名がUnicode対応にアップグレードした。現状のものはこれだ。
cf. 新字舊字對照表.txt https://github.com/ne-sachirou/c4se-web/blob/89eb31cca22e7ca44571253aea7d1e596db40c3a/lib/SeiJi/%E6%96%B0%E5%AD%97%E8%88%8A%E5%AD%97%E5%B0%8D%E7%85%A7%E8%A1%A8.txt

強制変換するのは簡単だ。サーバー上でも実行できるやうに、PHPで実装した。

<?php
namespace SeiJi;

class SeiJiTranslator
{
    private $dic = [];

    public function __construct()
    {
        $dic = file_get_contents(__DIR__.'/新字舊字對照表.txt');
        $dic = preg_replace('/\A---.*\n---\n/ms', '', $dic);
        $dic = preg_replace('/#.*$/m'           , '', $dic);
        $dic = preg_replace('/\s+$/m'           , '', $dic);
        $this->dic = array_reduce(
            preg_split('/\n+/', $dic),
            function ($carry, $line) {
                $line = preg_split('/\s+/', $line);
                if (count($line) > 2) {
                    return $carry;
                }
                $carry[$line[0]] = $line[1];
                return $carry;
            },
            []
        );
    }

    public function translate($text)
    {
        foreach ($this->dic as $from => $to) {
            $text = str_replace($from, $to, $text);
        }
        return $text;
    }

    public function translateFile($path)
    {
        if (!is_readable($path)) {
            return;
        }
        if (!($file = fopen($path, 'r+'))) {
            throw new Exception("Can't open the file $path.");
        }
        if (!flock($file, LOCK_EX)) {
            fclose($file);
            throw new Exception("Can't get a lock of $path.");
        }
        $text = fread($file, filesize($path));
        $text = $this->translate($text);
        rewind($file);
        ftruncate($file, 0);
        fwrite($file, $text);
        flock($file, LOCK_UN);
        fclose($file);
    }
}

選択肢が複数ある辞書の行はオミットしてゐる。これを走らせる実行ファイルは、

#!/usr/bin/env php
<?php
require __DIR__.'/../vendor/autoload.php';
use SeiJi\SeiJiTranslator;

function help()
{
  echo "新字 → 舊字変換
bin/seiji_translator FILENAME

FILENAME\t変換するFileのPath。上書きします。";
}

if (!isset($argv[1])) {
  help();
  exit;
}
set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) {
  echo "$errno: $errstr in $errfile on $errline\n";
  var_dump($errcontext);
  echo "\n";
  help();
  die();
});
try {
  $filename = $argv[1];
  $translator = new SeiJiTranslator();
  $translator->translateFile($filename);
} catch(\Exception $ex) {
  echo (string) $ex;
  help();
}
// vim:ft=php:

これをAnt互換のPhingから、各ファイルに対してループして実行する。

<?xml version="1.0" encoding="UTF-8"?>
<!-- build.xml -->
<project name="c4se-web" default="main">
    <fileset dir="." id="views">
        <include name="src/views/**/**.html"/>
    </fileset>

    <target name="translate_to_seiji">
        <apply executable="bin/seiji_translator" checkreturn="true">
            <fileset refid="views"/>
        </apply>
    </target>
</project>

phing translate_to_seijiでsrc/viewsフォルダ内のHTMLファイルが舊字に変換される。

新字を舊字にする候補を提示し、選択に從ひ変換する

選択肢が複数ある舊字は、質問を表示して選んでもらうふ。

<?php
namespace SeiJi;

class SeiJiProposer
{
    private $dic = [];

    public function __construct()
    {
        $dic = file_get_contents(__DIR__.'/新字舊字對照表.txt');
        $dic = preg_replace('/\A---.*\n---\n/ms', '', $dic);
        $dic = preg_replace('/#.*$/m'           , '', $dic);
        $dic = preg_replace('/\s+$/m'           , '', $dic);
        $this->dic = array_reduce(
            preg_split('/\n+/', $dic),
            function ($carry, $line) {
                $line = preg_split('/\s+/', $line);
                if (count($line) <= 2) {
                    return $carry;
                }
                $carry[$line[0]] = array_slice($line, 1);
                return $carry;
            },
            []
        );
    }

    public function propose($text)
    {
        $processed = [];
        foreach (explode("\n", $text) as $line) {
            $line = $this->proposeLine($line);
            $processed[] = $line;
        }
        return implode("\n", $processed);;
    }

    public function proposeFile($path)
    {
        if (!is_readable($path)) {
            return;
        }
        if (!($file = fopen($path, 'r+'))) {
            throw new Exception("Can't open the file $path.");
        }
        if (!flock($file, LOCK_EX)) {
            fclose($file);
            throw new Exception("Can't get a lock of $path.");
        }
        $text = fread($file, filesize($path));
        $text = $this->propose($text);
        rewind($file);
        ftruncate($file, 0);
        fwrite($file, $text);
        flock($file, LOCK_UN);
        fclose($file);
    }

    private function proposeLine($line)
    {
        $processed = '';
        for ($i = 0, $iz = mb_strlen($line, 'UTF-8'); $i < $iz; ++$i) {
            $char = mb_substr($line, $i, 1, 'UTF-8');
            if (isset($this->dic[$char])) {
                echo "$line
0:\t$char (no change)\n";
                foreach ($this->dic[$char] as $n => $choice) {
                    echo ($n + 1).":\t$choice\n";
                }
                echo "?:\t";
                $selection = (int) fgets(STDIN);
                if ($selection && isset($this->dic[$char][$selection])) {
                    $char = $this->dic[$char][$selection - 1];
                }
            }
            $processed .= $char;
        }
        return $processed;
    }
}

対応する新字 (もしかすると既に舊字かもしれない) を見つけたら、質問を表示してプログラムを停止し、getsで答えを取得する。それ以外はSeiJiTranslatorと変らない。
実行ファイルとPhingのbuild.xmlは上とほぼ同じなので省略する。

Webフォント

舊字をひろい環境で適切に表示できるやうにするためのWebフォントの作り方は以下に書いた。
cf. Unicode舊字をAndroidで表示する為に、Notoフォントの部分フォントを作りWebフォントにする http://c4se.hatenablog.com/entry/2015/05/03/051133