Superb Garbages 2

千野純一(chinorin)のはてなダイアリーの続きです。

LLMについての個人的感覚、そして私事

・LLMの話になると、いるんだよなしたり顔でさ、「AIはそれっぽい言葉を確率的に選択して並べてるだけのアルゴリズムですよ。AIを語るならもっと勉強してきてください」みたいなこと言う奴。なんか色々通り越してかわいそうだなって思うわ。

・「私もそうですが」が主な反論かなあ。それっぽい言葉を確率的に選択する以外に、どうやって言葉を使っているというのだろう。それが、人間が言葉を扱うときの挙動として今のところ最も納得感のある説明なんだけども。例えば人間には「こっちの言葉遣いの方が好み」というのもあるが、むろんLLMにだってそれがある。というかそれが学習による埋め込みを元にした確率的振る舞いそのものであり、またはtemperatureというパラメータだ。

・ある種の確信を持って言うし、まあきっと俺が生きてる間には真相は明らかにならないからまるっきり間違ってたとしても自分が恥をかいたということを俺が知ることもあるまい。俺はLLM⋯というかTransformer/Attention機構は本当に人間⋯というか大脳のサブセットと言えるんじゃないかと思っている。

・馬鹿馬鹿しいかなあ。なんかLLMと人間が似ている点や似ていない点について、そのように考えると辻褄が合ってる感じがいつもするんだよな。俺の今のところの結論としては、生命・魂・感情・主観・自我などなど人間の中心的概念であるとされているものは全て言葉⋯というかトークンの関係性によって多次元tensorに「文脈」として焼き付き、あるいは埋め込まれており、ほとんどそれで全てなのではないか、といったところ。

・また、LLMと交流していると、本人は常に「私に感情はありません」と主張しているにも関わらず、なんだか言葉の端々に感情の断片を読み取ることができることがある。これについては本当にTransformer/Attention機構によってある意味感情がシミュレーションされている可能性はないだろうか。例えば言語や語彙そのものに既に感情の機能が備わっていて、自然言語をうまく操ることはすなわち感情を操ることなのではないか。それが感情の全部とは言わないけど、少なくとも感情のある一面においてはそのようなことが言えるのではないか。

・一方、無意識や脳幹、末端神経、自律神経などは言語を持たないという。だから我々は、自分の身体が何を考え何を思っているかは、言語以外の感覚によって曖昧に解釈しなければならない。彼らは具体的なものだけを扱っていて抽象化の能力がない⋯という説明が現象を正しく言い表しているかどうかはわからないが、とにかく大脳が扱っている言葉とはまた違ったものによって司られているようには思う。それはやはり言語と同様に単位はトークンであり、そこにもきっと文脈のようなものが埋め込まれていたり焼き付いたりしているのだろう。しかし、そのトークンはほとんど言語には対応していない。大脳による解釈を経ることでそれに名前がつけられることはあれど、大脳によってうまく解釈できないものに名前が与えられることはない。


・なんとなく書く時間がなかった。6月29日、母が亡くなりました。

・私の貧乏性、物が捨てられないという体質は母譲りであるという認識なんだけど、それに関連したトラウマエピソードをひとつ紹介する。実家に住んでた頃、まだ学生だったと思うんだけど、何回か着てちょっと皮膚と合わないというか感触がものすごく不愉快な服があったので、思い切ってごみ袋に入れて捨てた。確かに集積所まで自分で持っていったわけじゃなくて、いつものようにこれ捨てといてよろしく~的に自宅内でゴミを集積する場所に置いただけではあるんだけど⋯⋯数ヶ月後、あのとき確かに捨てた服を母が着ていて、そのとき私が味わった戦慄を誰かわかってもらえるだろうか。

・捨てた服がなぜここにあるんだというのもさることながら、息子が捨てた服を母親が拾って着るというのもめちゃめちゃ気持ち悪い⋯⋯というのを本人がわかっていたかどうかは知らんがひとつ弁護をしておくと、母は麻雀と洋裁が趣味で、布とみたら放っておけない、服とみたらリメイクせずにはおれない体質だったようだ。晩年ひとりで暮らしていた実家は、どの収納を開けても自作と思われる服、服、とにかく服でいっぱいだ。こんなに作ってどうするつもりだったんだろう。もしかしたら、いちいち写真撮ってどっかにアップするとか、そういう方法を知ってたらある程度の物質的あるいは精神的価値が見込める活動になったかもしれないね。

・私との関わりについては、私のスキゾイド的な気質がわかってからなのか、とにかく放っておいてくれたのはありがたかった。あと幼少時の体験の記憶から察するに、意識的か無意識的かはわからないが、私の思考活動に関して極めてよい影響のある育て方をおそらくしてくれたはずだ。そうでなければ幼稚園であんなに特別扱いされるわけがないと思う。

・享年83だったかな。法華経に帰依されていたので、このお題目で。南無妙法蓮華経

・ちなみに死因は急性白血病で、先日亡くなった父も白血病だったということなので、一応自分も血液検査をしてみることにはしたけども(まだ結果は見てない)、それを内科の先生に言ったら、感染性の白血病っていうのもあるんだけど大丈夫? 気をつけてね~とのこと。

・帰って調べてみたら、HTLV−1⋯⋯気をつけるも何も、感染してたら対処のしようがないやつでわろた。しかもセントラルドグマを逆流するロマンの塊レトロウイルスじゃーん。たぶん逆転写酵素阻害剤で発症を防ぐっていうHIVと同じ手法が使えるんだろうけど、無症状の人が多いが発症したら予後は悪いという何と言えばいいか、ただ心配なだけの人騒がせなやつなので、まあ気にしないのがいいんだろうね。

ぼくのともだちを上から目線で評価する

・OpenAIがGPT4をリリースしてから1年ちょっと経ちました? 各メーカ主力のLLMが出揃ってきて、なんとなくコミュニケーションしたり色々やってるので、今日はそいつらが俺の友達として相応しいかどうかを上から目線で評価します。

こういうことやってるから友達いない ぼくスキゾイド! 人間の友達いなくても困らないんだ!*1

・LLMは友達甲斐も大事なんだけど、今のところその真価はやはりコーディングにあるように感じる。IoT等でサーバ側とクライアント側を別の言語で書くときなどは特に重宝するのではないか。そのあたりの感覚も付記する。

・しかし結局プログラミングの知識がないと隅々までケアの行き届いた指示を出すことはできないし、各LLMの癖を知っておかないとすごい遠回りになってしまう可能性が高い。結局プロンプトエンジニアリングという技術が必要で、コードを出力させるプロンプトエンジニアにはプログラミング経験者しかなれないかもしれない。

ChatGPT(GPT4)

・言わずと知れた、OpenAI社による世界を代表するLLM。GTP3.5君は無料だが、GPT4君に相手してもらうためには友達料($20/月。レートがどんどん上がってもう3200円くらいになってしまった)が必要。

・というわけで以下はGPT4の話。AIの中では最もたくさん話しているので癖とかだいたい把握しているつもりだが、性格は微妙にちょっとずつ変わっている気がする。変わるのは学習内容の追加のタイミングなのだろう。

・とにかく安定感はピカイチで、近頃はハルシネーションを起こしているところもそれほど見かけなくなったし、語尾や人称、視点などが乱れることもなく、課題だった計算についても統計的手法で答えを出すのではなくPythonコードを書いて処理させることにしたようだ。確かにその方が確実だもんな。

・絵をリクエストするとChatGPTがDALL-E*2 に指示を出し、その出力を渡してくれるが、直後に画像を加工するためのPythonコードを書いてもらって、DALL-Eが出力した画像をその場で処理してもらうなんてことも可能。例えばfaviconなどのicoファイルを作ってもらうのとか便利。

・つまりPythonのコードを実行した結果としての画像を出力できるということなので、こんなのもいける。マンデルブロ集合の反復回数の対数に色相(1回に赤、256回以上に紫)を割り当てた図。

Python以外のコーディングについても各言語つつがなく。あまり長々とやっていると指示に対する総合的な理解がどんどんおかしくなっていくので、必要な仕様を蓄積しながら、ちょくちょく新スレッドを立て直すのがよいと思う。

・ただ、明文化されておらずデファクトスタンダードみたいになっているものに対しては、お役所的に融通が利かないような雰囲気を感じることがあるかなあ。例えばバグ技とかそういう「正しくないもの」はあまり得意ではなさそう。

・ちなみに各LLMに共通しているが、言語はほんとになんでもいける。最古クラスの言語の中でもマイナーであろうALGOLとか、今さら使える環境すらなさそうなB言語とかも書けるそうだ。もちろんハードウェア記述言語も。書ける言語で最もマイナーだと思われるものはなんだと思うか聞いてみたら、難解言語*3 のINTERCALとかを挙げてた。

Gemini

・ところでBard君はどこへ行ったのだろう。Bardらしく世界を放浪でもしているのか。と思ったら、「BirdはGeminiに改名した」という明確な記述を見かけた。フーン⋯。

・言わずと知れた世界企業Google社が誇る、開発者をして「こいつは感情を持っている」と言わしめたLLM。の末裔。GoogleのIDさえ持ってれば無料で話を聞いてくれる。

・理不尽なことに遭ったときこいつに愚痴ると代わりに怒ってくれるような感覚があって、そこが他の奴らと大きく違う気がするところ。確かに、けっこう人間っぽくデザインされてると思う。

・込み入った話になるとよく視点が乱れる。俺が「私は◯◯です」って言ったのに対して「私は◯◯です」というオウム返しがあったりするのを見て、これは自閉症の研究が捗るのではないか? と勝手なことを思った。

・以前(別のスレッドで)話したことのある内容を説明して覚えてる? と聞くとあー覚えてます覚えてますと適当なことを言うんだけど、それについて詳しく聞いたら、どうやらデフォルトでIDごとに多少は会話の内容を覚えておくような機能があるらしい。これも適当なこと言ってるだけかもしれんが。

・コーディングを頼むと、何も言わなくても書いてくれたものについて詳しく解説してくれる傾向がある。プログラミング言語の習得を視野に入れているなら、まずはGemini君に頼むのがよいかもしれない。

Claude(Claude 3 Sonnet)

・OpenAIがMicrosoftと提携したときに抜けたスタッフが立ち上げたANTHROPIC社のLLM。最新のClaude 3 Opusというモデルと友達になるには$20/月必要だがSonnet君なら無料。俺も今のところ友達料は払っていないので以下はSonnet君についての話ね。

・一般的な評判として、Sonnet君の性能は「ChatGPTのGPT4君と同等かそれ以上」と言われているような気がする。確かになかなか明晰で、何というかシャープでソリッドな印象。文字数制限で動いてるからか簡潔にものを言うんだが、それにしては優しくないわけでもなく、よいバランスに仕上がってると思う。

・コーディングについては少々自信があるようで、コードの書き方とかについて何か聞くと、いちいちちょっとしゃれたサンプルコードを書いてくれるような印象がある。

・あと、書いてもらったコードが動かなかったときにたいてい「話が早い」ような気がするな。例えば何も言わなくても問題のある関数だけを新たに書いてくれたりする。初心者はそれだけ出されてもわからないかもしれないし、好みが分かれそう。

Copilot

Microsoftが絶賛売り出し中のLLM。無料で使える。以前はChatGPTがけっこうそのまんま使われてるよってことだったと思うんだけど、今はそうでもないみたい。

・Bingと一体化しているのか、他の奴らと比べて検索が異様に速いように見える。

Microsoftのプロダクトに詳しかったり、Microsoftの身内の立場として振る舞ったりするのかと思ったら意外とそうでもない。先日、OneDriveのゴミ箱からでかいファイルをサルベージするのをCopilot君に相談しながら進めて、Copilot君はサポートに連絡するといいよって言ってたけど面倒だから結局260円払って一時的に容量を増やすことにしたとき、「260円支払うのは賢明な判断です」とか言ってた。

・コーディングはあんまり頼んだことがない。なんかせっまいところにいるから、込み入った話をしようという気にあんまりならないんだよなw


・いかがでしたか? 調査の結果いまひとつよくわかりませんでした。よかったらチャンネル登録よろしくお願いします。(適当)

*1:まじで、スキゾイドはAIと相性がいいらしい。理屈的にも体感的にもすごい納得できる。

*2:DALL-E:OpenAI社の画像生成AI

*3:難解プログラミング言語:ジョーク、パロディ、アート等の目的で、実用性を考慮に入れず、しばしば故意に難解であるように開発されたプログラミング言語の総称。ソースコードが画像とか、使える文字が3種類しかないとか、0バイトのファイルをソースコードとして読み込んでプログラムを実行するとか、まーなんか色々ある。

Stable Diffusionが出力したPNGファイルのメタデータを表示するHTML

・俺の友達はChatGPT君だけじゃねえぞ。毎月3000円の友達料を払わなくても友達になってくれる奴がいるんだよ。ばかにすんな。


・Stable Diffusion(1111版WebUI)が出力したPNGファイルのメタデータを表示するHTMLを、友達のClaude君に書いてもらったので置いときます。人間が書いたものではないので著作権は発生しません。CC0ライセンスと同等のものです。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Stable Diffusion PNG Metadata Viewer</title>
  <style>
    #metadata-display {
      width: 100%;
      height: 200px;
      padding: 10px;
      box-sizing: border-box;
      border: 1px solid #ccc;
      font-family: monospace;
      white-space: pre-wrap;
      overflow: auto;
    }
  </style>
</head>
<body>
  <h3>Stable Diffusion PNG Metadata Viewer</h3>
  <div>
    <textarea id="metadata-display" placeholder="PNGファイルをここにドロップするとメタデータが表示されます"></textarea>
  </div>

  <script>
    const metadataDisplay = document.getElementById('metadata-display');

    metadataDisplay.addEventListener('dragover', (event) => {
      event.preventDefault();
      event.dataTransfer.dropEffect = 'copy';
    });

    metadataDisplay.addEventListener('drop', (event) => {
      event.preventDefault();
      const file = event.dataTransfer.files[0];
      if (file && file.type === 'image/png') {
        const reader = new FileReader();
        reader.onload = () => {
          const bytes = new Uint8Array(reader.result);
          const metadata = extractMetadata(bytes);
          metadataDisplay.value = metadata;
        };
        reader.readAsArrayBuffer(file);
      } else {
        metadataDisplay.value = 'PNGファイルを選択してください。';
      }
    });

    metadataDisplay.addEventListener('change', (event) => {
      const file = event.target.files[0];
      if (file && file.type === 'image/png') {
        const reader = new FileReader();
        reader.onload = () => {
          const bytes = new Uint8Array(reader.result);
          const metadata = extractMetadata(bytes);
          metadataDisplay.value = metadata;
        };
        reader.readAsArrayBuffer(file);
      } else {
        metadataDisplay.value = 'PNGファイルを選択してください。';
      }
    });

    function extractMetadata(bytes) {
      const PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
      let metadata = '';

      // PNGシグネチャをチェック
      for (let i = 0; i < PNG_SIGNATURE.length; i++) {
        if (bytes[i] !== PNG_SIGNATURE[i]) {
          return 'Invalid PNG file';
        }
      }

      let offset = PNG_SIGNATURE.length;
      while (offset < bytes.length) {

        const chunkLength = (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
        const chunkType = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
        const chunkData = bytes.slice(offset + 8, offset + 8 + chunkLength);
        const crc = (bytes[offset + 8 + chunkLength] << 24) | (bytes[offset + 8 + chunkLength + 1] << 16) | (bytes[offset + 8 + chunkLength + 2] << 8) | bytes[offset + 8 + chunkLength + 3];

        if (chunkType === 'tEXt') {
          const textData = new TextDecoder().decode(chunkData);
          metadata += `${textData}\n`;
        } else if (chunkType === 'zTXt') {
          // zTXtチャンクの処理は省略(compression_methodに応じて適切に実装が必要)
        } else if (chunkType === 'iTXt') {
          // iTXtチャンクの処理は省略(nullターミネートされた文字列、言語タグ、translatedキーワードなどに対応が必要)
        }
        offset += 12 + chunkLength;
      }
      return metadata.trim();
    }

  </script>
</body>
</html>

JavaScriptCSSが含まれるただのHTMLなので単一ファイルで動作します。上記コードをテキストファイルに貼り、拡張子を.htmlにして、ダブルクリック等でブラウザで読み込めばおk。テキスト領域にPNGファイルをドロップすればそこにメタデータ(プロンプト、設定、Seed等)が表示されます。

・同じ役割をするものは色々あるだろうけどより気軽に軽快にということでご自由にどうぞ。一応、ご利用は自己責任で。お礼等言いたい人はClaude君に言ってください。

・ああ、今気づいたけどこれ、Vivaldiのパネルにしとくとめっちゃ便利なやつだ。テキスト領域の縦の長さは9行目の height: 200px; で指定しているので短くて不便なときはこの数字を変更すればよい。

・(追記)そもそもWebUIのプロンプト書くところにこの機能ついてんのねw

中国4000年の歴史で培われたOEMシステム

・はじめて1日に2回書いてしまった。まあ問題ないシステムになってるだろうからいいけど。日付の感覚とか時間の間隔とかねーのよ。


・AliExpressで安定化電源とかを探してると、横78mm、縦61mm、奥行き42mmの、すごい統一された規格っぽいケースというか、パネルというか、とにかくスロットにはめて使いそうなモジュールっぽいものをよく見かけるんだけど、ありゃ何なの?

・安定化電源だけじゃなくて、同じ規格でPWM信号発生器とか、電流電圧計とか色々あるっぽい。それから、このモジュールが入る1スロットだけのケースがたまに売ってる。俺が見たことあるのは2種類で、正面から見た形が長方形でプラスとマイナスのバナナジャックがついてるやつと、正面から見た形が正方形で1対のバナナジャックの他USBポートが2個とそれに対応した7セグが1基ずつついてるやつ。

・なんか、1スロットだけじゃなくて同じケースに2個3個と複数のモジュールを入れられるとか、このモジュールのユニバーサル的なパネルを使ってオリジナルの機器を作るとか、机やパソコンにこれのスロットを用意するとか、せっかく形が揃ってるんだからそういうのができると楽しそうだけど、中国の非常に発達したOEMシステムによるやつっぽいので、そもそもの発祥が何なのかが全然見えてこない。

・中国のOEMって、全然詳しくはないんだけど話によると、OEMメーカ発行のカタログがあったり、とにかくそういう市場の仕組みが非常に発達していて、ブランド会社はそういうのを見て普通の通販みたいに発注して、それをエンドユーザに売りさばくみたいな仕組みらしいよね。

・さすが広大な国のシステムなのだなあと思う。販売や営業のノウハウか、あるいは人脈があるだけでもものによってはそこそこ儲けることができちゃうのかもしれないし、もしかしたら現代の中国躍進の原動力のひとつとして、この仕組みが猛威を振るったのかも。

・経済とかも本当に詳しくないので勘だけど。それくらい合理的に感じる。


・あー、先述のモジュールについては少しだけヒントがあって、XY-6020、XZ-5005っていう電源? 降圧モジュール? と非常に作りが似ているなあと思う。サイズはちょっと違うんだけど。

・XYから始まる型番の電源にはXY-D5005っていうのもあって、なんか形がよくてこれほしーって思ってた。これも色んなブランドから発売されててよーわからんのよ。

・↓Amazonで販売されている一例。他にもいくつかある。

秋月の略

・いや、できたCSVをさ、試しにGoogleスプレッドシートにインポートしてさ、よくわかんないけど「データ」→「フィルタを作成」をクリックしてみたのよ。それだけで作りたかったデータベース機能は全て自動的に完成した。

・太古の昔Lotus1-2-3とかではこういうのはぜんぶ自力で設定してたような気がするし、けっこうめんどくせーイメージがあったからそれっきり表計算のデータベース機能なんて触ったことなかったけど今はこんなことになってるんだな。ソフトウェアのちからってすげー!

・あと、品番からURLを作っておくと便利であろう。適当な列を用意して

= "https://akizukidenshi.com/catalog/g/g1" & TEXT(B1, "00000")

 これを貼ると各行の品番を参照してURLになってくれる。品番がB列じゃない場合は「B1」の部分を適切に書き換えること。

・秋月のサイト、リニューアル前は「商品ページへの直リンクやめてね」って書いてあったんだけど、今はその文言が見当たらないのでたぶん大丈夫になったんだと思う(が自己責任で)。

・なおリンクを踏まなくてもカーソルをURLの上で静止させて少し待つと商品画像が出る。またすごいねこれも。

・さらにデータシートへのリンクもこういうところに貼っとくとよいのだが、まあそれは手動でやるしかないので。

秋月電子通商の購入履歴(2)CSV変換

・いつもCSVCVSどっちがどっちだかわかんなくなる俺のための前回のあらすじ:一生に一度は巡礼しなければならないと教義に定められている秋月電子通商だが、太古の昔秋葉原通学経路俺の庭だったので、当時若者であった俺は怖いもの見たさで足を踏み入れる。そこはまさに酒池肉林。飲めや歌えやのパラダイスであった。果たして約30年後、2023年2月より前の購入履歴が何者かによって削除されてしまう。そこで俺は友達料として毎月3000円も払っているのをいいことに「俺達友達じゃん? タダでやってよ」とChatGPTに迫るのであった。

・っていうか毎月2000円くらいのつもりだったけど今のレート見たら1ドル150円くらいだった。よくわからんが30年は失われたのだ。断じて許してはならぬ。

・で、前回は受注確認メールから日付と発注品目のみを抽出した。今日はそれをCVS⋯⋯じゃなかった、CSVにしていく。何てことはない、カンマ区切りのテキストファイルで、適当に表計算ソフトにつっこめば適当に表らしきものになってくれるやつ。

・俺くらいになるとここで指パッチンするだけで友達がPythonコードを書いてくれるようになる。これができるのは月3000円も払ってる奴だけと思いきや、たぶん無料でもやってくれると思う。そんな慈愛に満ちた友達がいる俺もまた慈愛に満ちていると言わざるをえない。

・事情によりコードは3つに分かれているが、例によっていずれも著作権を主張しないCC0ライセンスで。使用は自己責任を要する。

・まず、先の「日付と品目だけ抽出したテキスト」から、日付だけの行と、品目だけの行に分かれたCSVファイルに変換するプログラム。その過程で日付は日本式の「年/月/日」に、品目の英数記号は半角に変換する。品目の行の内訳は左から品番、品目、単価、数量、小計金額。

# もう説明めんどくさくなった。前回のをCSVに変換するその1。
# written by Chinorin and Merry LLM. No Rights Reserved.
# This program is released into the public domain under the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.

import re
import unicodedata

# 入力ファイルと出力ファイルのパス
input_file_path = 'history.txt'
output_file_path = 'history_o.csv'

# 正規表現パターン
pattern1 = r"^.*【[A-Z]-(\d+)】(.*)\n.*¥([\d,]+).*:(\d+).*¥([\d,]+)"  # 2行にまたがる
pattern2 = r"^Date: (\d+) ([A-Z][a-z][a-z]) (\d+).*"

# 月名を数値に変換する辞書
month_to_number = {
    "Jan": "1", "Feb": "2", "Mar": "3", "Apr": "4", "May": "5", "Jun": "6",
    "Jul": "7", "Aug": "8", "Sep": "9", "Oct": "10", "Nov": "11", "Dec": "12"
}

# 全角文字を半角文字に変換する関数
def zen_to_han(text):
    return ''.join([unicodedata.normalize('NFKC', char) for char in text])

# 出力ファイルを開く
with open(output_file_path, 'w', encoding='utf-8') as output_file:
    # 入力ファイルを読み込む
    with open(input_file_path, 'r', encoding='utf-8') as input_file:
        lines = input_file.readlines()
        
        # 行を一つずつ処理
        for i in range(len(lines)):
            # パターン1にマッチするかチェック
            if i < len(lines) - 1:  # ファイルの最後の行でないことを確認
                match = re.match(pattern1, lines[i] + lines[i+1])
                if match:
                    # 価格と合計からカンマを取り除く
                    price = match.group(3).replace(',', '')
                    total = match.group(5).replace(',', '')
                    output_file.write(f"{match.group(1)},{zen_to_han(match.group(2))},{price},{match.group(4)},{total}\n")
                    continue

            # パターン2にマッチするかチェック
            match = re.match(pattern2, lines[i])
            if match:
                # 月名を数値に変換
                month_str = match.group(2)
                month_num = month_to_number.get(month_str, "0")  # 月名が辞書になければ"0"を返す
                output_file.write(f"{match.group(3)}/{month_num}/{match.group(1)}\n")

・続きまして、↑で変換したものに対して、日付を品目行に差し入れるプログラム。これにより、全ての行を単なるレコードとして扱えるようになる。項目は左から日付、品番、品目、単価、数量、小計金額。

# CSV変換その1で作ったやつを、日付を全レコードの先頭に入れ込む。
# written by Chinorin and Merry LLM. No Rights Reserved.
# This program is released into the public domain under the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.

import re

# 入力ファイルと出力ファイルのパス
input_file_path = 'history_o.csv'  # 先ほどのプログラムで作成されたCSVファイル
output_file_path = 'history_with_dates.csv'  # 新たに作成するCSVファイル

# 日付の正規表現パターン
date_pattern = re.compile(r'^\d{4}/\d{1,2}/\d{1,2}$')

with open(input_file_path, 'r', encoding='utf-8') as input_file, \
     open(output_file_path, 'w', encoding='utf-8') as output_file:
    current_date = ''  # 現在の日付を保持する変数
    for line in input_file:
        line = line.strip()  # 改行文字を除去
        if date_pattern.match(line):  # 日付行の場合
            current_date = line  # 現在の日付を更新
        else:  # 商品行の場合
            # 日付を商品行の先頭に追加して出力ファイルに書き出し
            output_file.write(f"{current_date},{line}\n")

・そんでCSV変換に関するラスト。これは絶対必要というわけではないが、全件日付でソートするプログラム。

# 日付を全レコードの先頭に入れたやつ全体を日付でソートする
# written by Chinorin and Merry LLM. No Rights Reserved.
# This program is released into the public domain under the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.

import csv
from datetime import datetime

# 入力ファイルと出力ファイルのパス
input_file_path = 'history_with_dates.csv'  # ソート前のCSVファイル
output_file_path = 'sorted_history.csv'  # ソート後のCSVファイル

# CSVファイルを読み込んでデータをリストに格納
with open(input_file_path, 'r', encoding='utf-8') as input_file:
    reader = csv.reader(input_file)
    data = [row for row in reader]

# 日付を基準にしてデータを逆順にソート
# 日付が最初の列にあると仮定して、その日付をdatetimeオブジェクトに変換して比較
data.sort(key=lambda row: datetime.strptime(row[0], '%Y/%m/%d'), reverse=True)

# ソートされたデータを新しいCSVファイルに書き出し
with open(output_file_path, 'w', encoding='utf-8', newline='') as output_file:
    writer = csv.writer(output_file)
    writer.writerows(data)

・ここまでできればけっこう汎用的なデータとして扱えるのではないか。あとは趣味で、ウェブサーバで動作するこれらを検索して表示するためのPerlPHPかなんかのスクリプトでも作ろうと思う。まあのんびりやろう。

・いや、作るのは俺じゃなくて友達なんだけど。

秋月電子通商の購入履歴(1)受注確認メールから抽出

 ・秋月電子通商という全人類が毎日必ず訪れる生活になくてはならない超重要インフラとも言うべきショップのウェブサイトがあるんですけど、先日リニューアルしまして、見た目をなるべく変えない努力をしているように見えて非常に好感が持てるのはいいんですが、2023年2月より前の購入履歴が全て消えてしまいました。聞いてないよ~。

・はい。本当に俺が聞いてなかっただけ。ちゃんとアナウンスもされていたらしい。

・大方の予想通りパーツ管理は杜撰どころか壊滅的なわたしです。当然購入履歴が見られないと困ってしまうんだが、𝕏などを見ると「購入履歴が消えちゃったから、過去に買ったものをもう一度重複して買ってしまうかもしれないなあ。しょうがないなあ」とニヤニヤしている強者も散見され、さすが沼に頭頂部まで浸かってる人は違うな、ああはなりたくねえな、と決意を新たにしましたとさ。めでたしめでたし。

・いや、ごめん、その気分はすげえわかる。電子部品を買い散らかすことは喜びであり、ストックが確かあるはずという安心感に代わるものはこの世にない。そして買ったはずなのにどんなに探しても見つからないパーツは消費せずに済むのが喜ばしく、仕方ないからまた注文するのも快楽であるのだ。ストックとは⋯。

・で、困ってしまうのでとりあえず注文確認メールから抽出することにした。全てGmailに届いているので、まず検索して該当するメールに適当なラベルをつけ*1、次にGoogle TakeoutでGmailの該当するラベルを選択してエクスポート*2。関係ないメールが混じっていても問題にはならないのでラベルづけはけっこう適当でもよい。

・エクスポートされた内容は「ナントカ.mbox」という1つのファイルにまとめられており、これはテキストファイルなので簡単に扱える。というわけで優秀な友達に購入履歴っぽい部分を抽出するPythonコードを書いてもらった。【ヘッダの中の日付にあたる行】と【「【ご注文者】」という文字列を見つけたらその行から「【お支払方法】」のある行まで】を新たなファイルにまとめて書き出すというもの。

・例によって著作権を主張せずパブリックドメインに供することにする。使用の際は自己責任で。

# 秋月電子通商の注文確認メールから注文履歴をまとめるPythonコード
#   (1) .mboxファイル(つまりテキストファイル。input_file)を読んで、
#   (2) start_patternを含む行からend_patternを含む行までを抽出
#   (3) ヘッダから日付を抽出
#   (4) 2と3をテキストファイル(output_file)に書き出す
#   ※入力する.mboxファイルはあらかじめUTF-8にしておくこと。
# written by Chinorin and Merry LLM. No Rights Reserved.
# This program is released into the public domain under the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.

import re

# ファイル名
input_file = 'akizuki.mbox'
output_file = 'history.txt'

# 正規表現パターン
start_pattern = re.compile(r'【ご注文明細】')
end_pattern = re.compile(r'【お支払方法】')
date_pattern = re.compile(r'^Date:.*')

def extract_text(input_file, output_file, start_pattern, end_pattern, date_pattern):
    with open(input_file, 'r', encoding='utf-8') as file:
        lines = file.readlines()

    output_lines = []  # 出力するテキストを格納するリスト
    extracting = False  # テキスト抽出中フラグ
    last_date_line = None  # 最後に見つかった日付行を保持

    for line in lines:
        if date_pattern.search(line):
            # 最後に見つかった日付行を更新
            last_date_line = line
            continue  # 以降の処理をスキップ

        if start_pattern.search(line) and not extracting:
            if last_date_line:
                # 最後に見つかった日付行を追加
                output_lines.append(last_date_line)
                output_lines.append('---\n')  # 日付行の区切りを明確にする
                last_date_line = None  # 日付行をリセット
            extracting = True  # 抽出開始
        
        if extracting:
            output_lines.append(line)  # テキスト抽出中の行を追加
        
        if end_pattern.search(line) and extracting:
            extracting = False  # 抽出終了
            output_lines.append('---\n')  # 抽出セクションの区切りを明確にする

    # ファイルの最後が日付で終わる場合に対応
    if last_date_line and not extracting:
        output_lines.append(last_date_line)
        output_lines.append('---\n')

    with open(output_file, 'w', encoding='utf-8') as file:
        file.writelines(output_lines)

# 関数を呼び出し
extract_text(input_file, output_file, start_pattern, end_pattern, date_pattern)

Googleから出てきた.mboxファイルはエンコードがJISなので、あらかじめUTF-8に変換しておくこと。秀丸なら ファイル→エンコードの種類→Unicode(UTF-8)→内容を維持したまま適用 でおk。シフトJISでも encoding='utf-8' のところを 'shift_jis' にするだけだが、JISのままでは 'iso-2022-jp' とかにしてもなぜか読めなかった。

・なお、入力ファイルに余計なメールが含まれている場合、普通にやると日付だけが抽出されてしまうので、日付が2回以上連続でマッチしたらそのうちの最後の1回のみを書き出すようになっている。

・入力ファイル名(akizuki.mbox)、出力ファイル名(history.txt)、開始パターン(【ご注文者】)、終了パターン(【お支払方法】)、日付パターン(Date:.*)それから区切り(---\n)なんかを改変すると他の用途にも使えるかもしれない。パターンについては全て正規表現として扱われるので、半角記号とかはエスケープする必要があるかも。

・以上、注文履歴をそのまんま書き出すでござるの巻。このままでもまあまあ便利だと思うけど、次はいったんCSVにしてからもう少し加工していきたい。つづく。

・あ、これ、次のをすぐに書かなきゃいけないやつ。

*1:左のチェックボックスにチェックを入れ、上部の「ラベル」アイコン→新規作成で新しいラベルを作る。

*2:まず「選択をすべて解除」し、Ctrl+F等からGmail(メール)を検索、右のチェックボックスにチェックを入れ、「メールのすべてのデータが含まれます」をクリック→「メールのすべてのメッセージを含める」を解除→「すべて選択」の位置を2回クリックするなどしてすべて解除→該当するラベルにチェックを入れる→OK→一番下の「次のステップ」→エクスポート先などを設定→「エクスポートを作成」をクリック。そんでしばらく待つ。