iT邦幫忙

0

從零打造輔大課表神器:Chrome Extension 開發實戰 30 天 - Day 20

  • 分享至 

  • xImage
  •  

Day 20:基礎資料提取架構設計

🎯 系列目標:用 30 天時間,從零開始打造一個專屬輔大學生的課表生成 Chrome 擴充功能

💻 作者:輔大智慧資安 412580084

📅 Day 20:基礎資料提取架構設計

🔗 課程系列回顧

經過前 19 天的學習,我們已經建立了完整的 Chrome 擴充功能基礎架構。今天開始進入資料處理的核心階段,我們要設計一個強健的資料提取架構。

📋 學習目標

今天我們要完成:

  1. 🏗️ 設計模組化的資料提取架構
  2. 📊 建立資料驗證機制
  3. 🔧 實作基礎工具函數
  4. 🧪 測試架構的穩定性

🏗️ 資料提取架構設計

1.1 架構設計原則

我們的資料提取架構需要滿足以下原則:

  • 模組化:每個功能獨立,易於維護
  • 容錯性:能處理各種異常情況
  • 可擴展:支援未來功能擴展
  • 效能優化:提升爬取速度和準確性

1.2 核心架構組件

這裡我們定義了一個 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}`);
    }
  }
}

1.3 基礎工具函數

專門處理 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);
    });
  }
}

資料驗證機制

2.1 學生資訊驗證器

// 學生資訊驗證器
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;
}

2.2 課程資料驗證器

// 課程資料驗證器
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% 的課程資料有效
}

提取器實作

3.1 學生資訊提取器

用來檢查學生資訊的完整性與正確性。它會確認必填欄位(系級、學號、姓名、總學分)是否存在

// 學生資訊提取器
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
  };
}

3.2 課程資料提取器

檢驗課程清單的結構是否正確,並計算課程資料的有效率。若課程名稱、上課時間或基本時間資訊(星期、節次)缺失,就會視為不完整資料。

// 課程資料提取器
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;
}

🧪 整合測試

4.1 建立完整的提取器實例

// 建立並配置資料提取器
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;
  }
}

4.2 在控制台中測試

要測試這個架構,請在我們在content.js的尾端添加測試功能使其執行:

testDataExtraction().then(results => {
  console.log('🎉 測試完成!', results);
}).catch(error => {
  console.error('❌ 測試失敗:', error);
});

然後我們在輔大學生入口網 點擊 選課清單 後並按下 F12 可以看到以下:
0

可以發現已經將課程資料提取,並做出結構化處理。


🎯 明日預告

今天我們建立了基礎資料提取架構設計,明天我們要處理基礎的時間轉換系統


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言