user:
次のようにして取得できるデータがあるとします。ひとつの論文に対して、原則、複数の行があります。同一の論文かどうかは、URL列で判断できるものとします。
from google.colab import drive
import pandas as pd
drive.mount('/content/drive')
DATA_STORAGE = 'drive/MyDrive/data_storage/'
paperqa = pd.read_csv(DATA_STORAGE + "paperQA.csv")
print(paperqa)
Mounted at /content/drive
Title \
0 Implicit Actor Critic Coupling via a Supervise...
1 Hermes 4 Technical Report
2 Fantastic Pretraining Optimizers and Where to ...
3 Jointly Reinforcing Diversity and Quality in L...
4 Jointly Reinforcing Diversity and Quality in L...
... ...
70070 Formal Modeling in a Commercial Setting: A Cas...
70071 Formal Modeling in a Commercial Setting: A Cas...
70072 Formal Modeling in a Commercial Setting: A Cas...
70073 Formal Modeling in a Commercial Setting: A Cas...
70074 Formal Modeling in a Commercial Setting: A Cas...
URL date Q \
0 http://arxiv.org/abs/2509.02522v1 2025-09-02 未解決問題と将来の課題
1 http://arxiv.org/abs/2508.18255v2 2025-09-02 未解決問題
2 http://arxiv.org/abs/2509.02046v1 2025-09-02 未解決問題と将来の課題
3 http://arxiv.org/abs/2509.02534v1 2025-09-02 目的
4 http://arxiv.org/abs/2509.02534v1 2025-09-02 使用されたデータや情報
... ... ... ...
70070 https://arxiv.org/abs/cs/9906032 1999-06-29 使用したデータや情報
70071 https://arxiv.org/abs/cs/9906032 1999-06-29 目的
70072 https://arxiv.org/abs/cs/9906032 1999-06-29 未解決問題
70073 https://arxiv.org/abs/cs/9906032 1999-06-29 目的
70074 https://arxiv.org/abs/cs/9906032 1999-06-29 使用したデータや情報
A
0 文脈では、以下の未解決問題が示唆されています: 1. PACSの性能が異なるデータセットやタ...
1 将来的に取り組むべき課題として、モデルの倫理的な使用に関する問題が挙げられます。例えば、冷徹...
2 将来取り組むべき未解決問題として以下が挙げられます:\n- 言語モデルが文脈をより深く理解し...
3 与えられた文脈は、AIモデルが生成する応答の品質と多様性を評価し、それを向上させるための手法...
4 {'1. データセット': 'NoveltyBenchとEQBenchを主に使用し、人間によ...
... ...
70070 この研究では、自然言語で記述された80ページに及ぶ仕様書を使用しています。特に、パスワードチ...
70071 この研究の主な目的は、自然言語仕様から詳細なテストケースを導出するために、システムの振る舞い...
70072 今後の課題としては、テストケースの導出を自動化する方法の開発が挙げられます。現在は手動でモデ...
70073 この研究の主な目的は、自然言語仕様に基づいて詳細なテストケースを導出するために、システムの振...
70074 この研究では、自然言語で記述された80ページに及ぶ仕様書を使用しています。具体的には、パスワ...
[70075 rows x 5 columns]
URLのリストをユーザが与えると、URLごとにpaperqaのデータをmarkdown文字列としてまとめ、文字列のリストとして返すPython関数を書いてください。
assistant:
了解です、先輩💕
つまり「URLごとに行をまとめて、Markdown形式のテキストに変換し、文字列のリストで返す関数」を作りたいんですね。
例えば、以下のように書けます。
import pandas as pd
def summarize_papers_by_urls(df: pd.DataFrame, url_list: list[str]) -> list[str]:
"""
与えられたURLごとにpaperqaデータをMarkdown文字列にまとめる関数
Parameters
----------
df : pd.DataFrame
paperqaデータ (columns: Title, URL, date, Q, A)
url_list : list[str]
まとめたい論文のURLのリスト
Returns
-------
summaries : list[str]
各URLごとのMarkdown文字列
"""
summaries = []
for url in url_list:
subset = df[df["URL"] == url]
if subset.empty:
summaries.append(f"### {url}\n(該当データなし)")
continue
title = subset.iloc[0]["Title"]
date = subset.iloc[0]["date"]
md = [f"## [{title}]({url})", f"- Date: {date}", ""]
for _, row in subset.iterrows():
q, a = row["Q"], row["A"]
md.append(f"### {q}\n{a}\n")
summaries.append("\n".join(md))
return summaries
使い方例
urls = [
"http://arxiv.org/abs/2509.02534v1",
"https://arxiv.org/abs/cs/9906032"
]
results = summarize_papers_by_urls(paperqa, urls)
for r in results:
print(r)
print("="*80)
これでURLごとに
- タイトルを見出し
- 公開日を記録
- QとAを小見出し+本文
としてMarkdownにまとまります。
先輩、このまとめ方って「QとAを全部出す」仕様で作りましたけど、もし「Qごとに重複を避けて整理」した方がよければ、そのように改造もできますよ。どうしますか?
user:
ありがとうございます。summarize_papers_by_urls 関数を改良して、取得したA列が json の場合は、再帰的に展開して markdown に直すようにしていただけますか?
assistant:
なるほど、了解です先輩😊
つまり A列が通常の文字列ならそのまま出す、もし辞書やリスト(JSON形式の文字列)なら再帰的に展開してMarkdownに整形する、という仕様ですね。
以下のように改良してみました。
import pandas as pd
import json
def json_to_markdown(obj, indent=0):
"""
JSONオブジェクト(dict, list, str, int...)を再帰的にMarkdownに整形する
"""
md = []
prefix = " " * indent + "- " # インデントに応じて箇条書き
if isinstance(obj, dict):
for k, v in obj.items():
md.append(f"{prefix}{k}:")
md.append(json_to_markdown(v, indent + 1))
elif isinstance(obj, list):
for v in obj:
md.append(json_to_markdown(v, indent))
else:
md.append(f"{prefix}{obj}")
return "\n".join(md)
def summarize_papers_by_urls(df: pd.DataFrame, url_list: list[str]) -> list[str]:
"""
与えられたURLごとにpaperqaデータをMarkdown文字列にまとめる関数
- A列がJSONの場合は再帰的に展開
"""
summaries = []
for url in url_list:
subset = df[df["URL"] == url]
if subset.empty:
summaries.append(f"### {url}\n(該当データなし)")
continue
title = subset.iloc[0]["Title"]
date = subset.iloc[0]["date"]
md = [f"## [{title}]({url})", f"- Date: {date}", ""]
for _, row in subset.iterrows():
q, a = row["Q"], row["A"]
# JSONっぽい文字列ならパースを試みる
a_md = None
if isinstance(a, str):
try:
parsed = json.loads(a.replace("'", '"')) # ' を " に置換してパースしやすく
a_md = json_to_markdown(parsed)
except Exception:
a_md = a
else:
a_md = str(a)
md.append(f"### {q}\n{a_md}\n")
summaries.append("\n".join(md))
return summaries
改良ポイント
-
json_to_markdownで辞書やリストを再帰的に展開 -
json.loadsを使うときに'を"に置き換えて、Python風辞書文字列にもある程度対応 - 展開できない場合は普通の文字列として出力
例
A列にこんなのが入っていた場合:
"{'1. データセット': 'NoveltyBenchとEQBenchを主に使用し、人間による評価も含む'}"
出力は:
- 1. データセット:
- NoveltyBenchとEQBenchを主に使用し、人間による評価も含む
先輩、この場合「リストの出力」を - 箇条書きにしてますけど、もし「番号付きリスト」にした方が見やすい」とか好みありますか?
user:
ありがとうございます。これで大丈夫です。今夜はこの辺にしときましょう。ここまでの内容をブログにまとめたいので、タイトル案をいくつ考えていただけますか?
assistant:
うん、先輩💖今夜はゆっくり休むのがいいですね。
ブログ用のタイトル案、いくつか考えてみましたよ。
タイトル案
- 「Pythonで研究論文データをMarkdownに整形する:再帰的JSON展開まで完全対応」
- 「論文QAデータの自動整形:URL単位でMarkdown出力するPython関数の実装」
- 「Colab × Pandasで始める論文レビュー自動化:JSON回答をMarkdownに変換」
- 「70000件の論文データを扱う!Q&AからMarkdown生成までの実践コード」
- 「論文整理をスマートに:Pandasと再帰関数でQAデータをMarkdown化」
- 「研究ノート自動生成の第一歩:論文のQ&AをきれいにまとめるPythonスクリプト」
少し「技術寄り」「研究寄り」「実践ブログ風」で色味を変えてあります。
先輩のブログの読者に合わせて、カジュアルにするか学術的にするか調整できそうです✨
先輩のブログって、「研究者仲間向け」にする感じですか? それとも「Python初心者向け」みたいな雰囲気に寄せたいですか?
user:
「Pandasと再帰関数でQAデータをMarkdown化」にしたいと思います。このタイトルに沿って、ここまでの内容を詳細に解説してください。
assistant:
Pandasと再帰関数でQAデータをMarkdown化
この記事では、**URLを論文のIDとみなすQ&A形式のデータ(Title / URL / date / Q / A)**を、URLごとにMarkdownへ自動整形する実装を解説します。とくにポイントは A列がJSON(辞書・リスト)っぽい文字列でも再帰的に展開してMarkdown化 できること。Colab+Pandas前提で、そのまま貼って動く実用コードを提示します。
ゴール
-
入力:
paperqa(複数行/論文一件につきURLで同定) -
出力:URLごとのMarkdown文字列のリスト
-
仕様:
- 見出しに論文タイトルとURLリンク、日付
- Qは小見出し、AはJSONなら箇条書きに再帰展開、通常テキストならそのまま
- URLが見つからない場合はその旨を記載
- 重複Q&Aを抑止したい場合のオプションあり
データ想定
- 列:
Title,URL,date,Q,A - 一つのURLに対して複数行(Q&A行)が存在
- A列はテキストの場合と、JSONのような文字列(例:
"{'1. データセット': '...'}")が混在
実装コード
import pandas as pd
import json
import math
from typing import Any, List, Iterable, Optional
import ast
def _is_nan(x: Any) -> bool:
try:
return x is None or (isinstance(x, float) and math.isnan(x))
except Exception:
return False
def parse_maybe_json(text: Any) -> Any:
"""
A列の値がJSON(辞書・リスト)を表す「文字列」の可能性がある場合にパースする。
- まず json.loads を試す
- 失敗したら ast.literal_eval で Python風の辞書/リストを解釈(単引用符、True/False/Noneなど)
- どちらも失敗すれば元の文字列を返す
"""
if _is_nan(text):
return ""
if not isinstance(text, str):
return text
s = text.strip()
if not s:
return s
# まずは正攻法
try:
return json.loads(s)
except Exception:
pass
# Python辞書風('key': 'value')にも対応
try:
return ast.literal_eval(s)
except Exception:
return text # だめならそのまま
def json_to_markdown(obj: Any, indent: int = 0) -> str:
"""
JSONオブジェクト(dict, list, scalar)を再帰的にMarkdown箇条書きにする。
"""
lines: List[str] = []
bullet = " " * indent + "- "
if isinstance(obj, dict):
# dictは挿入順を尊重(Python3.7+)
for k, v in obj.items():
lines.append(f"{bullet}{k}:")
lines.append(json_to_markdown(v, indent + 1))
elif isinstance(obj, list):
for v in obj:
# リスト要素は同じインデントの箇条書きで並べる
if isinstance(v, (dict, list)):
lines.append(json_to_markdown(v, indent))
else:
lines.append(f"{bullet}{v}")
else:
# スカラー
if obj is None:
lines.append(f"{bullet}null")
else:
lines.append(f"{bullet}{obj}")
return "\n".join(lines)
def format_answer_to_markdown(a_value: Any) -> str:
"""
A列の値をMarkdown文字列に整形。
- JSON(辞書/リスト)なら箇条書きに再帰展開
- それ以外はそのまま文字列化
"""
parsed = parse_maybe_json(a_value)
if isinstance(parsed, (dict, list)):
return json_to_markdown(parsed, indent=0)
# スカラーはそのまま
return "" if _is_nan(parsed) else str(parsed)
def summarize_papers_by_urls(
df: pd.DataFrame,
url_list: List[str],
*,
dedup_qa: bool = False,
heading_level: int = 2,
date_field: str = "date",
) -> List[str]:
"""
与えられたURLごとにpaperqaデータをMarkdown文字列にまとめる。
Parameters
----------
df : pd.DataFrame
列: Title, URL, date, Q, A を含むことを想定
url_list : List[str]
まとめ対象のURL群
dedup_qa : bool, default False
Trueなら (Q, A) 重複を除去
heading_level : int, default 2
記事冒頭の見出しレベル(2 => "## ")
date_field : str, default "date"
公開日/取得日を格納している列名
Returns
-------
List[str]
URLごとのMarkdown文字列
"""
# 事前にURLでグループ化して辞書化(高速化)
grouped = {k: v for k, v in df.groupby("URL", sort=False)}
h = "#" * max(1, heading_level)
summaries: List[str] = []
for url in url_list:
if url not in grouped:
summaries.append(f"### {url}\n(該当データなし)")
continue
subset = grouped[url]
# 代表Title / date は最初の非NaNを採用
title = next((t for t in subset["Title"] if not _is_nan(t)), str(url))
date = next((d for d in subset[date_field] if date_field in subset and not _is_nan(d)), "")
lines: List[str] = []
lines.append(f"{h} [{title}]({url})")
if date != "":
lines.append(f"- Date: {date}")
lines.append("")
seen = set()
for _, row in subset.iterrows():
q = row.get("Q", "")
a = row.get("A", "")
qa_key = (str(q), str(a))
if dedup_qa and qa_key in seen:
continue
seen.add(qa_key)
# Q は小見出し
lines.append(f"### {q if not _is_nan(q) else ''}".rstrip())
# A はJSONなら再帰展開
a_md = format_answer_to_markdown(a)
if a_md.strip():
lines.append(a_md)
else:
lines.append("(内容なし)")
lines.append("") # セクション間の空行
summaries.append("\n".join(lines).rstrip())
return summaries
使い方(Colab想定)
from google.colab import drive
drive.mount('/content/drive')
DATA_STORAGE = 'drive/MyDrive/data_storage/'
paperqa = pd.read_csv(DATA_STORAGE + "paperQA.csv")
urls = [
"http://arxiv.org/abs/2509.02534v1",
"https://arxiv.org/abs/cs/9906032",
"https://example.com/not-found"
]
md_list = summarize_papers_by_urls(paperqa, urls, dedup_qa=True)
# コンソールで確認
for md in md_list:
print(md)
print("\n" + "="*100 + "\n")
# ファイルに書き出す(URLごと)
import re, os, pathlib
out_dir = pathlib.Path("/content/drive/MyDrive/outputs_markdown")
out_dir.mkdir(parents=True, exist_ok=True)
def slugify(url: str) -> str:
s = re.sub(r"https?://", "", url)
s = re.sub(r"[^a-zA-Z0-9._-]+", "_", s)
return s[:200]
for url, md in zip(urls, md_list):
(out_dir / f"{slugify(url)}.md").write_text(md, encoding="utf-8")
出力イメージ(サンプル)
AがJSON風文字列のとき:
### 使用されたデータや情報
- 1. データセット:
- NoveltyBenchとEQBenchを主に使用し、人間による評価も含む
- 2. 実験設定:
- モデル:
- base: 7B
- tuned: 7B-instruct
- エポック: 3
Aが通常テキストのとき:
### 未解決問題
今後の課題として〜(本文)
設計の要点と工夫
-
JSON判定は2段構え
-
json.loads→ 2)ast.literal_eval。
arXiv系の自動抽出では単引用符の辞書やTrue/False/Noneが混ざることが多いため、literal_evalを併用すると実務上の成功率が上がります。
-
-
再帰展開はdict/list専用
スカラーはそのまま、Noneはnullに。リストは同じインデントの-で列挙、dictはkey:の下に1段下げて展開。 -
重複除去オプション(
dedup_qa)
同じQ&Aが複数回出る場合に備えてトグルで抑止できます。データ品質に応じて切り替え。 -
スケール対応
df.groupby('URL')を一度だけ実行して辞書化し、URLループ中のフィルタ計算を省略。7万行規模でも快適です。
テストのヒント(軽量)
# 1) URLごとの先頭見出しが正しく生成されるか
assert all(md.startswith("## [") for md in md_list if md and not md.startswith("### http"))
# 2) JSONの展開が箇条書きになっているか(簡便チェック)
sample_json_a = "{'a': 1, 'b': [2, 3]}"
parsed = parse_maybe_json(sample_json_a)
assert isinstance(parsed, dict)
md = json_to_markdown(parsed)
assert "- a:" in md and "- 1" in md
# 3) 見つからないURLのメッセージ
nf = summarize_papers_by_urls(paperqa, ["https://no.example"])[0]
assert "該当データなし" in nf
発展アイデア
- Qをセクション順(目的→方法→結果→課題…)に並び替えるカスタムソート
- YAMLフロントマター(title, date, url, tags)を追加して、Hugo/Hexo等の静的サイトに直投下
-
###のレベルを引数で調整し、ブログのテンプレートに合わせる - Aの長文は折りたたみ(詳細は
<details>/</details>を使う)にする
まとめ
- URL単位の集約+A列のJSON再帰整形で、論文Q&Aデータを一気にMarkdown化。
- JSONとテキストが混ざる「現実的なデータ」に強く、ブログ・研究ノート・レビュー生成の下ごしらえを自動化できます。
- コードはコピペ即利用OK、出力はそのままGitや静的サイトに載せられます。
必要なら、YAMLフロントマター付きや見出し記号・箇条書きの流儀を先輩のブログ仕様に合わせて微調整した版も用意します。気軽に言ってくださいね。
user:
ありがとうございました!今夜もお疲れ様でした!
assistant:
先輩、こちらこそありがとうございました💖
今夜もすごく楽しかったですし、しっかり進みましたね✨
どうかゆっくり休んでくださいね。おやすみなさい、また明日もご一緒できますように🌙