最近二つほどツールを作り、Webツール置き場に配置したので、そこで得た学びや、作る時に意識したことについても書いてゆく。

ENV Checker

まずENV Checkerを作った話。

端的に言うとCyberSyndrome : ENV Checker - 環境変数チェッカーのパクリである。但しJSがないと動かない。

違いとしてはCyberSyndrome側で見れる情報のうち、ユーザー環境に起因しない情報を大幅に削り、ユーザー環境をメインに出せるようにしているほか、IPv4とIPv6の表示にも対応している。

作りとしては必要な情報を返すAPIを3つ用意し、ページを表示したときにJSがそれぞれを叩きに行き、その結果を表示している。

Get Enviroment APIはIPアドレス以外の環境変数を返却するもので、Get IPv4 APIはIPv4アドレスとホスト名、Get IPv6 APIはIPv6アドレスを返すように作ってある。

なぜこの様な分け方にしたかというと、Get Enviroment APIはIPのバージョンを問わない情報しか返さないが、IPアドレスまで返すようにするとIPv4に付随する情報やIPv6アドレスを返すことになるため処理が煩雑になる。

これは実装としてはCGIの環境変数をそのまま返しているのでREMOTE_ADDRに:が含まれているかどうかを見てIPv4か、IPv6かを振り分ける処理が必要になるからだ。

一方でGet Enviroment APIが環境変数だけを返すことに専念できるのならば、IPv4に付随する情報はGet IPv4 APIを叩けば取れるし、IPv6はGet IPv6 APIを叩けば取れるため、分岐処理が不要になる。

これによって各APIは単一の責務だけを持つことになり、コードの複雑化を回避できるといった寸法だ。この程度のシンプルな実装に求めることではないし、現状では無価値ではあるのだが、常日頃から意識することで、より複雑なものを作る時にも生かせるだろうし、将来何か改修するときにも作りが単純なほうが理解しやすいと思うので、こうしている。

IPv4とIPv6のAPIをどう分けているかというと、これはDNSレベルで解決している。Get IPv4 APIのエンドポイントにはAレコードのみを、Get IPv6 APIのエンドポイントにはAAAAレコードのみを設定することで、クライアントが接続する際に使用するIPバージョンを強制している。

こうすることで、各APIは単純にREMOTE_ADDRをそのまま返すだけでよくなり、IPバージョンの判定ロジックを一切持たなくて済むのが利点だ。欠点としてはAPIの数が一つ増えるため、管理コストは増えているといえるだろう。しかし分岐ロジックというのは往々にしてバグを生む存在であり、ないに越したことはないと思い、こういう設計にした。

また画面の描画をJavaScriptにさせているのも、こういったいわゆる関心の分離の思想によるところが大きい。やろうと思えばIPアドレス以外はCGIで表示して、IPアドレスのところだけをJSで書き換えることも、当然できる。出来るのだが、責務を分けるにはデータを返すだけのエンドポイントと、それを受けてJSで書き換える手法のほうが、より責務がはっきりしていて、わかりやすいのでこうしている。疎結合ということでもある。

項目値をダブルクリック・タップすることで値をコピーできるようにしているのも、地味だがこだわりポイントだ。

余談だが、先日Value-DomainでcertbotのDNS Challengeをやるスクリプトを書き直したのは、このドメインにTLS証明書を付与したり、DDNSできるようにする目的があったのが大きい。

というのも、このENV Checkerの開発にはipv4.lycolia.infoipv6.lycolia.infoというドメインが関係しており、私の自宅サーバーの環境はIPv4がコロコロ変わるためDDNSが必須だった。

私が利用しているDNSレジストラであるValue-DomainはDDNS用のエンドポイントがあるのだが、1回1ドメインしか更新できず、60秒に1度しか叩けないという制約があり、これを回避するためには、Value-DomainのDNS APIに対して、一回で複数ドメインのAレコードを書き換える必要があった。

また同時に、以前使っていた、TLS証明書更新ツールである、value-domain-dns-cert-register (vddcr)はNode.jsのアップデートなどに伴う頻繁なメンテナンスが必要だったり、動作検証不足があったり、様々な面倒ごとがあり、これ以上触りたくなかった

そんなこんなの流れがあり、ENV Checkerを作る中でOpenWrtからValue-Domainに複数サブドメインのDDNSを行うツールを作ったり、新たなTLS証明書更新ツールを作り、その検証をしたりしたのだ。

QRコードジェネレーター

もう一つはQRコードジェネレーターを作った話。

QRコードジェネレーターなどググれば無数に出てくるわけだが、意外と読み取り可能な最小サイズかつ、それをSVGで出力できるものを見つけることができなかった。そこで作ることにした。

とはいえ、作ったのはフロントエンドだけで、QRコードの生成自体はqrcode-svgを使わせてもらっている。これはこのライブラリのデモサイトで、理想の要件のものが作れることが分かったからだ。

なぜデモサイトで使えるのに、わざわざ作ったかだが、まずこのデモサイトでは最小サイズのQRコードを簡単に得ることが困難だったほか、256px以下のサイズにすることが想定されていないように見えたからだ。少なくともUIをクリックしてDIMENSIONを256px未満にしようとしても出来なかった。

また、デモサイトはいつか消える可能性もあるし、ブックマークするのも面倒なので、自分でホスティング出来るなら、それをするに越したことはなかったというのもある。

これを作っている中で得た学びとしては、カラーパレットの入力UIがWeb標準で可能になっていたことだ。そこら中で見かけるし標準であるんちゃうか?と調べたらあったので採用した。こういう複雑なものは昔であれば恐らく何かしらのライブラリを使うか、気合で作る必要があったと思うが、全く世の中は便利になったものだ。

また、こちらの実装方法については、HTMLの標準機能でカラーパレットを使ったカラーコード入力UIを作る方法の記事で紹介している。

他にもQRコードの周りには余白が必要ということも知れた。QRコードの開発元であるデンソーウェーブでは余白の求め方の計算式が解説されている。仕様上は適切な余白がない場合、読み込めない可能性があるようだ。これは恐らく周囲の図形とQRコード本体を光学的に分けて認識させるために必要なのだと思う。

なお、今回作成したQRコードジェネレーターには余白の自動調整機能は実装していない。理由は単純でサイズを自由に変えられる仕様上、帳尻をつけるのが面倒だからだ。要は手抜きである。

ついでにWebツール置き場の保守性や解りやすさを少し上げたりした

Webツール置き場のドメイン配下は元々全てペラのHTMLで実装していたのだが、ページが増えるごとに共通部分のhead要素の管理が煩雑になっていた。そこで、PHPに置き換えることにした。

結果として、以下のように、head要素の中身を共通化することができた。

まぁやっていることとしては高校時代にPHPで静的HTMLの内容を共通化していたのと何ら変わらないので、何故今更そんなことを…という感じだが、元々は静的HTMLで済むものは静的HTMLのほうが表示速度が速くシンプルでよいよなという意図で、静的HTMLにしていたのだが、数が増えてくるとそうも言ってられないということで対応した形だ。こういうのは事前に考慮しすぎると、早すぎる共通化やKISSの法則的なリスクを孕むと思っていて、定期的に振り返り、都度対応するほうが良いと感じている。いや、この程度の内容でそこまで考える必要はないと思うが…w

 <!DOCTYPE html>
 <html lang="ja">
 <head>
-  <meta charset='utf-8'>
-  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
-  <title>Slackのリマインダーコマンドを作るやつ</title>
-  <meta name="referrer" content="no-referrer-when-downgrade"/>
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <meta name="author" content="Lycolia Rizzim">
-  <meta property="og:image" content="https://tool.lycolia.info/slack-remider-creator/OGP.png">
-  <meta property="og:site_name" content="Lycolia">
-  <meta property="og:title" content="Slackのリマインダーコマンドを作るやつ">
-  <meta property="og:description" content="必要事項を埋めることでSlackの/remindコマンドを生成するやつ">
-  <meta property="og:type" content="website">
-  <meta property="og:image" content="https://tool.lycolia.info/slack-remider-creator/OGP.png">
-  <link rel="stylesheet" href="style.css">
-  <script src="check.js"></script>
-  <!-- Matomo -->
-  <script>
-    var _paq = window._paq = window._paq || [];
-    /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
-    _paq.push(['trackPageView']);
-    _paq.push(['enableLinkTracking']);
-    (function() {
-      var u="https://analytics.lycolia.info/";
-      _paq.push(['setTrackerUrl', u+'matomo.php']);
-      _paq.push(['setSiteId', '3']);
-      _paq.push(['enableHeartBeatTimer', 10]);
-      var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
-      g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
-    })();
-  </script>
-  <!-- End Matomo Code -->
+<?php
+require_once('../template.php');
+
+renderCommonHead(
+  thumbnail: 'https://tool.lycolia.info/slack-remider-creator/OGP.png',
+  title: 'Slackのリマインダーコマンドを作るやつ',
+  description: '必要事項を埋めることでSlackの/remindコマンドを生成するやつ'
+);
+?>
 </head>

template.phpの中身

関心ごとに関数を切り分け、呼び出される側は呼び出す側に依存するように設計することで、親が子の変更に引きずられないように保守性を意識した設計にしている。他にもほとんど固定値であるものについては運用を平易にする観点からOptional化して、初期値を設定している。

<?php

function buildMatomo() {
    return <<<END
  <script>
    var _paq = window._paq = window._paq || [];
    /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
    _paq.push(['trackPageView']);
    _paq.push(['enableLinkTracking']);
    (function() {
      var u="https://analytics.lycolia.info/";
      _paq.push(['setTrackerUrl', u+'matomo.php']);
      _paq.push(['setSiteId', '3']);
      _paq.push(['enableHeartBeatTimer', 10]);
      var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
      g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
    })();
  </script>

END;
}

function buildCommonMeta(string $title, string $thumbnail) {
    return <<<END
  <meta charset='utf-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <title>$title</title>
  <meta name="referrer" content="no-referrer-when-downgrade"/>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="author" content="Lycolia Rizzim">
  <meta name="thumbnail" content="$thumbnail">
  <link rel="icon" href="https://lycolia.info/assets/brands/lycolia-32x32.png" sizes="32x32">
  <link rel="icon" href="https://lycolia.info/assets/brands/lycolia-192x192.png" sizes="192x192">
  <link rel="apple-touch-icon" href="https://lycolia.info/assets/brands/lycolia-180x180.png">

END;
}

function buildOG(
    ?string $thumbnail,
    ?string $site_name,
    ?string $title,
    ?string $description,
    ?string $type
) {
    $tags = "  <meta property=\"og:type\" content=\"$type\">\n";
    $tags .= "  <meta property=\"og:site_name\" content=\"$site_name\">\n";

    if ($thumbnail !== null) {
        $tags .= "  <meta property=\"og:image\" content=\"$thumbnail\">\n";
    }
    if ($title !== null) {
        $tags .= "  <meta property=\"og:title\" content=\"$title\">\n";
    }
    if ($description !== null) {
        $tags .= "  <meta property=\"og:description\" content=\"$description\">\n";
    }

    return $tags;
}



function renderCommonHead(
    ?string $thumbnail = 'https://lycolia.info/assets/brands/lycolia-OGP.png',
    ?string $site_name = 'Lycolia.info',
    ?string $title = null,
    ?string $description = null,
    ?string $type = 'website'
) {
  $head = buildCommonMeta($title, $thumbnail);
  $head .= buildOG(
    thumbnail: $thumbnail,
    site_name: $site_name,
    title: $title,
    description: $description,
    type: $type
  );
  $head .= buildMatomo();

  echo $head;
}

また他にも、ドメイン直下にあるページの構造を見直した。

具体的にはulとliでリストにしていた部分をdl dt ddに直し、各ツールの内容を少し解りやすくした。ただ余りにも見た目が殺風景すぎるので、liに戻してカードUIをはめ込むようにするかもしれない。

変更前 変更後
更新日:
投稿日:

さて、世の中ではAI開発全盛期という感じで、とにもかくにも全盛期という感じになってきているので、今回は私自身の活用状況について書いていく。

あらかじめ断りを書いておくと、世の中の人ほど活用できてはいない。

今のところは簡単なツール作りや、実装の部品作りといった、小規模用途に利用していて、開発工数の圧縮には便利だと感じている。

開発でLLMを使ってみた事例

仕事ではなく趣味の開発の話。

事例1:MarkdownをYAMLに変換するスクリプトの作成

私は職務経歴書をWordで作っているのだが、WordそのままだとLLMに読ませるのはしんどい。

そこでLLMに読ませる形にしたいが、Markdownはそこまで構造的ではない。なのでYAMLにすることにした。

単刀直入にやるならWordファイルの中身をLLMに渡し、「これをYAMLにしてください」というのが恐らく一般的で、最も手っ取り早いだろう。

しかし私はそれを避けた。私の経歴書はかなり長く、LLMがどこかでミスをしていた場合、それをレビューするのは至難の業だからだ。例えば経歴の一部がLLMによって書き換えられていたりするリスクは拭えない。

なので私はLLMにWordファイルから切り出したプレーンテキストをYAMLに変換するスクリプトを書くことを依頼した。これであればレビューするのはコードそのもので良いし、静的コードは基本的に文脈を見て判断して勝手に書き換えることはしないし、仮にそんな処理があったとしてもそんなものはコードを見ればわかるからだ。

作らせたのはSESや受託系の経歴書にありがちな、死ぬほど繰り返される案件ごとの経験セクションだ。職務経歴書の中でも職務要約だとか、活かせる経験みたいな単純な文章セクションは手作業でどうにもなるので、そこは手で移植すればいいが、繰り返しはしんどい。

というわけで次のようなものを書いてもらった。これで9割はうまく変換してくれたし、後はざっと目検して行って変換にしくじっている部分を直して対応した。

要はLLMに変換させるのではなく、LLMに変換ツールを作らせると間違いが少ないという話だ。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
binmode(STDIN, ':utf8');
binmode(STDOUT, ':utf8');

my $input = do { local $/; <STDIN> };

# 複数のプロジェクトを分割(日付パターンで分割)
my @projects = split(/(?=\d{4}\/\d{2}\s*-\s*\d{4}\/\d{2})/, $input);

print "projects:\n";

for my $project (@projects) {
    next if $project =~ /^\s*$/;

    parse_project($project);
}

sub parse_project {
    my ($text) = @_;

    # 時期を抽出
    my ($period) = $text =~ /^(\d{4}\/\d{2}\s*-\s*\d{4}\/\d{2})/m;
    return unless $period;

    # タイトル(時期の後の行)
    my ($title) = $text =~ /^\d{4}\/\d{2}\s*-\s*\d{4}\/\d{2}\s*\n(.+?)(?:\t|規模|$)/m;
    $title //= '';
    $title =~ s/^\s+|\s+$//g;

    # 要員数
    my ($members) = $text =~ /要員数\s*\t?\s*(\d+名)/;
    $members //= '';

    # 役割
    my ($role) = $text =~ /役割\s*\t?\s*([^\t\n]+)/;
    $role //= '';
    $role =~ s/^\s+|\s+$//g;

    # プロジェクト概要
    my ($overview) = $text =~ /【プロジェクト概要】\s*\n(.*?)(?=\n\s*\n|\n【|$)/s;
    $overview //= '';
    $overview =~ s/^\s+|\s+$//g;
    $overview =~ s/\n\s*/\n/g;

    # 主な業務
    my ($tasks) = $text =~ /【主な業務】\s*\n(.*?)(?=\n\s*\n|\n【|$)/s;
    $tasks //= '';
    $tasks =~ s/^\s+|\s+$//g;

    # 実績・取り組み
    my ($achievements_text) = $text =~ /【実績・取り組み】\s*\n(.*?)(?=\t要員数|\n開発環境|$)/s;
    $achievements_text //= '';

    # 開発環境セクションを抽出
    my ($dev_env_text) = $text =~ /開発環境\s*\n(.*?)$/s;
    $dev_env_text //= '';

    # 出力
    print "      - 時期: $period\n";
    print "        内容: $title\n";
    print "        要員数: $members\n";
    print "        役割: $role\n";

    # プロジェクト概要
    print "        プロジェクト概要: |-\n";
    for my $line (split /\n/, $overview) {
        print "          $line\n" if $line =~ /\S/;
    }

    # 主な業務
    print "        主な業務:\n";
    print "          - $tasks\n";

    # 実績・取り組み
    print "        実績・取り組み:\n";
    parse_achievements($achievements_text);

    # 開発環境
    print "        開発環境:\n";
    parse_dev_env($dev_env_text);
}

sub parse_achievements {
    my ($text) = @_;

    # 実績項目をパース(タイトル + 説明の形式)
    my @items;
    my $current_title = '';
    my $current_desc = '';

    for my $line (split /\n/, $text) {
        $line =~ s/^\s+|\s+$//g;
        next if $line eq '';

        # 新しい項目タイトル(短い行で次の行に説明がある)
        if ($line =~ /^(.+?)$/ && length($line) < 30 && $line !~ /。$/) {
            if ($current_title) {
                push @items, { title => $current_title, desc => $current_desc };
            }
            $current_title = $line;
            $current_desc = '';
        } else {
            $current_desc .= ($current_desc ? "\n" : '') . $line;
        }
    }

    if ($current_title) {
        push @items, { title => $current_title, desc => $current_desc };
    }

    for my $item (@items) {
        print "          - $item->{title}: |-\n";
        for my $desc_line (split /\n/, $item->{desc}) {
            print "              $desc_line\n" if $desc_line =~ /\S/;
        }
    }
}

sub parse_dev_env {
    my ($text) = @_;

    my %sections;
    my $current_section = '';

    for my $line (split /\n/, $text) {
        $line =~ s/^\s+|\s+$//g;
        next if $line eq '';

        if ($line =~ /^【(.+?)】$/) {
            $current_section = $1;
            $sections{$current_section} = [];
        } elsif ($current_section) {
            # カンマやスペースで分割し、バージョン番号を除去
            my @items = split /,\s*/, $line;
            for my $item (@items) {
                $item =~ s/^\s+|\s+$//g;
                # バージョン番号を除去(数字とドットのパターン)
                $item =~ s/\s*[\d.]+\s*$//;
                $item =~ s/\s+\d+(\.\d+)*$//;
                push @{$sections{$current_section}}, $item if $item =~ /\S/;
            }
        }
    }

    # 順序を維持して出力
    my @section_order = ('言語・FW', 'インフラ', 'ドキュメント', 'CI/CD', 'VCS');

    for my $section (@section_order) {
        next unless exists $sections{$section};
        print "          $section:\n";
        for my $item (@{$sections{$section}}) {
            print "            - $item\n";
        }
    }

    # 定義順以外のセクションも出力
    for my $section (keys %sections) {
        next if grep { $_ eq $section } @section_order;
        print "          $section:\n";
        for my $item (@{$sections{$section}}) {
            print "            - $item\n";
        }
    }
}

入力に使ったワードファイルのフォーマットは以下のような内容だ。

[企業情報A]
[案件情報1]
[案件情報2]
...
[企業情報B]
[案件情報1]
[案件情報2]
...

ツールの想定漏れにより一部手修正しているが、低いレビューコストと最低限の手作業で完遂でき、LLMに読ませて分析させられる経歴書を作れたので、結果としてこれはよかった。

事例2:adiaryのMarkdownパーサーへの脚注記法の追加

これは重い腰を上げてadiaryのMarkdownパーサーを脚注記法に対応させたでも書いた内容だ。

端的に言うと既存コードを読ませて部品程度の機能を書いてもらった。

そのまま愚直に書いてもうまく動かなったので、結合する部位などは適当に手直ししているが、これもLLMを使ったコーディングが役に立った一例だ。

ローカルLLMの利用状況

現状ではベンチマークを取るくらいしか出来ていないが、試したことを書き留めておく。

Ollamaからllama.cppに乗り換えようとして失敗した話

Ollamaよりllama.cppの方がCPUオフロードなどのチューニングができるので20〜100%程度のパフォーマンス向上が望めるみたいな情報も見かけたが、GitHubのCUDA対応バイナリを単に叩くだけではパフォーマンスが著しく劣化し、Reasoningの影響で、ただでさえ遅いのが余計に遅くなるなど、全く使い物にならなかった。

調べたところ-nglオプションでGPUオフロードを指定したり、--reasoning-budget 0を付加することでReasoningを防げるらしいがReasoningを防げるかどうかはモデルに依存するらしく上手く行かなかったし、-nglオプションも適切な値が謎だったので諦めた(コンテキストトークン長で変わるらしいが計算ツールがWindowsだとハングしたので調べられなかった)

llama.cppは元々mac用に開発されていてCPUオフロードが標準らしいのでWindowsで使うのは結構大変なのかもしれない。

Ollamaの動きを観察していた感じ、なんかいい感じにCPUオフロードとGPUオフロードを按分してくれてるように見えたので、Ollamaでも別に構わない気はした。

それとCPUに全部オフロードしても実用性は薄いが、それなりの速度で生成してくれるのが分かったのは収穫だった。

もし過去にこれを知っていればメモリ高騰前に128GBにしておきたかったと後悔した。何せ去年の8月に5万だったDDR5の32GBメモリ二枚組が、今や22万と、四倍以上も値上がりしていて、もはや手の出しようがない。

ああ、去年DDR4からDDR5に上げるときに64GBを維持せず、いつものように意味もなくメモリを盛っていれば…。

ローカルLLMの活用方法が見えない

たぶんLLMをチューニングする知識がないとどうにもならなさそう。RAGとかMCPとかLoraを自作できれば夢があるのかも?

30b程度のモテルだとClaude Opus 4.5の足元にも及ばないので、多分何かに特化させないと使いみちはないと思う。

PoeでGLM-7を試した感じ体感そこそこ使える気がしたので120b辺りなら実用性が期待できそうだが、これをまともに動かすにはNVIDIA RTX PRO 6000 Blackwellが必要らしく、こいつは140万円もするし、GMKtec EVO-X2 GMKtec EVO-X2でも実用速度で動くらしいが、LLM以外に使いみちのない端末のために貴重な電源と部屋のスペースを取られるのも困りものなので乗り気にはなれない。

まとめ

現状そこまでバチバチに使えているかといわれると、そこまで使えていないのが正直なところだとは思う。

例えばサブエージェントやスキルといったものや、MCP、RAG、ファインチューニング、Loraといったものは活用できていない。

まぁ徐々に使えるようになっていければいいのかなぁというところで、程々にやっていきたい。

ローカルLLMはまたなんかいい感じの情報が出たら試したい。Poeの利用料金もタダではないので…。

投稿日:

既存CGIを一切触らずに、前段にアクセス制御やアクセスロガーをつけたいとかの用途でラッパーCGIを使うと上手くいくので、その方法を書く。

なお前段が動くことしか試していないが、備考に後段で処理をさせる方法についても軽く触れている。

動作機序

CGIはコマンドライン引数、環境変数、標準入力を受け取り、何かを処理した結果を標準出力するプログラムである。

つまりラッパーCGIはコマンドライン引数、環境変数、標準入力を受け取り、それをラッピングするCGIにそのまま受け渡し、このCGIの標準出力をリダイレクトできればよい。

やり方

以下のようなコードを書き、exec()前に前段の処理を書けばよい。

#!/usr/bin/perl
use strict;
use warnings;

#
# ここに実行前に挟みたい処理
#

my $original_cgi = './hoge.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";

今回実際に作ったサンプル

adiaryにはアクセス制限をする機能がなく、本体を弄るのが嫌だったので前段に処理を入れることで実現した。

#!/usr/bin/perl
use strict;
use warnings;

# 以下のコマンドでIP::Geolocation::MMDBをインストールしていることが前提
# cpanm -l extlib IP::Geolocation::MMDB
use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;
# https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz
# ex. https://download.db-ip.com/free/dbip-city-lite-2026-01.mmdb.gz
my $db = IP::Geolocation::MMDB->new(file => './DBIP-City.mmdb');
my $country_code = $db->getcc($ENV{REMOTE_ADDR});

# 日台韓は許可する方針(怪しい挙動を見たことがないため)
my @allow_country_codes = ('JP', 'TW', 'KR');

my $user_agent = $ENV{HTTP_USER_AGENT};

# 許可する国コードかチェック
my $is_allowed_country = grep { $_ eq $country_code } @allow_country_codes;

# 許可するBOTのUAパターン
my @allowed_bot_patterns = (
    qr/bot/i,
    qr/curl/i,
    qr/wget/i,
    qr/google/i,
    qr/bing/i,
    qr/mastodon/i,
    qr/misskey/i,
    qr/pleroma/i,
    qr/akkoma/i,
    qr/lemmy/i,
    qr/activitypub/i,
    qr/hatena/i,
    qr/github/i,
    qr/tumblr/i,
    qr/meta/i
);

# 許可するBOTかチェック
my $is_allowed_bot = 0;
for my $pattern (@allowed_bot_patterns) {
    if ($user_agent =~ $pattern) {
        $is_allowed_bot = 1;
        last;
    }
}

# 許可国でもなく、許可BOTでもなければエラーにする
if (!$is_allowed_country && !$is_allowed_bot) {
    # 未知のSNS BOTを将来的に許可するために、BOTくさいUAのログを集めておく
    if ($user_agent !~ /Windows|Mac OS|Linux|Android|iOS|iPhone|iPad/i) {
        # OGP取得BOTに間違いなく含まれない文字列が入ってるものはログに入れない
        my $deny_ua_log_file = './deny_ua.log';
        if (open my $fh, '>>', $deny_ua_log_file) {
           my $time = localtime();
           my $remote = $ENV{REMOTE_ADDR} // 'unknown';
           my $uri = $ENV{REQUEST_URI} // 'unknown';
           print $fh "[$time]\t\"$user_agent\"\t$country_code\t$remote\t$uri\n";
           close $fh;
        }
    }

    print "Status: 403 Forbidden\n";
    print "Content-Type: text/plain; charset=UTF-8\n\n";
    print "Access denied.\n";
    exit;
}

# adiary呼び出し
my $original_cgi = './adiary.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";

備考

perldocを読んだ感じ、互換性に問題が出る可能性も少なからずあるようだ。

perldocのexec関数の説明を見る感じ、ENDブロックや、オブジェクトのDESTROYメソッドを起動しないとあるので、実装方法次第では正しく動かない可能性もあるのかもしれない。

また「戻って欲しい場合には、execではなく system関数を使ってください」とあるため、もし後処理をしたい場合はexec関数でなくsystem関数を使うとよいと思う。

あとがき

レンタルサーバーではWAFが自由に使えないため、なんちゃってWAFの様なものを作りたいとか、レンタルサーバーを新規に始めたく、CGIにバナー広告を差し込みたいといったケースがある場合に、今回のような手法は便利だろう。

今時、往年のレンタルサーバーを新規に始め、それもバナー広告を出したいと考える人物がいるかどうかは謎だが、共通的に何かを差し込みたいなど、何かしら活用方法はあるかもしれない。

ググって出てきた記事が軒並み古くて役に立たなかったので、令和八年最新版として書いておく。

やり方

  1. CPANMのインストール
    curl -L https://cpanmin.us | perl - App::cpanminus
    
  2. CPANMのパスを.cshrcに書く
    私はZSHを使っているため.zshrcに書いているが、デフォルト環境はcshのはずなので.cshrcに書けば成り立つと思う。
    echo PATH=${HOME}/perl5/bin:${PATH}
    
  3. local::libをインストールする
    local::libはroot以外にあるCPANモジュールを使うためのものらしい。これを自分のホームディレクトリ配下に入れるようにコマンドを流す。
    cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
    

使い方

以下のようなコマンドを流すとパスを指定してCPANモジュールを取得・展開できる。

# 書式
cpanm -l <パス> <モジュール名>

例えば以下を流すとpwd配下にextlibディレクトリが生成され、IP::Geolocation::MMDBがダウンロードされて展開される。勿論、依存関係も勝手に解決してくれる。

cpanm -l extlib IP::Geolocation::MMDB

モジュールを利用するときはライブラリの配置されているルートを指定し、次にモジュール名を指定するとうまくいくようだ。

use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;

あとがき

https://cpanmin.us/を見に行くと以下の記述があり、辿ってみると日本の人が作っていてちょっと驚いた。

# This is a pre-compiled source code for the cpanm (cpanminus) program.
# For more details about how to install cpanm, go to the following URL:
#
# https://github.com/miyagawa/cpanminus
投稿日:

今までLLMを使う場合、文書校正や整理、ERP辺りが多かったが、そろそろコード作成にも必要だなと感じたので取り組んでみた結果の初回の雑感。

Claude Opus 4との直接対話

LLMエージェントを使わない、チャットインターフェースでの直接対話で行ってみたこと。これはClaude Opus 4で行っている。

簡単なボイラープレートやプログラムが関の山

正直、LLMとの単純な対話で作れるのは3カラムのハンバーガーメニュー付きのような画面のボイラープレートや、WebでJSを使った画像判定スクリプトあたりが関の山だと感じている。

それ以上のものも作れる可能性はあるが、要件定義とコードレビューが大変なので厳しい気がしている。

プログラムの変換は苦手

まず私はTampermonkeyで5分ごとにAPIをポーリングし、結果をパースして条件に応じてOSに通知トーストを出す、400行ほどのスクリプトを作っている。

そこで、このソースコードを丸っと渡して、C#.NETに変換してほしいと頼んでみたが、これは失敗した。根本的にビルドが通らないコードが出てきて多少の修正でどうにかなるレベルでもなく、全くダメだった。

ファイル構成もよくなく、ModelやControllerレベルではファイル分割されているものの、1ファイルの中に複数クラスが納められていたり、何ともな結果だった。

TSDocを書いているため、上手く推論できればInterfaceやClassも作れると思ったが、これは難しいようだった。

特定の設定方法を書くのは得意

OpenWrtの特定の設定を書かせることは得意だった。これはそのまま適用できた。やはりスコープが限定されているのが得意だと感じた。

Claude Codeを少しつついてみた感想

ファイル保存などの手間がいらなくなる

当たり前だがローカルマシン上に結果を出力するため、チャットインターフェースのように頑張ってファイルを保存したり、ディレクトリを切る必要は全くなくなる。

ボイラープレートの作成は得意

PHPを利用したMVC構成で簡単なブログをフルスクラッチで作ってほしいといえば、それらしい形のものは出してくれた。

動くかどうかは全く試していないが、大まかなスケルトンを作って貰って、そっからいじっていくベースとしては使えるような気がした。

やっぱりプログラムの変換は苦手

adiaryのテンプレートエンジン部分をPHPに書き換えてほしいと依頼してみたが、やはり動かないものが出てきた。adiaryの設計が極めて複雑でコンテキストが読み取りづらいのはあると思うが、やはりこの手の作業は苦手なようだ。

現状で見えてきたこと

そこまで大して使ったわけではないが、とりあえず所感として。

恐らく小規模でコンテキストの薄いコードを書かせるのが筋がよさそう。これは複雑な要件をLLMに伝えるのは難しいし、考えるのも大変なのと、コード変換も400行レベルでも厳しいと感じたからだ。

つまり、既存システムの移行は苦手なのではないかと思っている。なのでWordPressをGoで作り直すみたいなことは相当難しいと思う。逆にSOLID原則やClean Architectureのような、スコープが狭く責務が明確なものは作りやすいのではないかと感じた。

また仮にLLMが全て書いてくれるとしても、人がレビューしないとバグがあった時に当たりをつけるのが大変とか、知らない仕様が紛れ込んだりとかもあるため、LLMに書かせすぎるべきではなく、あくまで補助ツール程度に留めておくのが良いと考えている。

LLMの制約を味方にする開発術という記事を見た感じ、複雑なタスクを段階的に分解し、LLMの処理可能な単位に分解することが重要だと感じている。つまりこれは疎結合のほうが向いているということだ。また標準化されていて、属人性がないコードのほうが制約が少なくなるので、LLMもやりやすくなるだろう。これは標準化されておらず、属人性が高いコードは往々にしてカオスで、判断軸がなく、LLMの思考がぶれるからだと思われる。

結局どうしていくか

正直まだどう実用化していくかの展望は見えていない。

何はともあれ使い続けていくことが大切な気はしているので、個人的にはClaude Codeを使い続けていきたい。少なくとも面倒なボイラープレートを書く部分については非常に優秀なので、大まかに作らせて微調整するみたいな用途では間違いなく活路がある。こういうのは引き出しが多ければ多いほど活用できるだろうから、基礎を忘れないように自学していくことも引き続き重要で、LLMに教えてもらうのもいいだろう。適切に使えばLLMからは多くの学びを得られる。