🎯 系列目標:用 30 天時間,從零開始打造一個專屬輔大學生的課表生成 Chrome 擴充功能
💻 作者:輔大智慧資安 412580084
📅 Day 20:基礎資料提取架構設計
經過前 19 天的學習,我們已經建立了完整的 Chrome 擴充功能基礎架構。今天開始進入資料處理的核心階段,我們要設計一個強健的資料提取架構。
今天我們要完成:
我們的資料提取架構需要滿足以下原則:
這裡我們定義了一個 DataExtractor
類別,它是整個資料提取架構的核心。這個類別的設計目的在於 模組化管理提取器、驗證器和格式器,讓程式在執行不同類型資料提取時能保持一致流程。透過 extract()
方法,會依序完成「提取 → 驗證 → 格式化 → 回傳」的完整流程,並在過程中加入容錯機制與日誌輸出,方便偵錯與維護。
// 資料提取架構核心類別
class DataExtractor {
constructor() {
this.validators = new Map();
this.extractors = new Map();
this.formatters = new Map();
this.debugMode = true;
}
// 註冊資料驗證器
registerValidator(type, validator) {
this.validators.set(type, validator);
this.log(`📋 已註冊驗證器: ${type}`);
}
// 註冊資料提取器
registerExtractor(type, extractor) {
this.extractors.set(type, extractor);
this.log(`🔧 已註冊提取器: ${type}`);
}
// 註冊資料格式器
registerFormatter(type, formatter) {
this.formatters.set(type, formatter);
this.log(`🎨 已註冊格式器: ${type}`);
}
// 執行完整的資料提取流程
async extract(type, element = document) {
try {
this.log(`🚀 開始提取 ${type} 資料`);
// 步驟 1:檢查提取器是否存在
const extractor = this.extractors.get(type);
if (!extractor) {
throw new Error(`未找到 ${type} 的提取器`);
}
// 步驟 2:執行資料提取
const rawData = await extractor(element);
this.log(`📊 ${type} 原始資料提取完成`);
// 步驟 3:資料驗證
const validator = this.validators.get(type);
if (validator) {
const isValid = validator(rawData);
if (!isValid) {
this.log(`⚠️ ${type} 資料驗證失敗,使用容錯機制`);
}
}
// 步驟 4:資料格式化
const formatter = this.formatters.get(type);
const formattedData = formatter ? formatter(rawData) : rawData;
this.log(`✅ ${type} 資料提取成功`);
return {
success: true,
data: formattedData,
type: type,
timestamp: new Date().toISOString()
};
} catch (error) {
this.log(`❌ ${type} 資料提取失敗: ${error.message}`);
return {
success: false,
error: error.message,
type: type,
timestamp: new Date().toISOString()
};
}
}
// 日誌輸出
log(message) {
if (this.debugMode) {
console.log(`[DataExtractor] ${message}`);
}
}
}
專門處理 DOM 操作的安全方法,可以避免直接呼叫 querySelector 或存取 DOM 元素時可能發生的錯誤,並且為了以後安全查詢、批量查詢、文字提取可以更方便。
// DOM 查詢工具集
class DOMUtils {
// 安全的元素查詢
static safeQuery(selector, context = document) {
try {
return context.querySelector(selector);
} catch (error) {
console.warn(`查詢失敗: ${selector}`, error);
return null;
}
}
// 安全的文字內容提取
static safeTextContent(selector, context = document, defaultValue = '') {
const element = this.safeQuery(selector, context);
return element ? element.textContent.trim() : defaultValue;
}
// 批量查詢元素
static safeQueryAll(selector, context = document) {
try {
return Array.from(context.querySelectorAll(selector));
} catch (error) {
console.warn(`批量查詢失敗: ${selector}`, error);
return [];
}
}
// 等待元素出現
static waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = this.safeQuery(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver((mutations) => {
const element = this.safeQuery(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`元素 ${selector} 等待超時`));
}, timeout);
});
}
}
// 學生資訊驗證器
function validateStudentInfo(data) {
const required = ['department', 'studentId', 'name', 'totalCredits'];
for (const field of required) {
if (!data[field] || data[field] === '未找到系級' ||
data[field] === '未找到學號' || data[field] === '未找到姓名' ||
data[field] === '未找到學分') {
console.warn(`學生資訊驗證失敗: ${field} 欄位無效`);
return false;
}
}
// 學號格式檢查(9位數字)
if (!/^\d{9}$/.test(data.studentId)) {
console.warn('學號格式不正確');
return false;
}
console.log('學生資訊驗證通過');
return true;
}
// 課程資料驗證器
function validateCourseData(courses) {
if (!Array.isArray(courses) || courses.length === 0) {
console.warn('課程清單格式錯誤或為空');
return false;
}
let validCourses = 0;
courses.forEach((course, index) => {
if (course.課程名稱 && course.上課時間 && course.上課時間.length > 0) {
const timeInfo = course.上課時間[0];
if (timeInfo.星期 && timeInfo.節次) {
validCourses++;
} else {
console.warn(`課程 ${index + 1} 時間資訊不完整`);
}
} else {
console.warn(`課程 ${index + 1} 基本資訊不完整`);
}
});
const validRate = validCourses / courses.length;
console.log(`課程資料有效率: ${(validRate * 100).toFixed(1)}%`);
return validRate >= 0.8; // 至少 80% 的課程資料有效
}
用來檢查學生資訊的完整性與正確性。它會確認必填欄位(系級、學號、姓名、總學分)是否存在
// 學生資訊提取器
function extractStudentInfo(context = document) {
console.log('開始提取學生資訊');
const studentInfo = {
department: DOMUtils.safeTextContent('#LabDptno1', context, '未找到系級'),
studentId: DOMUtils.safeTextContent('#LabStuno1', context, '未找到學號'),
name: DOMUtils.safeTextContent('#LabStucna1', context, '未找到姓名'),
totalCredits: DOMUtils.safeTextContent('#LabTotNum1', context, '未找到學分')
};
// 學期資訊提取
const semesterSelect = DOMUtils.safeQuery('#DDL_YM', context);
const semester = semesterSelect ? semesterSelect.value : '未知學期';
console.log('📋 學生資訊提取完成:', studentInfo);
return {
...studentInfo,
semester: semester
};
}
檢驗課程清單的結構是否正確,並計算課程資料的有效率。若課程名稱、上課時間或基本時間資訊(星期、節次)缺失,就會視為不完整資料。
// 課程資料提取器
function extractCourseData(context = document) {
console.log('📚 開始提取課程資料');
const courses = [];
const courseTable = DOMUtils.safeQuery('#GV_NewSellist', context);
if (!courseTable) {
console.warn('未找到課程表格');
return courses;
}
const rows = DOMUtils.safeQueryAll('tr:not(:first-child)', courseTable);
console.log(`找到 ${rows.length} 行課程資料`);
rows.forEach((row, index) => {
try {
const cells = DOMUtils.safeQueryAll('td', row);
if (cells.length >= 17) {
const courseName = cells[7]?.textContent?.trim();
const weekday = cells[14]?.textContent?.trim();
const timeInfo = cells[16]?.textContent?.trim();
const classroom = cells[17]?.textContent?.trim();
if (courseName && weekday && timeInfo) {
courses.push({
課程名稱: courseName,
上課時間: [{
星期: weekday,
節次: timeInfo,
教室: classroom || '未指定教室'
}],
原始索引: index
});
}
}
} catch (error) {
console.warn(`處理第 ${index + 1} 行課程時出錯:`, error);
}
});
console.log(`課程資料提取完成,共 ${courses.length} 門課程`);
return courses;
}
// 建立並配置資料提取器
function createDataExtractor() {
const extractor = new DataExtractor();
// 註冊提取器
extractor.registerExtractor('studentInfo', extractStudentInfo);
extractor.registerExtractor('courseData', extractCourseData);
// 註冊驗證器
extractor.registerValidator('studentInfo', validateStudentInfo);
extractor.registerValidator('courseData', validateCourseData);
// 註冊格式器(暫時使用原始資料)
extractor.registerFormatter('studentInfo', data => data);
extractor.registerFormatter('courseData', data => data);
return extractor;
}
// 測試函數
async function testDataExtraction() {
console.log('🧪 開始測試資料提取架構');
const extractor = createDataExtractor();
try {
// 測試學生資訊提取
const studentResult = await extractor.extract('studentInfo');
console.log('📊 學生資訊測試結果:', studentResult);
// 測試課程資料提取
const courseResult = await extractor.extract('courseData');
console.log('📚 課程資料測試結果:', courseResult);
console.log('✅ 資料提取架構測試完成');
return { studentResult, courseResult };
} catch (error) {
console.error('❌ 測試過程中發生錯誤:', error);
throw error;
}
}
要測試這個架構,請在我們在content.js的尾端添加測試功能使其執行:
testDataExtraction().then(results => {
console.log('🎉 測試完成!', results);
}).catch(error => {
console.error('❌ 測試失敗:', error);
});
然後我們在輔大學生入口網 點擊 選課清單
後並按下 F12
可以看到以下:
可以發現已經將課程資料提取,並做出結構化處理。
今天我們建立了基礎資料提取架構設計,明天我們要處理基礎的時間轉換系統