一開始,我只是為了某個Knockout程式範例,打算花十分鐘把3+2郵遞區號XML轉檔程式稍做整理,改寫成輸出3碼資料JSON而已,沒想到魔鬼都在細節裡,資料裡幾則特殊案例,搞得我手忙腳亂汗水直滴,最後花了一個半小時才達成目的,驗證傳說中"工程師預估的時程根本是放屁只能純參考"所言不虛...

故事是這樣的,中華郵政網站可以下載3碼郵遞區號表(Text、Word及Excel格式)及3+2碼郵遞區號資料檔(Text, Excel, XML),要用程式處理,XML顯然是較佳的資料來源,但偏偏我需要一個行政區/市/鄉/鎮對應一個3碼郵遞區號的資料,只有Text/Excel沒有XML,所以我打算自己由5碼XML中整理出對應:

<NewDataSet>
  <zip32>
    <Zip5>10001</Zip5>
    <City>台北市</City>
    <Area>中正區</Area>
    <Road>中山南路</Road>
    <Scope>   20號</Scope>
  </zip32>
  <zip32>
    <Zip5>10002</Zip5>
    <City>台北市</City>
    <Area>中正區</Area>
    <Road>中山南路</Road>
    <Scope>    7號</Scope>
  </zip32>
  <zip32>
    <Zip5>10005</Zip5>
    <City>台北市</City>
    <Area>中正區</Area>
    <Road>重慶南路1段</Road>
    <Scope>   30號</Scope>
  </zip32>

5碼XML格式如上,有縣市(City),有行政區(Area),有郵遞區號(Zip5),用XDocument + LINQ三兩下就能完成統計,再用Json.NET轉成JSON即可輕鬆搞定。

誰都別想難倒他X的程式魔人

誰都別想 (小河馬Style)

不過,程式寫好了,卻遇到XML內含同一3碼郵遞區號重複對應到多個行政區域衍生的錯誤,追查發現這XML的內情不如我想像得單純,有幾個例外狀況必須克服:

  1. 會出現A行政區的部分路段會用B行政區的郵遞區號的情況
    例如: 五股是248,新莊是242,但是新莊區/五工二路/雙100號以下卻是掛24888,用了五股的248
  2. 嘉義市與新竹市有多個多行政區,但全部共享一個3碼郵遞區號,但在XML等同有多個Area對應到同一個Zip碼
  3. 釣魚台在行政上City應屬宜蘭縣,但多了一筆City=釣魚台的資料重複資料
  4. 東沙群島與南沙群島City應屬高雄市,但也各自多了一筆City=南海島的重複資料

經整理會產生衝突的資料如下: (括號中為筆數)

248=>新北市/五股區(83),新北市/新莊區(15)
260=>宜蘭縣/宜蘭市(217),宜蘭縣/壯圍鄉(1)
300=>新竹市/北區(234),新竹市/東區(344),新竹市/香山區(142),新竹縣/寶山鄉(7)
290=>釣魚台/釣魚台(1),宜蘭縣/釣魚台(1)
600=>嘉義市/東區(268),嘉義市/西區(552)
741=>台南市/善化區(137),台南市/新市區(39)
817=>南海島/東沙群島(1),高雄市/東沙群島(1)
819=>高雄市/南沙群島(1),南海島/南沙群島(1)

第2,3,4點屬特殊邏輯,恐得用Hard Coding硬解,而第一點有點意思,當3碼郵遞區號被兩個以上的行政區共用時,我決定讓程式聰明一點,由資料筆數多的行政區取得擁有權,其餘筆數少的資料略過不計。

最後擬定演算法如下:

  1. 由XDocument讀取XML,取出zip32節點,產生有City, Area, ZipCode(3碼)三個屬性的物件集合
  2. 進行Hard-Coding處理,調整新竹市、嘉義市、釣魚台、東沙/南沙群島的City及Area
  3. 依City, Area, ZipCode分群(Group By),並統計每一群的資料筆數
  4. 找出同一個ZipCode有兩個以上City/Area的資料,比較其資料筆數,只保留最多的一組City/Area,同ZipCode的其餘City/Area刪除忽略
  5. 建立以City為Key的Dictionary,將該City所屬的Area集合在一起
  6. 依XML中City出現的順序將全部資料組裝成一個資料物作,輸出成JSON字串

最後完成的程式碼如下,一些LINQ花式用法的補充說明已直接寫在註解中,其中GroupBy及Select傳回匿名類別/用Except剔除子集合... 等做法頗富"趣味"(魔人觀點)也很好用,讓程式碼精簡不少,不妨參考。

<%@ WebHandler Language="C#" Class="TaiwanZipJson" %>
 
using System;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Specialized;
 
public class TaiwanZipJson : IHttpHandler {
 
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";
        XDocument xd = XDocument.Load(context.Server.MapPath(
            "~/App_Data/zip32_10105_2012_0810upd.xml"));
        var filtered = xd.Root.Elements("zip32")
            //使用Elements("zip32")取出所有節點,用Select轉成物件
            .Select(o => new
            {
                City = o.Element("City").Value,
                Area = o.Element("Area").Value,
                ZipCode = o.Element("Zip5").Value.Substring(0, 3)
            })
            //針對一些例外資料,進行修正
            .Select(o => {
                string area = o.Area;
                string city = o.City;
                //修正行政區域: 新竹市與嘉義市不分區
                if (city == "新竹市" || city == "嘉義市")
                    area = city;
                //南海諸島在行政上應隸屬高雄市
                else if (city == "南海島")
                    city = "高雄市";
                //釣魚台行政區域應屬宜蘭縣
                else if (city == "釣魚台")
                    city = "宜蘭縣";
                //由於匿名類別的屬性都是唯讀的,修正後重新定義新類別傳回
                return new
                {
                    City = city,
                    Area = area,
                    ZipCode = o.ZipCode
                };
            })
            //以City/Area/ZipCode進行GroupBy, 主要在於統計出現的筆數
            //留待之後同一ZipCode有多組City/Area時,判斷誰是多數
            .GroupBy(o => string.Format("{0}/{1}/{2}",
                o.City, o.Area, o.ZipCode))
            //GroupBy後,操作的對象會變成特殊的IGrouping介面(即Select中的g)
            //http://msdn.microsoft.com/en-us/library/bb344977.aspx
            //.Key則是分群依據的屬性值
            //由g本身可以做First()/Last()/Count()等標準LINQ操作
            .Select(g => {
                //同一Group的City/Area/ZipCode都相同,取第一筆當代表
                var o = g.First();
                //再傳回新的匿名類別,加上同City/Area/ZipCode的資料筆數
                return new
                {
                    o.City,
                    o.Area,
                    o.ZipCode,
                    RowCount = g.Count()
                };
            });
 
        //24888之Area標為新莊區,但248為五股區的三碼區號
        //利用GroupBy挑出這種少數特例加以剔除
        var excep = filtered.GroupBy(o => o.ZipCode)
            //找出同一郵遞區號由兩個區域以上共用擁有者
            .Where(g => g.Count() > 1)
            .Select(o => new
            {
                o.Key,
                //將同郵遞區號的區域做成集合
                Areas = o.ToList()
            });
        context.Response.Write("/** 以下郵遞區號被跨行政區使用,筆數少者刪除\r\n");
        foreach (var e in excep) {
            //Debug用,顯示害群之馬
            context.Response.Write( 
                e.Key + "=>" + string.Join(",", e.Areas.Select(o => 
                    string.Format("{0}/{1}({2})", o.City, o.Area, o.RowCount)).ToArray()) +
                "\r\n"
                );
            //刪除筆數少者: 以筆數由大至小排序,只保留第一筆,其餘刪除
            filtered = 
                filtered.Except(e.Areas.OrderByDescending(o => o.RowCount).Skip(1));
        }
        context.Response.Write("**/\r\n");
 
        //產生以城市名稱為Key, JObject為Value的Dictionary
        Dictionary<string, JProperty> cities =
            //取出不重複的城市名清單
            filtered.Select(o => o.City).Distinct()
            //利用ToDictionary()直接轉成Dictionary
            .ToDictionary(o => o, o => new JProperty(
                o, new JObject()));
        
        //將所有的郵遞區號跟區域名歸檔到該城市JObject中
        foreach (var a in filtered)
        {
            (cities[a.City].Value as JObject)
                .Add(new JProperty(a.Area, a.ZipCode));
        }
        
        JObject result = new JObject();
        //依城市在XML的出現順序加入各城市的JObject
        foreach (var city in 
            xd.Root.Descendants("City").Select(o => o.Value).Distinct())
        {
            if (cities.ContainsKey(city))
                result.Add(cities[city]);
        }
        //傳回JSON
        context.Response.Write(result.ToString());
    }
    public bool IsReusable {
        get {
            return false;
        }
    }
}

雖然本文重點在於魚竿及花式釣法,最後還是附上魚獲給急著要下鍋或不在.NET海域捕魚的朋友享用。

{
  "台北市": {
    "中正區": "100",
    "大同區": "103",
    "中山區": "104",
    "松山區": "105",
    "大安區": "106",
    "萬華區": "108",
    "信義區": "110",
    "士林區": "111",
    "北投區": "112",
    "內湖區": "114",
    "南港區": "115",
    "文山區": "116"
  },
  "基隆市": {
    "仁愛區": "200",
    "信義區": "201",
    "中正區": "202",
    "中山區": "203",
    "安樂區": "204",
    "暖暖區": "205",
    "七堵區": "206"
  },
  "新北市": {
    "萬里區": "207",
    "金山區": "208",
    "板橋區": "220",
    "汐止區": "221",
    "深坑區": "222",
    "石碇區": "223",
    "瑞芳區": "224",
    "平溪區": "226",
    "雙溪區": "227",
    "貢寮區": "228",
    "新店區": "231",
    "坪林區": "232",
    "烏來區": "233",
    "永和區": "234",
    "中和區": "235",
    "土城區": "236",
    "三峽區": "237",
    "樹林區": "238",
    "鶯歌區": "239",
    "三重區": "241",
    "新莊區": "242",
    "泰山區": "243",
    "林口區": "244",
    "蘆洲區": "247",
    "五股區": "248",
    "八里區": "249",
    "淡水區": "251",
    "三芝區": "252",
    "石門區": "253"
  },
  "連江縣": {
    "南竿鄉": "209",
    "北竿鄉": "210",
    "莒光鄉": "211",
    "東引鄉": "212"
  },
  "宜蘭縣": {
    "宜蘭市": "260",
    "頭城鎮": "261",
    "礁溪鄉": "262",
    "壯圍鄉": "263",
    "員山鄉": "264",
    "羅東鎮": "265",
    "三星鄉": "266",
    "大同鄉": "267",
    "五結鄉": "268",
    "冬山鄉": "269",
    "蘇澳鎮": "270",
    "南澳鄉": "272",
    "釣魚台": "290"
  },
  "新竹市": {
    "新竹市": "300"
  },
  "新竹縣": {
    "竹北市": "302",
    "湖口鄉": "303",
    "新豐鄉": "304",
    "新埔鎮": "305",
    "關西鎮": "306",
    "芎林鄉": "307",
    "寶山鄉": "308",
    "竹東鎮": "310",
    "五峰鄉": "311",
    "橫山鄉": "312",
    "尖石鄉": "313",
    "北埔鄉": "314",
    "峨眉鄉": "315"
  },
  "桃園縣": {
    "中壢市": "320",
    "平鎮市": "324",
    "龍潭鄉": "325",
    "楊梅市": "326",
    "新屋鄉": "327",
    "觀音鄉": "328",
    "桃園市": "330",
    "龜山鄉": "333",
    "八德市": "334",
    "大溪鎮": "335",
    "復興鄉": "336",
    "大園鄉": "337",
    "蘆竹鄉": "338"
  },
  "苗栗縣": {
    "竹南鎮": "350",
    "頭份鎮": "351",
    "三灣鄉": "352",
    "南庄鄉": "353",
    "獅潭鄉": "354",
    "後龍鎮": "356",
    "通霄鎮": "357",
    "苑裡鎮": "358",
    "苗栗市": "360",
    "造橋鄉": "361",
    "頭屋鄉": "362",
    "公館鄉": "363",
    "大湖鄉": "364",
    "泰安鄉": "365",
    "銅鑼鄉": "366",
    "三義鄉": "367",
    "西湖鄉": "368",
    "卓蘭鎮": "369"
  },
  "台中市": {
    "中區": "400",
    "東區": "401",
    "南區": "402",
    "西區": "403",
    "北區": "404",
    "北屯區": "406",
    "西屯區": "407",
    "南屯區": "408",
    "太平區": "411",
    "大里區": "412",
    "霧峰區": "413",
    "烏日區": "414",
    "豐原區": "420",
    "后里區": "421",
    "石岡區": "422",
    "東勢區": "423",
    "和平區": "424",
    "新社區": "426",
    "潭子區": "427",
    "大雅區": "428",
    "神岡區": "429",
    "大肚區": "432",
    "沙鹿區": "433",
    "龍井區": "434",
    "梧棲區": "435",
    "清水區": "436",
    "大甲區": "437",
    "外埔區": "438",
    "大安區": "439"
  },
  "彰化縣": {
    "彰化市": "500",
    "芬園鄉": "502",
    "花壇鄉": "503",
    "秀水鄉": "504",
    "鹿港鎮": "505",
    "福興鄉": "506",
    "線西鄉": "507",
    "和美鎮": "508",
    "伸港鄉": "509",
    "員林鎮": "510",
    "社頭鄉": "511",
    "永靖鄉": "512",
    "埔心鄉": "513",
    "溪湖鎮": "514",
    "大村鄉": "515",
    "埔鹽鄉": "516",
    "田中鎮": "520",
    "北斗鎮": "521",
    "田尾鄉": "522",
    "埤頭鄉": "523",
    "溪州鄉": "524",
    "竹塘鄉": "525",
    "二林鎮": "526",
    "大城鄉": "527",
    "芳苑鄉": "528",
    "二水鄉": "530"
  },
  "南投縣": {
    "南投市": "540",
    "中寮鄉": "541",
    "草屯鎮": "542",
    "國姓鄉": "544",
    "埔里鎮": "545",
    "仁愛鄉": "546",
    "名間鄉": "551",
    "集集鎮": "552",
    "水里鄉": "553",
    "魚池鄉": "555",
    "信義鄉": "556",
    "竹山鎮": "557",
    "鹿谷鄉": "558"
  },
  "嘉義市": {
    "嘉義市": "600"
  },
  "嘉義縣": {
    "番路鄉": "602",
    "梅山鄉": "603",
    "竹崎鄉": "604",
    "阿里山鄉": "605",
    "中埔鄉": "606",
    "大埔鄉": "607",
    "水上鄉": "608",
    "鹿草鄉": "611",
    "太保市": "612",
    "朴子市": "613",
    "東石鄉": "614",
    "六腳鄉": "615",
    "新港鄉": "616",
    "民雄鄉": "621",
    "大林鎮": "622",
    "溪口鄉": "623",
    "義竹鄉": "624",
    "布袋鎮": "625"
  },
  "雲林縣": {
    "斗南鎮": "630",
    "大埤鄉": "631",
    "虎尾鎮": "632",
    "土庫鎮": "633",
    "東勢鄉": "635",
    "褒忠鄉": "634",
    "台西鄉": "636",
    "崙背鄉": "637",
    "麥寮鄉": "638",
    "斗六市": "640",
    "林內鄉": "643",
    "古坑鄉": "646",
    "莿桐鄉": "647",
    "西螺鎮": "648",
    "二崙鄉": "649",
    "北港鎮": "651",
    "水林鄉": "652",
    "口湖鄉": "653",
    "四湖鄉": "654",
    "元長鄉": "655"
  },
  "台南市": {
    "中西區": "700",
    "東區": "701",
    "南區": "702",
    "北區": "704",
    "安平區": "708",
    "安南區": "709",
    "永康區": "710",
    "歸仁區": "711",
    "新化區": "712",
    "左鎮區": "713",
    "玉井區": "714",
    "楠西區": "715",
    "仁德區": "717",
    "關廟區": "718",
    "南化區": "716",
    "龍崎區": "719",
    "官田區": "720",
    "麻豆區": "721",
    "佳里區": "722",
    "西港區": "723",
    "七股區": "724",
    "將軍區": "725",
    "學甲區": "726",
    "北門區": "727",
    "新營區": "730",
    "後壁區": "731",
    "白河區": "732",
    "東山區": "733",
    "六甲區": "734",
    "下營區": "735",
    "柳營區": "736",
    "鹽水區": "737",
    "善化區": "741",
    "大內區": "742",
    "新市區": "744",
    "安定區": "745",
    "山上區": "743"
  },
  "高雄市": {
    "新興區": "800",
    "前金區": "801",
    "苓雅區": "802",
    "鹽埕區": "803",
    "鼓山區": "804",
    "旗津區": "805",
    "前鎮區": "806",
    "三民區": "807",
    "楠梓區": "811",
    "小港區": "812",
    "左營區": "813",
    "仁武區": "814",
    "大社區": "815",
    "東沙群島": "817",
    "岡山區": "820",
    "路竹區": "821",
    "南沙群島": "819",
    "阿蓮區": "822",
    "田寮區": "823",
    "燕巢區": "824",
    "橋頭區": "825",
    "梓官區": "826",
    "彌陀區": "827",
    "湖內區": "829",
    "永安區": "828",
    "鳳山區": "830",
    "大寮區": "831",
    "林園區": "832",
    "鳥松區": "833",
    "大樹區": "840",
    "旗山區": "842",
    "美濃區": "843",
    "六龜區": "844",
    "內門區": "845",
    "甲仙區": "847",
    "杉林區": "846",
    "桃源區": "848",
    "茄萣區": "852",
    "茂林區": "851",
    "那瑪夏區": "849"
  },
  "澎湖縣": {
    "馬公市": "880",
    "西嶼鄉": "881",
    "望安鄉": "882",
    "湖西鄉": "885",
    "七美鄉": "883",
    "白沙鄉": "884"
  },
 
  "金門縣": {
    "金沙鎮": "890",
    "金湖鎮": "891",
    "金寧鄉": "892",
    "金城鎮": "893",
    "烈嶼鄉": "894",
    "烏坵鄉": "896"
  },
  "屏東縣": {
    "屏東市": "900",
    "三地門鄉": "901",
    "霧台鄉": "902",
    "瑪家鄉": "903",
    "九如鄉": "904",
    "里港鄉": "905",
    "高樹鄉": "906",
    "鹽埔鄉": "907",
    "長治鄉": "908",
    "麟洛鄉": "909",
    "竹田鄉": "911",
    "內埔鄉": "912",
    "萬丹鄉": "913",
    "潮州鎮": "920",
    "泰武鄉": "921",
    "萬巒鄉": "923",
    "來義鄉": "922",
    "崁頂鄉": "924",
    "新埤鄉": "925",
    "南州鄉": "926",
    "林邊鄉": "927",
    "東港鎮": "928",
    "琉球鄉": "929",
    "佳冬鄉": "931",
    "新園鄉": "932",
    "枋寮鄉": "940",
    "枋山鄉": "941",
    "獅子鄉": "943",
    "車城鄉": "944",
    "牡丹鄉": "945",
    "恆春鎮": "946",
    "滿州鄉": "947",
    "春日鄉": "942"
  },
  "台東縣": {
    "台東市": "950",
    "綠島鄉": "951",
    "蘭嶼鄉": "952",
    "卑南鄉": "954",
    "鹿野鄉": "955",
    "關山鎮": "956",
    "海端鄉": "957",
    "池上鄉": "958",
    "延平鄉": "953",
    "東河鄉": "959",
    "成功鎮": "961",
    "長濱鄉": "962",
    "太麻里鄉": "963",
    "大武鄉": "965",
    "達仁鄉": "966",
    "金峰鄉": "964"
  },
  "花蓮縣": {
    "花蓮市": "970",
    "新城鄉": "971",
    "秀林鄉": "972",
    "吉安鄉": "973",
    "壽豐鄉": "974",
    "鳳林鎮": "975",
    "光復鄉": "976",
    "豐濱鄉": "977",
    "瑞穗鄉": "978",
    "玉里鎮": "981",
    "萬榮鄉": "979",
    "富里鄉": "983",
    "卓溪鄉": "982"
  }
}

Comments

# by 當麻許

正好用到 感恩黑大

# by 張旭

教釣魚又送魚,正佛心來的!姍姍來遲的美佛?

# by 貓老大

下載 「3+2郵遞區號查詢應用系統」安裝完之後 在 安裝目錄\DBF 底下有 AREA.DBF 裡面就是三碼郵遞區號

# by Jeffrey

to 貓老大,謝謝你的情資,原來隱藏寶物在別的關卡裡!

# by 貓老大

來到您的地盤,不貢獻一些暗黑情報就說不過去了

Post a comment