book_scraper / index.html
Lashtw's picture
Update index.html
07df01b verified
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客來書單抓取工具</title>
<!-- 載入 Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* 使用 Inter 字體 */
body {
font-family: 'Inter', sans-serif;
}
/* 讓 textarea 有基本的樣式 */
textarea {
font-family: monospace;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center p-4">
<div class="bg-white p-8 rounded-lg shadow-xl w-full max-w-4xl">
<h1 class="text-3xl font-bold text-center text-blue-600 mb-6">博客來書單抓取工具</h1>
<!-- 步驟說明 -->
<div class="mb-6 bg-blue-50 border border-blue-200 p-4 rounded-lg text-blue-700">
<h2 class="font-bold text-lg mb-2">如何使用:</h2>
<ol class="list-decimal list-inside space-y-1">
<li>在瀏覽器中打開您要抓取的博客來書籍頁面。</li>
<li>在頁面空白處按右鍵,選擇「<b>檢視網頁原始碼</b>」(View Page Source)。</li>
<li>複製「所有」的網頁原始碼 (通常是 Ctrl+A, Ctrl+C)。</li>
<li>將原始碼貼到下方的「網頁原始碼」輸入框中。</li>
<li>點擊「<b>解析並加入清單</b>」按鈕。</li>
</ol>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 左側:輸入區域 -->
<div>
<label for="sourceHtml" class="block text-sm font-medium text-gray-700 mb-2">
1. 貼入網頁原始碼 (HTML):
</label>
<textarea id="sourceHtml" rows="15" class="w-full p-3 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" placeholder="請在此貼上博客來網頁的完整原始碼..."></textarea>
<div class="mt-4 flex space-x-2">
<button id="parseButton" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 font-semibold shadow-md transition-colors">
解析並加入清單
</button>
<button id="clearSourceButton" class="w-1/3 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400 font-semibold shadow-md transition-colors">
清除
</button>
</div>
</div>
<!-- 右側:結果區域 -->
<div>
<label for="resultList" class="block text-sm font-medium text-gray-700 mb-2">
2. 您的購書清單 (CSV 格式):
</label>
<textarea id="resultList" rows="15" class="w-full p-3 border border-gray-300 rounded-md shadow-sm bg-gray-50" readonly placeholder="解析結果將會顯示在這裡..."></textarea>
<div class="mt-4 flex space-x-2">
<button id="copyButton" class="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 font-semibold shadow-md transition-colors">
複製清單
</button>
<!-- *** 新增匯出按鈕 *** -->
<button id="exportCsvButton" class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 font-semibold shadow-md transition-colors">
匯出 CSV
</button>
<button id="clearListButton" class="w-1/3 bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 font-semibold shadow-md transition-colors">
清空
</button>
</div>
</div>
</div>
<!-- 訊息提示框 -->
<div id="messageBox" class="hidden fixed top-5 right-5 p-4 rounded-md shadow-lg transition-all">
<span id="messageText"></span>
</div>
</div>
<script>
const sourceHtmlEl = document.getElementById('sourceHtml');
const parseButtonEl = document.getElementById('parseButton');
const clearSourceButtonEl = document.getElementById('clearSourceButton');
const resultListEl = document.getElementById('resultList');
const copyButtonEl = document.getElementById('copyButton');
<!-- *** 取得新按鈕 *** -->
const exportCsvButtonEl = document.getElementById('exportCsvButton');
const clearListButtonEl = document.getElementById('clearListButton');
const messageBoxEl = document.getElementById('messageBox');
const messageTextEl = document.getElementById('messageText');
// 顯示提示訊息
function showMessage(text, type = 'success') {
messageTextEl.textContent = text;
if (type === 'success') {
messageBoxEl.className = 'fixed top-5 right-5 p-4 rounded-md shadow-lg bg-green-500 text-white transition-all';
} else {
messageBoxEl.className = 'fixed top-5 right-5 p-4 rounded-md shadow-lg bg-red-500 text-white transition-all';
}
messageBoxEl.classList.remove('hidden');
setTimeout(() => {
messageBoxEl.classList.add('hidden');
}, 3000);
}
// 解析 HTML 並抓取資料
function parseHtml() {
const htmlString = sourceHtmlEl.value;
if (!htmlString) {
showMessage('原始碼為空!', 'error');
return;
}
try {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
// *** 新增:優先解析 meta description ***
const metaDescContent = doc.querySelector('meta[name="description"]')?.getAttribute('content') || '';
const metaData = {};
if (metaDescContent) {
// 使用全形逗號 ',' 分割
const parts = metaDescContent.split(',');
parts.forEach(part => {
// 使用全形冒號 ':' 分割
const pieces = part.split(':');
if (pieces.length === 2) {
const key = pieces[0].trim(); // e.g., "作者"
const value = pieces[1].trim(); // e.g., "款款"
metaData[key] = value;
}
});
}
// *** meta description 解析完畢 ***
// 1. 書名 (og:title 優先,metaData['書名'] 為備案)
const title = doc.querySelector('meta[property="og:title"]')?.getAttribute('content') || metaData['書名'] || '未找到書名';
// 輔助函數:從 li 列表中尋找特定開頭的項目
const allListItems = Array.from(doc.querySelectorAll('li'));
const findLiText = (prefix) => {
const li = allListItems.find(item => item.textContent.trim().startsWith(prefix));
return li ? li : null;
};
// 2. 作者/譯者
// *** 修正:優先從 metaData 抓取 ***
let author = metaData['作者'] || '';
let translator = metaData['譯者'] || '';
// 如果 metaData 找不到作者,才使用 li 尋找
if (!author) {
const authorLi = findLiText('作者:');
// *** 修正:改用更穩健的方式抓取作者 ***
if (authorLi) {
const authorLinks = Array.from(authorLi.querySelectorAll('a'));
if (authorLinks.length > 0) {
// 方案 A:抓取所有 <a> 標籤的文字,用 / 分隔
author = authorLinks.map(a => a.textContent.trim()).join('/');
} else {
// 方案 B:如果沒有 <a>,直接取 li 的文字並移除前綴
author = authorLi.textContent.replace('作者:', '').trim();
}
}
}
// 如果 metaData 找不到譯者,才使用 li 尋找
if (!translator) {
const translatorLi = findLiText('譯者:');
// *** 修正:改用更穩健的方式抓取譯者 ***
if (translatorLi) {
const translatorLinks = Array.from(translatorLi.querySelectorAll('a'));
if (translatorLinks.length > 0) {
translator = translatorLinks.map(a => a.textContent.trim()).join('/');
} else {
translator = translatorLi.textContent.replace('譯者:', '').trim();
}
}
}
// *** 修正點:將 const 改為 let ***
let authorTranslator = [author, translator].filter(Boolean).join('/');
if (!authorTranslator) {
authorTranslator = '未找到作者';
}
// 3. 出版社
// *** 修正:優先從 metaData 抓取 ***
let publisher = metaData['出版社'] || '';
if (!publisher) {
const publisherLi = findLiText('出版社:');
publisher = publisherLi?.querySelector('a')?.textContent.trim() || '未找到出版社';
}
// 4. ISBN
// *** 修正:優先從 metaData 抓取 ***
let isbn = metaData['ISBN'] || '';
if (!isbn) {
const isbnLi = findLiText('ISBN:');
isbn = isbnLi ? isbnLi.textContent.replace('ISBN:', '').trim() : '未找到ISBN';
}
// 5. 數量
const quantity = 1;
// 6. 定價 (*** 修正:改為優先抓取 "定價",而不是優惠價 ***)
let price = '未找到價格';
const priceLi = findLiText('定價:');
if (priceLi) {
// 抓取 li 內的文字,例如 "定價:$450" 或 "定價:450元"
price = priceLi.textContent
.replace('定價:', '') // 移除 "定價:"
.replace('$', '') // 移除 $ 符號
.replace('元', '') // 移除 "元"
.trim(); // 移除空白
} else {
// 備用方案 (如果找不到 "定價:" li):嘗試抓取 meta tag (可能是優惠價)
const metaPrice = doc.querySelector('meta[property="product:price:amount"]')?.getAttribute('content');
if (metaPrice) {
price = metaPrice;
}
// (移除了 .price_e strong b 的備用方案,因為那確定是優惠價)
}
// *** 修正點:將 ISBN 格式化以防止 Excel 轉為科學記號 ***
// 透過 `="<ISBN>"` 格式強制 Excel 將其視為文字
const excelSafeIsbn = `="${isbn}"`;
// 組合 CSV 字串
const csvRow = [title, authorTranslator, publisher, excelSafeIsbn, quantity, price].join(',');
// 加入到結果列表
const currentList = resultListEl.value;
resultListEl.value = currentList ? (currentList + '\n' + csvRow) : csvRow;
// 清空來源框
sourceHtmlEl.value = '';
showMessage('成功解析並加入!', 'success');
} catch (error) {
console.error('解析失敗:', error);
showMessage('解析失敗,請確認貼上完整的 HTML 原始碼。', 'error');
}
}
// 複製到剪貼簿
function copyToClipboard() {
const text = resultListEl.value;
if (!text) {
showMessage('清單是空的!', 'error');
return;
}
// 使用 document.execCommand (因為 navigator.clipboard 在 iframe 中可能受限)
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showMessage('已複製到剪貼簿!', 'success');
} catch (err) {
showMessage('複製失敗。', 'error');
}
document.body.removeChild(textArea);
}
// *** 新增匯出 CSV 函式 ***
function exportAsCsv() {
const text = resultListEl.value;
if (!text) {
showMessage('清單是空的!', 'error');
return;
}
// 加上 CSV 標頭
const header = "書名,作者/譯者,出版社,ISBN,數量,定價\n";
// 加上 BOM (UFEFF) 確保 Excel 正確讀取 UTF-8 (尤其是中文)
const bom = "\ufeff";
const csvContent = bom + header + text;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
// 產生下載連結
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
// 產生檔案名稱 (包含日期時間)
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '_');
link.setAttribute("download", `book_list_${timestamp}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showMessage('CSV 檔案已開始下載!', 'success');
}
// 綁定事件
parseButtonEl.addEventListener('click', parseHtml);
clearSourceButtonEl.addEventListener('click', () => {
sourceHtmlEl.value = '';
showMessage('已清除原始碼。', 'success');
});
copyButtonEl.addEventListener('click', copyToClipboard);
<!-- *** 綁定新按鈕事件 *** -->
exportCsvButtonEl.addEventListener('click', exportAsCsv);
clearListButtonEl.addEventListener('click', () => {
resultListEl.value = '';
showMessage('已清空列表。', 'success');
});
</script>
</body>
</html>