Geminiと白熱した議論を交わし、「これは良いアイデアが出たぞ!」と意気揚々と全体をコピーしてメモ帳に貼り付ける。
しかし、いざ見返してみると「あれ? 最初の方の会話がごっそり消えている……!?」という悲劇に見舞われたことはありませんか?
あるいは、せっかくAIが綺麗に書いてくれたプログラムのコードが、ただのベタ打ちテキストになってしまい、書式が崩壊して絶望した経験があるかもしれません。
の記事では、そんな「Geminiの長い会話が途切れる・綺麗にコピペできない問題」の原因をサクッと解説し、ボタン一発で全文を美しい書式(Markdown)のまま保存する最強の解決策をお伝えします。
1. なぜGeminiの長い会話は「全部コピペ」できないのか?
まずは敵を知りましょう。なぜ、マウスで上から下までビーッと選択してコピーしても、過去のログが消えてしまうのでしょうか。
1.1 諸悪の根源は「遅延読み込み(Lazy Loading)」
原因は、現代のWebサイトが採用している「遅延読み込み(Lazy Loading)」や「仮想スクロール」と呼ばれる仕組みにあります。
Geminiのチャット履歴が長くなると、ブラウザはメモリを節約するために「今、画面に表示されていない過去の会話」を裏側からこっそり削除(または簡略化)します。
つまり、あなたが見ている画面上には文字があるように見えても、ブラウザの内部データとしては「空っぽ」になっているのです。だから、全選択してコピーしても、見えていない部分のデータはクリップボードに入りません。
1.2 アナログな「気合の全スクロール作戦」はもうやめよう
この仕様に気づいた人の中には、「一番上まで戻って、下まで超高速でスクロールして、画面が消す前に素早くコピーする!」という職人技(?)を編み出す人もいます。
しかし、この方法は確実性に欠けますし、何より面倒くさいですよね。
私たちはAIを使って効率化をしているはずなのに、データの保存で体力を使うのは本本末転倒です。もっとスマートな方法に切り替えましょう。
2. 【結論】ボタン一発!会話の全文を綺麗に保存する2つの方法
画面に表示されている文字(DOM)をコピーしようとするから失敗するのです。
解決策はシンプル。「裏側にあるチャットデータそのものを直接引っこ抜く」ツールを使えば良いのです。
ここでは、導入が簡単な順に2つのアプローチを紹介します。どちらも、テキストを「Markdown(マークダウン)形式」という、見出しやコードブロックの書式を維持した美しい状態でダウンロードしてくれます。
2.1 誰でも簡単!専用の「ブラウザ拡張機能」を使う
一番手っ取り早いのは、Gemini専用に作られたブラウザ拡張機能(Chrome拡張機能など)を使うことです。
汎用的な「Webページ全体を保存するクリッパー」ではなく、「Geminiの画面から対話データだけを抽出する」ことに特化したものを選んでください。
- 探し方: Chromeウェブストアで
Export for GeminiやAI Chat Exporter等を検索します。 - 使い方: 拡張機能をインストールすると、Geminiの画面上に「Export」や「Download」といったボタンが追加されます。対話が終わったら、そのボタンをポチッと押すだけ。一瞬で全文がMarkdownファイルとしてパソコンに保存されます。
スクロール位置を気にする必要は一切ありません。これが最も万人におすすめできる最速の解決策です。
2.2 より確実・柔軟に!自作スクリプト「Gemini to Markdown」を特別公開
「拡張機能を入れるのは少し抵抗がある」「外部のサーバーにデータが渡らないか心配」というあなたのために、この記事限定で自作のユーザースクリプト「Gemini to Markdown」を公開します。
ブラウザの仕様変更の影響を受けにくく、裏側のデータから直接、欠落のない完全な対話ログを抽出する強力なスクリプトです。
【使い方】
- ブラウザに Tampermonkey などのユーザースクリプト管理拡張機能を入れます。
- 以下のコードをコピーし、Tampermonkeyの「新規スクリプトを追加」画面に貼り付けて保存します。
// ==UserScript==
// @name Gemini to Markdown (Patient Scroll)
// @namespace http://tampermonkey.net/
// @version 1.88
// @description Repeatedly scrolls up with increased patience to load full history.
// @author Gemini
// @match https://gemini.google.com/*
// @grant GM_setClipboard
// @grant GM_addStyle
// @icon https://www.google.com/s2/favicons?sz=64&domain=obsidian.md
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ★設定エリア:環境に合わせて調整してください
const USER_PROMPT_PREFIX = '> [!QUESTION] ';
const MAX_SCROLL_ATTEMPTS = 50; // 最大スクロール回数(増やしました)
const SCROLL_WAIT_MS = 3000; // 1回の待ち時間(ミリ秒)。3000 = 3秒
const MAX_RETRIES = 5; // 高さが変わらなくても何回まで粘るか
function domToMarkdown(node) {
if (node.nodeType === Node.TEXT_NODE) return node.textContent;
if (node.nodeType !== Node.ELEMENT_NODE) return '';
let content = '';
node.childNodes.forEach(child => content += domToMarkdown(child));
const tagName = node.tagName.toLowerCase();
switch (tagName) {
case 'p': return `\n${content}\n`;
case 'strong': case 'b': return `**${content}**`;
case 'em': case 'i': return `*${content}*`;
case 'ul': return `\n${content}\n`;
case 'ol':
let orderedContent = '';
const listItems = Array.from(node.children).filter(child => child.tagName.toLowerCase() === 'li');
listItems.forEach((li, index) => {
orderedContent += `${index + 1}. ${domToMarkdown(li).trim()}\n`;
});
return `\n${orderedContent}\n`;
case 'li':
if (node.parentNode && node.parentNode.tagName.toLowerCase() === 'ol') return content;
return `* ${content.trim()}\n`;
case 'hr': return '\n\n---\n\n';
case 'a': return `[${content}](${node.getAttribute('href')})`;
case 'code': return `\`${content}\``;
case 'img':
const altText = node.getAttribute('alt') || 'image';
const srcUrl = node.getAttribute('src');
return srcUrl ? `\n\n\n\n` : '';
case 'h1': return `\n# ${content}\n`;
case 'h2': return `\n## ${content}\n`;
case 'h3': return `\n### ${content}\n`;
case 'h4': return `\n#### ${content}\n`;
case 'h5': return `\n##### ${content}\n`;
case 'h6': return `\n###### ${content}\n`;
default: return content;
}
}
// --- コピー実行 ---
function extractAndCopy() {
const titleElement = document.querySelector('span.conversation-title.gds-title-m');
let title = titleElement ? titleElement.textContent.trim() : '';
let fullMarkdown = title ? `# ${title}\n\n` : '';
const conversationTurns = document.querySelectorAll('message-content, user-query');
if (!conversationTurns.length) {
console.warn('No content found.');
if(!title) return false;
}
conversationTurns.forEach(turn => {
const userQueryLines = turn.querySelectorAll('.query-text-line.ng-star-inserted');
if (userQueryLines.length > 0) {
let userText = '';
userQueryLines.forEach(line => userText += line.textContent.trim() + '\n');
let formattedUserText = userText.trim().replace(/\n/g, '\n> ');
fullMarkdown += `${USER_PROMPT_PREFIX}${formattedUserText}\n\n`;
}
const userImages = turn.querySelectorAll('img[data-test-id="uploaded-img"]');
userImages.forEach(img => {
const alt = img.getAttribute('alt') || 'uploaded image';
const src = img.getAttribute('src');
if (src) fullMarkdown += `\n\n`;
});
const geminiResponse = turn.querySelector('.markdown.markdown-main-panel');
if (geminiResponse) {
let responseMarkdown = domToMarkdown(geminiResponse);
fullMarkdown += responseMarkdown.trim().replace(/\n{3,}/g, '\n\n') + '\n\n';
}
});
GM_setClipboard(fullMarkdown.trim(), 'text');
return true;
}
// --- スクロールコンテナ特定 ---
function findScrollContainer() {
const allElements = document.querySelectorAll('*');
let maxScrollHeight = 0;
let container = null;
for (const el of allElements) {
if (el.scrollHeight > el.clientHeight &&
(window.getComputedStyle(el).overflowY === 'auto' || window.getComputedStyle(el).overflowY === 'scroll')) {
if (el.scrollHeight > maxScrollHeight) {
maxScrollHeight = el.scrollHeight;
container = el;
}
}
}
return container || document.scrollingElement || document.documentElement;
}
// --- ★反復スクロール処理(忍耐強化版) ---
async function handleButtonClick() {
const button = document.getElementById('gemini-to-obsidian-button');
const originalText = button.textContent;
button.style.backgroundColor = '#d97706';
const scrollContainer = findScrollContainer();
let previousHeight = scrollContainer.scrollHeight; // 初期の高さを記録
let noChangeCount = 0; // 高さが変わらなかった回数
// 指定回数ぶん、しつこく上にスクロールを繰り返す
for (let i = 1; i <= MAX_SCROLL_ATTEMPTS; i++) {
button.textContent = `読込中 (${i}/${MAX_SCROLL_ATTEMPTS}) - 待機...`;
// 1. 強制的に一番上へ
scrollContainer.scrollTo({ top: 0, behavior: 'instant' });
window.scrollTo({ top: 0, behavior: 'instant' });
// 2. 長めにロード待ち
await new Promise(resolve => setTimeout(resolve, SCROLL_WAIT_MS));
// 3. 変化チェック
const currentHeight = scrollContainer.scrollHeight;
if (currentHeight === previousHeight) {
noChangeCount++;
button.textContent = `応答なし (${noChangeCount}/${MAX_RETRIES})...`;
// ★ここが変更点: すぐに諦めず、MAX_RETRIES回連続でダメな時だけ終了する
if (noChangeCount >= MAX_RETRIES) {
console.log('Reach Top confirmed (Retries exhausted).');
break;
}
} else {
// 高さが変わった=読み込み成功。カウンターをリセットして続行
noChangeCount = 0;
console.log(`Height changed: ${previousHeight} -> ${currentHeight}`);
}
previousHeight = currentHeight;
}
// 4. 最下部へ戻る
button.textContent = '整形中...';
scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'instant' });
await new Promise(resolve => setTimeout(resolve, 1500)); // 描画待ちも少し延長
// 5. 抽出
const success = extractAndCopy();
if (success) {
button.textContent = 'コピー完了!';
button.style.backgroundColor = '#10b981';
} else {
button.textContent = '失敗';
button.style.backgroundColor = '#ef4444';
}
setTimeout(() => {
button.textContent = originalText;
button.style.backgroundColor = '#4a4a5e';
}, 2000);
}
function createCopyButton() {
const button = document.createElement('button');
button.id = 'gemini-to-obsidian-button';
button.textContent = 'Markdown保存';
button.addEventListener('click', handleButtonClick);
document.body.appendChild(button);
GM_addStyle(`
#gemini-to-obsidian-button {
position: fixed; bottom: 20px; right: 20px;
z-index: 2147483647;
padding: 10px 15px; background-color: #4a4a5e; color: white;
border: none; border-radius: 8px; cursor: pointer; font-size: 14px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.2s;
}
#gemini-to-obsidian-button:hover { filter: brightness(1.2); transform: translateY(-2px); }
#gemini-to-obsidian-button:active { transform: translateY(0); }
`);
}
if (document.readyState === 'complete') {
createCopyButton();
} else {
window.addEventListener('load', createCopyButton);
}
})();- Geminiの画面を開くとエクスポートボタンが出現します。対話終了後にクリックするだけで、全文がMarkdownファイルとしてダウンロードされます!
3. 【一歩先へ】保存したデータを「自分だけの備忘録」に育てる
さて、ボタン一発でGeminiの会話全文を美しい.mdファイルとして保存できるようになりました。
ここで終わっても十分便利ですが、せっかくならこのデータを「生きた知識」として活用してみませんか?
3.1 ノートアプリ(Obsidian等)に放り込むメリット
ダウンロードしたMarkdownファイルは、ただのテキストファイルです。これを、Obsidian(オブシディアン)やNotionといった高機能なノートアプリに放り込んでみましょう。
特にローカルで動作するObsidianは、AIとの対話ログを管理する「セカンドブレイン(第二の脳)」として最適です。
- 検索性が爆上がりする: 過去にAIと一緒に書いたコードや、練り上げた企画書を一瞬で検索できます。
- 点と点が繋がる: ノート同士をリンクさせることで、「半年前にAIと話したアイデア」と「今日思いついたアイデア」が結びつく瞬間が生まれます。
3.2 過去の全履歴を一気に救出する「Google Takeout」+自動変換スクリプト
「これから」の対話は自作スクリプトで保存できても、「過去」の膨大な履歴はどうすればいいのでしょうか? 一つ一つ画面を開いてエクスポートするのは苦行です。
そんな時は、Google公式の「Google Takeout」を使いましょう。
Takeoutから「Gemini」のデータだけを指定してダウンロードすると、過去の全対話履歴が conversations.json という一つのファイルで手に入ります。
さらに、この複雑なJSONファイルを一瞬で美しいMarkdownファイル群に分割・変換するPythonスクリプトもここで公開します。
【一括変換スクリプトの使い方】
- Takeoutでダウンロードした
conversations.jsonと同じフォルダに、以下のコードをgemini2md.pyとして保存します。 - コマンドライン(ターミナル等)で
python3 gemini2md.pyを実行します。
import json
import os
import re
from datetime import datetime
# 設定: 出力先ディレクトリ
OUTPUT_DIR = "Gemini_Archive"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def clean_filename(title):
# ファイル名として安全な文字のみ残す
return re.sub(r'[\\/*?:"<>|]', "", title)[:100]
def parse_gemini_json(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
print("Error: conversations.json が見つかりません。")
return
# データ構造のバージョンの違いを吸収
conversations = data if isinstance(data, list) else data.get('conversations', [])
print(f"{len(conversations)} 件の対話履歴が見つかりました。処理を開始します...")
for conv in conversations:
title = conv.get('title', 'Untitled_Chat')
created_time = conv.get('createTime', datetime.now().isoformat())
safe_title = clean_filename(title)
file_name = f"{OUTPUT_DIR}/{safe_title}.md"
markdown_content = f"---\ntitle: {title}\ncreated: {created_time}\nsource: Google Takeout\ntags: [gemini-archive]\n---\n\n# {title}\n\n"
events = conv.get('events', [])
if not events:
events = conv.get('messages', [])
for event in events:
role = event.get('role', 'unknown')
parts = event.get('parts', [])
header = "## User" if role == 'user' else "## Gemini"
text_content = ""
for part in parts:
if 'text' in part:
text_content += part['text'] + "\n"
if text_content.strip():
markdown_content += f"{header}\n{text_content}\n"
try:
with open(file_name, 'w', encoding='utf-8') as f:
f.write(markdown_content)
except Exception as e:
print(f"Skipped: {safe_title} ({e})")
print("完了しました。生成されたフォルダをノートアプリに移動してください。")
if __name__ == "__main__":
parse_gemini_json("conversations.json")
たったこれだけで、クラウドに眠っていたあなたの知的資産が、全てローカル環境に救出されます。
4. まとめ:AIとの対話を「使い捨て」から「資産」へ
Geminiの長い会話が途切れてしまう原因と、その解決策を解説しました。
- 原因: ブラウザの「遅延読み込み」が画面外のテキストを消している。
- 対策: 画面のコピーではなく、「専用の拡張機能」か「自作スクリプト」を使って裏側のデータをMarkdownとして一括ダウンロードする。
AIとの対話は、単なる検索の代替ではありません。あなたの思考のプロセスそのものであり、二度と同じ回答は得られない貴重なデータです。
コピペのイライラから解放され、AIの知恵を「使い捨て」ではなく「一生モノの資産」として、あなたのローカル環境にストックしていきましょう!
