한국어 | English
ChatGPT / Gemini / Claude Takeout(데이터 내보내기)을 옵시디언 호환 마크다운으로 변환하는
통합 파이프라인. 각 벤더의 원본 export 형식이 완전히 달라서 파싱 로직은 벤더별로 분리돼
있지만, 코드펜스 안전장치·frontmatter 조립·callout 포맷 같은 공통 로직은 common/에
하나로 모아서 공유한다.
-
벤더가 준 원본 zip을 압축을 풀지 않고 그대로 아래 위치에 넣는다 (이미 풀어서 넣어도 동작함 — 아래 "실제 벤더 export가 어떻게 생겼는지" 참고).
data/ ├── chatgpt/ # ChatGPT의 Data export zip을 그대로, 또는 압축을 푼 내용 ├── gemini/ # Google Takeout zip을 그대로, 또는 압축을 푼 내용 └── claude/ # Claude(Anthropic)의 데이터 내보내기 zip을 그대로, 또는 압축을 푼 내용run.py가 각 벤더 폴더에서 필요한 파일을 못 찾으면 그 폴더 안의*.zip을 자동으로 그 자리에 풀어본 뒤 다시 찾는다 (common/zip_extract.py). Google Takeout처럼 여러 파트 zip으로 쪼개져 있으면 전부 같은 폴더에 넣으면 된다 — 파트별로 순서 상관없이 풀려서 자연스럽게 합쳐진다. 원본 zip 파일은 지우지 않는다.실제 벤더 export가 어떻게 생겼는지 (압축 풀었을 때 기준):
- ChatGPT ("설정 → 데이터 제어 → 내보내기"로 받는 zip): 보통 폴더로 안 감싸져
있고
conversations.json,chat.html,file_*.dat등이 압축 최상위에 바로 나온다. 압축 해제 도구에 따라 폴더 하나로 한 번 더 감싸일 수도 있는데, 그 경우도 재귀 탐색으로 찾으므로 상관없다. - Gemini (Google Takeout에서 "Gemini 앱"만 선택해서 받는 zip): 항상
Takeout/<서비스명>/처럼 한 겹 이상 감싸져 있고, 그 안에내 활동.html과 첨부 미디어 파일들이 나란히 들어있다.Takeout/폴더째로data/gemini/에 넣으면 된다 (하위 폴더를 직접 뒤져서 꺼낼 필요 없음). - Claude ("설정 → 계정 → 데이터 내보내기"로 받는 zip): 압축 최상위에
conversations.json(프로젝트에 안 묶인 일반 대화 전체가 배열 하나로),design_chats/(프로젝트에 묶인 대화가 파일 하나당 하나씩),projects/(프로젝트 메타데이터, 있는 경우만) 등이 나온다. 계정 데이터가 많으면data-...-batch-0000.zip처럼 여러 파트로 나뉠 수 있는데, 현재는 파트 하나만 지원한다 — 여러 파트를 같은 폴더에 풀면conversations.json/design_chats/가 파트끼리 서로 덮어써서 일부 대화가 누락될 수 있으므로, 파트가 여러 개라면 각 파트를 따로 처리해야 한다.
- ChatGPT ("설정 → 데이터 제어 → 내보내기"로 받는 zip): 보통 폴더로 안 감싸져
있고
-
실행한다.
python run.py
data/아래 존재가 감지되는 벤더만 자동으로 골라 실행한다. 특정 벤더만 실행하려면--vendor chatgpt,--vendor gemini,--vendor claude중 하나를 지정한다.--dry-run을 붙이면 실제 파일을 만들지 않고 파싱 결과(세션 수, 스킵 수, 첨부파일 해석 성공/실패 수)만 콘솔에 출력한다.원본을
data/<vendor>/로 옮기고 싶지 않으면(예: 다운로드 폴더에 있는 zip을 그대로 쓰고 싶을 때)--input으로 위치를 직접 지정할 수 있다 — 코드 어디에도 실제 경로가 박혀있지 않고 매 실행마다 원하는 곳을 가리킬 수 있다:python run.py --vendor gemini --input "gemini=C:\Users\me\Downloads\takeout.zip"폴더를 넘기면 그 폴더를 그대로 원본으로 쓰고(아무것도 복사/이동 안 함),
.zip파일을 넘기면 원본은 그대로 둔 채 내용만data/<vendor>/에 풀어서 쓴다. -
결과는
result/<vendor>/*.md(+result/<vendor>/Attachments/)에 생성된다. Claude는 예외적으로 프로젝트에 묶인 대화만result/claude/<프로젝트명>/*.md처럼 프로젝트별 하위 폴더에 생성되고, 프로젝트에 안 묶인 일반 대화는 다른 벤더와 동일하게result/claude/*.md에 바로 생성된다. -
검토가 끝났으면
--publish로 실제 옵시디언 vault에 반영한다 (아래 "설정" 참고). 변환(2번)과 vault 반영(4번)을 분리해둔 이유는, 실제 PKM 저장소에 파일을 쓰는 건 되돌리기 까다로운 작업이라result/를 먼저 검토할 수 있게 하기 위함이다.python run.py --publish
세 가지 경로 — takeout 원본 위치, 마크다운 변환 결과 위치, 실제 옵시디언 vault 위치 —
를 config.json으로 관리한다. 처음 실행하면 프로젝트 루트에 기본값으로 자동 생성된다
(구조는 config.example.json 참고). 개인 경로가 들어가는 파일이라 .gitignore 대상이다.
{
"takeout_paths": { "chatgpt": "", "gemini": "", "claude": "" },
"markdown_output_dir": "result",
"obsidian_vault_dir": "",
"vault_subdirs": { "chatgpt": "ChatGPT", "gemini": "Gemini", "claude": "Claude" }
}우선순위: CLI 플래그 > config.json > 내장 기본값. 아무것도 안 정하면 기본값(원본은
data/<vendor>/, 변환 결과는 result/, vault는 미설정)으로 동작한다. 매번 다른 위치를
쓰고 싶으면 CLI로 그 실행만 오버라이드하고, 계속 같은 위치를 쓰고 싶으면 config.json을
직접 고치면 된다.
| 경로 | config.json 키 | CLI 오버라이드 | 기본값 |
|---|---|---|---|
| takeout 원본 | takeout_paths.<vendor> |
--input VENDOR=PATH |
data/<vendor>/ |
| 마크다운 변환 결과 | markdown_output_dir |
--output-dir PATH |
result/ |
| 옵시디언 vault | obsidian_vault_dir |
--vault-dir PATH (--publish와 함께) |
미설정(발행 안 함) |
--publish로 vault에 반영할 때는 vault_subdirs에 설정된 이름으로 벤더별 하위 폴더가
자동 생성된다(<vault>/ChatGPT/, <vault>/Gemini/, <vault>/Claude/). 단순 미러링이라
vault_dir/<vendor_subdir>/<filename>(Claude는 프로젝트 대화의 경우
vault_dir/<vendor_subdir>/<프로젝트명>/<filename>) 위치만 기준으로 upsert한다 —
사용자가 vault 안에서 노트를 다른 폴더로 옮기거나 이름을 바꿔도 추적하지 않으므로, 그
세션 내용이 나중에 바뀌면 옮긴 자리가 아니라 원래 위치에 새로 하나가 다시 생길 수 있다.
같은 이유로 Claude 프로젝트 이름이 나중에 바뀌면 새 하위 폴더에 파일이 다시 생기고
예전 폴더에는 고아 파일이 남을 수 있다.
0: 정상 완료.1: 실행된 벤더가 하나도 없음 (data/<vendor>/에 아무것도 없음).2: 일부 벤더가 부분적으로만 성공함 (예:conversations-*.json중 하나가 깨져서 파싱 실패) — 콘솔의[경고]/⚠️로그를 확인해야 한다. 자동화 스크립트에서 이 파이프라인을 호출한다면 반드시 종료 코드를 검사할 것.
세션(대화) 1개당 마크다운 노트 1개. frontmatter에 title/session_id/url/date/
turns_count/content_hash/tags를 담고, 본문은 > [!question]- User (...) /
> [!tip]- <Vendor> (...) 콜아웃으로 turn을 나열한다. 이미지·파일 첨부는 Attachments/로
복사되고 가능하면 ![[...]]로 임베드된다.
각 콜아웃 바로 앞에는 <!-- turn: {"turn_index": 0, "role": "user", "parent_turn_index": null, "has_attachment": false} --> 형태의 HTML 주석이 붙는다. Obsidian 미리보기에는 안
보이지만, RAG 청킹 파이프라인이 콜아웃 문법([!question] vs [!tip])이나 "다음 질문
직전까지" 같은 순서 휴리스틱 없이 바로 QA 쌍·세션 경계·첨부 맥락을 읽어갈 수 있다.
parent_turn_index: 그 답변이 어느 질문(turn_index)에 대한 것인지. 질문 턴은 항상null(새 turn window의 시작). 같은 질문에 답변이 여러 턴으로 나뉘어도(실제로 발생함 — 긴 응답이 메시지 여러 개로 쪼개지는 경우) 전부 같은parent_turn_index를 가리킨다.has_attachment: 그 턴 바로 뒤에 첨부파일 블록이 붙는지 여부.
파일명은 title이 아니라 session_id 기준이다 — Gemini는 ChatGPT와 달리 대화별
title을 제공하지 않기 때문이다 (설계 이유는 위키의
Output Format 참고).
Claude 관련 범위 밖 항목: memories.json(기억 기능 요약), login_history.json(로그인
이력), users.json(계정 정보), projects/*.json의 docs 필드(프로젝트 지식 파일)는
대화가 아니므로 변환하지 않는다. 이 export에는 첨부파일 실 바이트가 전혀 들어있지 않아서
(참조 파일명만 있음) 첨부파일은 항상 "누락" 안내 텍스트로만 표시된다.
런타임 파이프라인 자체는 표준 라이브러리만 사용한다 (Python 3.10+). 외부 패키지 설치
불필요. 테스트를 돌리려면 pip install -r requirements-dev.txt (pytest만 추가됨).
common/ # 벤더들이 공유하는 로직
├── markdown_safety.py # 코드펜스 안전장치
├── text.py # first_sentence / yaml_quote / sanitize_filename / format_callout
├── session_markdown.py # frontmatter + callout 마크다운 조립, content_hash 계산/추출
├── attachment_cache.py # 첨부파일 리졸버 공통 뼈대 (캐싱, dry-run 복사, 집계)
├── attachment_types.py # 첨부파일 확장자 분류 (임베드 가능 여부 등, 벤더 공통)
├── zip_extract.py # data/<vendor>/의 *.zip을 그 자리에 압축 해제 (zip slip 방어 포함)
├── fs_discovery.py # __MACOSX 등 압축 도구 쓰레기 경로 필터링, 후보 모호성 처리
├── upsert.py # content_hash 비교 기반 upsert 쓰기 (result/용)
├── publish.py # result/ → 실제 vault 미러링 (--publish용, upsert 재사용,
│ # result_dir 하위 폴더까지 재귀 미러링)
└── config.py # config.json 로더 (없으면 기본값으로 생성)
vendors/
├── base.py # 벤더 모듈 인터페이스 계약(Protocol) + 런타임 검증 + 자동 탐색
├── chatgpt.py # conversations*.json 트리 파싱 + .dat 첨부파일 복원
├── gemini.py # "내 활동.html" 블록 파싱 + 로컬 첨부파일 매칭
└── claude.py # conversations.json(일반 대화) + design_chats/*.json(프로젝트
# 소속 에이전틱 대화, 별도 스키마) 파싱
run.py # CLI: config 로딩 + 경로 우선순위 해석 + 벤더 실행 + 발행
config.example.json # config.json 구조 예시 (실제 config.json은 .gitignore 대상)
tests/ # pytest — common/ 순수 함수 + 벤더 파싱 로직(트리 브랜치 선택,
# KST 파싱 등) + config/publish 유닛 테스트
pip install -r requirements-dev.txt
pytesttests/는 unit/regression/integration/smoke 4개 계층으로 나뉘어 있고,
pytest -m smoke처럼 계층별로 따로 돌릴 수 있다. 계층별 구성과 테스트 격리 방식은
위키의 Development 참고.
실제 대용량 takeout 데이터를 이용한 전체 파이프라인 검증(기존 결과물과의 byte-diff)은 자동 테스트에 포함하지 않았다 — 개인 데이터라 커밋할 수 없기 때문에, 회귀가 의심될 때 수동으로 재실행해서 비교한다.