拆解區議會分區建議pdf


最近參與了一個項目,目的是想把歷屆區議會的相關資訊製作成互動的網站,而其中涉及到一些區議會的選區分界資訊。政府的網站中雖然有著往年的紀錄,但卻是令人可恨的pdf格式。故此專門為了這堆PDF寫了一個簡單的小程序,可以把歷年的pdf轉換成json。萬幸的是這些pdf都是統一格式,因此在開發過程中省卻了很多的麻煩。

目的

  • 取得區議會選區代號、名稱、預計人口、偏離百份比等數據
  • 更甚者希望可以取得主要屋邨範圍
  • 輸出json格式

TL;DR

  • 使用pdf2json庫寫一個nodejs程序,
  • 先把pdf資料讀取成坐標(x,y)及文字
  • 把y軸相近的資料歸納為array
  • 再使用regex去辨認資料,並以簡單的state machine去把資料分類

詳細步驟

先安裝pdf2json

npm i pdf2json

簡單的用2019的pdf來跑一下範例

let fs = require('fs'),
  PDFParser = require("pdf2json");

let pdfParser = new PDFParser();

pdfParser.on("pdfParser_dataError", errData => {
  console.error(errData.parserError)
});

pdfParser.on("pdfParser_dataReady", pdfData => {
  fs.writeFileSync("output.json", JSON.stringify(pdfData, null, 2));
});

pdfParser.loadPDF("raw/pdf/2019_A.pdf");

這邊會把pdf解壓成一個json,pdf2json的文檔有說明大概格式

{
  "formImage": {
    ...
    "Pages": [{
      ...
      "Texts": [
          {
            "x": 2.938,
            "y": 3.503,
            "w": 2,
            "sw": 0.32553125,
            "clr": 0,
            "A": "left",
            "R": [
              {
                "T": "%E5%9C%B0%E5%8D%80",
                "S": -1,
                "TS": [
                  0,
                  15,
                  0,
                  0
                ]
              }
            ]
          },
    }, ...]
}

pdf2json把pdf解壓成一個array

Pages裡面裝著每一頁的所有東西,包括文字、線條等等

然裡面的Textsarray就是裝著該頁的所有文字

接下來我們就需要把文字處理,先把文字跟(x,y)都打印出來

const texts = page.Texts;
for (const text of texts) {
  console.log(`${text.x}, ${text.y}, ${text.R.map(r => decodeURIComponent(r.T)).join('')}`)
}

// 2.938, 3.503, 地區
// 4.82, 3.503, :
// 5.442, 3.503, 中西區
// 22.527, 3.615, 建議
// 24.282, 3.615, 區議會選區範圍
// 2.938, 6.368, 代號
// 5.652, 6.368, 建議
// 7.152, 6.368, 選區名稱
// 19.438, 6.368, 區界說明
// 28.527, 6.368, 主要屋
// 30.777, 6.368, 
// 31.723, 6.368, /
// 32.3, 6.368, 範圍
// 40.528, 6.368, 預計人口
// 44.188, 4.642, 標準人口基數
// 44.563, 5.618, 偏離百
// 46.813, 5.618, 分
// 47.57, 5.618, 比
// 45.155, 6.57, (16
// 46.34, 6.57, 599)
// 2.938, 7.529999999999999, A01
// 5.652, 7.553000000000001, 中環
// 40.528, 7.4030000000000005, 13 351
// 45.635, 7.4030000000000005, -
// 45.837, 7.4030000000000005, 19.57
// 14.068, 9.772, 北
// 15.402, 9.772, 區界線
// 28.527, 9.547, 1.
// 29.608, 9.547, 荷李活華庭
// 13.752, 10.8, 東北
// 15.402, 10.8, 區界線
// 14.068, 11.835, 東
// 15.402, 11.835, 區界線
// 13.752, 12.862, 東南
// 15.402, 12.862, 堅尼地道、萬茂里
// 14.068, 13.897, 南
// 15.402, 13.897, 花園道、堅尼地道、下亞厘畢道
// 15.402, 14.895, 麥當勞道
// 13.752, 15.93, 西南
// 15.402, 15.93, 亞畢諾道、贊善里、伊利近街
// 15.402, 16.927, 下亞厘畢道、奧卑利街、卑利街
// 15.402, 17.925, 士丹頓街、雲咸街
// 14.068, 18.96, 西
// 15.402, 18.96, 鴨巴甸街、急庇利街、干諾道中
// 15.402, 19.957, 荷李活道、樓梯街、皇后大道中
// 15.402, 20.955, 士丹頓街
// 13.752, 21.99, 西北
// 15.402, 21.99, 中港道
// 25.678, 34.537, A
// 26.12, 34.537, 1

從中可以看到(y < 7)的都是頁首一些資訊,可以扔掉

然後把相鄰資料的y接近的話(|y2 - y1| < 1.0|)放在同一行

整理後得出這些資料

[ [ 'A01', '中環', '13 351', '-', '19.57' ],
  [ '北', '區界線', '1.', '荷李活華庭' ],
  [ '東北', '區界線' ],
  [ '東', '區界線' ],
  [ '東南', '堅尼地道、萬茂里' ],
  [ '南', '花園道、堅尼地道、下亞厘畢道', '麥當勞道' ],
  [ '西南', '亞畢諾道、贊善里、伊利近街', '下亞厘畢道、奧卑利街、卑利街', '士丹頓街、雲咸街' ],
  [ '西', '鴨巴甸街、急庇利街、干諾道中', '荷李活道、樓梯街、皇后大道中', '士丹頓街' ],
  [ '西北', '中港道' ],
  [ 'A', '1' ] ]

這裡只有兩種資料

  1. 該區的metadata
  2. 整行的區界說明/屋邨範圍資料(或混合顥示)

剩下就只需要一些簡單的regex及constant就可以判定該行到底屬於哪一種 然後就可以把資料放在不同的array了

這個項目也放在Github上,有興趣的朋友可以clone來看看

參考