マークダウンで書いてしまった潰れる表を瞬時に画像化する方法
はじめに
皆さん、Qiitaに記事を投稿したとき、こんな経験はありませんか?
「やった!トレンド入りしたぞ!」と喜んだのもつかの間、次の日にはもうランキングの影も形も見当たらない…。
筆者は何度となく、この「トレンドのジェットコースター」 を経験してきました。せっかく書いた記事がすぐに埋もれていくのは、とても悲しいですよね。
この現象について、最近モヤモヤしていたので、今回はその原因と、考えた対策を語ってみたいと思います。
なぜ、筆者の記事はトレンドからすぐ落ちるのか?
最初に思いついたのは、記事の内容がイケてないせいか、という自虐的な考えでした。
もちろんそれは大きな原因であることは分かり切っていますし、そう簡単にイケてる記事なんて書けないんで諦めましょう!
でも、冷静に分析してみると、どうもそれだけではないようです。
Qiitaのトレンドは、公開直後の「いいね」やアクセス数が強く影響します。つまり、「初速」が速いとトレンド入りしやすい。でも、その勢いが続かないと、後から来る新しい記事にどんどん追い抜かれてしまう。そして、その翌日には、トレンドグッパイ👋
では、なぜ勢いが続かないのか?
ある犯人に行き着きました。それは……「スマホで潰れるマークダウン記法の表」です。
Macで綺麗に見えていた表が、スマホで見ると横にはみ出したり、形が歪になったり、そして何より、数字が縦書きになったりしてしまって、えも言われぬ怪文書が出来上がってしまいます。
潰れる表の例
表1 中期投資に適したRCIコンビの探索(過去記事より引用)
Entry_Pattern | Exit_Pattern | 探索期間A_Return | 評価期間B_Return | 評価期間C_Return | 探索期間A_Sharpe | 評価期間B_Sharpe | 評価期間C_Sharpe | 平均保有期間 |
---|---|---|---|---|---|---|---|---|
Entry_21_34 | Exit_21_34 | 10.60% | 14.20% | 7.42% | 0.683 | 2.360 | 0.681 | 214.0日 |
Entry_13_21 | Exit_5_8 | 2.41% | 4.25% | 3.02% | 0.763 | 1.469 | 0.776 | 51.6日 |
Entry_21_34 | Exit_13_34 | 12.34% | 16.25% | 9.40% | 0.788 | 2.256 | 0.777 | 251.3日 |
Entry_5_8 | Exit_21_34 | 16.11% | 26.20% | 6.84% | 0.786 | 3.255 | 1.344 | 197.4日 |
Entry_5_8 | Exit_34_55 | 14.96% | 31.30% | 14.49% | 1.509 | 1.709 | 1.651 | 269.7日 |
Entry_13_21 | Exit_5_13 | 5.23% | 10.14% | 9.41% | 1.240 | 1.260 | 0.761 | 111.5日 |
Entry_13_21 | Exit_5_21 | 9.65% | 8.02% | 16.41% | 1.402 | 0.925 | 0.958 | 225.6日 |
表2 本当にあった?ご近所さんとのトラブル
喧嘩の原因 | 隣人の反応 | 自分の反応 | 結果 | 解決方法 | 再発防止策 | 関係修復度 | 今後の展望 | 備考 |
---|---|---|---|---|---|---|---|---|
煮干し出汁の匂い | 激怒 | 謝罪 | 和解 | 出汁をやめる | 無臭出汁に変更 | 80% | 良好 | 風向きに注意 |
騒音問題 | 訴訟 | 弁護士相談 | 裁判中 | インターホン押して叫んでみる | NHKの真似が効果的 | 0% | 最悪 | 法廷で争い中 |
夜の遊び声 | 騒音苦情 | 位置変更 | 和解 | 心覚えがない | お隣さんのお隣さんのせいだった | 85% | 良好 | 夫婦睦まじく |
エアコンの室外機 | 騒音苦情 | 位置変更 | 和解 | 室外機を室内に置く | 暑さに慣れる | 45% | 不明 | 主治医に相談 |
ペットの顔がブサイク | 無視 | 抗議 | 継続中 | ペットの顔を隠す | ペットの美容整形 | 30% | 不明 | 獣医で相談予定 |
ペットの散歩 | 文句 | 説明 | 和解 | ペットは散歩させない | 代わりにルンバ散歩させる | 70% | 良好 | ルンバは健気で可愛い |
庭の毛掃除 | 逆ギレ | 撮影 | 警察介入 | 毛掃除をやめる | 脱毛 | 10% | 悪化 | 毛量が多い |
洗濯物の干し方 | 脅迫 | 録画 | 和解 | 干し方を変更 | 代わりに煮干しを干す | 60% | 改善 | 今度は鰹節干す |
洗濯機の時間 | 苦情 | 謝罪 | 和解 | 洗濯機を使用しない | トイレの水流で洗う | 95% | 良好 | 洗剤がすぐになくなる |
洗車の水はね | 苦情 | 謝罪 | 和解 | 洗車しない | 汚れてもカッコいい戦車買う | 90% | 良好 | ビックカメラで相談済み |
庭木の剪定 | 切り取られた | 抗議 | 継続中 | 専門業者に依頼 | 剪定スケジュール作成 | 50% | 不明 | 自治会で協議 |
花壇の水やり | 嫉妬 | 時間変更 | 和解 | 隣人の庭にも種蒔いた | 花が育つのを待つ | 88% | 良好 | 隣人『あら綺麗な花♡」 |
ゴミ出しの時間 | 通報 | 反論 | 継続中 | ゴミを出さない | 庭でコンポストにする | 40% | 不明 | 隣人にばら撒きたい |
駐車場の使い方 | 暴力 | 警察通報 | 逮捕 | 駐車場で寝ない | 寝るならテントはる | 20% | 悪化 | 保護命令申請中 |
自転車の置き場所 | 薙ぎ倒された | 抗議 | 継続中 | テリトリーを犯さない | 隣人の駐車場に設置 | 0% | 悪化 | 今度は三輪車おいてやる |
表3 本当にあった?健全な批判
批判の内容 | アンチの反応 | 自分の反応 | 結果 | 解決方法 | 再発防止策 | 関係修復度 | 今後の展望 | 備考 |
---|---|---|---|---|---|---|---|---|
投資資金100万円は少ない | 激怒 | 謝罪 | 和解 | 資金額を増やす | アンチから1000万もらいたい | 80% | 良好 | スポンサーを増やす |
月利3%なんて、お前なんかにまず無理 | 通報 | 反論 | 継続中 | 実績を証明 | その前に一旦通院 | 40% | 不明 | 動物病院で相談 |
俺より稼いでから記事を書け | 訴訟 | 弁護士相談 | 裁判中 | 収益を増やす | 会社からボーナスもらう | 0% | 最悪 | 給与明細見せる |
バイトした方がまし | 無視 | 抗議 | 継続中 | 会社員やめて、バイトを始める | 辞表出す | 30% | 不明 | 上司に相談予定 |
内容が教科書レベル | 家凸 | 撮影 | 警察介入 | 内容を深掘り | 専門家に相談 | 10% | 悪化 | 引越し検討中 |
10年前の古い技術で記事を書いて何が楽しい | 脅迫 | 撮影 | 和解 | よく勉強する | 国語の勉強 | 60% | 改善 | 自分はおバカ |
タイトルが釣り | 撤去 | 抗議 | 継続中 | 収益が出そうなことは書かない | 月利OO%にして紛らわす | 50% | 不明 | これはこれで意外と読まれる |
ドクターなんか行かずに、小学校からやり直せば | 暴力 | 警察通報 | 逮捕 | 小学校の先生に相談 | 幼稚園からやり直す | 20% | 悪化 | 両親が出会う前に戻れれば最高 |
チビカス間抜け | 文句 | 説明 | 和解 | 身長を伸ばす | 身長向上計画 | 70% | 良好 | 子ども会で調整 |
表がみにくい | 苦情 | 謝罪 | 和解 | 画像にする | 今記事書いてます | 90% | 良好 | 意外と書くの大変 |
リンクが切れている | 苦情 | 位置変更 | 和解 | リンクを修正 | リンクチェック機能 | 85% | 良好 | 業者に依頼済み |
スペルミスが多い | 脅迫 | 時間変更 | 和解 | スペルチェック | 自動校正ツール | 75% | 良好 | ホームセンターで購入 |
参考文献がない | メール凸 | 片付け | 和解 | 自分の思考を当てにしない | まずは調べる | 95% | 良好 | 仮説→調査の方が早いのに |
更新されていない | 電凸 | ケーブル抜いた | 継続中 | 定期的に更新 | 更新スケジュール | 45% | 不明 | 気が向いたらする |
コメント欄が荒れている | チラチラテケテケ | ちんちろりん | 和解 | コメント欄閉鎖 | モデレーション強化 | 88% | 良好 | 宅配ボックス購入済み |
おそらくそういう記事に直面すると、書いた著者にトロイの木馬を送ってみたくなる人もいるのではないでしょうか?
読みにくいという不快感が読者の離脱を招き、「滞在時間」、「LGTM」、「ストック」が伸びず、トレンドグッパイになってしまうと、思い止まったわけです。
小難しい記事も見苦しい記事も書いてはいけません、自分が会社の見苦しい資料を呼び飛ばすように、読みにくい自分の記事も多くの人が...
解決策:表を画像にしてしまえばいいじゃない
この問題を解決するために、シンプルな方法を思いつきました。
「Markdownの表を、どんなデバイスでも崩れない画像に変換してしまえばいいじゃないか!」
誰でも思いつく、超初歩的なことです。その辺で泳いでいるオタマジャクシでも思いつきます。
しかし、問題が一つあります。
一旦書き上げてしまった、マークダウンの記事の大量の表を、スクショを一つ一つとって、手作業で全部貼り付け直すのか?...
他にもいっぱいやりたいことがあるのに、そんな苦行はしたくありません。
そこで、マークダウンの記事から自動で表の画像ファイルを作成できる、Pythonスクリプトを作りました。
使い方は簡単。Markdownファイルを食わせてやれば、一瞬でそれなりの表が画像で生成されます。
スクリプトの解説
このスクリプトの主な特徴は以下の通りです:
1. Markdownファイルから表を自動抽出
- BeautifulSoupを使ってHTMLテーブルを検出
- 正規表現による事前検出でデバッグ情報を提供
- 複数の検出方法を組み合わせて確実に表を見つける
2. 列数に応じた動的調整
- 列数が4以下: フォントサイズ15、幅倍率1.5(見やすい大きなフォント)
- 列数が5-10: フォントサイズ10、幅倍率1.7(バランスの取れたサイズ)
- 列数が11以上: フォントサイズ8、幅倍率1.7(コンパクトな表示)
- 文字数が多い場合: 自動的にフォントサイズを2ポイント下げる(最小6ポイント)
3. 論文風のデザイン
- 縦線なし: セル間の縦線を削除してスッキリとした見た目
- ヘッダー強調: グレー背景(#d9d9d9)と太字でヘッダーを目立たせる
- 横線のみ: ヘッダー上下と最下部に横線のみ表示
- 中央揃え: すべてのセルを中央揃えで統一
4. 日本語フォント対応
- 優先順位付きフォント検索: Hiragino Sans GB → Hiragino Kaku Gothic ProN → Yu Gothic → Meiryo → MS Gothic → Noto Sans CJK JP → Arial Unicode MS → DejaVu Sans
- 自動フォント検出: システムにインストールされている日本語フォントを自動検出
- フォールバック機能: 日本語フォントが見つからない場合はデフォルトのsans-serifフォントを使用
5. 高品質な画像出力
- 300dpi: 高解像度で印刷にも対応
- 自動サイズ調整: 表の内容に応じて画像サイズを自動計算
- PNG形式: 透明度対応で高品質な画像を生成
6. エラーハンドリングとデバッグ機能
- 詳細なログ出力: 各処理段階での進捗状況を表示
- エラー追跡: 問題が発生した場合の詳細なエラー情報を提供
- 空テーブル検出: 空のテーブルを自動的にスキップ
7. パフォーマンス最適化
- メモリ効率: 処理後にmatplotlibのfigureを自動的にクローズ
- バッチ処理: 複数のテーブルを一度に処理
- 自動ディレクトリ作成: 出力ディレクトリが存在しない場合は自動作成
出力される表は、マークダウンファイルの上から順に表番号を振るので、
生成AI搭載のエディタに、すでに書いてしまったマークダウンの表を、画像の表番号を参照して上から順に置き換えるようにプロンプト書くと、
一瞬で置換してくれます。
webに公開する場合は、一旦アップロードして、下記のようなリンクもらわないといけませんが、
その辺は、ドラッグアンドドロップで全部の画像を一気にアップロードできるので、そんなに手間ではないと思います。

import os
import matplotlib.pyplot as plt
import pandas as pd
from bs4 import BeautifulSoup
import markdown
import matplotlib.font_manager as fm
import re
def process_markdown_tables_to_images(markdown_file_path, output_image_dir):
"""
Markdownファイルから表を抽出し、論文風(縦線なし、ヘッダー上下と最下部に横線)の画像として保存する。
"""
if not os.path.exists(markdown_file_path):
print(f"エラー: Markdownファイルが見つかりません: {markdown_file_path}")
return []
os.makedirs(output_image_dir, exist_ok=True)
print(f"\n--- Markdownファイルから表を抽出中: {markdown_file_path} ---")
try:
with open(markdown_file_path, 'r', encoding='utf-8') as f:
markdown_content = f.read()
except Exception as e:
print(f"エラー: Markdownファイルの読み込み中に問題が発生しました: {e}")
return []
# デバッグ: マークダウンコンテンツの確認
print(f"マークダウンコンテンツの長さ: {len(markdown_content)} 文字")
# マークダウンの表を正規表現で検出
table_pattern = r'(\|[^\n]*\|[\r\n]+(?:\|[^\n]*\|[\r\n]+)+)'
table_matches = re.findall(table_pattern, markdown_content)
print(f"正規表現で検出された表の数: {len(table_matches)}")
html = markdown.markdown(markdown_content, extensions=['tables'])
soup = BeautifulSoup(html, 'html.parser')
tables = soup.find_all('table')
if not tables:
print("Markdownコンテンツ内に表が見つかりませんでした。")
# デバッグ: HTMLの内容を確認
print("生成されたHTMLの一部:")
print(html[:1000])
return []
print(f"BeautifulSoupで検出された表の数: {len(tables)}")
print(f"画像を '{output_image_dir}' ディレクトリに保存します。")
saved_image_paths = []
font_keywords = ['gothic', 'yu gothic', 'hiragino', 'meiryo', 'noto sans cjk jp', 'arial unicode ms']
available_fonts = [f.name for f in fm.fontManager.ttflist if any(kw.lower() in f.name.lower() for kw in font_keywords)]
font_priority = ['Hiragino Sans GB', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Meiryo', 'MS Gothic', 'Noto Sans CJK JP', 'Arial Unicode MS', 'DejaVu Sans']
selected_font = next((font for font in font_priority if font in available_fonts), None)
if selected_font:
plt.rcParams['font.family'] = selected_font
print(f"使用フォント: {selected_font}")
else:
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans', 'Liberation Sans']
print("指定された日本語フォントが見つからなかったため、デフォルトのsans-serifフォントを使用します。")
plt.rcParams['axes.unicode_minus'] = False
for i, table_tag in enumerate(tables, start=1):
try:
print(f"\n--- テーブル {i} を処理中 ---")
print(f"テーブルHTML: {str(table_tag)[:200]}...")
df = pd.read_html(str(table_tag))[0]
if df.empty:
print(f"スキップ: 空のテーブルが検出されました (テーブル番号: {i})")
continue
rows, cols = df.shape
print(f"テーブル {i} のサイズ: {rows}行 x {cols}列")
# 列数に応じて設定値を動的に決める
if cols <= 4:
font_size = 15
fig_width_scale = 1.5
elif cols <= 10:
font_size = 10
fig_width_scale = 1.7
else:
font_size = 8
fig_width_scale = 1.7
# 文字数が多い場合はフォントサイズをさらに小さく
max_text_length = 0
for _, row in df.iterrows():
for cell in row:
if isinstance(cell, str) and len(cell) > max_text_length:
max_text_length = len(cell)
if max_text_length > 50:
font_size = max(6, font_size - 2)
print(f"文字数が多いため、フォントサイズを {font_size} に調整しました")
scale_factor = 2
header_rows = 1
inch_per_point = 1 / 72
# 動的な変数を使って画像のサイズを計算
fig_width = max(12, cols * fig_width_scale)
fig_height = ((rows + header_rows) * font_size * scale_factor) * inch_per_point + 0.7
print(f"画像サイズ: {fig_width} x {fig_height}")
fig, ax = plt.subplots(figsize=(fig_width, fig_height))
ax.axis('off')
table = ax.table(cellText=df.values, colLabels=df.columns, loc='center', cellLoc='center')
table.auto_set_font_size(False)
table.set_fontsize(font_size)
table.scale(1, scale_factor)
# セルの幅を自動調整
table.auto_set_column_width(col=list(range(cols)))
line_width = 1
for (row_idx, col_idx), cell in table.get_celld().items():
cell.set_text_props(color='black')
if row_idx == 0:
cell.set_facecolor('#d9d9d9')
cell.set_text_props(weight='bold', color='black')
cell.set_linewidth(line_width)
cell.visible_edges = 'TB'
elif row_idx == rows:
cell.set_linewidth(line_width)
cell.visible_edges = 'B'
else:
cell.visible_edges = ''
output_filename = f"article_table_{i:02d}.png"
output_path = os.path.join(output_image_dir, output_filename)
plt.tight_layout()
plt.savefig(output_path, bbox_inches='tight', pad_inches=0, dpi=300)
plt.close(fig)
saved_image_paths.append(output_path)
print(f"保存しました: {output_path}")
except Exception as e:
print(f"エラー: テーブル番号 {i} の画像化中にエラーが発生しました: {e}")
import traceback
traceback.print_exc()
continue
return saved_image_paths
if __name__ == "__main__":
try:
current_script_dir = os.path.dirname(os.path.abspath(__file__))
except NameError:
current_script_dir = os.getcwd()
#mdファイルをそのまま飲ませるだけでOKです
markdown_article_path = os.path.join(current_script_dir, "trend_drop_analysis_article.md")
output_images_directory = os.path.join(current_script_dir, "generated_table_images")
print(f"Markdown記事のパス: {markdown_article_path}")
print(f"生成画像の出力ディレクトリ: {output_images_directory}")
processed_image_paths = process_markdown_tables_to_images(markdown_article_path, output_images_directory)
if processed_image_paths:
print("\n--- 全ての処理が完了しました ---")
print("以下の画像ファイルが生成されました。")
for p in processed_image_paths:
print(p)
else:
print("\n画像は生成されませんでした。指定されたMarkdownファイルに表が存在するか確認してください。")
出力される表
表1 中期投資に適したRCIコンビの探索(過去記事より引用)
このスクリプトを使えば、もう読者が記事を読むのを諦めることはないはずです。
結果は...
結論から言うと、まだまだトレンドグッパイ多数です。
正直、期待していたほどの劇的な改善は見られていません。でも、これは当然のことかもしれません。
なぜ改善されなかったのか?
- サンプルサイズが小さい - まだ数記事しか試していない(統計的に有意とは言えないレベル)
- 他の要因も影響している - 記事が最近やたら小難しい。技術が古い。Qiitaの主な読者のwebエンジニア向けの内容ではない。
- 改善には時間がかかる - 読者の行動パターンの変化はすぐには現れない(SEO効果と同じで時間がかかる)
でも、諦めるのはまだ早い!
この方法は、少なくとも技術的な問題(スマホでの表の表示崩れ)は解決できます?。
そして、読者の体験が改善されれば、長期的には以下の効果が期待できます?:
- 滞在時間の向上 - 表が読みやすくなれば、記事を最後まで読んでくれる気がする。
- ストック数の増加 - 保存しておきたい記事として認識されるされるはず。
- リピート読者の獲得 - 読みやすい記事はまた読みたくなる気がする。
- SNSでのシェア率向上 - 見た目が良い記事はシェアされやすい気がする。
そんな期待を胸に、記事を書き続けていれば、きっと良いことあるはず?
まとめ
Qiitaのトレンドからすぐに落ちる現象の原因として、「スマホで潰れる表」という技術的な問題との関連に関して妄想しました。
その解決策としてマークダウン記法のファイルに含まれる表を自動で画像化するPythonスクリプトを紹介しました。
根本的な原因とはずれていると思いますが、読者の皆様の体験を改善する一歩として、この方法を試してみる価値はあると思います。
皆さんも、見た目を気を付けると、長くトレンドに残り続けるかもしれません。
案外、内容よりも見た目で人は判断していることが多いような、そうでもないような...
ご連絡
3〜4週間ほど前に、嬉しいことに、
「バックテスト用の過去の財務スクリーニングデータって、どうやって取るんですか?」
というご質問をいただきました。
コメント欄で説明しようとしたのですが…
無理でした。説明が入りきらない。
なので記事にまとめると宣言してから、早、数週間。
…ええ、すみません、時間かかってます。
でもご安心を。近日中に
J-Quantsを使った財務データ取得法の記事をお届けします。
お待ちいただいた分だけ、ちょっと愛情多めでお送りしますので、
もう少しだけ、お茶かコーヒーか青汁でも飲んで、お待ちいただけると幸いです。