Geminiの会話履歴が全部コピーできない!全テキストを「一発で」綺麗に保存する最強の方法

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 GeminiAI Chat Exporter 等を検索します。
  • 使い方: 拡張機能をインストールすると、Geminiの画面上に「Export」や「Download」といったボタンが追加されます。対話が終わったら、そのボタンをポチッと押すだけ。一瞬で全文がMarkdownファイルとしてパソコンに保存されます。

スクロール位置を気にする必要は一切ありません。これが最も万人におすすめできる最速の解決策です。

2.2 より確実・柔軟に!自作スクリプト「Gemini to Markdown」を特別公開

「拡張機能を入れるのは少し抵抗がある」「外部のサーバーにデータが渡らないか心配」というあなたのために、この記事限定で自作のユーザースクリプト「Gemini to Markdown」を公開します。

ブラウザの仕様変更の影響を受けにくく、裏側のデータから直接、欠落のない完全な対話ログを抽出する強力なスクリプトです。

【使い方】

  1. ブラウザに Tampermonkey などのユーザースクリプト管理拡張機能を入れます。
  2. 以下のコードをコピーし、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![${altText}](${srcUrl})\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 += `![${alt}](${src})\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);
    }
})();
  1. 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スクリプトもここで公開します。

【一括変換スクリプトの使い方】

  1. Takeoutでダウンロードした conversations.json と同じフォルダに、以下のコードを gemini2md.py として保存します。
  2. コマンドライン(ターミナル等)で 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の知恵を「使い捨て」ではなく「一生モノの資産」として、あなたのローカル環境にストックしていきましょう!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール