hugoの形式で書いた記事のメタデータをプログラムから扱う

hugoの形式で書いた記事のMetadataをプログラムから扱いたい。

一つ前の記事で、ブログのカテゴリーやタグの方針について書いた。 しかし、どう考えても手作業でそれを管理していくのは無理である。 なので、ここでは自動でカテゴリーやタグを設定する方法を考える。

自動でタグやカテゴリーを設定する方法を考える前に、プログラムから簡単にタグやカテゴリーを設定する方法を確立しておきたい。

前提知識

hugoでは、記事のメタデータは各記事ファイルに埋め込むことができる。 そのメタデータは front-matterと呼ばれる。

https://gohugo.io/content-management/front-matter/

front-matterは、yaml、toml、またはjsonで記述することができる。このブログの場合はtoml形式を利用している。

例えば以下のようになる。

draft = true
date = 2023-07-15T22:46:12+09:00
title = "hugoの形式で書いた記事のMetadataをプログラムから扱う"
description = ""
slug = ""
authors = []
tags = []
categories = []
externalLink = ""
series = []

Pythonでfront-matterを読み込み、編集して再書き込みする

以下のプログラムを動かしたところ、動くことが確認できた。 といってもほとんどChatGPTに書いてもらっている。

from pathlib import Path
import toml

BASE_DIR = Path(__file__).resolve().parent.parent


def read_front_matter(file_path):
    """
    front_matterを読み込む
    """
    with open(file_path, "r") as file:
        lines = file.readlines()

    # ファイルの先頭から'+++'で囲まれた部分を抽出
    front_matter_lines = []
    content_start_index = 0
    for i, line in enumerate(lines):
        if line.strip() == "+++":
            if not front_matter_lines:  # front matterの始まりを検出
                front_matter_lines.append(line)
            else:  # front matterの終わりを検出
                content_start_index = i + 1
                break
        elif front_matter_lines:  # front matterの内部を読み込む
            front_matter_lines.append(line)

    front_matter_str = "".join(front_matter_lines[1:])  # 最初の'+++'を除去
    front_matter = toml.loads(front_matter_str)  # TOMLをPythonの辞書に変換

    # コンテンツの部分も抽出
    content = "".join(lines[content_start_index:])

    return front_matter, content


def write_front_matter(file_path, front_matter, content):
    """
    指定されたファイルにfront_matter, contentを書き込む
    """
    front_matter_str = toml.dumps(front_matter)  # Pythonの辞書をTOMLに変換
    front_matter_lines = ["+++\n", front_matter_str, "+++\n"]  # '+++'で囲む
    lines = front_matter_lines + [content]

    with open(file_path, "w") as file:
        file.writelines(lines)


target_file = (
    BASE_DIR / ".....md", # TODO: 実際に存在するファイルに
)

# Hugoのブログ記事ファイルのメタデータを読み込む
front_matter, content = read_front_matter(target_file)
print(front_matter)

# メタデータを編集
front_matter["draft"] = False

# 編集したメタデータをファイルに書き戻す
write_front_matter(target_file, front_matter, content)

クラスにして使いやすくする

このままの状態では少々使いにくいので、クラスにした。

class Post:
    """
    投稿を表すクラス
    """

    def __str__(self):
        return self.front_matter["title"]

    @property
    def title(self):
        return self.front_matter["title"]

    @title.setter
    def title(self, value):
        self.front_matter["title"] = value

    @property
    def tags(self):
        return self.front_matter["tags"]

    @tags.setter
    def tags(self, value):
        self.front_matter["tags"] = value

    @property
    def categories(self):
        return self.front_matter["categories"]

    @categories.setter
    def categories(self, value):
        self.front_matter["categories"] = value

    def __init__(self, path):
        self.path = path
        self._load()

    def _load(self):
        """
        ファイルから投稿を読み込む
        """
        front_matter, content = self._read_front_matter()
        self.front_matter = front_matter
        self.content = content

    def _read_front_matter(self):
        """
        front_matterを読み込む
        つまり、メタデータを読み込む
        """
        with open(self.path, "r") as file:
            lines = file.readlines()

        # ファイルの先頭から'+++'で囲まれた部分を抽出
        front_matter_lines = []
        content_start_index = 0
        for i, line in enumerate(lines):
            if line.strip() == "+++":
                if not front_matter_lines:  # front matterの始まりを検出
                    front_matter_lines.append(line)
                else:  # front matterの終わりを検出
                    content_start_index = i + 1
                    break
            elif front_matter_lines:  # front matterの内部を読み込む
                front_matter_lines.append(line)

        front_matter_str = "".join(front_matter_lines[1:])  # 最初の'+++'を除去
        front_matter = toml.loads(front_matter_str)  # TOMLをPythonの辞書に変換

        # コンテンツの部分も抽出
        content = "".join(lines[content_start_index:])

        return front_matter, content

    def save(self):
        """
        投稿を保存する
        """
        front_matter_str = toml.dumps(self.front_matter)  # Pythonの辞書をTOMLに変換
        front_matter_lines = ["+++\n", front_matter_str, "+++\n"]  # '+++'で囲む
        lines = front_matter_lines + [self.content]

        with open(self.path, "w") as file:
            file.writelines(lines)