小技巧 - 從動態網頁抓取資料

 緯育的 Java 養成班是一門將近五個月的長期課程,它們機構以網頁系統的方式提供課表給學生查詢。但是,我已經很習慣用自己的 Google 行事曆了,規劃生活行程都只想用它,不想另外上一個網站查課表。那麼,有辦法將網頁上的資訊以迅速有效的方式存下來,方便我後續整理、甚至移轉到行事曆上嗎?嘿,有的,而且其實並不難。這篇就來介紹我是怎麼做的。

作法

你可以從上圖看到,課表排成月曆的形式呈現,所以右鍵複製下來也難以處理。Google 試算表內建有 IMPORTHTML() 和 IMPORTXML() 兩種用來抓取資料的函式,但是我覺得它們沒那麼好用,發揮空間不大。因此,我最後選擇:直接取用網頁呼叫後端 API 所回傳的課表資料。

現在大多數網站都有豐富的動態互動,我指的不是畫面裡的視覺元素是動態、或是動畫,而是指在整個網頁沒有重新載入的情況下,頁面呈現的內容可隨著你的操作而變化。以緯育的課表系統而言,就是按左上角的箭頭改變月份,底下的課表隨之更新。實現這樣效果的方式有很多,有簡單的、或比較複雜的。這個課表系統就是簡單的方式。

取得資料

首先,打開你任一種瀏覽器的開發人員工具,或是在畫面上找個地方按右鍵選 檢閱 進入。移動到 網路 (Networking) 這頁面,這裡就是顯示網頁載入後執行、或收到的大大小小的程式或資料。左邊區塊裡可能有非常多檔案,但通常我們的注意力先放在副檔名為 .json 或 .js 的項目即可,因為現在網頁前後端的主流資料傳輸格式都是 JSON。更進一步,我們已經知道我們要找的資料是課表,所以我們可以用跟課表、行程相關的英文關鍵字來在過濾 URL 的欄位搜尋,不用一個一個檢查。如上圖,我們可以看到 schedule-list 的某一節點內容和網頁裡 12/5 的行程十分一致,因此,可以大膽認定這就是網頁向後端查詢 12 月份的課表,所得到的 json 回傳。

然而,JSON 的結構畢竟不是用來給人閱讀的形式,我們還要再將整份資料轉化成大家習慣的表格形式,方便運用。這一步我們可以自己寫 apps script,也可以使用網路上現成的 JSON 轉換工具。

轉換資料

範例裡用的是 ConvertCSV,把 json 轉成常見的 csv 檔,便可再匯入 Google 試算表或 Excel。你會發現顯示轉換結果的表格裡有很多空欄位,這是該工具處理原本 json 裡的 null (空值) 的方式,不想要這些空欄位的話,後續刪除即可。大部分遇到的的資料整理跟編修需求,其實都可以在試算表軟體裡用內建功能或函式完成,我想只有當資料量非常大時才會一定需要寫程式。例如,Java 課程有少數的課會有兩名老師,這個在轉換 csv 結果裡就會是用兩欄呈現,一個欄位裡只有一名老師。在試算表裡你就可以用 JOIN() 將他們合併成一個字串。又或者,抓到的資料裡時間欄位是 Unix Timestamp,一個代表秒數 / 毫秒數的多位數數字,也可以用簡單的公式轉換成年月日時分秒

最後附上我當初寫 Google Apps Script 的作法。JSON 內容後來應該有變,所以會發現程式碼裡有些地方和上面的現在版本對不起來,但這不影響原理。這個腳本雖然是用來處理緯育的課表 json,但面對其他 json 資料只要改一下鍵 (key) 的名稱跟一些細節即可,畢竟檔案結構都大同小異。

以 Apps Script 將 json 存入 Sheets


/**
 * 主程式
 */
function addToSpreadsheet() {
  const ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TGA105_schedule"); // 存到試算表檔案裡,符合該名稱的分頁
  const output = converToSheetData(tibame_23_Mar.data.scheduleList); // 一次輸入一個月份的資料
  const range = ws.getRange(ws.getLastRow() + 1, 1, output.length, 14);
  range.setValues(output);
}

/**
 * 將整月的資料轉成二維陣列,範圍寫入 setValues() 所需
 */
function converToSheetData(data) {
  const arr = [];

  for (let i = 0; i < data.length; i++) {
    if (data[i].eventAbbreviation != "國定假日") { // 排除課名是國定假日的項目
      arr.push(convert(data[i])); // 轉換步驟
    }
  }

  arr.sort(function (a, b) { return a[0] - b[0] }); // 按日期排序
  return arr;
}

/** 
 * 將一節課的資料轉成一列
 */
function convert(item) {
  const d = new Date(item.date); // 資料裡的 date 為 Unix Timestamp
  const dateString = `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`; // 日期格式: 2022/5/12
  return [item.date,
  item.scheduleUid,
    dateString,
  item.interval,
  item.classRoomUid,
  item.eventUid,
  item.perHours,
  item.classRoomID,
  item.location,
  item.eventName,
  item.eventAbbreviation,
  item.classID,
  item.hours,
  list(item.teacherList)];
}

/**
 * 針對老師清單部分合併
 */
function list(teacherList) {
  if(teacherList.length == 0) {
    return "N/A";
  } else {
    return teacherList.map(t => t.name)
                      .join(", ");
  }
}

留言

這個網誌中的熱門文章

那天