全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / 1cto.jp制作記 / Vol.26 1cto.jp制作記 Vol.3

記事 02

MarkdownファイルからタイトルとサマリーをPythonで自動抽出する設計

先頭に`# タイトル`という行があり、そのあとに本文が続く。データベースには`title`と`summary`の2カラムがある。ファイルを読んでこの2つを取り出す処理が必要だった。

2026-06-07 公開

記事ファイルはMarkdown形式だ。

先頭に# タイトルという行があり、そのあとに本文が続く。データベースにはtitlesummaryの2カラムがある。ファイルを読んでこの2つを取り出す処理が必要だった。


タイトルの抽出は簡単だ。

def extract_title(content):
    for line in content.splitlines():
        line = line.strip()
        if line.startswith('# '):
            return line[2:].strip()
    return '(タイトル不明)'

先頭から1行ずつ見て、# で始まる行を見つけたら返す。見つからなかったらフォールバックの文字列を返す。

記事が約100本あっても、タイトル抽出の失敗は0件だった。全記事に# から始まる行があった。


要約の抽出は少し複雑だ。

要約は記事の冒頭部分を使う。ただし、見出し行(#で始まる)やコードブロック( ` で囲まれた部分)やテーブル(|で始まる行)は含めたくない。本文の地の文を取りたい。

def extract_summary(content):
    lines = content.splitlines()
    paragraphs = []
    buf = []
    for line in lines:
        stripped = line.strip()
        if stripped.startswith('#') or stripped.startswith('```') or stripped.startswith('|'):
            if buf:
                paragraphs.append(' '.join(buf))
                buf = []
            continue
        if stripped == '---' or stripped == '':
            if buf:
                paragraphs.append(' '.join(buf))
                buf = []
        else:
            buf.append(stripped)
    if buf:
        paragraphs.append(' '.join(buf))
    for p in paragraphs:
        if len(p) > 20:
            return p[:180] + ('…' if len(p) > 180 else '')
    return ''

行バッファを使う。空行や区切り線でバッファを段落としてフラッシュする。見出しやコードブロックはバッファをフラッシュして自身はスキップする。

段落が集まったら20文字以上の最初のものを選ぶ。180文字で切って末尾にを付ける。


このロジックで全98記事の要約を抽出した。問題になったケースはいくつかあった。

記事の冒頭が引用(>で始まる行)だと、引用がそのまま本文バッファに入って要約になる。引用記号はリストに入れ忘れていた。ただし該当した記事は少数で、要約として読めない内容にはならなかった。

コードブロックの開始と終了が同じ ` で表現される点は注意が必要だ。単純にstartswith('`')でスキップすると、コードブロックの終了行もスキップされてしまい、バッファが変な状態になりうる。今回は全記事でコードブロックが本文の後半にあったため問題が出なかったが、冒頭にコードがある記事があれば対処が必要だ。


is_free_sample(無料サンプルフラグ)の設定も自動化した。

各ボリュームの第1記事(seq == 1)にTrueを設定する。未ログインのユーザーでも読める記事だ。

for seq, (file_no, fpath) in enumerate(files, 1):
    is_free = (seq == 1)
    insert_article(cur, v['vol_no'], seq, title, content, summary, is_free)

enumerateで連番を振り、最初だけis_freeをTrueにする。ファイル名の番号(file_no)は使わず、ボリューム内の順序(seq)を使う。ファイルが05から始まるボリュームでも、article_no=1から振り直す。


自動化の肝は「どこまで汎用化するか」のバランスだ。

完璧な汎用化を目指すと複雑になる。今回は既存の記事100本弱を想定して作った。10万本を処理するようなケースは想定していない。実際のデータに合わせて「大体動く」実装で十分なことが多い。

次回は、日本語パスを含むディレクトリのSCPがPowerShellで失敗してBashで成功した話の詳細を書く。

シンプルシステム株式会社 代表 伊藤勝彦