Exportar tus guardados de X (Twitter) a CSV o Markdown

Guarda todas las publicaciones que tienes en Guardados de X en una hoja de cálculo, una nota Markdown para Notion u Obsidian, o una lista de enlaces.

X movió los guardados a x.com/i/history («Guardados»). El enlace antiguo /i/bookmarks redirige allí; este script funciona en la página nueva y también en tu pestaña de «Me gusta».

export-bookmarks.js · 127 líneaspégalo en la consola (F12) de x.com
// Responsive Muse · X (Twitter) Bookmarks Exporter · v1.1 · 2026-10-22
// Saves all your X bookmarks to CSV, Markdown or TXT: date, author, @handle, text and link.
// Also works on your Likes tab. Runs only on x.com/i/history (X's new home for bookmarks and
// likes; the old x.com/i/bookmarks redirects there). X only keeps on screen the posts you can
// see, so the script collects them while it scrolls. It makes no network requests of its own
// and never reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Exportar guardados de X';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets), 'md' (Notion, Obsidian) or 'txt' (links only)
  const DELAY = 1200;          // pause between scrolls, in milliseconds

  if (!/^(x|twitter)\.com$/.test(location.hostname) || !/^\/i\/(history|bookmarks)/.test(location.pathname)) {
    alert('Abre primero tus guardados (con tu sesión iniciada):\nhttps://x.com/i/bookmarks');
    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 = '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(); },
    };
  })();
  // Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
  const deliver = (summary, body, file) => {
    const save = () => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
      a.download = file;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`${summary}\nGuardado como ${file} (mira tu carpeta de Descargas).`);
    };
    const copy = () => navigator.clipboard.writeText(body)
      .then(() => panel.text(`${summary}\nCopiado. Pégalo en una hoja de cálculo o en una nota.`))
      .catch(() => panel.text(`${summary}\nEl navegador ha bloqueado el portapapeles: usa Descargar.`));
    panel.actions(summary, [['Cerrar', false, () => panel.close()], ['Copiar', false, copy], ['Descargar', true, save]]);
  };
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const posts = new Map();

  // Read the posts currently on screen
  const collect = () => {
    for (const art of document.querySelectorAll('article[data-testid="tweet"]')) {
      const time = art.querySelector('a[href*="/status/"] time');
      const link = time?.closest('a')?.getAttribute('href') || '';
      const id = (link.match(/\/status\/(\d+)/) || [])[1];
      if (!id || posts.has(id)) continue;
      const names = [...art.querySelectorAll('[data-testid="User-Name"] span')].map(s => s.textContent.trim()).filter(Boolean);
      posts.set(id, {
        date: time.getAttribute('datetime') || '',
        author: names[0] || '',
        handle: names.find(n => n.startsWith('@')) || '@' + link.split('/')[1],
        text: (art.querySelector('[data-testid="tweetText"]')?.innerText || '').replace(/\s+\n/g, '\n').trim(),
        media: art.querySelectorAll('[data-testid="tweetPhoto"], [data-testid="videoPlayer"]').length,
        url: 'https://x.com' + link.replace(/\/(analytics|photo|video).*$/, ''),
      });
    }
  };

  // 1. Scroll to the end, collecting as we go, until nothing new appears four times in a row
  let stop = false;
  panel.stoppable(() => { stop = true; });
  window.scrollTo(0, 0);
  await sleep(DELAY);
  let idle = 0;
  while (idle < 4 && !stop) {
    const before = posts.size;
    collect();
    window.scrollBy(0, window.innerHeight * 0.8);
    await sleep(DELAY);
    idle = posts.size === before ? idle + 1 : 0;
    panel.text(`Recogiendo publicaciones… ${posts.size}`);
  }
  const list = [...posts.values()];
  if (!list.length) {
    panel.finish('No se han encontrado publicaciones. Puede que X haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }

  // 2. Build the file and offer it
  const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
  const body = FORMAT === 'txt' ? list.map(p => p.url).join('\n')
    : FORMAT === 'md' ? list.map(p => `- **${p.author}** (${p.handle}) · ${p.date.slice(0, 10)} · [enlace](${p.url})\n  ${p.text.replace(/\n/g, '\n  ')}`).join('\n')
    : ['Fecha,Autor,Usuario,Texto,Multimedia,URL',
       ...list.map(p => [p.date, p.author, p.handle, p.text, p.media, p.url].map(cell).join(','))].join('\n');
  const kind = /likes/.test(location.pathname) ? 'likes' : 'bookmarks';
  deliver(`${stop ? 'Parado antes de terminar. ' : ''}Listo: ${many(list.length, 'publicación')}.`, body, `x-${kind}.${FORMAT}`);
  return list.length;
})();

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

Por qué es seguro ejecutarloComprobación automática superada 23 oct 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 x.com cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en x.com. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 127 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: 82b003b70170c371…
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 x.com.
¿Te ha funcionado?

Cómo usarlo

  1. Abre x.com/i/bookmarks con tu sesión iniciada (te lleva a la nueva página Historial > Guardados).
  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. Elige las opciones, copia el script, pégalo en la consola y pulsa Enter. Al terminar aparece un recuadro: pulsa Descargar (o Copiar).

Por qué otros exportadores fallan

La mayoría te piden entrar con tu cuenta de X y pagar acceso a la API, y los scripts antiguos buscan una página que ya no existe. Además, X quita las publicaciones de la pantalla al desplazarte, así que un script que lee la página una vez solo consigue unas diez. Este las recoge mientras se desplaza.

Preguntas frecuentes

¿X tiene una forma de exportar los guardados?

No. El archivo de datos de X solo incluye los guardados en algunas regiones y tarda un día o más. Este script lee la página de guardados que ya ves.

¿Por qué se desplaza solo?

X solo mantiene unas diez publicaciones en pantalla y quita el resto al desplazarte, así que el script las recoge mientras baja. Deja la pestaña abierta hasta que diga «Guardado».

¿Puedo exportar también mis «me gusta»?

Sí. Abre la pestaña de «Me gusta» (x.com/i/history/likes) y ejecuta el mismo script.

Más herramientas gratis

Limpiar