在網頁中提取鏈接的「三板斧」
- 2019 年 11 月 24 日
- 筆記
宿舍有個樹莓派常年開著吃灰,裝了 Transmission 用來掛種子。由於掛下來的資源大部分都是影片類型,所以也不必下載下來。於是我配置了一個開了 autoindex 的 Nginx 伺服器,在校園網內,直接通過一個內網的 URL 來實現資源的訪問。

最近開始接觸紙片人,有時候想把一個文件夾裡面的劇集加到播放列表,幾十個影片一個個添加顯然是一件很麻煩的事情。觀察發現,PotPlayer 可以批量添加 URL,所以說,我只要把所有的鏈接一起導出、添加就好了。
面對這個需求,一開始不假思索地就有了這樣的想法:先用 document.querySelectorAll 把所有的 a 標籤拿出來,然後循環遍歷,再進行過濾,收集數據等操作,寫出的程式碼大概是這樣的(直接在瀏覽器控制台運行的 JavaScript):
var list = document.querySelectorAll("a"); var result = ""; for (var i = 0; i < list.length; i++) { if (list[i].herf.indexOf("mkv") !== -1) { // 篩選 result += list[i].href + "n"; // 收集結果 } } console.log(result);原本我以為這次還是和往常一樣,但恰好昨天看了一點 《函數式編程思維》,我看待這個問題的角度也開始發生了變化。
這本書介紹了函數式編程範式與命令式編程的差別。命令式的編程的思維,需要我們定義狀態,然後也要自己去管理狀態的保存,迭代等等細節。當底層的狀態的操作和需要解決的問題的邏輯混在一起以後,因為人腦把控的有限,程式出錯的可能性也大大增加。函數式的編程的思維,程式設計師只需專註於更高層次的業務場景的抽象上,那些瑣碎的控制,則交給 runtime 去處理和優化。這個過程對於腦力的解放,就如類似 Java 的有垃圾回收機制的語言讓 runtime 接管了複雜容易出錯的記憶體管理一般。
對於我目前面臨的這個問題,抽象來說,是一種列表數據的處理,所以關鍵是一個列表的數據結構。從函數式的角度來看,上面的整個過程,可以歸納為書中提到的操作列表的「三板斧」 —— 篩選(filter)、映射(map)、摺疊(fold)/化約(reduce)。
而這「三板斧」各自的具體操作,則可以通過匿名函數的方式傳入。
JavaScript 本身的列表對象也提供了體現函數式思想的 API,對於列表類型 Array 本身也封裝了「三板斧」裡面涉及到的方法:

於是整個過程也可以按照這個思路進行:
1. 拿到數據源的列表 這裡通過瀏覽器的 Selector API 將 DOM 中所有的鏈接元素提取出來,由於 document.querySelectorAll 方法返回的對象不是Array 數組類型,所以這裡通過 ES6 的 ... 擴展語法,將它變為一個真正的 JS 數組。
[...document.querySelectorAll('a')]
2. 過濾 對這個數組執行 filter 方法,傳入一個判斷函數,這裡的判斷條件是,鏈接是否包含 "mkv"(代表影片格式的後綴)。
[...document.querySelectorAll('a')] .filter((item) => item.href.indexOf('mkv') !== -1 )我們可以看到,經過這一段處理,返回一個過濾後只剩下 25 個元素的數組。

3. 將鏈接元素映射為鏈接字元串 這裡的數組的元素全都是 DOM 中的鏈接元素節點,但我要的是字元串,所以這裡需要通過映射(map)的方式把元素轉為鏈接,鏈接在這裡表現為 a 元素的 href 屬性。
[...document.querySelectorAll('a')] .filter((item) => item.href.indexOf('mkv') !== -1 ) .map((item) => item.href)數組執行 map 方法以後,返回的是每一個元素經過映射函數之後的數組。

4. 獲得最終結果 由於這裡是一個數組,我們最後要的是一個可以複製出來的字元串,所以最後還要進行一個化約(reduce)的操作。JS 數組的 reduce 方法至少需要傳入一個接收兩個參數的累積函數,每一步累積的變數 accumulator,代表累積過程的當前元素 currentValue,由於這裡是給鏈接字元串之間添加一個換行符,所以累積函數可以寫成:
function (accumulator, currentValue) { return accumulator + 'n' + currentValue; }簡化為箭頭函數的形式,則是 (accumulator, currentValue) => accumulator + 'n' + currentValue
然後得到最終的程式碼:
[...document.querySelectorAll('a')] .filter((item) => item.href.indexOf('mkv') !== -1 ) .map((item) => item.href) .reduce((accumulator, currentValue) => accumulator + 'n' + currentValue)reduce 方法執行時,會一個個遍歷列表的元素,按照函數所描述的規則去累積結果。

如此簡單幾步,我們完全不用去思考循環的變數應該怎麼寫,整個程式碼的演化過程,也從一點點修改和調試程式碼,變成解決問題的步驟的一行行增加,人腦在實際問題和電腦指令之間翻譯的開銷讓步於成本更低的 runtime,可以說是十分地舒服了。借鑒這樣的思路,可以更優雅快速準確地解決很多類似的甚至更加複雜的問題。
把結果複製到播放器,愉快地開啟新一輪的補番之旅~

寫到最後,突然想起了 Unix 命令行的管道機制。在命令行下我們常用 cat grep awk head tail 之類的命令,程式與程式之間,通過統一的標準輸入輸出流和「管道」,以一個個 byte 進行資訊的傳遞,或許也可以說是某種意義上的「函數式編程」?
