ChatGPTのWebページに表示されている、チャット内容をテキストファイルに保存する。ブックマークレットを作成します。
テキストファイルとして保存しますが、見やすいようにマークダウンで修飾します。
Webページの仕様が公開されているわけではないので、部分的に作りながら確認していきます。
コードは最終的にはブックマークレットに変換しますが、開発中は開発者ツールにコードを張り付けて動作を確認します。
土台として Web ページの main タグに囲まれた部分のテキストをファイルに保存するコードを以下に記述します。
この JavaScript に問題があれば指摘して下さい。
(() => {
const main = document.querySelector('main');
const conversationText = main.textContent
const file = new Blob([conversationText], {
type: 'text/plain'
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = 'chatgpt-conversation.txt';
a.click();
a.remove();
})();
このJavaScriptコードは、基本的には正しいですが、マークダウンで修飾されたテキストを取得する部分が足りません。また、コードの整形にも改善の余地があります。以下に、改善されたコードを提供します。
(() => {
function generateMarkdown(main) {
// ここでマークダウン形式に変換する処理を実装します
// 例えば、ユーザーとAIの会話の要素をそれぞれ取得し、見出しとリストで修飾するなど
return markdownText;
}
const main = document.querySelector("main");
const markdownText = generateMarkdown(main);
const file = new Blob([markdownText], {
type: "text/plain"
});
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = "chatgpt-conversation.md";
a.click();
a.remove();
})();
このコードでは、generateMarkdown関数を追加し、その中でマークダウン形式に変換する処理を実装します。具体的なマークダウン形式の変換方法は、Webページの構造に依存するため、適宜調整が必要です。また、ダウンロードするファイルの拡張子を.mdに変更しています。これにより、マークダウン形式のファイルとして保存されます。
調査の結果、document.querySelector("main > div.flex-1 > div.h-full > div > div").children
で取得した HTMLCollection の中に保存すべき内容が含まれていることが、判明しました。
この HTMLCollection の最初の要素に含まれている文字列はがモデル名です。また最後の要素には "Regenerate responce" というテキストが含まれていました。
のこりの要素がそれぞれユーザーとアシスタントの会話が入って言います。
HTMLCollection のそれぞれに含まれているテキストを抽出し '---' で区切った MarkDown テキストを保存するようにコードを修正してください。ただし 最後の要素には不要です。
指定された要素を使用して、マークダウン形式で会話を整形し、ファイルに保存するコードを以下に記述します。
(() => {
function generateMarkdown(elements) {
let markdownText = "";
const lastIndex = elements.length - 1;
for (let i = 0; i < lastIndex; i++) {
markdownText += elements[i].textContent;
// 最後の要素でなければ、'---'を追加
if (i !== lastIndex - 1) {
markdownText += "\n---\n";
}
}
return markdownText;
}
const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children;
const markdownText = generateMarkdown(elements);
const file = new Blob([markdownText], {
type: "text/plain"
});
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = "chatgpt-conversation.md";
a.click();
a.remove();
})();
このコードは、generateMarkdown関数で、指定された要素をマークダウン形式に整形します。最後の要素を除いて、各要素のテキストを取得し、'---'で区切ります。最後に、マークダウン形式のテキストをファイルに保存します。
調査の結果会話の中で会話中で <pre>
で囲まれた部分はプログラムのコード等でした。また pre > div > :first-child > span
には "プログラム言語名" が pre > div code
にコード本体が入っていることが分かりました。
これを
```プログラム言語名
コード本体
```
のような形で整形するようにプログラムを修正してください。なお、プログラムコードは複数含まれている場合もあります。
指定された要素に基づいて、プログラム言語名とコード本体を含むマークダウン形式に整形し、ファイルに保存するコードを以下に記述します。
(() => {
function generateMarkdown(elements) {
let markdownText = "";
const lastIndex = elements.length - 1;
for (let i = 0; i < lastIndex; i++) {
const preElements = elements[i].querySelectorAll("pre");
if (preElements.length > 0) {
for (const preElement of preElements) {
const language = preElement.querySelector("div > :first-child > span");
const code = preElement.querySelector("div code");
if (language && code) {
const formattedCode = `\n\`\`\`${language.textContent}\n${code.textContent}\n\`\`\`\n`;
markdownText += elements[i].textContent.replace(preElement.textContent, formattedCode);
} else {
markdownText += elements[i].textContent;
}
}
} else {
markdownText += elements[i].textContent;
}
// 最後の要素でなければ、'---'を追加
if (i !== lastIndex - 1) {
markdownText += "\n---\n";
}
}
return markdownText;
}
const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children;
const markdownText = generateMarkdown(elements);
const file = new Blob([markdownText], {
type: "text/plain"
});
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = "chatgpt-conversation.md";
a.click();
a.remove();
})();
このコードでは、generateMarkdown関数で、指定された要素の
タグを検索し、プログラム言語名とコード本体を含むマークダウン形式に整形します。プログラムコードが複数含まれている場合も考慮しています。最後に、マークダウン形式のテキストをファイルに保存します。
調査の結果ユーザーの会話には
<img alt="Hiroshi Okada" srcset="/_next/image?url=https%3A%2F%2Flh3.googleusercontent.com%2Fa%2FAGNmyxbqi9Fv_N1dRqoa94XBEsD1NhoHKSf0swYR5qWMGA%3Ds96-c&w=32&q=75 1x, /_next/image?url=https%3A%2F%2Flh3.googleusercontent.com%2Fa%2FAGNmyxbqi9Fv_N1dRqoa94XBEsD1NhoHKSf0swYR5qWMGA%3Ds96-c&w=64&q=75 2x" src="/_next/image?url=https%3A%2F%2Flh3.googleusercontent.com%2Fa%2FAGNmyxbqi9Fv_N1dRqoa94XBEsD1NhoHKSf0swYR5qWMGA%3Ds96-c&w=64&q=75" decoding="async" data-nimg="intrinsic" class="rounded-sm" style="position: absolute; inset: 0px; box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;">
のようなイメージが含まれていることが判明しました。 alt 属性は ユーザー名です。
またアシスタントの会話には
<svg width="41" height="41" viewBox="0 0 41 41" fill="none" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" class="h-6 w-6"><path
で始まる svg イメージが含まれている事が判明しました。
これらの情報を元に、ユーザーアシスタントの個々の会話の先頭に
ユーザの場合は
### ユーザー名
アシスタントの場合は
### アシスタント名
をつけるようにしてください。なお属性は多少揺らぎがあっても大丈夫なように厳密にはチェックしないでください。
また、これらの要素が見つからなかった場合は
### ?
を追加してください。
指定された要素を使用して、ユーザー名とアシスタント名を含むマークダウン形式に整形し、ファイルに保存するコードを以下に記述します。
(() => { function getUserName(element) { const imgElement = element.querySelector("img"); if (imgElement && imgElement.alt) { return imgElement.alt; } return "?"; } function getAssistantName(element) { const svgElement = element.querySelector("svg"); if (svgElement) { return "アシスタント"; } return "?"; } function generateMarkdown(elements) { let markdownText = ""; const lastIndex = elements.length - 1; for (let i = 0; i < lastIndex; i++) { const userName = getUserName(elements[i]); const assistantName = getAssistantName(elements[i]); const speaker = userName !== "?" ? userName : assistantName; markdownText += `### ${speaker}\n\n`; markdownText += elements[i].textContent; // 最後の要素でなければ、'---'を追加 if (i !== lastIndex - 1) { markdownText += "\n---\n"; } } return markdownText; } const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children; const markdownText = generateMarkdown(elements); const file = new Blob([markdownText], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); a.download = "chatgpt-conversation.md"; a.click(); a.remove(); })();このコードでは、getUserNameとgetAssistantName関数を追加し、ユーザー名とアシスタント名を取得します。その後、generateMarkdown関数で、ユーザー名とアシスタント名を含むマークダウン形式に整形します。指定された要素が見つからない場合は、?を追加します。最後に、マークダウン形式のテキストをファイルに保存します。
うまくいきませんでした。ユーザー名以外にも
img
エレメントがあるようです。幸いにもユーザー名以外にはalt
属性がついていないので、alt 属性の存在のみはチェックしてください。また svg エレメントも高さと幅が が 30~80 ピクセルの範囲にあることはチェックしてください。
ユーザー名が含まれる要素がalt属性を持ち、アシスタント名が含まれる要素の幅と高さが30〜80ピクセルの範囲にあることを確認するコードを以下に記述します。
(() => { function getUserName(element) { const imgElement = element.querySelector("img"); if (imgElement && imgElement.alt) { return imgElement.alt; } return "?"; } function getAssistantName(element) { const svgElement = element.querySelector("svg"); if (svgElement) { const width = parseInt(svgElement.getAttribute("width")); const height = parseInt(svgElement.getAttribute("height")); if (width >= 30 && width <= 80 && height >= 30 && height <= 80) { return "アシスタント"; } } return "?"; } function generateMarkdown(elements) { let markdownText = ""; const lastIndex = elements.length - 1; for (let i = 0; i < lastIndex; i++) { const userName = getUserName(elements[i]); const assistantName = getAssistantName(elements[i]); const speaker = userName !== "?" ? userName : assistantName; markdownText += `### ${speaker}\n\n`; markdownText += elements[i].textContent; // 最後の要素でなければ、'---'を追加 if (i !== lastIndex - 1) { markdownText += "\n---\n"; } } return markdownText; } const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children; const markdownText = generateMarkdown(elements); const file = new Blob([markdownText], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); a.download = "chatgpt-conversation.md"; a.click(); a.remove(); })();このコードでは、getUserName関数で要素がalt属性を持っているかどうかをチェックし、getAssistantName関数で要素の幅と高さが30〜80ピクセルの範囲にあるかどうかを確認します。その後、generateMarkdown関数でマークダウン形式に整形し、ファイルに保存します。
そうでなくて img や svg エレメントを全て取得して、条件を満たしているかどうか確認する必要があります。なぜなら条件を満たしているエレメントとそうでないエレメントが含まれる可能性があるからです。
条件を満たすおよび要素をすべて取得し、それらの要素の中で条件を満たすものがあるかどうかを確認するようにコードを修正しました。
(() => { function getUserName(element) { const imgElements = element.querySelectorAll("img"); for (const imgElement of imgElements) { if (imgElement.alt) { return imgElement.alt; } } return "?"; } function getAssistantName(element) { const svgElements = element.querySelectorAll("svg"); for (const svgElement of svgElements) { const width = parseInt(svgElement.getAttribute("width")); const height = parseInt(svgElement.getAttribute("height")); if (width >= 30 && width <= 80 && height >= 30 && height <= 80) { return "アシスタント"; } } return "?"; } function generateMarkdown(elements) { let markdownText = ""; const lastIndex = elements.length - 1; for (let i = 0; i < lastIndex; i++) { const userName = getUserName(elements[i]); const assistantName = getAssistantName(elements[i]); const speaker = userName !== "?" ? userName : assistantName; markdownText += `### ${speaker}\n\n`; markdownText += elements[i].textContent; // 最後の要素でなければ、'---'を追加 if (i !== lastIndex - 1) { markdownText += "\n---\n"; } } return markdownText; } const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children; const markdownText = generateMarkdown(elements); const file = new Blob([markdownText], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); a.download = "chatgpt-conversation.md"; a.click(); a.remove(); })();このコードでは、getUserNameおよびgetAssistantName関数がquerySelectorAllを使用して、それぞれおよび要素をすべて取得します。その後、それぞれの要素に対して条件を満たすかどうかを確認し、条件を満たす要素があれば対応するユーザー名またはアシスタント名を返します。最後に、generateMarkdown関数でマークダウン形式に整形し、ファイルに保存します。
説明のために全部取得してと言いましたが、例えば img[alt] というセレクタを使えば同じことができるような気がします。また svg につても height と width を持っている物だけを選択した方が効率的に処理できるような気がします。
ご指摘の通り、img[alt]セレクタを使用してalt属性を持つ要素を取得し、[height][width]セレクタを使用してheightおよびwidth属性を持つ要素を取得することで、効率的に処理できます。以下のコードでは、そのようにセレクタを使用して要素を取得しています。
(() => { function getUserName(element) { const imgElement = element.querySelector("img[alt]"); if (imgElement) { return imgElement.alt; } return "?"; } function getAssistantName(element) { const svgElements = element.querySelectorAll("svg[height][width]"); for (const svgElement of svgElements) { const width = parseInt(svgElement.getAttribute("width")); const height = parseInt(svgElement.getAttribute("height")); if (width >= 30 && width <= 80 && height >= 30 && height <= 80) { return "アシスタント"; } } return "?"; } function generateMarkdown(elements) { let markdownText = ""; const lastIndex = elements.length - 1; for (let i = 0; i < lastIndex; i++) { const userName = getUserName(elements[i]); const assistantName = getAssistantName(elements[i]); const speaker = userName !== "?" ? userName : assistantName; markdownText += `### ${speaker}\n\n`; markdownText += elements[i].textContent; // 最後の要素でなければ、'---'を追加 if (i !== lastIndex - 1) { markdownText += "\n---\n"; } } return markdownText; } const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children; const markdownText = generateMarkdown(elements); const file = new Blob([markdownText], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); a.download = "chatgpt-conversation.md"; a.click(); a.remove(); })();このコードでは、getUserName関数でimg[alt]セレクタを使用し、getAssistantName関数でsvg[height][width]セレクタを使用しています。それぞれの関数が効率的に要素を取得し、条件を満たすかどうかを確認しています。
会話がすすむにつれ、最初にプログラムコードを処理していた部分が無くなっているようです。
申し訳ありません。プログラムコードの処理を再度含めたコードを以下に示します。
(() => { function getUserName(element) { const imgElement = element.querySelector("img[alt]"); if (imgElement) { return imgElement.alt; } return "?"; } function getAssistantName(element) { const svgElements = element.querySelectorAll("svg[height][width]"); for (const svgElement of svgElements) { const width = parseInt(svgElement.getAttribute("width")); const height = parseInt(svgElement.getAttribute("height")); if (width >= 30 && width <= 80 && height >= 30 && height <= 80) { return "アシスタント"; } } return "?"; } function processCodeBlocks(element) { const codeBlocks = element.querySelectorAll("pre > div"); for (const codeBlock of codeBlocks) { const language = codeBlock.querySelector(":first-child > span").textContent; const code = codeBlock.querySelector("code").textContent; const formattedCode = `\n\`\`\`${language}\n${code}\n\`\`\`\n`; codeBlock.parentElement.innerHTML = formattedCode; } } function generateMarkdown(elements) { let markdownText = ""; const lastIndex = elements.length - 1; for (let i = 0; i < lastIndex; i++) { processCodeBlocks(elements[i]); const userName = getUserName(elements[i]); const assistantName = getAssistantName(elements[i]); const speaker = userName !== "?" ? userName : assistantName; markdownText += `### ${speaker}\n\n`; markdownText += elements[i].textContent; // 最後の要素でなければ、'---'を追加 if (i !== lastIndex - 1) { markdownText += "\n---\n"; } } return markdownText; } const elements = document.querySelector("main > div.flex-1 > div.h-full > div > div").children; const markdownText = generateMarkdown(elements); const file = new Blob([markdownText], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); a.download = "chatgpt-conversation.md"; a.click(); a.remove(); })();processCodeBlocks関数を追加し、generateMarkdown関数内で各要素に対してコードブロックの処理を行っています。この関数は、pre > divセレクタでコードブロックを検索し、言語名とコード本体を取得して、マークダウン形式に整形します。整形されたコードは、元のコードブロックの位置に挿入されます。