Exportar un chat de ChatGPT a PDF, Word o Markdown

Guarda cualquier conversación de ChatGPT como documento: tus preguntas y cada respuesta, con bloques de código, tablas y fórmulas. Gratis y sin extensiones.

Abre la conversación que quieras en chatgpt.com y ejecuta allí el script.

export-chatgpt.js · 204 líneaspégalo en la consola (F12) de chatgpt.com
// Responsive Muse · ChatGPT Conversation Exporter · v1.0 · 2026-10-22
// Saves the ChatGPT conversation you have open as Markdown, Word (.doc) or PDF, with code
// blocks, lists and tables kept. Runs only on chatgpt.com. ChatGPT only keeps part of a long
// chat on screen, so the script scrolls from the top and collects every message on the way.
// It makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Exportar chat de ChatGPT';
  const NAMES = { user: 'Tú', ai: 'ChatGPT' };   // headings used in the file
  const DELAY = 900;                               // pause between scrolls, in milliseconds

  if (!/^(chatgpt\.com|chat\.openai\.com)$/.test(location.hostname) || !/\/c\//.test(location.pathname)) {
    alert('Abre primero la conversación de ChatGPT que quieres exportar (chatgpt.com/c/…).');
    return;
  }
  // A small box on the page shows progress and asks before changing anything,
  // so you never need to read or edit the console.
  const many = (n, word) => `${n} ${n === 1 ? word : ({ canal: 'canales', 'canción': 'canciones', 'publicación': 'publicaciones', 'elemento guardado': 'elementos guardados' })[word] || word + 's'}`;   // 1 vídeo, 2 vídeos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = 'display:block!important;position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
      'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
      'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
    const head = document.createElement('strong');
    head.textContent = TOOL;
    const msg = document.createElement('p');
    msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
    box.append(head, msg, row);
    document.body.append(box);
    const button = (label, primary, onClick) => {
      const b = document.createElement('button');
      b.textContent = label;
      b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
        (primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
      b.onclick = onClick;
      row.append(b);
    };
    return {
      text(t) { msg.textContent = t; },
      ask(t, yes) {
        msg.textContent = t;
        row.replaceChildren();
        return new Promise(r => {
          button('Cancelar', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Parar', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Cerrar', true, () => box.remove()]]); },
      close() { box.remove(); },
    };
  })();
  // Turn a rendered message (HTML) back into Markdown: headings, lists, code, tables, links
  const toMarkdown = root => {
    const skip = el => el.matches('button, svg, script, style, [aria-hidden="true"], .sr-only, [data-markdown-copy="exclude"], ' +
      '.code-block-decoration, [class*="code-block-header"]');
    const walk = (node, depth) => {
      if (node.nodeType === 3) return node.textContent;
      if (node.nodeType !== 1 || skip(node)) return '';
      const kids = () => [...node.childNodes].map(n => walk(n, depth)).join('');
      const tag = node.tagName.toLowerCase();
      // Math (KaTeX) is on the page three times; keep only its TeX source
      if (node.classList.contains('katex')) {
        const tex = node.querySelector('annotation[encoding="application/x-tex"]')?.textContent.trim();
        if (tex) return node.closest('.katex-display') ? `\n\n$$${tex}$$\n\n` : `$${tex}$`;
      }
      if (/^h[1-6]$/.test(tag)) return `\n\n${'#'.repeat(+tag[1])} ${kids().trim()}\n\n`;
      if (tag === 'p') return `\n\n${kids().trim()}\n\n`;
      if (tag === 'br') return '\n';
      if (tag === 'hr') return '\n\n---\n\n';
      if (tag === 'strong' || tag === 'b') return `**${kids()}**`;
      if (tag === 'em' || tag === 'i') return `*${kids()}*`;
      if (tag === 'a') return `[${kids()}](${node.href})`;
      if (tag === 'img') return node.alt ? `![${node.alt}](${node.src})` : '';
      if (tag === 'pre') {
        const code = node.querySelector('code') || node;
        const lang = ((code.className || '').match(/language-([\w+-]+)/) || [])[1] || '';
        return `\n\n\`\`\`${lang}\n${code.textContent.replace(/\n$/, '')}\n\`\`\`\n\n`;
      }
      if (tag === 'code') return '`' + node.textContent + '`';
      if (tag === 'blockquote') return '\n\n' + kids().trim().split('\n').map(l => '> ' + l).join('\n') + '\n\n';
      if (tag === 'ul' || tag === 'ol') {
        const items = [...node.children].filter(li => li.tagName === 'LI').map((li, i) => {
          const body = [...li.childNodes].map(n => walk(n, depth + 1)).join('').trim().replace(/\n{2,}/g, '\n');
          return '  '.repeat(depth) + (tag === 'ol' ? `${i + 1}. ` : '- ') + body;
        });
        return '\n' + items.join('\n') + '\n';
      }
      if (tag === 'table') {
        const rows = [...node.querySelectorAll('tr')].map(tr =>
          '| ' + [...tr.children].map(c => walk(c, depth).trim().replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ') + ' |');
        if (rows.length) rows.splice(1, 0, '|' + ' --- |'.repeat(rows[0].split(' | ').length));
        return '\n\n' + rows.join('\n') + '\n\n';
      }
      return kids();
    };
    return walk(root, 0).replace(/\n{3,}/g, '\n\n').trim();
  };
  // Offer the chat as Markdown, Word or PDF (all built here, in your browser)
  const deliverChat = (title, messages, names, note = '') => {
    const date = new Date().toISOString().slice(0, 10);
    const md = `# ${title}\n\n_Exportado el ${date} desde ${location.href}_\n\n` + messages.map(m =>
      `## ${m.role === 'user' ? names.user : names.ai}\n\n${m.markdown}`).join('\n\n---\n\n') + '\n';
    const esc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    const html = '<!doctype html><html><head><meta charset="utf-8"><title>' + esc(title) + '</title><style>' +
      'body{font:15px/1.6 Georgia,serif;max-width:760px;margin:40px auto;padding:0 20px;color:#111}' +
      'h1{font:700 26px system-ui,sans-serif}h2{font:600 15px system-ui,sans-serif;text-transform:uppercase;letter-spacing:.05em;color:#555;margin-top:32px}' +
      '.msg{white-space:pre-wrap}.user{background:#f3f4f6;padding:12px 16px;border-radius:8px}' +
      'pre,code{font:13px Consolas,monospace}hr{border:0;border-top:1px solid #ddd}</style></head><body>' +
      `<h1>${esc(title)}</h1><p><em>Exportado el ${date}</em></p>` + messages.map(m =>
        `<h2>${esc(m.role === 'user' ? names.user : names.ai)}</h2><div class="msg ${m.role}">${esc(m.markdown)}</div>`).join('<hr>') +
      '</body></html>';
    const file = (title || 'chat').replace(/[\\/:*?"<>|]+/g, '_').slice(0, 80);
    const save = (body, ext, type) => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([body], { type }));
      a.download = `${file}.${ext}`;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`Guardado como ${file}.${ext} (mira tu carpeta de Descargas).`);
    };
    const pdf = () => {
      const w = window.open('', '_blank');
      if (!w) { panel.text('El navegador ha bloqueado la ventana nueva. Permite las ventanas emergentes en esta web y vuelve a probar.'); return; }
      w.document.write(html);
      w.document.close();
      w.focus();
      w.print();
      panel.text('Elige «Guardar como PDF» como impresora en la ventana que se ha abierto.');
    };
    const copy = () => navigator.clipboard.writeText(md)
      .then(() => panel.text('Copiado en Markdown. Pégalo en Notion, Obsidian o cualquier editor.'))
      .catch(() => panel.text('El navegador ha bloqueado el portapapeles: usa Markdown.'));
    panel.actions(`Listo: ${many(messages.length, 'mensaje')}.${note ? '\n' + note : ''}`, [
      ['Copiar', false, copy],
      ['Markdown', false, () => save(md, 'md', 'text/markdown;charset=utf-8')],
      ['Word', false, () => save('\ufeff' + html, 'doc', 'application/msword')],
      ['PDF', true, pdf],
    ]);
  };
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const turns = () => [...document.querySelectorAll('[data-turn-key]')];
  const first = turns()[0];
  let box = first;
  while (box && !(box.scrollHeight > box.clientHeight + 50 && /(auto|scroll)/.test(getComputedStyle(box).overflowY))) box = box.parentElement;
  if (!first || !box) {
    panel.finish('No se han encontrado mensajes. Puede que ChatGPT haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }

  // 1. Scroll to the very top until no older messages load
  let last = -1, idle = 0;
  while (idle < 3) {
    box.scrollTop = 0;
    await sleep(DELAY);
    const n = turns().length;
    idle = n === last ? idle + 1 : 0;
    last = n;
    panel.text('Cargando el principio de la conversación…');
  }

  // 2. Scroll down, collecting each turn (your message, then ChatGPT's answer) once
  const seen = new Map();
  const collect = () => {
    for (const t of turns()) {
      const key = t.getAttribute('data-turn-key');
      if (seen.has(key)) continue;
      const parts = [];
      const user = t.querySelector('[data-user-message-bubble]');
      const ai = t.querySelector('[data-markdown-text-style="assistant-message"]');
      // Images you attached (they're private and their links expire, so only note them)
      const images = [...t.querySelectorAll('img')].filter(img => !ai || !ai.contains(img)).length;
      const note = images ? `[${many(images, 'image')} attached]` : '';
      const said = [user ? user.textContent.trim() : '', note].filter(Boolean).join('\n\n');
      if (said) parts.push({ role: 'user', markdown: said });
      if (ai) parts.push({ role: 'ai', markdown: toMarkdown(ai) });
      if (parts.length) seen.set(key, parts);
    }
  };
  idle = 0;
  while (idle < 3) {
    const before = seen.size;
    collect();
    const atEnd = box.scrollTop + box.clientHeight >= box.scrollHeight - 5;
    box.scrollTop += box.clientHeight * 0.8;
    await sleep(DELAY);
    idle = seen.size === before && atEnd ? idle + 1 : 0;
    panel.text(`Leyendo mensajes… ${many([...seen.values()].flat().length, 'mensaje')}`);
  }
  const messages = [...seen.values()].flat();
  if (!messages.length) {
    panel.finish('No se han encontrado mensajes. Puede que ChatGPT haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }
  deliverChat(document.title.replace(/\s*[-|]\s*ChatGPT$/, '') || 'Conversación de ChatGPT', messages, NAMES);
  return messages.length;
})();

🛡️ ¿Chrome te dice «No pegues código…»? Es normal. Escribe allow pasting en la consola (o permitir pegar si tu Chrome está en español), pulsa Intro y vuelve a pegar. Solo hace falta una vez.

¿Por qué avisa Chrome?

Chrome lo muestra la primera vez que alguien pega algo en la consola, para proteger de estafas en las que te piden pegar código desconocido. Hace bien en avisar, y por eso este script está entero a la vista: 204 líneas comentadas que no envían nada a ningún sitio ni tocan tus contraseñas o cookies. Por qué es seguro.

¿Prefieres no usar la consola? Arrastra el botón Exportar chat de ChatGPT a tu barra de marcadores y haz clic en él estando en chatgpt.com. Así no sale el aviso.

Marcador: arrastra el botón del medio a tu barra de marcadores, abre chatgpt.com y pulsa el marcador. Ejecuta el mismo script con las opciones de arriba.

Por qué es seguro ejecutarloComprobación automática superada 9 nov 2026 · probado en una cuenta real el 22 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de chatgpt.com cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en chatgpt.com. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 204 líneas comentadas, sin minificar ni ofuscar.
  • Muestra su progreso en la página. Un pequeño recuadro te dice qué está haciendo y al final te ofrece Descargar o Copiar.
  • El mismo código para todos. SHA-256 de la versión por defecto: 5c4cf53c7ea2562e…
Compruébalo tú mismo en 30 segundos
  1. Léelo: todas las líneas están arriba, con comentarios.
  2. Busca en el código fetch, XMLHttpRequest, cookie, localStorage o eval. No aparece ninguna.
  3. Abre la pestaña Red (Network) antes de ejecutarlo: solo verás peticiones a chatgpt.com.
¿Te ha funcionado?

Cómo usarlo

  1. Abre la conversación en chatgpt.com.
  2. Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe permitir pegar (o allow pasting si lo tienes en inglés) y pulsa Enter.
  3. Copia el script, pégalo en la consola y pulsa Enter. Aparece un recuadro: pulsa Markdown, Word o PDF (o Copiar para pegarlo en Notion u Obsidian).

Mejor que copiar y pegar

Al copiar a mano se pierden los bloques de código, las tablas se desmontan y las fórmulas salen triplicadas. El script convierte cada respuesta a Markdown limpio y, desde ahí, a Word o PDF.

Preguntas frecuentes

¿No basta con la exportación de ChatGPT?

La opción «Exportar datos» de ChatGPT te manda toda la cuenta en un zip por email, en JSON y HTML sin formato. Esto guarda solo la conversación que estás leyendo, como documento limpio, en segundos.

¿Se conservan el código, las tablas y las fórmulas?

Sí. El código mantiene su lenguaje, las tablas pasan a tablas Markdown y las fórmulas se exportan en TeX ($…$), que Notion, Obsidian y la mayoría de editores Markdown muestran como fórmulas.

¿Cómo saco un PDF?

Pulsa PDF en el recuadro: se abre una vista de impresión limpia. Elige «Guardar como PDF» como impresora. No se sube nada a ningún sitio.

¿Funciona con chats muy largos?

Sí. La web solo mantiene en pantalla parte de un chat largo, así que el script se desplaza desde el principio y recoge cada mensaje por el camino. Deja la pestaña abierta hasta que el recuadro muestre los botones.

¿Y las imágenes que envié?

Se indican como «[N images attached]». Sus enlaces son privados y caducan, así que no se copian en el archivo.

Más herramientas gratis

Exportar y hacer copia