發表文章

目前顯示的是 2022的文章

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

圖片
 緯育的 Java 養成班是一門將近五個月的長期課程,它們機構以網頁系統的方式提供課表給學生查詢。但是,我已經很習慣用自己的 Google 行事曆了,規劃生活行程都只想用它,不想另外上一個網站查課表。那麼,有辦法將網頁上的資訊以迅速有效的方式存下來,方便我後續整理、甚至移轉到行事曆上嗎?嘿,有的,而且其實並不難。這篇就來介紹我是怎麼做的。 作法 你可以從上圖看到,課表排成月曆的形式呈現,所以右鍵複製下來也難以處理。Google 試算表內建有  IMPORTHTML()  和  IMPORTXML()  兩種用來抓取資料的函式,但是我覺得它們沒那麼好用,發揮空間不大。因此,我最後選擇:直接取用網頁呼叫後端 API 所回傳的課表資料。 現在大多數網站都有豐富的動態互動,我指的不是畫面裡的視覺元素是動態、或是動畫,而是指在整個網頁沒有重新載入的情況下,頁面呈現的內容可隨著你的操作而變化。以緯育的課表系統而言,就是按左上角的箭頭改變月份,底下的課表隨之更新。實現這樣效果的方式有很多,有簡單的、或比較複雜的。這個課表系統就是簡單的方式。 取得資料 首先,打開你任一種瀏覽器的開發人員工具,或是在畫面上找個地方按右鍵選 檢閱  進入。移動到 網路 (Networking) 這頁面,這裡就是顯示網頁載入後執行、或收到的大大小小的程式或資料。左邊區塊裡可能有非常多檔案,但通常我們的注意力先放在副檔名為 .json 或 .js 的項目即可,因為現在網頁前後端的主流資料傳輸格式都是 JSON 。更進一步,我們已經知道我們要找的資料是課表,所以我們可以用跟課表、行程相關的英文關鍵字來在過濾 URL 的欄位搜尋,不用一個一個檢查。如上圖,我們可以看到 schedule-list 的某一節點內容和網頁裡 12/5 的行程十分一致,因此,可以大膽認定這就是網頁向後端查詢 12 月份的課表,所得到的 json 回傳。 然而,JSON 的結構畢竟不是用來給人閱讀的形式,我們還要再將整份資料轉化成大家習慣的表格形式,方便運用。這一步我們可以自己寫 apps script,也可以使用網路上現成的 JSON 轉換工具。 轉換資料 範例裡用的是  ConvertCSV

Sheets - 資料反轉 (下)

圖片
Sheets - 資料反轉 (上)   在上一篇結尾,我們得到一個 INDIRECT() 公式,可以根據儲存格得到我們希望的資料陣列反轉後的值。但每次資料量變多,或是擺放的位置有變,參照的列跟欄隨之變動,我們都會需要修改公式再重新套用。一次兩次沒關係,但三次以上就會想說:我們能不能再進一步省去這樣的麻煩操作呢?拜 Google Sheets 的進步所賜,現在已經做得到了。這篇就來介紹如何透過新的陣列函式提升公式的便利性。 作法 1. MAP()、LAMBDA() 今年 Sheets 新增了一系列新函式跟新功能,使用體驗上的升級我認為是非常有感。這裡要用到的是剛推出不久的 MAP() 和 LAMBDA() 。其實它們的作用說起來很簡單,就是把參照範圍裡每一個儲存格都送進設定好的公式裡運算,得到的值依相對位置輸出在新的陣列。依上圖所示,就是將 A1 格代入 LAMBDA() 裡的參數 cell,再依公式運算:假設 A1 值為整數 8, 公式為 cell * 2,那 A1' 的值就是 16,依此類推。我們的 INDIRECT() 公式代入 MAP() 時,記得將 ROW() 和 COLUMN() 裡的參照改成儲存格變數名稱 (自由取名) 即可,如下: = MAP ( A1:E7 , LAMBDA ( cell , INDIRECT ( "R" & 8 - ROW ( cell ) & "C" & 6 - COLUMN ( cell ) , false ) ) ) 這樣一來,我們只需要輸入一次公式,即可產生反轉過的資料,不需要再一直拖拉來完成套用。看來非常美好,對吧?然而,它還是存在一些美中不足的地方。 2. MIN()、MAX() 你不難發現,INDIRECT() 取得列數和欄數的過程仰賴我們先算出的定值:以 A1:E7 作為來源範圍的話,定值就分別是 8 和 6。記得我們在 INDIRECT() 裡是採用 R1C1 表示的嗎,這裡的 A 和 E 分別就是第 1 欄和第 5 欄。顯然,8 和 6 就是從陣列長寬頂點的列 數相加 (1 + 7) 以及欄數相加 (1 + 5) 所得來。 認識到這一點,再來的問題就只是:該如何求得陣列頂點的位址呢?我們知道如何取得儲存格的列和欄,也知道陣列的一端是數

Sheets - 資料反轉 (上)

圖片
Java 班剛開訓時,班上需要做一份座位表給老師看。大家各自選定座位後,在一份模擬教室格局的 Sheets 表格裡,於適當位置填進名字,範例如下:                                                                                 綠色是前門和後門;藍色是老師的位置 好了,我們現在有這張表,但是我還有個問題:如果我想透過它認識班上同學,表得反過來看,但我腦袋常常轉不過來,想按照自己的視角,那該怎麼辦? 以這張資料量很小的表而言,我從結尾 Front 複製到新的起始位置,如此反覆動作也不會花上太多時間。但要是今天遇到的資料量很大,就不適合這麼做了。那 Google 試算表有內建方便的反轉功能嗎?就我所知,應該沒有。內建的排序不符合直接倒轉的需求,因為它是根據規則來排;函式 TRANSPOSE() 也不適合,它是以整列、整欄反轉,並不是反轉一格一格儲存格的順序。 一如往常,我又動手寫公式來解決這問題。 作法 我們知道在試算表裡每一格儲存格都有它的參照位址,即所處列 (Row) 和欄 (Column)。如果想在某一格顯示另一個儲存格的值,我們用這樣的方式:   = A1   ,或    = B2:F8   。但若要反轉順序,我們勢必要設一些條件操縱它們的位址,而在 Sheets 裡,這個可以幫我操縱位址再進而得到值的函式就是 INDIRECT() 。例如    = INDIRECT ( "A1" , true )     或    = INDIRECT ( "R2C2:R8C6" , false )   ,回傳結果分別對應到一開始的基本用法。第一個參數是參照字串;第二個參數 true/false 代表要使用哪種參照表示法,false 即是採用 R1C1 表示法:列欄都使用數字排序,以英文字 R 跟 C 區分兩者。我的經驗裡,多半是用 R1C1 比較好發揮。 1. ROW()、COLUMN() 現在我們有一個方向了,接下來的問題就是,該如何操縱、變化參照字串,進而得到我們希望的結果?INDIRECT() 使用 R1C1 參照必須經由數字,而 Sheets 裡

Java Homework - 學生分數排序器

 Java 班第四份 Java 作業的其中一題,給定 n 個學生在 m 次考試內的所有成績,求每位學生取得單 次考試內最高分的次數。原題目有給出 8 個學生考 6 次考試的每個分數 (共 48 個),不過我是寫了個隨機分數產生器 randomAllScores(m, n) ,方便測試用。 作法 首先,考量方便直接從一次考試的所有分數內找出最高的分數,randomAllScores() 產生的二維陣列 allScores ,設計成:   {考試 1:{學生 1 分數, 學生 2 分數, 學生 3 ...}, 考試 2:{ ... }, ... ... } 再來準備了一組外層是學生個數,內層含學生編號和次數紀錄的二維陣列 studentRecords ,用於最後結果輸出。 計算過程,利用迴圈從全部分數按考試取得 highest 這個整數陣列,表示同學裡誰拿最高分。若是某學生的分數最高,則將該學生的元素 + 1 表示,例如 {0, 0, 1, 0, 0, 0, 0, 0} 即代表考最高分的是第三個學生。之後便可將這陣列的所有元素按順序加到 studentRecords 裡每個學生的次數元素,有加到 1 便是最高分次數加一次。所有考試都執行過這組動作即完成計算,最後輸出。 由於取得 highest 的 studentsWithHighestScore()  會檢查所有的學生分數 (迴圈),然後 hasHighestScore() 每檢查一個便會拿該分數跟所有分數比較 (迴圈內再迴圈),顯然這是個沒有特別技巧的暴力解法:計算步驟為 n^2。當學生人數 n 很大時,程式的效率並不好。 用 array、rank、largest element 等關鍵字搜尋可以查到很多相關題型的作法,目前看到最順眼的是 GeeksforGeeks 這篇 k largest (or smallest) elements in an array 裡的 quick sort 作法,之後研究怎麼應用到這一題。 package junli.hw4; public class StudentScoreRanker { public sta

Java Homework - 年度日期計數器

 Java 班第四份作業的其中一題,依年、月、日順序,輸入三個整數後求此日期是該年度的第幾天。有兩個作法。 作法 1 邏輯上很簡單,但因為用 switch case 判斷月份所以寫起來很長(SE 17 有更簡潔的寫法)。然後重要的地方在於開始熟悉 Java 裡對日期時間的處理。 關於 isValidInput() 裡為什麼使用位元運算的 & 運算子 (AND),是因為要避免掉 Java 裡 && 的短路求值 (short circuit evaluation) 特性,其意思是在計算一個邏輯表達式 (logical expression) 的真假值時,從左算到右若已經可以決定整句的真假,便會略過後面不執行。這本該是對於程式執行效率有利的性質,然而因為我想確保做過全部年月日的驗證,以印出相應提示訊息,在此短路求值就不是我想要的。 package junli.hw4; // 請設計由鍵盤輸入三個整數,分別代表西元yyyy年,mm月,dd日,執行後會顯示是該年的第幾天 例:輸入 1984 9 8 三個號碼後, // 程式會顯示「輸入的日期為該年第252天」 // (提示1:Scanner,陣列) // (提示2:需將閏年條件加入) // (提示3:擋下錯誤輸入:例如月份輸入為2,則日期不該超過29) import java.util.Calendar; import java.util.Scanner; public class DayOfYearCounter { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("請輸入 yyyy mm dd 三個正整數"); int yearNum; int monthNum; int dayNum; do { yearNum = sc.nextInt(); monthNum = sc.nextInt(); dayNum = sc.nextInt();

Java Homework - 樂透下注產生器

記錄一下我寫緯育 Java 班第三份作業的程式碼,共三種作法。 程式碼區塊的 highlight 效果是透過  highlight.js  實現,步驟可參考 這支影片 ,挺容易的。 作法 1 numbersWithout() 負責產生可選擇的號碼這部分,關鍵是使用餘數運算去取得個位數數字,以及將整個數目減去個位數再除以十得到十位數數字。 randomBets() 負責亂數取得 6 個不重複的號碼,想法是每次選中某個號碼後,將後面所有號碼都指定到前一位置,因此選中的號碼被蓋過不見,然後再建立一個新的陣列但長度比原本的減 1,如此之後亂數選擇便不會重複數字。 package junli.hw3; import java.util.Arrays; import java.util.Scanner; //阿文很喜歡簽大樂透(1~49),但他是個善變的人,上次討厭數字是4,但這次他想要依心情決定討厭哪個數字 //請您設計一隻程式,讓阿文可以輸入他不想要的數字(1~9),畫面會顯示他可以選擇的號碼與總數 //(提示:Scanner) //(進階挑戰:輸入不要的數字後,直接亂數印出6個號碼且不得重複) public class Lottery { public static void main(String[] args) { // TODO Auto-generated method stub Lottery aWen = new Lottery(); Scanner sc = new Scanner(System.in); System.out.println("阿文...請輸入你討厭的數字"); int numNotWanted; while(true) { numNotWanted = sc.nextInt(); if (numNotWanted < 1 || numNotWanted > 9) { System.out.println("請輸入1~9之間的數字"); } else {