こんばんは、なめほです。
chatGPTのアカウントを乗り換える必要に迫られたので、chatGPTのチャットログを出力する方法をメモします。
コードは下にあります。
1、プロジェクト内のチャットをプロジェクトから外に出します。
プロジェクト内のチャットはなんか出力できなかったです。
2、chatGPTのサイトにアクセスしてるので、開発者ツールで画像のようなものを探します。
3、cURLをbashとしてコピーする。
4、下記コードの$curlText変数にコピーしたものを代入する。
なお、コピーするときにスペースとか入るとpowershellの文字として認識がおかしくなるので、気を付けること。
5、Powershellでコードを実行する。
6、C:\ChatGPT_Historyに出力される。
なお、トークンやらがログに表示されてしまうので悪用厳禁です。
追加の注意(公開時のReadmeに一言)
このスクリプトは個人利用のエクスポート用途を想定。サービスの利用規約やレート制限を守ってください。
# ==========================================================
# ChatGPT 履歴一括エクスポート for PowerShell 5(プロジェクト非対応版)
# - DevTools の Copy-as-cURL(bash) を $curlText にそのまま貼る
# - HttpClient を使用(エラー時も本文を取得)
# - 各会話を Markdown で保存: yyyyMMdd_Title_ConversationID.md
# ==========================================================
# 出力先(必要なら変更)
$outputDir = "C:\ChatGPT_History"
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
# ---- ここに DevTools の Copy as cURL(bash) を丸ごと貼る ----
$curlText = @'
curl 'https://chatgpt.com/backend-api/conversations?offsethogehoge' \
-H 'accept: */*' \
-H 'accept-language: hogehoge' \
-H 'authorization: hogehoge' \
-b 'oai-did=hogehoge' \
-H 'dnt: hogehoge' \
-H 'oai-client-version: hogehoge' \
-H 'oai-device-id: hogehoge' \
-H 'oai-language: ja-JP' \
-H 'priority: hogehoge' \
-H 'referer: https://chatgpt.com/' \
-H 'sec-ch-ua: hogehoge' \
-H 'sec-ch-ua-mobile: ?1' \
-H 'sec-ch-ua-platform: hogehoge' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'user-agent: hogehoge' \
-H 'x-kl-kis-ajax-request: Ajax_Request'
'@
# -----------------------------------------------------------
# ----------------- helper functions -----------------------
function Parse-CurlHeadersUniversal([string]$curl) {
# 継続行・改行つぶし(bash/Windows混在対応)
$one = $curl `
-replace "\\\s*`r?`n", " " `
-replace "'\s*`r?`n", " " `
-replace '"\s*`r?`n', " "
# URL
$url = $null
$m = [regex]::Match($one, "curl\s+(['""])(?<u>https?://[^'""]+)\1")
if ($m.Success) { $url = $m.Groups["u"].Value }
# -H 'Key: Value'
$headers = [ordered]@{}
$matchesH = [regex]::Matches($one, "\s-H\s+(['""])(?<kv>.+?)\1")
foreach ($mh in $matchesH) {
$kv = $mh.Groups["kv"].Value
$idx = $kv.IndexOf(":")
if ($idx -gt 0) {
$k = $kv.Substring(0,$idx).Trim()
$v = $kv.Substring($idx+1).Trim()
if ($k) { $headers[$k] = $v }
}
}
# -b/--cookie → Cookie ヘッダへ統一
$mc = [regex]::Match($one, "(?:\s-b|\s--cookie)\s+(['""])(?<cookie>.+?)\1")
if ($mc.Success) {
$cookieVal = $mc.Groups["cookie"].Value.Trim()
if ($cookieVal -and -not $headers.Contains("Cookie")) {
$headers["Cookie"] = $cookieVal
}
}
# Authorization の大小文字ゆれを統一
foreach ($k in @("authorization","Authorization")) {
if ($headers.Contains($k)) {
$headers["Authorization"] = $headers[$k]
if ($k -ne "Authorization") { $headers.Remove($k) }
break
}
}
# base ホスト(PowerShell 5 互換:三項演算子は使わない)
$base = $null
if ($url -and ($url -match "^(https?://[^/]+)")) {
$base = $Matches[1]
}
return @{
url = $url
headers = $headers
base = $base
raw = $one
}
}
function Get-NormalizedCurlText([string]$url, [hashtable]$headers) {
if (-not $url) { $url = "https://chatgpt.com" }
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("curl '$url'")
foreach ($k in $headers.Keys) {
$v = $headers[$k]
if (-not [string]::IsNullOrWhiteSpace($v)) {
# 変数展開は $() で囲ってコロン誤解釈を回避
[void]$sb.AppendLine(" -H '$($k): $($v)'")
}
}
return $sb.ToString().TrimEnd()
}
function Convert-UnixToTokyo($sec) {
# セーフパース
$seconds = $null
if ($sec -eq $null -or "$sec".Trim() -eq "" -or "$sec" -eq "0") {
Write-Warning "timestamp is null/empty → UtcNow を使用(この件のみ日付がずれます)"
$seconds = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
} else {
if (-not [double]::TryParse("$sec", [ref]$seconds)) {
Write-Warning "timestamp is not a number → UtcNow を使用"
$seconds = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
}
}
try {
$dtoUtc = [DateTimeOffset]::FromUnixTimeSeconds([long][math]::Floor($seconds))
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
$dtoJst = [TimeZoneInfo]::ConvertTime($dtoUtc, $tz)
return $dtoJst.DateTime
} catch {
Write-Warning "時刻変換に失敗したためローカル時刻を返します: $($_.Exception.Message)"
return (Get-Date)
}
}
function Get-ConversationStart([object]$obj, [object]$conv, [array]$ordered) {
$candidates = @()
if ($obj.PSObject.Properties.Name -contains "create_time" -and $obj.create_time) { $candidates += $obj.create_time }
if ($conv -and $conv.PSObject.Properties.Name -contains "create_time" -and $conv.create_time) { $candidates += $conv.create_time }
if ($ordered) {
$msgTimes = $ordered | ForEach-Object { $_.message.create_time } | Where-Object { $_ -ne $null -and $_ -ne 0 }
if ($msgTimes) { $candidates += ($msgTimes | Measure-Object -Minimum).Minimum }
}
if ($obj.PSObject.Properties.Name -contains "update_time" -and $obj.update_time) { $candidates += $obj.update_time }
if ($conv -and $conv.PSObject.Properties.Name -contains "update_time" -and $conv.update_time) { $candidates += $conv.update_time }
$nums = @()
foreach ($c in $candidates) {
$v = $null
if ([double]::TryParse("$c", [ref]$v) -and $v -gt 0) { $nums += $v }
}
if ($nums.Count -gt 0) { return ($nums | Measure-Object -Minimum).Minimum }
Write-Warning "開始タイムスタンプが見つからないため now を使用します(この会話のみ今日の日付になります)"
return [double]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
}
function SanitizeFileName([string]$name) {
if (-not $name) { return "Untitled" }
$invalid = [IO.Path]::GetInvalidFileNameChars()
foreach ($c in $invalid) { $name = $name -replace [regex]::Escape("$c"), "_" }
$name = $name -replace "\s+", " "
$name = $name.Trim()
if ($name.Length -gt 120) { $name = $name.Substring(0,120) }
return $name
}
# HttpClient helper for GET with retries and returning full body/text
function HttpGetWithRetry($client, $url, $headersMap, $maxTries=3, $waitSec=2) {
for ($i=1; $i -le $maxTries; $i++) {
try {
$req = New-Object System.Net.Http.HttpRequestMessage([System.Net.Http.HttpMethod]::Get, $url)
# ヘッダ設定(TryAddWithoutValidation)
foreach ($k in $headersMap.Keys) {
$v = $headersMap[$k]
if ([string]::IsNullOrWhiteSpace($v)) { continue }
try { $req.Headers.TryAddWithoutValidation($k, $v) | Out-Null } catch {}
}
$resp = $client.SendAsync($req).Result
$status = [int]$resp.StatusCode
$body = $resp.Content.ReadAsStringAsync().Result
return @{ status = $status; body = $body; resp = $resp }
} catch {
Write-Warning ("試行 {0} 失敗: {1}" -f $i, $_.Exception.Message)
if ($i -lt $maxTries) { Start-Sleep -Seconds $waitSec }
}
}
throw "全試行失敗 (GET $url)"
}
function Save-Conversation($obj, $conv, $id, $outputDir) {
# ノード抽出
$nodes = $obj.mapping.PSObject.Properties.Value | Where-Object { $_.message -ne $null }
if (-not $nodes) { return }
$ordered = $nodes | Sort-Object { $_.message.create_time }
# 開始時刻
$convStart = Get-ConversationStart -obj $obj -conv $conv -ordered $ordered
$dt = Convert-UnixToTokyo $convStart
$dateForName = $dt.ToString("yyyyMMdd")
$dateForHeader = $dt.ToString("yyyy-MM-dd HH:mm:ss")
# タイトル
$title = if ($obj.title) { $obj.title } elseif ($conv.title) { $conv.title } else { "Untitled" }
$safeTitle = SanitizeFileName $title
# Markdown ヘッダ
$md = "# $title`n`n"
$md += "_Conversation ID: $id_`n`n"
$md += "_Started at: $dateForHeader_`n`n"
# 本文
foreach ($n in $ordered) {
$role = $n.message.author.role
switch -Regex ($role) {
"^user$" { $role = "User"; break }
"^assistant$" { $role = "Assistant"; break }
default { $role = $role }
}
$content = ""
if ($n.message.content -and $n.message.content.parts) {
$content = ($n.message.content.parts -join "`n")
} elseif ($n.message.content -and $n.message.content.text) {
$content = $n.message.content.text
} else {
$content = "(non-text content omitted)"
}
$tsRaw = if ($n.message.create_time) { $n.message.create_time } else { $convStart }
$tsStr = (Convert-UnixToTokyo $tsRaw).ToString("yyyy-MM-dd HH:mm:ss")
$md += "[$tsStr] **${role}:** $content`n`n"
}
# ファイル名: yyyyMMdd_Title_ConversationID.md
$fileName = "{0}_{1}_{2}.md" -f $dateForName, $safeTitle, $id
$path = Join-Path $outputDir $fileName
$md | Out-File -FilePath $path -Encoding utf8
Write-Host "保存: $path"
}
# ----------------- main flow ------------------------------
try {
$parsed = Parse-CurlHeadersUniversal -curl $curlText
$rawHeaders = $parsed.headers
$baseHost = $parsed.base
# 確認用: Cookie を -H に統一した表示
$normalizedCurl = Get-NormalizedCurlText -url $parsed.url -headers $parsed.headers
Write-Host "`n[Normalized cURL]" -ForegroundColor DarkCyan
Write-Host $normalizedCurl
if (-not $rawHeaders.Contains("Cookie")) { throw "Cookie ヘッダが見つかりません。Copy-as-cURL に -b または Cookie ヘッダが必要です。" }
if (-not $rawHeaders.Contains("User-Agent")) { throw "User-Agent ヘッダが見つかりません。" }
# HttpRequest 用ヘッダ
$headersMap = [ordered]@{}
foreach ($k in $rawHeaders.Keys) { $headersMap[$k] = $rawHeaders[$k] }
if ($rawHeaders.Contains("Authorization")) {
$headersMap["Authorization"] = $rawHeaders["Authorization"]
}
Write-Host "抽出ヘッダ(主要):" -ForegroundColor Cyan
$headersMap.GetEnumerator() | ForEach-Object { Write-Host ("{0}: {1}" -f $_.Key, $_.Value) }
# HttpClient
Add-Type -AssemblyName System.Net.Http
$handler = New-Object System.Net.Http.HttpClientHandler
$handler.UseCookies = $true
$handler.CookieContainer = New-Object System.Net.CookieContainer
$client = New-Object System.Net.Http.HttpClient($handler)
$client.Timeout = [TimeSpan]::FromSeconds(60)
if ($headersMap.Contains("User-Agent")) {
try { $client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", $headersMap["User-Agent"]) | Out-Null } catch {}
}
if ($headersMap.Contains("Accept")) {
try { $client.DefaultRequestHeaders.TryAddWithoutValidation("Accept", $headersMap["Accept"]) | Out-Null } catch {}
}
# Cookie を CookieContainer にも流し込む
if ($headersMap.Contains("Cookie")) {
$cookieString = $headersMap["Cookie"]
$uri = if ($baseHost) { [Uri]$baseHost } else { [Uri]"https://chatgpt.com" }
foreach ($pair in $cookieString -split ';') {
$p = $pair.Trim()
if (-not $p) { continue }
$kv = $p -split('=',2)
if ($kv.Length -eq 2) {
$name = $kv[0].Trim()
$val = $kv[1].Trim()
try { $handler.CookieContainer.Add($uri, (New-Object System.Net.Cookie($name,$val))) } catch {}
}
}
$headersMap["Cookie"] = $cookieString
}
# 接続テスト(1件)
$testBase = if ($baseHost) { $baseHost } else { "https://chatgpt.com" }
$testUrl = "$testBase/backend-api/conversations?offset=0&limit=1"
Write-Host "`n接続テスト: $testUrl" -ForegroundColor Yellow
$res = HttpGetWithRetry -client $client -url $testUrl -headersMap $headersMap -maxTries 3 -waitSec 2
Write-Host ("Status: {0}" -f $res.status)
# JSON判定
$isJson = $false
$tmp = $null
try { $tmp = $res.body | ConvertFrom-Json; $isJson = $true } catch {}
if (-not $isJson) {
Write-Host "`n[応答がJSONではありません。Cloudflare等のHTMLの可能性。全文出力します]" -ForegroundColor Red
Write-Host $res.body
throw "テスト応答がJSONではありません。ブラウザで通った cURL を再取得してください。"
}
Write-Host ("接続OK. test returned items? {0}" -f ($tmp.items -ne $null)) -ForegroundColor Green
# 一覧取得 → 全件
$all = @()
$offset = 0
$limit = 28
while ($true) {
$u = "$testBase/backend-api/conversations?offset=$offset&limit=$limit&order=updated&is_archived=false&is_starred=false"
$r = HttpGetWithRetry -client $client -url $u -headersMap $headersMap -maxTries 3 -waitSec 2
$j = $null
try { $j = $r.body | ConvertFrom-Json } catch {
Write-Warning "一覧取得でJSON以外の応答。status: $($r.status)"
Write-Host $r.body
throw "一覧取得がJSONで返りませんでした。"
}
if (-not $j.items -or $j.items.Count -eq 0) { break }
$all += $j.items
$offset += $limit
$tot = if ($j.PSObject.Properties.Name -contains "total") { $j.total } else { "?" }
Write-Host ("一覧取得: {0}/{1}" -f $all.Count, $tot)
}
Write-Host ("総会話件数: {0}" -f $all.Count) -ForegroundColor Cyan
# 各会話の詳細を取得して保存
foreach ($conv in $all) {
$id = $conv.id
$detailUrl = "$testBase/backend-api/conversation/$id"
Write-Host ("取得中: {0}" -f $id)
$r = HttpGetWithRetry -client $client -url $detailUrl -headersMap $headersMap -maxTries 3 -waitSec 2
$obj = $null
try { $obj = $r.body | ConvertFrom-Json } catch {
Write-Warning "会話詳細がJSONで返らずスキップ: $id(status: $($r.status))"
Write-Host $r.body
continue
}
Save-Conversation -obj $obj -conv $conv -id $id -outputDir $outputDir
}
Write-Host "`n=== 完了 ===" -ForegroundColor Green
} catch {
Write-Host "`n[致命的エラー]" -ForegroundColor Red
Write-Host $_.Exception.Message
try {
if ($_.Exception.Response) {
$sr = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$body = $sr.ReadToEnd()
Write-Host "`n[応答本文]" -ForegroundColor DarkYellow
Write-Host $body
}
} catch {}
}


