從方格子打包文章 & 圖片

接著它就會開始抓文章(如下圖)

Screenshot 2025-12-04 at 7.07.16 PM.png

同時也會出現一個小框框顯示進度到哪

Screenshot 2025-12-04 at 7.05.46 PM.png

打包完成後的檔案為 xml 檔

:coding01: 文章打包程式

複製到 Console 後就可以跑了。

https://gemini.google.com/share/352a7e039a92

// --- 打包文章(只有文字)的部分 ---
async function exportToWordpress() {
    // --- 0. UI 初始化 ---
    const existingUi = document.getElementById('vocus-ui');
    if (existingUi) existingUi.remove();

    const ui = document.createElement('div');
    ui.id = 'vocus-ui';
    ui.style.cssText = 'position:fixed; top:20px; right:20px; z-index:999999; background:#222; color:#fff; padding:15px; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.3); font-family:sans-serif; min-width:300px;';
    ui.innerHTML = `
        <h3 style="margin:0 0 10px 0; color: #00bcd4;">🚀 方格子搬家工具 v9.0</h3>
        <div id="vocus-status" style="font-size: 14px; margin-bottom: 5px;">準備啟動...</div>
        <div style="background:#444; height:8px; border-radius:4px; overflow:hidden; margin-bottom:8px;">
            <div id="vocus-bar" style="width:0%; height:100%; background:#00bcd4; transition: width 0.3s;"></div>
        </div>
        <div id="vocus-log" style="font-size: 12px; color: #aaa; height: 150px; overflow-y: auto; border-top: 1px solid #444; padding-top: 5px;"></div>
    `;
    document.body.appendChild(ui);

    const updateStatus = (msg, percent) => {
        document.getElementById('vocus-status').innerText = msg;
        if (percent !== undefined) document.getElementById('vocus-bar').style.width = `${percent}%`;
    };
    const log = (msg) => {
        const div = document.getElementById('vocus-log');
        div.innerHTML = `<div style="margin-bottom:2px;">${msg}</div>` + div.innerHTML;
        console.log(msg);
    };

    // --- 1. 自動捲動邏輯 (新增) ---
    updateStatus("📜 正在自動載入所有文章...", 5);
    log("⏳ 開始自動捲動頁面...");

    let lastHeight = document.body.scrollHeight;
    let noChangeCount = 0;
    let scrollAttempts = 0;

    // 持續捲動直到高度不再變化
    while(noChangeCount < 3 && scrollAttempts < 100) { // 最多捲 100 次以防萬一
        window.scrollTo(0, document.body.scrollHeight);
        await new Promise(r => setTimeout(r, 1500)); // 等待 1.5 秒讓內容載入

        let newHeight = document.body.scrollHeight;
        let currentLinks = document.querySelectorAll('a[href*="/article/"], a[href*="/p/"]').length;
        
        if (newHeight === lastHeight) {
            noChangeCount++;
            log(`... 頁面似乎到底了 (${noChangeCount}/3)`);
        } else {
            noChangeCount = 0;
            lastHeight = newHeight;
            log(`📜 載入中... 目前偵測到約 ${currentLinks} 個連結`);
        }
        scrollAttempts++;
        updateStatus(`📜 自動捲動中... (第 ${scrollAttempts} 次)`, 10);
    }
    log("✅ 頁面載入完成!");

    // --- 輔助函式:地毯式搜尋 JSON ---
    const findContentInJson = (obj) => {
        if (!obj || typeof obj !== 'object') return null;
        if (obj._id && obj.title && typeof obj.content === 'string' && obj.content.length > 50) return obj;
        for (let key in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
                const result = findContentInJson(obj[key]);
                if (result) return result;
            }
        }
        return null;
    };

    // --- 輔助函式:從 HTML 字串直接挖內容 ---
    const extractContentFromHtml = (htmlString) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(htmlString, 'text/html');
        const selectors = ['.DraftEditor-root', 'div[class*="Editor_content"]', 'article', 'div[class*="ArticleContent"]', 'main'];
        for (let sel of selectors) {
            const el = doc.querySelector(sel);
            if (el && el.innerText.length > 100) return el.innerHTML;
        }
        return null;
    };

    // --- 2. 掃描連結 ---
    updateStatus("🔍 正在整理文章連結...", 15);
    let articleLinks = new Set();
    const links = document.querySelectorAll('a[href*="/article/"], a[href*="/p/"]'); 
    links.forEach(link => {
        let href = link.href;
        if (!href.includes('#') && !href.includes('/edit') && !href.includes('/comment')) {
            articleLinks.add(href);
        }
    });

    const targets = Array.from(articleLinks);
    if (targets.length === 0) {
        updateStatus("❌ 錯誤:找不到文章連結", 0);
        alert("找不到文章連結。");
        return;
    }
    log(`🎯 最終鎖定 ${targets.length} 篇文章`);
    
    // --- 3. 核心抓取 ---
    let allArticles = [];
    
    for (let i = 0; i < targets.length; i++) {
        const url = targets[i];
        const percent = 15 + Math.round(((i + 1) / targets.length) * 80); // 進度條從 15% 開始
        updateStatus(`正在處理 [${i+1}/${targets.length}]`, percent);

        try {
            const res = await fetch(url);
            if (!res.ok) throw new Error(res.status);
            const text = await res.text();

            let articleData = null;
            let source = "";

            const match = text.match(/<script id="__NEXT_DATA__" type="application\\/json">(.*?)<\\/script>/);
            if (match) {
                const json = JSON.parse(match[1]);
                const found = findContentInJson(json);
                if (found) {
                    articleData = found;
                    source = "JSON Deep Scan";
                }
            }

            if (!articleData || !articleData.content) {
                const htmlContent = extractContentFromHtml(text);
                if (htmlContent) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(text, 'text/html');
                    const title = doc.querySelector('h1')?.innerText || doc.title;
                    
                    articleData = {
                        _id: "html_" + Math.random().toString(36).substr(2, 9),
                        title: title,
                        content: htmlContent,
                        excerpt: "Generated from HTML scrape",
                        publishAt: new Date().toISOString(),
                        tags: []
                    };
                    source = "HTML DOM Scan";
                }
            }

            if (articleData) {
                log(`✅ [${source}] ${articleData.title.slice(0, 10)}...`);
                allArticles.push(articleData);
            } else {
                log(`⚠️ 無法解析: ${url.slice(-15)}...`);
            }

        } catch (e) {
            log(`❌ 錯誤: ${url.slice(-15)}...`);
        }
        await new Promise(r => setTimeout(r, 500 + Math.random() * 300));
    }

    if (allArticles.length === 0) {
        updateStatus("❌ 失敗:無法抓取內容", 100);
        return;
    }

    // --- 4. 生成 XML ---
    updateStatus("📦 正在打包 XML...", 100);
    
    let xmlContent = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
    xmlns:excerpt="<http://wordpress.org/export/1.2/excerpt/>"
    xmlns:content="<http://purl.org/rss/1.0/modules/content/>"
    xmlns:wfw="<http://wellformedweb.org/CommentAPI/>"
    xmlns:dc="<http://purl.org/dc/elements/1.1/>"
    xmlns:wp="<http://wordpress.org/export/1.2/>">
<channel>
    <title>Vocus Export</title>
    <description>Exported from vocus.cc</description>
    <pubDate>${new Date().toUTCString()}</pubDate>
    <generator>Vocus Exporter v9.0</generator>
    <wp:wxr_version>1.2</wp:wxr_version>`;

    for (let i = 0; i < allArticles.length; i++) {
        const fullData = allArticles[i];
        const title = (fullData.title || "Untitled")
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
            
        let content = fullData.content || "";
        content = content.replace(/<script\\b[^>]*>([\\s\\S]*?)<\\/script>/gm, "");
        content = content.replace(/]]>/g, "]]]]><![CDATA[>");
        
        const date = new Date(fullData.publishAt || new Date());
        const dateStr = date.toISOString().replace("T", " ").replace(/\\..+/, "");
        
        let tags = "";
        if (fullData.tags && Array.isArray(fullData.tags)) {
            fullData.tags.forEach(t => {
                const tagName = (typeof t === 'string' ? t : t.name) || "";
                if(tagName) tags += `<category domain="post_tag" nicename="${encodeURIComponent(tagName)}"><![CDATA[${tagName}]]></category>\\n`;
            });
        }

        xmlContent += `
    <item>
        <title>${title}</title>
        <link></link>
        <pubDate>${date.toUTCString()}</pubDate>
        <dc:creator><![CDATA[admin]]></dc:creator>
        <guid isPermaLink="false">${fullData._id}</guid>
        <description></description>
        <content:encoded><![CDATA[${content}]]></content:encoded>
        <excerpt:encoded><![CDATA[]]></excerpt:encoded>
        <wp:post_id>${i + 1000}</wp:post_id>
        <wp:post_date>${dateStr}</wp:post_date>
        <wp:post_date_gmt>${dateStr}</wp:post_date_gmt>
        <wp:comment_status>open</wp:comment_status>
        <wp:status>publish</wp:status>
        <wp:post_type>post</wp:post_type>
        ${tags}
    </item>`;
    }
    xmlContent += `</channel></rss>`;

    const blob = new Blob([xmlContent], { type: 'text/xml' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `vocus_export_v9_${new Date().toISOString().slice(0,10)}.xml`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    
    updateStatus("🎉 完成!檔案已下載", 100);
    setTimeout(() => ui.remove(), 8000);
}

exportToWordpress();

:coding01: 打包文章圖片

async function downloadImagesAsZip() {
    // --- 0. 載入 JSZip 壓縮套件 ---
    if (!window.JSZip) {
        console.log("正在載入壓縮套件...");
        await new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = "<https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js>";
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
        });
    }
    const zip = new JSZip();

    // --- 1. UI 初始化 ---
    const existingUi = document.getElementById('vocus-zip-ui');
    if (existingUi) existingUi.remove();

    const ui = document.createElement('div');
    ui.id = 'vocus-zip-ui';
    ui.style.cssText = 'position:fixed; top:20px; right:20px; z-index:999999; background:#222; color:#fff; padding:15px; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.3); font-family:sans-serif; min-width:320px;';
    ui.innerHTML = `
        <h3 style="margin:0 0 10px 0; color: #ff5722;">📦 方格子圖片打包工具 v12.0</h3>
        <div id="vocus-status" style="font-size: 14px; margin-bottom: 5px;">準備啟動...</div>
        <div style="background:#444; height:8px; border-radius:4px; overflow:hidden; margin-bottom:8px;">
            <div id="vocus-bar" style="width:0%; height:100%; background:#ff5722; transition: width 0.3s;"></div>
        </div>
        <div id="vocus-log" style="font-size: 12px; color: #aaa; height: 150px; overflow-y: auto; border-top: 1px solid #444; padding-top: 5px;"></div>
    `;
    document.body.appendChild(ui);

    const updateStatus = (msg, percent) => {
        document.getElementById('vocus-status').innerText = msg;
        if (percent !== undefined) document.getElementById('vocus-bar').style.width = `${percent}%`;
    };
    const log = (msg) => {
        const div = document.getElementById('vocus-log');
        div.innerHTML = `<div style="margin-bottom:2px;">${msg}</div>` + div.innerHTML;
        console.log(msg);
    };

    // --- 2. 自動捲動載入 ---
    updateStatus("📜 正在載入文章列表...", 5);
    log("⏳ 開始自動捲動...");

    let lastHeight = document.body.scrollHeight;
    let noChangeCount = 0;
    let scrollAttempts = 0;

    while(noChangeCount < 3 && scrollAttempts < 50) { 
        window.scrollTo(0, document.body.scrollHeight);
        await new Promise(r => setTimeout(r, 1500));
        let newHeight = document.body.scrollHeight;
        if (newHeight === lastHeight) noChangeCount++;
        else { noChangeCount = 0; lastHeight = newHeight; }
        scrollAttempts++;
        updateStatus(`📜 自動捲動中... (${scrollAttempts})`, 10);
    }
    log("✅ 列表載入完成!");

    // --- 3. 收集連結 ---
    updateStatus("🔍 正在整理連結...", 15);
    let articleLinks = new Set();
    const links = document.querySelectorAll('a[href*="/article/"], a[href*="/p/"]'); 
    links.forEach(link => {
        let href = link.href;
        if (!href.includes('#') && !href.includes('/edit') && !href.includes('/comment')) {
            articleLinks.add(href);
        }
    });

    const targets = Array.from(articleLinks);
    if (targets.length === 0) {
        alert("找不到文章連結。");
        return;
    }
    log(`🎯 鎖定 ${targets.length} 篇文章`);

    // --- 輔助:檔名處理 ---
    const cleanFilename = (text) => {
        return text.replace(/[\\\\/:*?"<>|]/g, '_').replace(/\\s+/g, ' ').trim().slice(0, 60);
    };

    // --- 輔助:下載圖片 Blob ---
    const fetchImageBlob = async (url) => {
        try {
            let finalUrl = url;
            // 嘗試解析高畫質連結
            if (url.includes('resize-image.vocus.cc') && url.includes('url=')) {
                 const match = url.match(/url=([^&]+)/);
                 if (match) finalUrl = decodeURIComponent(match[1]);
            }
            const res = await fetch(finalUrl);
            if(!res.ok) throw new Error(res.status);
            return await res.blob();
        } catch (e) {
            console.error("下載失敗", url, e);
            return null;
        }
    };

    // --- 4. 逐篇抓圖並加入 ZIP ---
    let totalImages = 0;

    for (let i = 0; i < targets.length; i++) {
        const url = targets[i];
        const percent = 15 + Math.round(((i + 1) / targets.length) * 75); // 留 10% 給壓縮
        updateStatus(`處理中 [${i+1}/${targets.length}]`, percent);

        try {
            const res = await fetch(url);
            const text = await res.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(text, 'text/html');

            // 抓標題 (作為資料夾名稱 & 備用檔名)
            let articleTitle = doc.querySelector('h1')?.innerText || `文章_${i+1}`;
            articleTitle = cleanFilename(articleTitle);
            
            // 建立該文章的資料夾
            const imgFolder = zip.folder(articleTitle);

            // 鎖定內容區塊
            let contentArea = doc.querySelector('.DraftEditor-root') || doc.querySelector('div[class*="Editor_content"]') || doc.querySelector('article');
            
            if (!contentArea) {
                log(`⚠️ 無法定位內容: ${articleTitle}`);
                continue;
            }

            const images = contentArea.querySelectorAll('img');
            let imgIndex = 0;
            let usedNames = new Set(); // 防止同篇文章檔名重複

            for (let img of images) {
                // 過濾小圖/頭像
                if (img.width > 0 && img.naturalWidth < 150) continue;
                if (img.src.includes('avatar')) continue;

                // 抓取圖說 (Caption)
                let caption = "";
                const figure = img.closest('figure');
                if (figure) {
                    const figcaption = figure.querySelector('figcaption');
                    if (figcaption) caption = figcaption.innerText;
                }
                // 檢查 alt,但排除類似 "raw-image" 這種無意義的 alt
                if (!caption && img.alt && !img.alt.includes('raw-image')) caption = img.alt;

                // 決定檔名
                let filename = "";
                if (caption && caption.trim().length > 0) {
                    // 情況 A: 有圖說 -> 用圖說命名
                    filename = cleanFilename(caption);
                } else {
                    // 情況 B: 沒圖說 -> 用文章標題 + 序號命名
                    imgIndex++;
                    filename = `${articleTitle}_${String(imgIndex).padStart(2, '0')}`;
                }

                // 防止檔名重複 (例如多張圖說明一樣)
                let finalName = filename;
                let counter = 1;
                while (usedNames.has(finalName)) {
                    finalName = `${filename}_${counter}`;
                    counter++;
                }
                usedNames.add(finalName);

                // 下載 Blob 並加入 ZIP
                const blob = await fetchImageBlob(img.src);
                if (blob) {
                    imgFolder.file(`${finalName}.jpg`, blob);
                    log(`📸 加入: ${finalName}.jpg`);
                    totalImages++;
                }
                
                // 輕微延遲
                await new Promise(r => setTimeout(r, 200));
            }

        } catch (e) {
            log(`❌ 錯誤: ${url.slice(-10)}`);
        }
        await new Promise(r => setTimeout(r, 500));
    }

    if (totalImages === 0) {
        updateStatus("❌ 沒抓到任何圖片", 100);
        return;
    }

    // --- 5. 壓縮並下載 ---
    updateStatus("📦 正在壓縮 ZIP 檔案 (請稍候)...", 95);
    log("⏳ 開始壓縮打包...");

    const zipContent = await zip.generateAsync({type:"blob"});
    const zipUrl = URL.createObjectURL(zipContent);
    
    const a = document.createElement('a');
    a.href = zipUrl;
    a.download = `vocus_images_smart_rename_${new Date().toISOString().slice(0,10)}.zip`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);

    updateStatus(`🎉 完成!已下載 ${totalImages} 張圖片`, 100);
    setTimeout(() => ui.remove(), 10000);
}

downloadImagesAsZip();