2025-10-16

Gist + GitHub Actions で、ブログ記事の投稿を Web で完結させる

今まで、ブログを Markdown で書いて、スクリプトで HTML に変換して Google Blogger に投稿していました。

しかし、自分の PC のスペック不足で、WSL と VS Code と Claude Code を開くと激重なんです。

Gemini に相談してみた

Web で Markdown 書けるやつと言えば Gist かなーと思って、Gist から HTML 変換を挟んで Blogger に投稿できないかと相談してみました。

返答を要約すると「Gist は Git リポジトリなので、クローンできるよ。GitHub Actions で変換と投稿させれば?」とのこと。

ということで、Claude Code に作ってもらいました。


システム設計

アーキテクチャ

以下の 2 つの投稿パターンに対応する設計とした。

パターン A: Gist 経由(GitHub Actions)

Gistで記事執筆
  ↓
GitHub Actions手動実行(Gist URL入力)
  ↓
Gist取得 → HTML変換 → Blogger投稿
  ↓
articles/にMarkdown + meta.json保存

パターン B: ローカル編集

ローカルでMarkdown作成(Claude Code使用可)
  ↓
npm run publish-from-local 実行
  ↓
HTML変換 → Blogger投稿
  ↓
meta.json保存

技術選定

Markdown→HTML 変換

要件として「数式が正しく表示される」ことが必須だったため、以下を採用した。

  • markdown-it: Markdown パーサー
  • KaTeX: 数式レンダリング(サーバーサイドで完結)
  • Prism.js: コードハイライト

KaTeX を選んだ理由は、サーバーサイドで HTML に変換できる点だ。MathJax と異なり、Blogger 投稿時に数式が既に HTML 化されているため、ブラウザ側で JavaScript 実行が不要になる。

メタデータ管理

Gist 経由の記事とローカル作成の記事を統一的に管理するため、以下の形式でメタデータを保存する。

ディレクトリ構成:

articles/
  20251010_article.md          # 記事本体
  20251010_article.meta.json   # メタデータ

meta.json の内容:

{
  "gistId": "a1b2c3d4e5f6g7h8i9j0",
  "gistUrl": "https://gist.github.com/username/...",
  "bloggerPostId": "1234567890123456789",
  "publishedAt": "2025-10-10T12:00:00Z",
  "lastUpdatedAt": "2025-10-10T15:30:00Z"
}

これにより、同じ Gist URL で再実行した場合に既存の Blogger 記事を更新できる。


実装

スクリプト構成

以下のモジュール構成で実装した。

scripts/
  fetch-gist.js           # Gist取得(GitHub API)
  parse-frontmatter.js    # Frontmatter解析
  markdown-to-html.js     # Markdown→HTML変換
  publish-blogger.js      # Blogger API投稿・更新
  manage-metadata.js      # meta.json管理
  publish-article.js      # メインスクリプト(統合)

Gist 取得スクリプト

GitHub API を使用して Gist を取得する。

function fetchGistData(gistId) {
  return new Promise((resolve, reject) => {
    const options = {
      hostname: 'api.github.com',
      path: `/gists/${gistId}`,
      method: 'GET',
      headers: {
        'User-Agent': 'neko-engineering-blog',
        Accept: 'application/vnd.github.v3+json',
      },
    };

    https
      .get(options, (res) => {
        const chunks = [];
        res.on('data', (chunk) => {
          chunks.push(chunk);
        });
        res.on('end', () => {
          const data = Buffer.concat(chunks).toString('utf8');
          if (res.statusCode === 200) {
            resolve(JSON.parse(data));
          } else {
            reject(new Error(`Gist取得失敗: ${res.statusCode}`));
          }
        });
      })
      .on('error', reject);
  });
}

認証なしで Public/Secret Gist の両方を取得できる。

Frontmatter 解析

YAML ライブラリを使わず、正規表現で解析した。

function parseFrontmatter(markdown) {
  const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
  const match = markdown.match(frontmatterRegex);

  if (!match) {
    throw new Error('Frontmatterが見つかりません');
  }

  const frontmatterText = match[1];
  const content = match[2].trim();

  // titleとtagsを抽出
  const titleMatch = frontmatterText.match(/title:\s*["'](.+?)["']/);
  const tagsMatch = frontmatterText.match(/tags:\s*\[(.*?)\]/);

  return {
    title: titleMatch[1],
    tags: tagsMatch
      ? tagsMatch[1].split(',').map((t) => t.trim().replace(/["']/g, ''))
      : [],
    content,
  };
}

依存関係を減らすため、シンプルな実装とした。

Markdown→HTML 変換

KaTeX と Prism.js を統合した変換処理。

const MarkdownIt = require('markdown-it');
const mk = require('markdown-it-katex');
const Prism = require('prismjs');

function markdownToHtml(markdown) {
  const md = new MarkdownIt({
    html: true,
    linkify: true,
    typographer: false,
    highlight: function (str, lang) {
      if (lang && Prism.languages[lang]) {
        return `<pre class="language-${lang}"><code class="language-${lang}">
          ${Prism.highlight(str, Prism.languages[lang], lang)}
        </code></pre>`;
      }
      return `<pre><code>${md.utils.escapeHtml(str)}</code></pre>`;
    },
  });

  md.use(mk); // KaTeX有効化
  return md.render(markdown);
}

KaTeX の CSS は CDN 経由で読み込む前提とし、HTML 内に埋め込んだ。

Blogger API 投稿

既存記事の更新と新規投稿を統一的に処理する。

async function publishOrUpdate({ title, content, labels = [], postId = null }) {
  const config = await loadConfig();
  const auth = await createAuthClient(config);
  const blogger = google.blogger({ version: 'v3', auth });

  const post = { kind: 'blogger#post', title, content, labels };

  if (postId) {
    // 更新
    const response = await blogger.posts.update({
      blogId: config.blogger.blogId,
      postId,
      resource: post,
    });
    return response.data.id;
  } else {
    // 新規投稿(下書き)
    const response = await blogger.posts.insert({
      blogId: config.blogger.blogId,
      resource: post,
      isDraft: true,
    });
    return response.data.id;
  }
}

メインスクリプト

Gist 経由とローカル実行の両方に対応する。

async function publishFromGist(gistUrl) {
  // 1. Gist取得
  const { gistId, filename, content } = await fetchGist(gistUrl);

  // 2. 既存記事を検索(meta.jsonからgistIdで検索)
  const existingArticlePath = await findArticleByGistId(gistId);
  const existingMetadata = existingArticlePath
    ? await loadMetadata(existingArticlePath)
    : null;

  // 3. Frontmatter解析
  const { title, tags, content: markdownContent } = parseFrontmatter(content);

  // 4. HTML変換
  const html = markdownToHtml(markdownContent);

  // 5. Blogger投稿または更新
  const postId = existingMetadata?.bloggerPostId || null;
  const bloggerPostId = await publishOrUpdate({
    title,
    content: html,
    labels: tags,
    postId,
  });

  // 6. 記事ファイル保存
  // 7. meta.json保存
}

GitHub Actions

手動トリガーで Gist URL を入力する形式とした。

name: Gist to Blogger

on:
  workflow_dispatch:
    inputs:
      gist_url:
        description: 'Gist URL'
        required: true
        type: string

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install
      - env:
          BLOGGER_CLIENT_ID: ${{ secrets.BLOGGER_CLIENT_ID }}
          BLOGGER_CLIENT_SECRET: ${{ secrets.BLOGGER_CLIENT_SECRET }}
          BLOGGER_BLOG_ID: ${{ secrets.BLOGGER_BLOG_ID }}
          BLOGGER_TOKEN: ${{ secrets.BLOGGER_TOKEN }}
        run: |
          echo '${{ secrets.BLOGGER_TOKEN }}' > blogger-token.json
          node scripts/publish-article.js --gist-url "${{ inputs.gist_url }}"
      - run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add articles/
          git diff --quiet && git diff --staged --quiet || (git commit -m "記事投稿: Gist経由 [skip ci]" && git push)

GitHub Secrets に以下を設定する必要がある:

  • BLOGGER_CLIENT_ID
  • BLOGGER_CLIENT_SECRET
  • BLOGGER_BLOG_ID
  • BLOGGER_TOKEN (blogger-token.json の内容を JSON 文字列として)

使い方

ローカル実行

npm run publish-from-local articles/20251010_test-article.md

Gist 経由(GitHub Actions)

  1. Gist で記事作成(Frontmatter 形式)
  2. GitHub Actions → "Gist to Blogger" → "Run workflow"
  3. Gist URL を入力して実行

まとめ

  • WSL 起動不要: Gist 経由ならブラウザだけで記事投稿が完結
  • 柔軟な執筆環境: ローカル(Claude Code 使用)と Gist の両方に対応
  • 既存記事の更新: meta.json で記事を管理し、再実行で更新
  • 数式・コード対応: KaTeX + Prism.js でリッチな記事を作成

ということで、だいぶラクできる環境を作れたかなと思います。

2025-10-13

データベーススペシャリスト受験体験記

2025 年 (令和 7 年) 秋期のデータベーススペシャリスト試験を受けてきました。

忘れないうちに受験体験記を書きます。細かい内容はセキスペのと被るので、デスペの勉強方法とかを中心に書いときます。

試験結果は後で追記します。

試験結果 (あとで追記)

受験までの経緯

  • ソフトウェアエンジニア歴はもうすぐ 7 年 (バックエンドもフロントエンドもやる)
  • 2024 年 (令和 6 年) 秋期 - 応用情報技術者試験 - 合格
  • 2025 年 (令和 7 年) 春期 - 情報処理安全確保支援士試験 - 合格

使用した教材

勉強方法

試験の 1 週間前まで : 午後の過去問をひたすら解く

実際に解き始めるとわかりますが、問題を印刷して書き込まないとしんどいです。会社に許可を取って、会社のプリンターを使わせてもらいました。助かりました。

特に、午後Ⅱの問題文が非常に長いので、どの記述を解答に使ったかとかを下線引いて目印付けていかないと、絶対に見落とします。

来年度から CBT 方式らしいけど、どうなるんだろうね。(他人事)

午後問題の概要

午後Ⅰは 2/3 問を選択、午後Ⅱ 1/2 問を選択です。

午後問題の種類は、以下の 2 通り。

  1. 概念データモデリング
  2. SQL とデータベースの実装・運用

個人的には、概念データモデリングの方が圧倒的にラクです。午後Ⅱは片方が概念データモデリングなので良し。午後Ⅰは 2/3 問が概念データモデリングなら良いですが、1/3 問の回も結構あります。

まぁ、午後Ⅰの SQL とデータベースの実装・運用はそんなに難易度も高くないので、過去問をしっかり解いておけば問題ないでしょう。ただし、たまに難しいので気を付けて。

概念データモデリングのコツ

コツは、問題文の記述を基本的に全部使うこと。

問題文を一文ずつ読んで、テーブル構造に既に反映されていたら下線を引いてチェック、ちょうど空欄に入りそうなら丸を付けておく。これをやっていけばほとんどの記述に何かしらの目印がつくはず。目印がついていない記述があったら、そのあとの設問で使う可能性が高いです。

あとは、テーブル構造の外部キーを 1 個ずつ確認して、概念データモデルに反映されているか (だいたい 1 対多の矢印) をチェックしていけば良し。

慣れれば論理パズルみたいで面白いですよ。

試験までの 1 週間 : 過去問道場をひたすら解く

午前Ⅱは 15/25 問の正答で合格です。

データベースとセキュリティ以外は応用情報レベルなので、過去問道場をやるときは、最初は分野を絞ると良いです。直前の数日間とかだけ全分野で。

午前Ⅱで特に覚えとくと良いこと

SQL

  • 等結合:結合条件の列も重複して表示
  • 和両立:テーブル構造が一緒
  • 和 (UNION), 差 (EXCEPT), 積 (INTERSECT)
    • 和両立じゃなきゃダメ
    • ALL がないときは、デフォで DISTINCT されてる
  • 更新可能なビュー
    • 特に、AVG とかの集約関数, GROUP BY, HAVING, DISTINCT があれば更新できない

関係スキーマ

  • 主キー \subset 候補キー \subset スーパーキー
  • 正規化 (午後でも使う)
    • 非正規形 : 単一値でない値が含まれる
    • 第 1 正規形 : すべて単一値だが、部分関数従属を含む
    • 第 2 正規形 : 部分関数従属を含まないが、推移的関数従属を含む
    • 第 3 正規形 : 推移的関数従属を含まない
    • それ以上は、言うてもほぼ出ない

データベース

  • トランザクション (よく出る)
    • ACID 特性
    • 直列化可能性
    • 2 相ロック方式
    • デッドロック
    • 隔離性水準 (よく出る)
ダーティリード
(コミット前を読む)
ノンリピータブルリード
(途中で更新される)
ファントムリード
(途中で追加される)
READ UNCOMMITTED 発生 発生 発生
READ COMMITTED - 発生 発生
REPEATABLE READ - - 発生
SERIALIZABLE - - -
  • 障害
    • トランザクション障害 : ロールバック
    • システム障害 : ロールバックとロールフォワード
    • 媒体障害 : ロールフォワード
  • B 木インデックス
    • 次数とか位数とかの定義がいろいろあるらしい。ググるときは要注意

分散データベース (よく出る)

  • 分散データベースの 12 のルール
    • 移動に対する透過性
    • 分割に対する透過性
    • 複製に対する透過性
  • 分散問合せ処理
    • ネスト・ループ法 : 単純で処理量が最悪
    • セミジョイン法 : 通信量削減のため
  • 分散トランザクション
    • 2 相コミットメント制御
      • 図を全部覚える
      • 2 相ロック方式と混同しないように

試験本番

午前Ⅱ

簡単だった。

午後Ⅰ

ノータイムで概念データモデリングの問 1 ・問 2 を選択。

難しくはなかったが、時間配分をミスって後半は考える時間が少し足りなかった。

午後Ⅱ

ノータイムで概念データモデリングの問 2 を選択。

主キーがほとんど明示されていなくて、結構やばかった。

デスペ特有のクソデカ複合主キー (3~5 属性ばっか) のバーゲンセールで、漢字の書き取り練習かってくらい手が痛かった。素直にサロゲートキー使えよ。てか年月日を主キーに使うな。実務でこんな設計したら、助走をつけて殴られるレベルのクソ設計。

そして、結構な長文の中に「入庫」「入荷」「出庫」「出荷」が高頻度で入り乱れていて、しょっちゅう見間違えてた。入庫出庫入荷出庫入荷出庫出荷入庫出庫入荷出荷出庫入荷出荷入庫出庫入庫入荷入庫出庫出荷出庫

受験しての感想

午前Ⅱは、22/25 問 (88%) で合格でした。

午後はどちらも時間が厳しめで、過去問を解くときはもう少し時間を測ってやるべきだったなと思いました。午後Ⅰは大丈夫だと思うけど、午後Ⅱは微妙かも。

ちなみにセキスペと比べると、デスペは遥かに勉強しがいがあって、過去問を解けば解くほど手応えがあり、本番でも経験がちゃんと生きていました。やっぱりセキスペの問題がヤバかったんだなぁ。

とはいえ、デスペの勉強が実務で役に立つかというと、うーん微妙かな。概念データモデリングばっかやってたけど、頻出の汎化・特化関係とか実務ではまず使わないし、複合主キーなんて多対多のときの連関エンティティでしか使わないし。素直にサロゲートキーを使うし。

デスペの問題は、実務とはあまりにもかけ離れているのでは?

こんなんでスペシャリストを名乗っていいのか?

まぁ、セキスペの時も同じことを思ってたけど。

次は、ネスペを受けようと思っています。応用情報の時はネットワークあまり得意じゃなかったけど、大丈夫だろうか。CBT 方式の初回なんだし、易化してくれないかなー。

ラビット・チャレンジ - Stage 3. 深層学習 前編 (Day 1)

提出したレポートです。 絶対書きすぎですが、行間を埋めたくなるので仕方ない。 Rabbit Challenge - Stage 3. 深層学習 前編 (Day 1) 0. 深層学習とは何か この講義(Day1)の内容では、ニューラルネットワークを用いた学習方法として、順...