5碼郵遞區號轉3碼JSON,一場LINQ花式操槍表演
5 | 20,642 |
一開始,我只是為了某個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的內情不如我想像得單純,有幾個例外狀況必須克服:
- 會出現A行政區的部分路段會用B行政區的郵遞區號的情況
例如: 五股是248,新莊是242,但是新莊區/五工二路/雙100號以下卻是掛24888,用了五股的248 - 嘉義市與新竹市有多個多行政區,但全部共享一個3碼郵遞區號,但在XML等同有多個Area對應到同一個Zip碼
- 釣魚台在行政上City應屬宜蘭縣,但多了一筆City=釣魚台的資料重複資料
- 東沙群島與南沙群島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碼郵遞區號被兩個以上的行政區共用時,我決定讓程式聰明一點,由資料筆數多的行政區取得擁有權,其餘筆數少的資料略過不計。
最後擬定演算法如下:
- 由XDocument讀取XML,取出zip32節點,產生有City, Area, ZipCode(3碼)三個屬性的物件集合
- 進行Hard-Coding處理,調整新竹市、嘉義市、釣魚台、東沙/南沙群島的City及Area
- 依City, Area, ZipCode分群(Group By),並統計每一群的資料筆數
- 找出同一個ZipCode有兩個以上City/Area的資料,比較其資料筆數,只保留最多的一組City/Area,同ZipCode的其餘City/Area刪除忽略
- 建立以City為Key的Dictionary,將該City所屬的Area集合在一起
- 依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 貓老大
來到您的地盤,不貢獻一些暗黑情報就說不過去了