發表文章

那天

有兩年我住在考試院附近的巷子裡。那時候,我是一間早午餐店的 VIP。 它賣的品項不多,主打美式漢堡,口味介於麥當勞和 Subway,但價位還比它們都低,聽起來還不錯,對吧?我會成為這間店的常客,原因不只是食物好吃和價格考量,更是因為店內的環境乾淨舒適,還有背景音樂經常是小野麗莎:她的 Moon River,是我的最愛。 經營的是一對三十多歲的年輕夫妻檔,平日兩個人都會自己下來做,老公主要在內場負責料理,老婆負責收銀和外場;十點後跟假日才有工讀生幫忙。我通常是在晚班下班後去光顧的,常常一連去個好幾天;早上八九點的時段很少內用客人,我特別喜歡這樣安靜的時光。如果不是在看書,那就是在看著窗外往來的車潮,想他們每個人的生活在忙些什麼、哪裡傳來了救護車聲又出了車禍。 同一時候,店內也常有一對中年夫婦來用餐,觀察下來,他們來店的頻率肯定不亞於我。其中,男方比較健談,常常跟老闆及老闆娘聊天,聊社會大事啊、小孩子啊、做生意啊。聽久了,對他們聊天的內容愈來愈聽出興趣,感覺他們人也挺有意思;雖然始終沒有認識對方,但我卻很習慣他們的存在。有一次,中年老夫還在談蝦皮的崛起跟在台灣的擴張,下一次就聽到他分享在蝦皮門市當店員的所見所聞。 相較之下,我平常跟老闆老闆娘的互動是很極簡的。我總是點完餐後坐在位子上,就進入自己的世界。但其實他們人都很親切,每次我要離開時,都會像朋友一樣跟我道再見,我也會回頭說拜拜。這是我工作一段時間後才學會的基本的禮貌:好好向人告別,就算彼此素昧平生。這也是我唯一想到可能的原因,是可以累積夠多的熟悉跟信賴,直到有一天我待得特別久,他們招待我一杯薑茶。那時候我才明白,哦,他們也記得我。 好景不常。後來因為工作調動的緣故,我從本來在新店,改成去板橋的駐點上班。長時間的通勤撐了半年之久,我終究決定趁著原本的租約到期,搬家到離上班地點很近的地方。在搬離原本租處的前一天,我把握機會去這間店。要離開前我才跟他們說我之後就要搬去板橋,這應該是最後一次來了。老闆老闆娘大吃一驚,覺得我幹嘛不早點講,連忙為我準備一杯飲料外帶,並隨口多聊幾句。一直以來造訪這麼久,都沒有試過多認識他們一點,直到告別的這一刻。 走出店門後,我感覺既踏實又有點落寞,彷彿參加完畢業典禮似的、許久未有的心情。到了現在,動筆記錄的這一刻,回頭看那時的搬家,以及鼓起勇氣向他們說再見的行動,是為自己

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

圖片
 緯育的 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 {