Command + Option + I)。allow pasting 並按 Enter 允許貼上。接著它就會開始抓文章(如下圖)

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

打包完成後的檔案為 xml 檔
複製到 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
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();
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();