iT邦幫忙

9

發布一個月破萬使用者:我做 LINE 機器人幫同學找大學科系!

  • 分享至 

  • xImage
  •  

筆者今年高三,所以前陣子在翻找大學申請簡章、整理志願標準跟面試日期,覺得翻紙本找好麻煩,我就在今年寒假做一個大學科系查詢 LINE 機器人,沒想到公開不到一個月就有一萬位使用者,因此寫這篇文章分享我用的語法、資源、遇到的問題與解法,給板上想開發類似聊天機器人的朋友參考。

banner

我的部落格原文網址

這篇文章的篇幅較長,我主要會分為想出點子的「 想法層面 」,和實際寫 code 的「 技術層面 」,如果你是單純好奇我怎麼想到這點子,或是怎麼抓到同學痛點、設計出破萬使用者的機器人,可以參考第一、第二段。

倘若你想直接看深層技術的開發過程( 包含怎麼寫程式碼 ),可以從第三段開始。當然也可以整篇都慢慢看完啦 XD 那我會很感謝你的閱讀!

LINE Bot 使用 Demo

大家看一下大學科系查詢 LINE 機器人的操作成果,只要傳送指定大學科系關鍵字,機器人會回傳該科系的資訊,以及校系分則連結( 裡面有學測標準等資料 ):
大學科系查詢小幫手 LINE Bot 截圖

點擊加入行事曆,會轉到 Google 行事曆,面試日期、標題資訊都套好了,只需要按儲存:
大學科系查詢小幫手行事曆建立截圖

點選收藏的話,機器人會紀錄起來,傳送「顯示收藏」即可查看之前收藏的科系:
 LINE 大學科系查詢小幫手截圖

前面加繁星兩個字,則可以查看繁星標準與轉系規定:
LINE 大學科系查詢小幫手截圖

是不是很酷?有需要的話,歡迎點選 LINE Bot 連結加入好友,或是輸入 ID : @958pgmas ( 輸入時需包含 @ )下文我會解說我製作這機器人的過程。


設計動機:想幫同學更有效率找科系

會聯想到用 LINE 找大學科系資訊、製作聊天機器人,主要有 3 個原因:

  • 翻紙本簡章不方便
  • 避免選到二階面試日期重疊的科系
  • 想省去另外整理目標科系的時間

設計動機

想走個人申請的高三學生都會買本幾百頁的個申簡章,包含筆者也有買,去年十二月我翻簡章時就覺得很麻煩,而且輔導老師都會提醒選志願時,要避免挑到面試日期重疊的科系,更重要的是我想節省手動剩下彙整目標科系的過程。

我就一直在思考:有沒有辦法能讓同學找科系更方便?

後來我想起我曾經做過一個線上課程預約的 LINE 群組機器人,那就再製作個聊天機器人,讓大家在熟悉的 LINE 上找大學科系資訊!


設計機器人的重點功能

在 2021 一月學測考完、開始放寒假後,我就著手設計大學科系查詢 LINE 機器人的 side project,第一步我先發想這機器人要有哪些功能?我從之前發現到的 3 個問題( 動機 )來著手:

  • 省去翻簡章的不便:就讓同學傳要搜尋的科系,機器人回傳該科系的資訊
  • 避免面試日期重疊:幫同學設定面試行事曆
  • 節省整理科系時間:設計收藏科系功能,同學無需另外整理

在設計機器人的功能時,我以「 解決當初觀察到的問題 」為準則,思考有什麼科技方法或工具能增加效率、減少麻煩?能否以自動化的程式取代以往得手動做的事情?

例如解決二階日期重疊問題時,我第一個念頭是用電腦、手機上的行事曆設定活動、來看有沒有重疊。

我在寫程式或教學文章時,常會思考有哪些操作對人們來說是個問題?是否有相對應的程式碼或手段能幫忙?

我不是從零開始製作一個工具出來,而是利用環境現有的資源,串接成能解決大家問題的模組成品而已。

設計架構與使用流程

計畫好重點功能後,我接著設計機器人的架構和搜尋流程,本段我會說明當時設計架構的思路,和現在我想到的替代方案。

架構設計:Google 全家桶

做查詢資料的 LINE 機器人,需要準備 2 個部分:

  • 雲端主機:負責執行機器人的程式碼
  • 資料庫:儲存要回傳給使用者的大學科系資料

而我在做這個 side project 之前,就有計劃找班上同學一起來參與,我負責程式部分,其他同學幫我彙整科系資料,讓同學也能透過我的專案累積他們的備審資料( 作爲回饋,我也教他們寫基礎程式 XD )

因此當時我選用 Google 試算表當作小型資料庫,因為它是最普遍、學生都會用的線上文書服務,而且要教同學寫 SQL 編輯資料庫會來不及。

使用google服務來做LINE機器人

語法部分我則選擇用 Google 開發的 App Script,它是基於 Javasript 的程式語言,內嵌許多 Google 服務 API,其中就包含讀寫 Google 文件、試算表的功能。

App Script 也跟 Google 文件一樣能在線上 IDE 編寫、執行,我就不用另外設定金鑰、串接到自己的 server,結果發現架構變成完全 Serverless 的 Google 全家桶了 XD

不過如果很要求效能的話,用真正的 SQL Database 和 GO, Java 語法去做會更好,App Script 本身執行速度,與 Google 試算表的讀寫速度表現並不是很快。

我這裡為了前期編輯的便利性而犧牲了一些效能,但後文我會說明我怎麼改善執行效能的。

搜尋流程

搞定架構設計後,我繼續規劃搜尋流程的部分,意指推演同學用 LINE 機器人查詢科系的操作步驟,我盡量保持簡單:

  1. 使用者傳送大學科系關鍵字
  2. 機器人判斷使用者要找哪個大學科系後,從試算表抓出相對應的科系資料並回傳
  3. 使用者點選搜尋結果的按鈕,看校系分則或建立行事曆

搜尋流程


LINE 機器人的開發紀錄

OK 準備進到寫 code 的環節了!本段會說明我怎麼把前面發想的功能,以程式碼的方式實踐出來,以及一些我寫程式時的思路等等紀錄。

程式開發對我而言算是興趣而已,不是電神、職業級的那種水準,還請各位前輩讀者指教!

前期科系資料彙整

前期我原本是寫 Python 爬蟲去抓大學甄選入學委員會的校系分則結果,之後發現網路上已經有知名補習業者《 甄戰 》彙整好的資料,所以我的科系 data 取自這兩個來源,不是手動翻紙本收錄,同學彙整資料的同時,我就處理程式的部分。

1 分析大學跟科系關鍵字( 包含簡稱跟縮寫 )

首先對我而言,最困難的就是分析使用者的關鍵字了,因爲每個科系都有它的縮寫與簡稱,像是資訊工程學系,我們普通都講資工;企業管理學系則是企管 ⋯⋯ 要預測使用者的科系搜尋意圖真的很麻煩。

這就是我請同學來幫忙的主要原因,我請他們針對每個科系去聯想可能的縮寫,並輸入到 Google 試算表裡,真的是「 人工 」智慧分析出來的。

下圖是資料表的截圖,A 欄位是同學輸入的預測關鍵詞:
google試算表資料庫

我預設同學是要搜尋某某大學的某某系,因此訊息一定包含「大學」跟「科系」兩個部分,設計的分析流程如下:

  1. 分析使用者要查哪間大學,跟哪個科系
  2. 先檢查該大學有沒有要搜尋的科系,有的話就回傳資料

那怎麼判斷關鍵字訊息裡哪部分是大學、哪部分是科系呢?我那時用了簡單粗暴的方法,以「 大 」或「 大學 」做分水嶺,前面的是大學名稱,後面就當科系名稱,開啟該大學的工作表,接著比對關鍵字欄位裡有沒有匹配的字詞。

例如台大資工,台大( 大字之前的訊息 )就當作大學,後面的資工就是科系,機器人開啟臺灣大學的工作表,回傳資工那行的科系資訊。

App Script 程式碼實作如下,我忘記留最初用 include 比對的版本,所以先放最新版的:

// 搜尋指定大學工作表裡的科系資訊
function search_department(code, department) {

  try {
    
    let school_sheet = SpreadSheet.getSheetByName(code); // 工作表皆以各大學英文代號命名,機器人會先分析大學名稱取得英文代號
    let lastrow = school_sheet.getLastRow();
    
    var info_array = [], status = ""; // info_array 負責存該大學所有科系
    var data_array = school_sheet.getRange(1, 1, lastrow, 1).getValues();
    data_array = data_array.flat();
    
    var position_array = []; // 用來儲存科系在工作表的行數

    var search_data = full_search(department, data_array); // full_search 是另外寫的模糊搜尋 function,分別投入關鍵字跟要搜尋的陣列
    for (var x = 0; x < search_data.length; x++) {
      if (search_data[x].score <= 0.25) { // score 越小,匹配度越高,我這裡設定 0.25
        position_array.push(search_data[x].refIndex + 1); // 存入匹配科系的行數,行數等於陣列 index 值 + 1
      }
    }

    extensive_search(); // 如果陣列長度為零( 完全沒有匹配的科系 ),增加 score 分數( 降低匹配門檻 )

    let time = position_array.length;
    
    // if time = 0, 此 loop 不執行,status = ""
    for (var x = 0; x < time; x++) {
      let target_row = position_array[x];
      status = "got"; 

      let department_info_array = school_sheet.getRange(target_row, 2, 1, 7).getValues();

      info_array = info_array.concat(department_info_array.flat());
      info_array.push(target_row);
    }
  }
  catch (e) {
    if (code == "not found") {
      status = "school not found";
    }
    else {
      console.log("error" + e);
    }
  }

  if (status == "got") {
    console.log("ok");
    console.log("info" + info_array);
    return info_array;
  }

  else if (status == "school not found") { // 沒有大學部分的搜尋結果
    return "school not found";
  }

  else { // 沒有科系的搜尋結果
    return "department not found";
  }

  function extensive_search() {
    if (position_array.length == 0) {
      for (var x = 0; x < search_data.length; x++) {
        if (search_data[x].score <= 0.5) {
          console.log(search_data[x])
          position_array.push(search_data[x].refIndex + 1);
          note_message = "未找到完全相符的科系,已啟用廣泛搜尋模式,下列結果可能較不準,請確認輸入的科系名稱是否正確或避免簡寫";
        }
      }
    }
  }
}

那這樣會有個問題,假如使用者沒傳「大」字呢?像東海法律、世新廣電?

我剛開始是要求傳訊息一定要包含大字,否則當作無效訊息,後來我使用 Fuzzy Search 模糊搜尋來比對,同時解決科系縮寫的問題,詳細資訊會在下文提到。

我把在試算表撈到的所有資料塞進一個 array,然後再把 array 每七個一組,拆成二維陣列並丟到 Flex Message 的 JSON 裡,回傳給使用者:

function format_flex(code, data_array) {

  try {
    var departments_array = separate(data_array);
  }
  catch (e) {
    console.log(e);
  }


  let result_array = [];

  for (var x = 0; x < departments_array.length; x++) {
    
    // Google Sheet 部分欄位的值是 int,全部變數都要轉為 String,否則 LINE 端會回傳錯誤

    let flex_title = departments_array[x][0], flex_recruit_num = String(departments_array[x][1]);
    let flex_plan_to_review = String(departments_array[x][2]), flex_recommend = String(departments_array[x][3]);
    let flex_fee = String(departments_array[x][4]);
    let flex_review_date = departments_array[x][5], flex_url = departments_array[x][6];
    let position = departments_array[x][7], flex_gcd_url = generate_calender_url(flex_title, flex_review_date)

    flex_title = flex_title.replace("\n", "");

    let flex_bubble_tmp = {
      "type": "bubble",
      "body": {
        "type": "box",
        "layout": "vertical",
        "contents": [
          {
            "type": "text",
            "text": "? 科系搜尋結果 " + (x + 1),
            "weight": "bold",
            "size": "20px",
            "margin": "10px"
          },
          {
            "type": "box",
            "layout": "vertical",
            "margin": "lg",
            "spacing": "sm",
            "contents": [

              {
                "type": "box",
                "layout": "baseline",
                "spacing": "sm",
                "contents": [
                  {
                    "type": "text",
                    "text": "科系名稱",
                    "color": "#aaaaaa",
                    "size": "sm",
                    "flex": 3
                  },
                  {
                    "type": "text",
                    "text": flex_title,
                    "wrap": true,
                    "color": "#000000",
                    "size": "sm",
                    "flex": 5
                  }
                ]
              }, {
                "type": "box",
                "layout": "baseline",
                "spacing": "sm",
                "margin": "10px",
                "contents": [
                  {
                    "type": "text",
                    "text": "招生名額",
                    "color": "#aaaaaa",
                    "size": "sm",
                    "flex": 3
                  },
                  {
                    "type": "text",
                    "text": flex_recruit_num,
                    "wrap": true,
                    "color": "#666666",
                    "size": "sm",
                    "flex": 5
                  }
                ]
              },
              {
                "type": "box",
                "layout": "baseline",
                "spacing": "sm",
                "margin": "10px",
                "contents": [
                  {
                    "type": "text",
                    "text": "預計甄試人數",
                    "color": "#aaaaaa",
                    "size": "sm",
                    "flex": 3
                  },
                  {
                    "type": "text",
                    "text": flex_plan_to_review,
                    "wrap": true,
                    "color": "#666666",
                    "size": "sm",
                    "flex": 5
                  }
                ]
              },
              {
                "type": "box",
                "layout": "baseline",
                "spacing": "sm",
                "margin": "10px",
                "contents": [
                  {
                    "type": "text",
                    "text": "離島外加名額",
                    "color": "#aaaaaa",
                    "size": "sm",
                    "flex": 3
                  },
                  {
                    "type": "text",
                    "text": flex_recommend,
                    "wrap": true,
                    "color": "#666666",
                    "size": "sm",
                    "flex": 5
                  }
                ]
              },
              {
                "type": "box",
                "layout": "baseline",
                "spacing": "sm",
                "contents": [
                  {
                    "type": "text",
                    "text": "甄試日期",
                    "color": "#aaaaaa",
                    "size": "sm",
                    "flex": 3
                  },
                  {
                    "type": "text",
                    "text": flex_review_date,
                    "wrap": true,
                    "color": "#666666",
                    "size": "sm",
                    "flex": 5
                  }
                ]
              }
            ]
          }
        ]
      },
      "footer": {
        "type": "box",
        "layout": "vertical",
        "spacing": "sm",
        "contents": [
          {
            "type": "button",
            "style": "link",
            "height": "sm",
            "action": {
              "type": "uri",
              "label": "查看校系分則",
              "uri": flex_url
            }
          },
          {
            "type": "button",
            "style": "link",
            "height": "sm",
            "action": {
              "type": "postback",
              "label": "加入收藏",
              "data": code + "-" + position + "-" + flex_title,
              "displayText": "加入收藏 " + flex_title
            }
          },
          {
            "type": "button",
            "style": "primary",
            "height": "sm",
            "action": {
              "type": "uri",
              "label": "加入行事曆",
              "uri": flex_gcd_url
            }
          },
          {
            "type": "spacer",
            "size": "sm"
          }
        ],
        "flex": 0
      }

    }
    result_array.push(flex_bubble_tmp);
  }

  if (note_message != "") {
    var flex_content = [
      {
        "type": "text",
        "text": "行事曆日期皆以各校簡章之第一天與最後一天做設定,實際的二階日期請點擊「 查看校系分則 」確認 ??" + "\n\n" + note_message
      },
      {
        "type": "flex",
        "altText": "找到了!請看大學相關科系的搜尋結果",
        "contents": {
          "type": "carousel",
          "contents": result_array
        }
      }
    ];
  }
  else {
    var flex_content = [
      {
        "type": "text",
        "text": "行事曆日期皆以各校簡章之第一天與最後一天做設定,實際的二階日期請點擊「 查看校系分則 」確認 ??"
      },
      {
        "type": "flex",
        "altText": "找到了!請看大學相關科系的搜尋結果",
        "contents": {
          "type": "carousel",
          "contents": result_array
        }
      }
    ];
  }
  return flex_content;
}

function separate(data_array) {

  var departments_array = [], time = data_array.length, tmp_array;

  for (var x = 0; x < time; x++) {
    if ((x + 1) % 8 == 0) {
      tmp_array = data_array.slice(x - 7, x + 1);
      departments_array.push(tmp_array);
    }
  }
  return departments_array;
}

2 設計收藏科系功能

再來是收藏功能,我在生成一個 Flex Message 時,在加入收藏的按鈕 postback 放入觸發詞 dash 號 “-” 跟科系名稱,當機器人收到的 postback 含有 dash,就用 split 語法以 dash 把 postback 切成大學代號與科系名稱,後面方法就跟找科系一樣了:

// line bot 收到 postback 時觸發
function handle_postback() {

    if (userPostback_data.includes("-") && userPostback_data.indexOf("remove") == -1) {
      let status = add_save(user_id, userPostback_data);
      switch (status) {
        case "department saved":
          reply_message = [
            {
              "type": "text",
              "text": "已成功收藏科系",
              "quickReply": {
                "items": [
                  {
                    "type": "action",
                    "action": {
                      "type": "message",
                      "label": "顯示收藏⭐️",
                      "text": "顯示收藏"
                    }
                  },
                  {
                    "type": "action",
                    "action": {
                      "type": "message",
                      "label": "清除收藏?",
                      "text": "清除收藏"
                    }
                  }
                ]
              }
            }
          ]

          break;
        case "fulled":
          reply_message = reply_text("收藏名單已達上限( 6 個科系 ),傳送「刪除收藏」後再重新加入。");
          break;
        case "repeated":
          reply_message = reply_text("你已收藏過此科系嘍,傳送「顯示收藏」,我就能顯示你的收藏科系~")
          break;
      }
    }
  
  function add_save(user_id, department) {

  const savelist = Save_SpreadSheet.getSheetByName("save_list");

  let lastrow = savelist.getLastRow();
  let lastcolumn = savelist.getLastColumn();
  let save_limit = 6;
  let target_row = 0;

  var x = 1;
  var userstatus = "";

  console.log("start");

  let id_array = savelist.getRange(1, 1, lastrow, 1).getValues();
  id_array = id_array.flat();

  let check_if_id = id_array.some(data => data.includes(user_id));


  if (check_if_id == false) {
    console.log("bad");
    userstatus = "new";
  }
  else {
    target_row = id_array.indexOf(user_id) + 1;
    console.log("trow:" + target_row);
    userstatus = "saved";
    console.log("user found!");
  }

  console.log("start check");

  function check_repeat() {
    for (x = 3; x <= (2 + save_limit); x++) {
      var tmp_cell_value = savelist.getRange(target_row, x).getValue();
      if (department == tmp_cell_value) {
        console.log("repeated");
        return "repeated";
      }
      else {
        if (tmp_cell_value == "") {
          savelist.getRange(target_row, x).setValue(department);
          console.log("department saved!!");
          return ("department saved");
        }
        else if (savelist.getRange(target_row, save_limit + 2).getValue() != "") {
          full = true;
          console.log("fulled");
          return ("fulled");
        }
      }
    }
  }

  function add_new_user() {
    console.log("start add new");
    savelist.getRange(lastrow + 1, 1).setValue(user_id);
    savelist.getRange(lastrow + 1, 2).setValue(department);
    console.log("new save successed!");
  }

  try {
    switch (userstatus) {
      case "saved":
        console.log("user was saved");
        return check_repeat();
        break;
      case "new":
        console.log("new!");
        add_new_user();
        break;
      default:
        console.log("out");
    }
  }
  catch (e) {
    console.log("error");
  }

  console.log("end");

}

隱私聲明:個人針對收藏資料庫有加強權限管理,只有機器人本身能去讀寫,其他幫忙的同學無法存取,我也只在網友同學回報問題時進去修正,亦不會洩漏使用者儲存的科系給任何人或第三方單位。


3 設計新增行事曆功能 ( Google行事曆 )

上面有提到:我想藉由建立行事曆的方式,幫同學檢查面試日期是否重疊,但在 LINE 裡面無法存取 iPhone 內建的行事曆。

所以我用 Google 線上行事曆來做,因為它支援 iOS /Android,安卓手機預設是用 Google Calendar,在 iOS 裝置上則會開啟網頁版。

Google Calendar 有個好處是可以透過 url 來稱建立活動,還能改參數來設定時間、標題跟說明,不用設定行事曆 API 去新增。我就寫一個動態生成行事曆 url 的 function,丟入科系名稱與時間,轉換成可點擊的連結:

// 生成 Google 行事曆連結
function generate_calender_url(department_title, review_date_text) {

  review_date_text = review_date_text.replaceAll("111", "2022");
  review_date_text = review_date_text.replaceAll("\n", "");
  var date_array = [];

  if (review_date_text.indexOf("~") != -1) {
    if (review_date_text.indexOf(" ~ ") != -1) {
      date_array = review_date_text.split(' ~ ');
    }
    else {
      date_array = review_date_text.split("~");
    }
  }

  else if (review_date_text.includes("至")) {
    date_array = review_date_text.split("至");
  }
  else if (review_date_text.includes("/")) {
    date_array = review_date_text.split(" / ");
  }
  else if (review_date_text == "--" || review_date_text.includes("、")) { // 沒有二階日期
    return "https://www.google.com/calendar/";
  }
  else {
    date_array.push(review_date_text);
    date_array.push(review_date_text);
  }

  let start_date = date_array[0].replaceAll(".", "");
  let end_date = date_array[1].replaceAll(".", "");

  department_title = encodeURI(department_title + "二階面試");
  let department_detail = encodeURI("本行事曆時間以個申簡章之第一天為主,若有多個日期則以第一天與第二天為設定,最終甄試日期請自行另外確認並修改");

  gcd_url = "https://www.google.com/calendar/render?action=TEMPLATE&text=" + department_title + "&details=" + department_detail + "&dates=" + start_date + "T000000Z%2F" + end_date + "T090000Z";

  //console.log(gcd_url);
  return gcd_url;

}

p.s 我目前還在整理機器人完整的程式碼,等註解都寫完後,我會把 GitHub 連結公開在這裡。

使用者暴增:我加快執行速度的方法

結果一月底我公開之後,經過兩次的使用者暴增潮( 一天內增加快 4000 位使用者 )而程式碼無法同步處理大量的查詢訊息,要先傳給 A 結果才會處理 B 的訊息,而且 App Script 的執行速度沒有到很快,於是我後期一直在改善程式碼的執行速度。

這裡我分享 3 個我改善 Google App Script 執行速度的方法:

  1. 多使用內建語法
  2. 使用快取,暫存要回傳的科系結果
  3. 在 postback 加入欄位數值,省去比對時間

使用內建語法

以往我檢查一個 Array 裡有沒有包含一個值,都是用 loop 檢查:

function contain_6_loop(){

    let test_array = [1,2,3,4,5,6,7,8];
    let time = test_array.length;

    for(let x = 0; x < time ; x++ ){
        if(test_array[x]==6){
            return True;
        }
        return False;
    }
}

console.log(contain_6_loop()) // True

後來為了加速程式碼執行速度,我研究一下,發現改用內建語法可以減少執行時間,像是可以改用 .some 來檢查,大幅改用內建語法後,程式的執行速度確實有變快一些:

let test_array = [1,2,3,4,5,6,7,8]:
let contain_6 = test_array.some(data => data.includes(6));

console.log(contain_6) // True

啟用快取

機器人在搜尋科系時,會頻繁的讀取試算表的資料,這樣也會花一段時間,後來我也發現 App Script 有支援快取( 不是收銀 )

快取是常被重複使用或運算,而暫存下來的資料,以我的 case 來說,假設同學 A 傳「 台大資工 」,機器人找到並回傳台大資工的訊息 JSON 後,就把這筆結果存為快取,之後同學 B 傳台大資工時,機器人就傳上次快取的訊息,不用再從頭搜尋試算表。

因為我要傳的內容不具有即時性,所以我用快取來減少搜尋資料庫的動作。倘若你要傳的內容常即時更新,像車次或虛擬貨幣價格,就建議縮短快取時效,或乾脆不要用快取以免撈到過期資料。

收藏 Postback 加入欄位值

剛開始使用者要看收藏科系時,機器人都得不斷在資料庫檢索目標科系在工作表裡哪一行,再抓取該行的科系資料,但收藏科系很多時,就要等很久才會叫出結果。

我就想到同學收藏科系之前就會搜尋科系,而在搜尋科系的同時機器人就會檢索資料庫了,那就在搜尋時紀錄下該科系在哪一行,這樣顯示收藏時就直接抓那行的資料,原本的 postback 如下:

ntu-國立臺灣大學資訊工程學系
// Bot 會分成 "ntu" 和 "國立臺灣大學資訊工程學系",接著比對

後來我就在 postback 裡面多塞一個目標行數:

ntu-27-國立臺灣大學資訊工程學系
// Bot 直接讀取 "ntu" 工作表第 27 行的資料,省去比對時間

修改後效果顯著,顯示結果的時間從 8 秒變到 3 秒左右( 在 IDE 端編譯的數據 )


開發遇到的問題與解法

接著分享一下我在開發大學科系查詢 LINE 機器人遇到的瓶頸問題,還有後來的解法。

縮寫太多種:引入模糊搜尋( Fuzzy Search )

儘管前期我請同學幫忙新增縮寫關鍵字,但難以涵蓋同學、家長用的縮寫,因此我研究怎麼把第三方的 Fuzzy Search 模組套用到 App Script 裡。

Fuzzy Search ( 模糊搜尋 )跟用 include 比對的差別在於:前者不用完全匹配才會判定為包含,假設我要判斷資工是否為資訊工程學系,include 完全匹配的結果如下:

var dp = "資工", target = "資訊工程學系"

if(target.include(dp)){
    console.log("包含!")
}
else{
    console.log("不包含")
}

// 會顯示不包含,因為 target 裡面沒有資工這個字詞

如果改用模糊搜尋就不一樣了,因爲資訊工程學系裡有資、工這兩個字,系統就能判斷為包含。

我是用 Fuse.js 這個模組來做模糊搜尋的,它載入不錯,回傳也會給予各個 value 的相似度( score )所以我會依照 score 的大小來判斷是否匹配。

Google App Script 不支援 npm install 外部模組,必須用 CDN 連結方式載入

Request 用量超額:一樣用快取暫存

之前載入 Fuse.js 時,我是設定每搜尋一次,就從 CDN request 載入一次檔案,而 App Script 第三方 request ( UrlFetch )是有用量限制的。結果後來人數暴增時,一天內就超標了,剛學會用 cache 的我才想起來:可以快取起來,這樣未來就不用頻繁 send request 了:

var cache = CacheService.getScriptCache(); // 啟用快取功能

function full_search(word,array) {

    function loadJSFromServer() {
    if(cache.get("full_search")==null){
    
      var url = 'https://cdn.jsdelivr.net/npm/fuse.js@6.5.3'; // from Fuse.js
      var javascript = UrlFetchApp.fetch(url).getContentText();
      cache.put("full_search",javascript,2592000)
    }
    else{
      javascript = cache.get("full_search");
    }
    eval(javascript);
  }

  const options = {
    includeScore: true
  }

  loadJSFromServer();


  const fuse = new Fuse(array, options)
  const fuse = new Fuse(array, options)
  const result = fuse.search(word)
  console.log(result); 
  return result;
}

在本次作品學到的事

在做「 大學科系查詢 LINE 機器人 」這個 side project 時,由於使用者遠遠傳出我的預期,我寒假花超大量的時間跟心力去製作跟維護,也學到很多部分:

  1. 將程式碼模組化,並利用 try catch 抓錯誤,所以 debug 時省很多時間
  2. 幫程式碼做最佳化,提升執行速度
  3. 蒐集使用者回饋意見,改善並新增功能

我有在機器人回覆訊息裡放我的工作用 email,所以陸陸續續有收到一些同學的建議與功能許願,像是有不少同學反應想加入繁星標準,我就另外寫查繁星的功能,希望讓這個成品更貼合同學和家長的需求。

收到的私訊


想說的話 / 技術總結

能開發出「 大學科系查詢 LINE 機器人 」真的是很特別的經驗,這是我第一次自己建立這麼多使用者的成品,在三個禮拜多內使用者就能破萬,不僅多累積了一個程式作品,我還因此受邀參加 LINE Developers 線上小聚,更重要的是我得以實踐去年的初衷,幫同學找科系更方便了。

LINE機器人粉絲數截圖

說實話,看到使用者飆升、跟持續收到私訊意見難免有點壓力,但現在來看算是很有成就感,從想出 idea、找同學整理資料、寫程式碼….自己走完一次開發程式的路,感觸許多。

特選落榜後的成果

“ Work hard, be kind, and amazing things will happen ”

這是我個人奉行的準則,本次製作 LINE 機器人的經驗也是我對這句話的實踐。

我想到用 LINE 找科系的時間點,是在得知特殊選才( 不看考試成績,只看特殊能力、作品的升學管道 )只有備取結果的幾天之後,剛開始的我以為落榜之後的生活是無望的,從未想到現在還能做出一些效果不錯的作品、幫助其他同學。

想表達的是:我們在十八歲碰到的失敗,並不代表未來幾十年都是不如意的,沒有升學目標或他人評論能來定義我們的價值。其實只要努力付出,保持良善,美好的事情終究會發生。

發布後就被「 致敬 」

此外這個機器人發布後,也有被其他開發者和部分品牌「 致敬 」,市面開始出現類似功能的 LINE Bot。現在的我其實很高興,因為我的初衷就是「 幫高中同學和學弟妹更方便找科系資料 」

哪怕別人借鑒我用 LINE 查大學科系標準的點子、製作類似的服務,對整體學生族群來說都是有利的,查科系申請資料不再是難事,所以致敬者也是間接幫我達成目標了,感謝他們的幫忙 XD

感謝你的閱讀!希望能幫到你開發 LINE 機器人或自己的資訊專案。如果你對我的開發歷程發表看法,或想給程式方面的建議,歡迎在本文下方留言!

相關連結

大學科系查詢機器人好友連結 ??
本文原網址??
做個 LINE 機器人記錄誰 +1!群組 LINE Bot 製作教學與分享

Load External JavaScript Libraries in Google Scripts with eval() – Digital Inspiration
Cache Service | Apps Script | Google Developers


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
deh
iT邦研究生 1 級 ‧ 2022-02-23 17:10:57

就程式面來說還有許多可以改進的地方
如var、let、const的使用,設計模式等
不過能做碼農的人多的是,能拿來解決現實問題的比例就少了
分享出來回饋社會的又更少了
個人當年高三時每天自習到晚上九點,六日不休
除了學測指考的內容與考試技巧外什麼都沒有
再看看作者已經做出萬人使用的Line bot
實在汗顏

個人在程式方面真的有很多不足的地方,這也是我今年會努力加強的目標,感謝您的指點。

個人只是恰好發現學生的某個問題,做個小工具幫忙才能有這麼大的迴響啦,我會繼續加油的!謝謝

/images/emoticon/emoticon41.gif

0
Pulin
iT邦新手 5 級 ‧ 2022-02-24 16:24:04

解決問題很棒!值得嘉許!
作者肯定能夠越來越強大的/images/emoticon/emoticon08.gif

謝謝您的勉勵!!我會繼續加油

1

讚,實際解決到問題才是真的強

0
arguskao
iT邦新手 3 級 ‧ 2022-06-02 08:31:39

真的很了不起!

我要留言

立即登入留言