[From medium]天下雜誌 2020 總統大選專題-技術紀錄

美美的台灣地圖

Originally published at https://medium.com/cw-it-group on Jan 15, 2020. 原文網址

天下雜誌於 2020 年總統大選期間推出總統大選專題,技術承接自 2018 年九合一選舉地圖,以此為基礎推出即時開票與翻盤地圖、成效出乎意料的機器人分析和即時更新文章的記者深度分析等頁面。這次有幸參與這個專案,學了不少東西,趁還沒忘記趕緊紀錄下來。

使用的資料來源與技術

  • 資料來源:中選會、財政部、內政部、氣象局(是的你沒看錯)

  • 資料處理:php、python、nodejs、QGIS(處理 shp 檔,後面說明)

  • 視覺呈現:d3.js、jQuery(不潮但很好用)

  • Infra:Google Cloud Platform

資料獲取

  1. 選舉資料:歷年選舉資料從中選會開放資料庫取得,即時開票資料似乎只有媒體業才有權限申請api,投開票所資料麻…一個一個找摟
  2. 人口統計資料、收入資料:社會經濟資料庫
  3. 縣市鄉鎮村里代碼對應資料:內政部戶政司
  4. 地理圖資:社會經濟資料庫人口統計資料有提供 shp 檔

資料清整

這個步驟毫不意外地投入了最多時間,不得不說中選會的資料有些地方真的蠻弔詭,大部分的區域會提供 pdf 以及 ods,pdf 有所屬村里可是 ods 裡面卻沒有真的是 what the…

有沒有業界夥伴想要分享一下心得啊~~

首先要感謝強大的團隊成員幫忙把歷屆選舉、人口統計與收入資料都先整理過,才能讓我有更多時間運用工人智慧檢查投開票所資料有沒有對錯。

中選會開票的結果資料是以投開票所為單位,每個投開票所有其所屬村里,因此計算時要把結果跟村里代碼對應表 mapping 後加總才能獲得相對準確的某村里總數據。

大多數投開票所資料都有提供 pdf 以及 ods 檔,看到 ods 當下心裡想的是「有 ods 耶~so sweet~~~❤」,很快地把 ods 裡的地址擷到村里作為該投開票所所屬村里,人工補上沒有村里的資料後開心的畫了一個地圖,看見台南市滿滿的破洞我就知道出來混總是要還的。

最後處理的方法是把能夠貼上 gsheet 的 pdf 先貼一次,下載成 csv 再用 vscode 處理;不能貼上的就先買罐眼藥水再開始作業。

然而這樣做還是會有一些漏掉的部分,所以還要加上中選會在選舉前兩週開放的查詢系統去對。

過程中最崩潰的是對字,有些是看起來一樣但程式就認為不一樣,比如說「龜壳里」跟「龜壳里」,以及各種工人智慧引發的錯誤。

人有悲歡離合,村里也有

2016 到 2020 這幾年,村里數從原本約 7840 個下降到 7761 個(還好 2018 到 2020 沒變),那投票率該怎麼處理才不會看起來很怪?我的做法是先產出一份兩個年份村里比對的 csv,再到 2018 年的地圖上一一挑出有變化的村里,依肉眼測量切分比例計算該村里各政黨的得票率占比,這也是為了機器人分析地圖切換時能正常運作的必要步驟。

技術介紹

系統架構

Cloud CDN 獨立於 GKE 之外

考慮到流量可能會很大,整體設計架構盡量以靜態為主:全部都丟 GCS,搭配 GKE ingress & nginx proxy 掛靜態檔案,回來吃 cloud cdn,除機器人分析因社群分享需求要走動態渲染之外,其他都是靜態。

最終額外多開的機器只有兩台

  1. 常駐的 n1-standard-1(撈深度分析頁資料)
  2. 開票時的 n2-highcpu-16 preemptible 拿來畫機器人分享圖跟即時開票資料處理(preemeptible 是先佔型虛擬機,很適合這種類型的專案)

非常感謝強大的 DevOps 團隊快速把架構部署完成,詢問過尖峰時段 pods 數量長到 111 個,nodes 開 21 個。

繪製地圖

地理圖資可以從社會經濟資料庫的人口統計資料取得 shp 檔,拿到後可以先丟到 mapsharper.org 看看長相,我一開始是拿國土繪測局的開放圖資畫,一畫發現跟 2018 年對不起來,幾經研究才知道 2018 的地圖有特別把離島拉近本島讓前端呈現比較好看,所以另外用 QGIS 對 2016 及 2020 的地圖做相同處理。

步驟如下:

  1. 選擇 TWD97 格式載入 2016 及 2020 地圖並轉存成 WGS84(2018 是 WGS84),編碼 UTF-8
  2. 載入 2018 地圖當作基準,分別載入 2016 及 2020 地圖調整離島位置
  3. 另存調整好的地圖為 shp

畫地圖與地圖縮放邏輯沿用之前的,詳細記錄參考 2018 年九合一選舉地圖技術紀錄,圖資整理流程大致如下:

  1. shp 轉 geojson
  2. 開票資料轉 ndjson
  3. concat 開票資料與 geojson
  4. geojson 轉 topojson 縮小 size
  5. 丟給 d3.js 畫

這一系列的動作都是用 nodejs 完成,方法是參考 Mike Bostock 寫的教學

記者深度分析

頁面下方推薦的文章需要即時更新,我又不想為了一次性的活動做後台,幾經思考想到可以用 Google App Script 把 gsheet 的資料轉成 json API,再用排程每分鐘抓取並丟到 GCS 上讓上面的 javacsript 可以隨時讀到最新的 json,同時為了避免快取有加上 timestamp。

這樣做主要的好處是後端不需特別裝套件也不用多開 service account 就能做到即時更新的效果,是個蠻不錯的方法~

程式碼的部分可以參考我個人的筆記

機器人分析

這頁出乎意料的紅,也許跟今年年輕人高投票率有關,還沒玩過的可以玩玩 https://web.cw.com.tw/2020-taiwan-presidential-election/data.html 資料來源 = 內政部 + 財政部 + 氣象局 + 中選會

這頁因為有分享到社群的需求要讓 fb/line/twitter 爬蟲能解析 meta,使用已經存在的 laravel site 當後端產出包含 meta 的 page,再用 GKE 的 ingress 把特定路徑 proxy 給他,其他靜態就吃原本 nginx 的 proxy 到 GCS 上。

圖片的部分是用 python 的 pillow 製作,一個區域要產 4 張(4 種 emoji),再乘上所有區域後總共要產的圖有 30000 多張,這麼大量的產圖不寫平行怎麼對得起自己???

平行化後原本要產 3 小時的圖縮減到只要 25 分鐘真的很有成就感。

分享畫面示意圖,圖片總數為 33320 張

氣象資料抓的是全台各地區天氣觀測預報,但天氣好壞的類別實在太多了,粗略計算就有 100 種左右,討論了一下我寫了一個簡單的判斷:當某些字元組合出時就是不好/好來呈現天氣文案。

One more thing

其實這個地圖有顆彩蛋,有閒的人應該會發現~

有個需求是開票完畢後要提供地圖 svg 給記者們出稿,本著可以增加睡眠時間的目的偷偷做了一個可以自己決定要下載哪個區域或全台地圖 svg 的彩蛋功能。(雖然最後還是天亮了…

黃色按鈕可以下載縣市/鄉鎮/村里等級的全台地圖,藍色按鈕則是先選擇要下載的區域後,依你點選的區域決定要往下幾個層級抓(比如點擊區域+lv1 ,會抓選擇區域到達鄉鎮層級的 svg),越底層功能越單一。

先開聲音後輸入 konami 的祕技你就可以得到它

關閉彩蛋也很簡單,輸入 gg 即可,沒反應就多按幾次~

技術方面的紀錄就寫到這,感謝各位。

cmd + /