記事ファイルはMarkdown形式だ。
先頭に# タイトルという行があり、そのあとに本文が続く。データベースにはtitleとsummaryの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で成功した話の詳細を書く。
シンプルシステム株式会社 代表 伊藤勝彦