// ==UserScript== // @name 腾讯文档全能导出助手 v3.2.1 // @namespace https://github.com/Mortalwangxin // @version 3.2.1 // @author 忘心 // @description 腾讯文档,一键提取文档,无视无法导出、不可复制 // @match *://docs.qq.com/sheet/* // @icon https://docs.qq.com/favicon.ico // ==/UserScript== (function () { 'use strict'; const CONFIG = { BUTTON_TEXT: '📤 导出数据', SCROLL_STEP_PX: 600, SCROLL_INTERVAL_MS: 60, WAIT_AFTER_SCROLL_MS: 2500, MAX_SCROLL_ROUNDS: 5, PROGRESS_LOG_INTERVAL: 200, MAX_RETRIES: 80, IFRAME_LOAD_TIMEOUT: 20000, // iframe 最长等 20s IFRAME_POLL_INTERVAL: 500, // 每 500ms 检查一次 }; function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function log(msg) { GM_log(`[导出助手] ${msg}`); } function excelDateToISO(v) { if (!v || isNaN(v) || v < 1) return ''; const d = new Date((v - 25569) * 86400000); if (isNaN(d.getTime())) return ''; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } // ========================= 单元格文本提取 ========================= function getCellText(cell) { if (!cell) return ''; let value = typeof cell.getValue === 'function' ? cell.getValue() : null; if (value === null || value === undefined) return ''; if (typeof value === 'number') { if (value > 40000 && value < 50000) return excelDateToISO(value); return String(value); } if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'; if (typeof value === 'string') return value; if (typeof value === 'object') { if (Array.isArray(value.r)) { const parts = value.r.filter(seg => seg && typeof seg.t === 'string' && seg.t).map(seg => seg.t); if (parts.length > 0) return parts.join(''); } if (value.formulaResult && value.formulaResult.value !== null && value.formulaResult.value !== undefined) { const fr = value.formulaResult.value; return typeof fr === 'number' ? (fr > 40000 && fr < 50000 ? excelDateToISO(fr) : String(fr)) : String(fr); } if (Array.isArray(value.richText)) { return value.richText.map(seg => typeof seg === 'string' ? seg : (seg.text || seg.t || seg.content || '')).join(''); } if (typeof value.text === 'string' && value.text) return value.text; if (value.value !== null && value.value !== undefined) { return typeof value.value === 'object' ? getCellText({ getValue: () => value.value }) : String(value.value); } if (value.url) return value.text || value.displayText || value.url; for (const key of ['displayText', 'label', 'title', 'name', 'result']) { if (value[key]) return String(value[key]); } try { const s = JSON.stringify(value); if (s && s !== '{}' && s !== '[]') return s; } catch (e) { /* ignore */ } } return String(value); } function findScrollContainer(doc) { const allDivs = doc.querySelectorAll('div'); let best = null, bestArea = 0; for (const div of allDivs) { if (div.scrollHeight > div.clientHeight + 200 && div.clientHeight > 100) { const area = div.clientWidth * div.clientHeight; if (area > bestArea) { bestArea = area; best = div; } } } return best; } async function scrollToBottomInIframe(iframeWin) { const doc = iframeWin.document; const container = findScrollContainer(doc); if (!container) { iframeWin.scrollTo(0, doc.body.scrollHeight); await sleep(CONFIG.WAIT_AFTER_SCROLL_MS); return; } let prevH = 0; for (let round = 0; round < CONFIG.MAX_SCROLL_ROUNDS; round++) { const target = container.scrollHeight; for (let pos = container.scrollTop; pos < target; pos += CONFIG.SCROLL_STEP_PX) { container.scrollTop = pos; await sleep(CONFIG.SCROLL_INTERVAL_MS); } container.scrollTop = container.scrollHeight; await sleep(CONFIG.WAIT_AFTER_SCROLL_MS); const newH = container.scrollHeight; if (newH <= prevH + 10) break; prevH = newH; } } async function extractSheetViaIframe(sheetId, sheetName) { log(` [iframe] 加载「${sheetName}」(id=${sheetId})...`); const baseUrl = location.origin + location.pathname; const url = `${baseUrl}?tab=${sheetId}`; const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;width:1920px;height:1080px;border:none;'; iframe.src = url; document.body.appendChild(iframe); try { await new Promise((resolve, reject) => { iframe.onload = resolve; iframe.onerror = reject; setTimeout(() => reject(new Error('iframe 加载超时')), CONFIG.IFRAME_LOAD_TIMEOUT); }); const iframeWin = iframe.contentWindow; const iframeDoc = iframeWin.document; let retries = 0; while (!iframeWin.SpreadsheetApp?.workbook?.worksheetManager) { await sleep(500); if (++retries > CONFIG.MAX_RETRIES) { throw new Error('iframe 内 SpreadsheetApp 未就绪'); } } const wm = iframeWin.SpreadsheetApp.workbook.worksheetManager; retries = 0; while (wm.getSheetList().length === 0) { await sleep(300); if (++retries > 60) { // 最多等 18s throw new Error('iframe 内 sheet 列表始终为空'); } } log(` [iframe] sheet 列表就绪 (${wm.getSheetList().length} 个), 等待 ${retries} 次`); const sheet = wm.getSheetBySheetId(sheetId); if (!sheet) throw new Error(`iframe 内未找到 sheet: ${sheetId}`); retries = 0; while (!sheet.getIsInitialized()) { await sleep(CONFIG.IFRAME_POLL_INTERVAL); if (++retries > 40) { // 最多等 20s throw new Error(`「${sheetName}」未初始化`); } } log(` [iframe] 「${sheetName}」已初始化,开始提取...`); await scrollToBottomInIframe(iframeWin); await sleep(800); const totalRows = sheet.getRowCount(); const totalCols = sheet.getColCount(); log(` [iframe] 「${sheetName}」声明行数: ${totalRows}, 列数: ${totalCols}`); const rowsData = []; let lastNonEmptyCol = 0; let emptyStreak = 0; for (let r = 0; r < totalRows; r++) { const row = []; let hasData = false; for (let c = 0; c < totalCols; c++) { let text = ''; try { const cell = sheet.getCellDataAtPosition(r, c); text = getCellText(cell); } catch (e) { /* skip */ } row.push(text); if (text) { hasData = true; if (c > lastNonEmptyCol) lastNonEmptyCol = c; } } if (hasData) { rowsData.push(row); emptyStreak = 0; } else { emptyStreak++; } if (emptyStreak >= 500) { log(` [iframe] 连续 ${emptyStreak} 行为空,提前结束`); break; } if ((r + 1) % CONFIG.PROGRESS_LOG_INTERVAL === 0) { log(` [iframe] 进度: ${r + 1}/${totalRows}, 有效: ${rowsData.length}`); } } const trimmed = rowsData.map(row => row.slice(0, lastNonEmptyCol + 1)); log(` [iframe] 「${sheetName}」最终: ${trimmed.length} 行, ${lastNonEmptyCol + 1} 列`); return trimmed; } finally { iframe.remove(); } } function cleanCellText(text) { if (!text || typeof text !== 'string') return text; if (text.startsWith('{') && text.includes('"r"')) { try { const obj = JSON.parse(text); if (Array.isArray(obj.r)) { const parts = obj.r.map(seg => seg.t || '').filter(Boolean); if (parts.length > 0) return parts.join(''); } } catch (e) { /* not valid JSON */ } } return text; } function downloadCSV(data, filename) { if (!data || data.length === 0) { log(`⚠️ ${filename}: 无数据`); return; } const cleaned = data.map(row => row.map(cleanCellText)); const csv = cleaned.map(row => row.map(cell => { const s = String(cell || '').replace(/"/g, '""').replace(/[\r\n]+/g, ' '); return /[,"\n\r]/.test(s) ? `"${s}"` : s; }).join(',') ).join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename.endsWith('.csv') ? filename : filename + '.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); log(`✅ 已下载: ${a.download}`); } async function runExport() { log('====== 导出助手 v3.2 启动 ======'); // 等待当前页 SpreadsheetApp(用于枚举 sheet 列表) let retries = 0; while (typeof SpreadsheetApp === 'undefined' || !SpreadsheetApp.workbook) { await sleep(500); if (++retries > CONFIG.MAX_RETRIES) { return; } } log('SpreadsheetApp 已就绪'); const wm = SpreadsheetApp.workbook.worksheetManager; const sheetsList = wm.getSheetList() .map(s => ({ id: s.getSheetId(), name: s.getSheetName(), state: typeof s.getSheetState === 'function' ? s.getSheetState() : 1, })) .filter(s => { if (s.state !== 1) { log(`跳过隐藏表: ${s.name} (state=${s.state})`); return false; } return true; }); log(`工作表 (${sheetsList.length}): ${sheetsList.map(s => s.name).join(', ')}`); if (!sheetsList.length) { alert('未找到工作表'); return; } // 用户选择 let choice = 'all'; if (sheetsList.length > 1) { const list = sheetsList.map((s, i) => `${i + 1}. ${s.name}`).join('\n'); choice = prompt(`选择导出的工作表:\n${list}\n\n输入 "all" 全部,或 "1,3" 指定`, 'all'); if (choice === null) return; } const indices = choice.toLowerCase() === 'all' ? sheetsList.map((_, i) => i) : choice.split(',').map(p => { const i = parseInt(p.trim(), 10) - 1; if (isNaN(i) || i < 0 || i >= sheetsList.length) { alert(`无效编号: ${p}`); return -1; } return i; }).filter(i => i >= 0); if (!indices.length) return; const results = []; for (let i = 0; i < indices.length; i++) { const sheet = sheetsList[indices[i]]; log(`\n[${i + 1}/${indices.length}] 处理: ${sheet.name}`); try { const data = await extractSheetViaIframe(sheet.id, sheet.name); if (data.length > 0) { downloadCSV(data, `${sheet.name}_腾讯文档导出`); results.push({ name: sheet.name, rows: data.length, ok: true }); } else { log(`⚠️ ${sheet.name}: 提取到 0 行数据`); results.push({ name: sheet.name, rows: 0, ok: false }); } } catch (e) { log(`❌ ${sheet.name} 导出失败: ${e.message}`); results.push({ name: sheet.name, rows: 0, ok: false, error: e.message }); } if (i < indices.length - 1) await sleep(1500); } const summary = results.map(r => `${r.ok ? '✅' : '❌'} ${r.name}: ${r.rows} 行${r.error ? ' (' + r.error + ')' : ''}` ).join('\n'); log('\n====== 导出完成 ======\n' + summary); alert('导出完成:\n' + summary); } // ========================= UI ========================= function createUI() { if (document.getElementById('td-exporter-btn')) return; const btn = document.createElement('button'); btn.id = 'td-exporter-btn'; btn.textContent = CONFIG.BUTTON_TEXT; Object.assign(btn.style, { position: 'fixed', top: '80px', right: '20px', zIndex: '99999', padding: '10px 20px', background: 'linear-gradient(135deg, #1890ff, #096dd9)', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: 'bold', boxShadow: '0 2px 12px rgba(24,144,255,0.4)', transition: 'all 0.2s', userSelect: 'none', }); btn.onmouseover = () => { btn.style.transform = 'translateY(-2px)'; }; btn.onmouseout = () => { btn.style.transform = ''; }; btn.onclick = async () => { btn.disabled = true; btn.textContent = '⏳ 导出中...'; btn.style.opacity = '0.7'; try { await runExport(); } catch (e) { log('导出异常: ' + e.message); alert('导出异常: ' + e.message); } finally { btn.disabled = false; btn.textContent = CONFIG.BUTTON_TEXT; btn.style.opacity = '1'; } }; document.body.appendChild(btn); log('UI 已创建'); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', createUI); else createUI(); })();