Perl を使って YukiWiki のデータを JSON フォーマットでエクスポートする

## 概要
- YukiWiki の YukiWikiDB モジュールで保存しているデータを JSON フォーマットでエクスポートする
- Perl のスクリプトでエクスポートする

## YukiWiki

YukiWiki は Wiki エンジンのひとつ。
Perl で書かれていた。
現在は開発が終了している。

[YukiWiki](https://www.hyuki.com/yukiwiki/)

> YukiWiki （結城ウィキ）は参加者が自由にページを追加・削除・編集できる不思議なWebページ群です。

[YukiWiki](https://www.hyuki.com/yukiwiki/)

> 2018-03-07: YukiWikiの運用ならびにスクリプトの公開を終了します。

最新版は2006年に公開されたもの。

[YukiWiki](https://www.hyuki.com/yukiwiki/)

> 2006-07-07 YukiWiki 2.1.3 (ykwk213.zip) 最新版

最新バージョンにも脆弱性があり、対策方法としては「YukiWiki を使用しない」となっている。

[JVN\#36343375: YukiWiki における複数の脆弱性](https://jvn.jp/jp/JVN36343375/)

> 対策方法
> YukiWiki を使用しない
> YukiWiki の開発は終了しています。YukiWiki の使用を停止してください。

## YukiWikiDB モジュール

YukiWiki で使われているデータベースエンジンのひとつ。
Perl の tie 関数に対応している。

YukiWiki 2.0.5 (2002年8月22日公開版 ykwk205.zip) に同梱されている YukiWikiDB.pm ファイルの中身をここに載せておく。

最新版 YukiWiki 2.1.3 (2006年7月7日公開版) ではもう少し長いコードになっているが、この 2.0.5 (2002年8月22日公開版) では基本的な処理内容がほぼ同じでシンプルな実装になっていてわかりやすいためこちらを載せておく。

ライセンスは「Perlと同じ配布条件」とのこと。

```perl
package Yuki::YukiWikiDB;

my $debug = 1;

# Constructor
sub new {
    return shift->TIEHASH(@_);
}

# tying
sub TIEHASH {
    my ($class, $dbname) = @_;
    my $self = {
        dir => $dbname,
        keys => [],
    };
    if (not -d $self->{dir}) {
        if (!mkdir($self->{dir}, 0777)) {
            print "mkdir(" . $self->{dir} . ") fail\n" if ($debug);
            return undef;
        }
    }
    return bless($self, $class);
}

# Store
sub STORE {
    my ($self, $key, $val) = @_;
    my $file = &make_filename($self, $key);
    if (open(FILE,"> $file")) {
        binmode(FILE);
        print FILE $val;
        close(FILE);
        return $self->{$key} = $val;
    } else {
        print "$file create error.";
    }
}

# Fetch
sub FETCH {
    my ($self, $key) = @_;
    my $file = &make_filename($self, $key);
    if (open(FILE, $file)) {
        local $/;
        $self->{$key} = <FILE>;
        close(FILE);
    }
    return $self->{$key};
}

# Exists
sub EXISTS {
    my ($self, $key) = @_;
    my $file = &make_filename($self, $key);
    return -e($file);
}

# Delete
sub DELETE {
    my ($self, $key) = @_;
    my $file = &make_filename($self, $key);
    unlink $file;
    return delete $self->{$key};
}

sub FIRSTKEY {
    my ($self) = @_;
    opendir(DIR, $self->{dir}) or die $self->{dir};
    @{$self->{keys}} = grep /\.txt$/, readdir(DIR);
    foreach my $name (@{$self->{keys}}) {
        $name =~ s/\.txt$//;
        $name =~ s/[0-9A-F][0-9A-F]/pack("C", hex($&))/eg;
    }
    return shift @{$self->{keys}};
}

sub NEXTKEY {
    my ($self) = @_;
    return shift @{$self->{keys}};
}

sub make_filename {
    my ($self, $key) = @_;
    my $enkey = '';
    foreach my $ch (split(//, $key)) {
        $enkey .= sprintf("%02X", ord($ch));
    }
    return $self->{dir} . "/$enkey.txt";
}

1;
```

## JSON モジュール

JSON データを解析・出力することができる Perl モジュール。
今回はこのモジュールを使用して JSON フォーマットでエクスポートする。

[JSON \- JSON \(JavaScript Object Notation\) encoder/decoder \- metacpan\.org](https://metacpan.org/pod/JSON)

## YukiWikiDB データを JSON フォーマットで出力する Perl スクリプト

### 動作確認環境

- macOS Catalina
- Perl 5.30.0
- YukiWiki で設定している文字エンコーディング: EUC-JP
- YukiWikiDB モジュール 2.0.5
- JSON モジュール 4.02

### ソースコード

```perl
use strict;
use lib './yukiwiki'; # Yuki/YukiWikiDB.pm のあるディレクトリ
use utf8;
use Encode;
use JSON; # cpan JSON コマンドで事前にインストールしておく
use Yuki::YukiWikiDB;

my $modifier_dir_data = './yukiwiki'; # YukiWiki 一式を設置したディレクトリ
my $dataname = "$modifier_dir_data/wiki"; # 本文データのディレクトリ
my $infoname = "$modifier_dir_data/info"; # メタデータのディレクトリ

my %database; # 本文データのデータベース
my %infobase; # メタデータのデータベース

# ここに出力するデータを詰めていく
my $data = {};

# データベースをオープン
tie(%database, 'Yuki::YukiWikiDB', $dataname);
tie(%infobase, 'Yuki::YukiWikiDB', $infoname);

# データベースをひととおりたどる
foreach my $page (keys %database) {

  # WikiName を取得
  my $name = $page;
  $name = Encode::decode('EUC-JP', $name);

  # タイトルを取得
  my $subject = $database{$page};
  $subject =~ s/\r?\n.*//s;
  $subject = Encode::decode('EUC-JP', $subject);

  # 本文を取得
  my $content = $database{$page};
  $content = Encode::decode('EUC-JP', $content);

  # 出力したいデータを詰めていく
  $data->{$name} ||= {};
  $data->{$name}{'Name'} = $name;
  $data->{$name}{'Subject'} = $subject;
  $data->{$name}{'Content'} = $content;

  # メタデータ (IsFrozen, LastModified など) を詰めていく
  my %info = map { split(/=/, $_, 2) } split(/\n/, $infobase{$page});
  foreach my $infokey (keys %info) {
    $data->{$name}{$infokey} = Encode::decode('EUC-JP', $info{$infokey});
  }
}

# データベースをクローズ
untie(%database);
untie(%infobase);

# JSON フォーマットで出力する
# canonical: キー順でソート
# pretty: 人が読みやすい形式に
# utf8: UTF-8 で文字エンコーディング
my $json = JSON->new->canonical->pretty->utf8->encode($data);
print $json;
```

### JSON 出力例

改行はCR+LF (環境によって異なるかもしれない)。
ページを凍結している場合は IsFrozen に 1 がセットされる。
LastModified はローカルタイムの日時。

```json
{
   "100" : {
      "Content" : "100とは\r\n10の10倍です。\r\n",
      "IsFrozen" : "1",
      "LastModified" : "Mon Sep 18 16:22:09 2006",
      "Name" : "100",
      "Subject" : "100とは"
   },
   "HogePage" : {
      "Content" : "サンプルページ\r\n\r\nHello, world.\r\n<>&\"'\r\nああああ。\r\n",
      "IsFrozen" : "0",
      "LastModified" : "Wed Nov 27 23:44:55 2019",
      "Name" : "HogePage",
      "Subject" : "サンプルページ"
   },
(以下略)
```

Name の項目は日本語でも問題なく出力できている。

```json
   "あいうえお" : {
      "Content" : "日本語名ページテスト\r\nほげほげ\r\n[[SamplePage]]\r\n",
      "IsFrozen" : "1",
      "LastModified" : "Mon Jul 17 09:12:34 2006",
      "Name" : "あいうえお",
      "Subject" : "日本語名ページテスト"
   },
```

## 参考資料
- [YukiWiki](https://www.hyuki.com/yukiwiki/)
- [Perlの組み込み関数 tie の翻訳 \- perldoc\.jp](https://perldoc.jp/func/tie)
- [perltie \- オブジェクトクラスを単純な変数に隠す方法 \- perldoc\.jp](https://perldoc.jp/docs/perl/perltie.pod)
- [JSON \- JSON \(JavaScript Object Notation\) encoder/decoder \- metacpan\.org](https://metacpan.org/pod/JSON)