Exportar comentarios de YouTube a Excel (gratis, sin API)

Descarga los comentarios de cualquier vídeo de YouTube en una hoja de cálculo: autor, comentario, me gusta, número de respuestas, cuándo se publicó, si está fijado o tiene el corazón del creador y un enlace a cada uno. Sin clave de API, sin registro y sin extensiones.

Abre el vídeo (no hace falta iniciar sesión), elige «Más destacados» o «Más recientes» encima de los comentarios si quieres y ejecuta el script.

export-comments.js · 146 líneaspégalo en la consola (F12) de www.youtube.com
// Responsive Muse · YouTube Comments Exporter · v1.0 · 2026-10-23
// Saves the comments of any YouTube video to CSV or TXT: author, text, likes, number of replies,
// when it was posted, pinned / hearted by the creator and a link to each comment. Uses the
// order you picked under the video (Top or Newest). YouTube loads more comments while the
// script scrolls. It makes no network requests of its own and never reads cookies,
// passwords or tokens.
(async () => {
  const TOOL = 'Exportar comentarios de YouTube';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets) or 'txt' (author and comment, one block each)
  const LIMIT = 1000;          // stop after this many comments (0 = all; big videos have millions)

  const video = new URLSearchParams(location.search).get('v') || (location.pathname.match(/^\/shorts\/([\w-]{11})/) || [])[1];
  if (location.hostname !== 'www.youtube.com' || !video) {
    alert('Abre primero el vídeo de YouTube, por ejemplo:\nhttps://www.youtube.com/watch?v=jNQXAC9IVRw');
    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 comments = new Map();
  const clean = s => (s || '').replace(/\s+/g, ' ').trim();

  // Read the comments loaded so far (top-level threads; replies are counted, not exported)
  const collect = () => {
    for (const thread of document.querySelectorAll('ytd-comment-thread-renderer')) {
      const c = thread.querySelector('#comment, ytd-comment-view-model, ytd-comment-renderer');
      if (!c) continue;
      const when = c.querySelector('#published-time-text a') || c.querySelector('#published-time-text');
      const id = ((when?.getAttribute('href') || '').match(/[?&]lc=([\w.-]+)/) || [])[1]
        || clean(c.querySelector('#author-text')?.textContent) + '|' + clean(c.querySelector('#content-text')?.textContent).slice(0, 80);
      if (comments.has(id)) continue;
      const replies = thread.querySelector('#more-replies button, #more-replies')?.textContent || '';
      comments.set(id, {
        n: comments.size + 1,
        author: clean(c.querySelector('#author-text')?.textContent),
        text: (c.querySelector('#content-text')?.textContent || '').trim(),
        likes: clean(c.querySelector('#vote-count-middle')?.textContent),
        replies: ((clean(replies).match(/[\d.,]+\s*[KkMm]?/) || [''])[0]).trim(),
        posted: clean(when?.textContent),
        pinned: c.querySelector('ytd-pinned-comment-badge-renderer, #pinned-comment-badge > *') ? 'sí' : '',
        hearted: c.querySelector('#creator-heart #hearted, ytd-creator-heart-renderer #hearted, #creator-heart-button [aria-pressed="true"]') ? 'sí' : '',
        url: id.includes('|') ? '' : `https://www.youtube.com/watch?v=${video}&lc=${id}`,
      });
      if (LIMIT && comments.size >= LIMIT) return;
    }
  };

  // 1. Scroll down in short steps (YouTube draws comments as they come into view), collecting as
  //    we go, until the limit or nothing new appears for about 20 seconds (if YouTube stalls,
  //    scrolling up and down again makes it resume)
  let stop = false;
  panel.stoppable(() => { stop = true; });
  document.querySelector('video')?.pause();   // otherwise autoplay can jump to the next video mid-export
  window.scrollTo(0, 600);   // the comments only start loading once they come into view
  await sleep(2000);
  let idle = 0;
  while (idle < 32 && !stop && !(LIMIT && comments.size >= LIMIT)) {
    const before = comments.size;
    if (idle && idle % 8 === 0) {
      window.scrollBy(0, -2500);
      await sleep(800);
    }
    window.scrollBy(0, 1500);
    await sleep(300);
    const more = document.querySelector('ytd-comments ytd-continuation-item-renderer');
    if (more && more.getBoundingClientRect().top < innerHeight * 2) more.scrollIntoView({ block: 'end' });
    await sleep(300);
    collect();
    idle = comments.size === before ? idle + 1 : 0;
    panel.text(`Leyendo comentarios… ${comments.size}${LIMIT ? ' de un máximo de ' + LIMIT : ''}`);
  }
  const list = [...comments.values()];
  if (!list.length) {
    panel.finish('No se han encontrado comentarios. ¿Están desactivados en este vídeo? Si no, puede que YouTube 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(c => `${c.author} · ${c.posted} · ${c.likes || 0} me gusta\n${c.text}`).join('\n\n')
    : ['#,Autor,Comentario,Me gusta,Respuestas,Publicado,Fijado,Corazón del creador,URL', ...list.map(c =>
        [c.n, c.author, c.text, c.likes, c.replies, c.posted, c.pinned, c.hearted, c.url].map(cell).join(','))].join('\n');
  const title = clean(document.querySelector('h1.ytd-watch-metadata, #title h1')?.textContent) || video;
  const name = ('comentarios-' + title).slice(0, 80).replace(/[\\/:*?"<>|]+/g, '_');
  deliver(`${stop ? 'Parado antes de terminar. ' : ''}Listo: ${many(list.length, 'comentario')}${LIMIT && list.length >= LIMIT ? ' (límite alcanzado)' : ''}.`, body, `${name}.${FORMAT}`);
  return list.length;
})();

Marcador: arrastra el botón del medio a tu barra de marcadores, abre www.youtube.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 23 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de www.youtube.com cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en www.youtube.com. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 146 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: f546548dcbad7d8a…
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 www.youtube.com.
¿Te ha funcionado?

Cómo usarlo

  1. Abre el vídeo en youtube.com. No hace falta iniciar sesión. Si quieres los más recientes en lugar de los destacados, elige Más recientes en el menú Ordenar por, encima de los comentarios.
  2. Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe permitir pegar y pulsa Enter.
  3. Copia el script, pégalo en la consola y pulsa Enter. Pausa el vídeo, recorre los comentarios y te ofrece el archivo.

Qué obtienes

Una fila por comentario con el @usuario del autor, el texto completo, los me gusta y respuestas tal como los muestra YouTube (4,8 M, 986), hace cuánto se publicó, si está fijado o tiene el corazón del creador y un enlace directo que abre el vídeo en ese comentario. Útil para investigar, hacer sorteos, gestionar la comunidad o guardar la conversación de tus propios vídeos.

¿Por qué no un exportador online?

Casi todos necesitan una clave de la API de YouTube o que inicies sesión, y muchos limitan la versión gratis a unos cientos de comentarios. Este script lee los comentarios ya cargados en tu propio navegador, así que no hay cuotas ni nada que conectar.

Preguntas frecuentes

¿Necesita una clave de API o mi cuenta de Google?

No. Los exportadores de comentarios suelen pedir una clave de la API de YouTube o que conectes tu cuenta. Este script lee los comentarios que YouTube ya te muestra, en cualquier vídeo, con sesión o sin ella.

¿Cuántos comentarios puede exportar?

Todos los que cargue YouTube, unos 500 por minuto. Los vídeos populares tienen millones, así que por defecto para en 1.000: sube el límite o pon 0 para todos.

¿Incluye las respuestas?

Cada comentario lleva su número de respuestas. Las respuestas en sí no se exportan en esta versión.

¿Puedo exportar los más recientes en lugar de los destacados?

Sí. Elige «Más recientes» en el menú Ordenar por, encima de los comentarios, antes de ejecutar el script; respeta el orden que ves.

Más herramientas para YouTube