Quantcast
Channel: 黑暗執行緒
Viewing all articles
Browse latest Browse all 2311

用 100 行 C# 打造 IP 所屬國家快速查詢功能

$
0
0

講到由 IP 地址查詢所屬國家,解決方案有兩種:第一種是直接呼叫線上查詢 API(付費或免費),再不然就要下載 IP 區段資料庫自寫查詢程式。考量應用場合不一定有 Internet 連線能力,加上擔心線上 API 無法滿足 IIS Log 等超大量 IP 解析的效能要求,選擇取回資料檔自幹。(其實是因為這題目大小難易適中,十分適合練功,一時手癢難耐,就…)

爬文找到一些 IP 國別對應資料來源:

最後決定選用 software77 的資料,看中它每日更新以及號稱 99.95% 的準確率。(各家資料格式大同小異,要更換來源並非難事,微調匯入邏輯即可)

由網站取回 IpToContry.csv 格式如下,前方有一大段註解直接略過即可。資料部分共有七欄,第1、2欄為 IP 區段的起始位址及結束位址(IP 位址不使用 aaa.bbb.ccc.ddd 字串格式,而是將四個 Byte 轉換為整數),第 5 欄為國別代碼,第 7 欄有國家名稱。

一般處理 IP 區段查詢,最常見做法是轉進資料庫後使用 SQL 查詢。評估資料筆數大約 17 萬筆,轉成物件陣列放進記憶體進行查詢對 C# 及當代電腦硬體是一碟小菜,不依賴資料庫的輕巧小程式更貼近我的開發哲學,用一個小類別打死才帥。

先不費太多腦力,用最直覺的 C# LINQ 來解:定義一個範圍物件 IPRange,屬性包含起始位址、結束位址、國別代碼、國家名稱。將資料檔轉為 List<IPRange>,查詢用 .SingleOrDefault(o => ip >= o.Start && ip <= o.End) 就能輕鬆達成, 50 行搞定:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
 
namespace IP2C.Net
{
publicclass RookieIPCountryFinder
    {
publicclass IPRange
        {
publicuint Start;
publicuint End;
publicstring CnCode;
publicstring CnName;
        }
 
        List<IPRange> Ranges = new List<IPRange>();
 
public RookieIPCountryFinder(string path)
        {
foreach (var line in File.ReadAllLines(path))
            {
if (line.StartsWith("#")) continue;
//"87597056","87599103","ripencc","1338422400","ES","ESP","Spain"
                    var p = line.Split(',').Select(o => o.Trim('"')).ToArray();
                    Ranges.Add(new IPRange()
                    {
                        Start = uint.Parse(p[0]),
                        End = uint.Parse(p[1]),
                        CnCode = p[4],
                        CnName = p[6]
                    });
                }
        }
publicuint GetIPAddrDec(string ipAddr)
        {
byte[] b = IPAddress.Parse(ipAddr).GetAddressBytes();
            Array.Reverse(b);
return BitConverter.ToUInt32(b, 0);
        }
 
publicstring GetCountryCode(string ipAddr)
        {
uint ip = GetIPAddrDec(ipAddr);
            var range = Ranges.SingleOrDefault(o => ip >= o.Start && ip <= o.End);;
if (range == null)
return"--";
else
return range.CnCode;
        }
    }
}

為了驗證結果,從 https://www.randomlists.com/ip-addresses取得 1024 筆隨機網址,以 http://www.ip2c.org/168.95.1.1方式查出國別,做好 1024 筆測試資料。以單元測試執行批次查詢與 ip2c.org 查詢結果進行比對,驗證結果是否一致。

測試前先觀察 ip2c.org API 執行速度方便比較,經實測一次耗時約 950 – 975ms。

執行單元測試,隨機 1024 筆資料查詢結果與 ip2c.org 查詢結果一致(綠燈),總查詢時間約 4.5 秒,換算每次查詢約 4.5ms,比呼叫 API 快 200 倍。

不過,LINQ 查詢固然直覺方便,若你以為它會像 SQL WHERE 查詢一樣有效率就錯了,恭喜跌入效能陷阱。如果講求效能,得換一顆更專業的查詢引擎。從已排序陣列找出指定數字落點,二分搜尋法是我心中的首選,原以為得捲袖子自已寫,卻發現 Array.BinarySearch在 .NET 已內建 ,哈里路亞!

配合二分搜尋,匯入資料結構也要調整,我的做法是將開始位址及結束位址轉成數字陣列,使用 Dictionary 對應國別碼。有個問題是範圍與範圍間可能存在未定義的空隙,發現範圍不連續時要補上一段開始、結束範圍指向未定義國別(國別碼填入"—"),才能精準回報未定義。另外,資料中有五個區段被重複定義指向兩個不同國家(實務可能發生,參見 FAQ),處理時也需排除。

查詢核心以 Array.BinarySearch 找出 IP 位址在已排序陣列的相對位置,若在陣列裡找不到該數字,BinarySearch 會傳回最接近位置的補數,轉換後可找到所屬範圍的位址。BinarySearch 版本範例如下,加上簡單的防錯,100 行搞定:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
 
namespace IP2C.Net
{
publicclass IPCountryFinder
    {
        Dictionary<string, string> CountryNames = new Dictionary<string, string>();
        Dictionary<uint, string> IP2CN = new Dictionary<uint, string>();
uint[] IPRanges;
publicstring DupDataInfo = string.Empty;
 
public IPCountryFinder(string path)
        {
if (!File.Exists(path)) thrownew ArgumentException($"{path} not found!");
string dupInfo = null;
            StringBuilder dupData = new StringBuilder();
uint lastRangeEnd = 0;
string unknownCode = "--";
            CountryNames.Add(unknownCode, "Unknown");
int count = 0;
try
            {
foreach (var line in File.ReadAllLines(path))
                {
if (line.StartsWith("#")) continue;
try
                    {
//"87597056","87599103","ripencc","1338422400","ES","ESP","Spain"
                        var p = line.Split(',').Select(o => o.Trim('"')).ToArray();
                        var st = uint.Parse(p[0]);
                        var ed = uint.Parse(p[1]);
                        var cn = p[4];
 
//range gap found
if (lastRangeEnd > 0 && st > lastRangeEnd)
                        {
//padding unknown range
                            IP2CN.Add(lastRangeEnd, unknownCode);
                            IP2CN.Add(st - 1, unknownCode);
                            count += 2;
                        }
 
                        dupInfo = $"{st}-{ed}-{cn}";
                        IP2CN.Add(st, cn);
                        IP2CN.Add(ed, cn);
                        lastRangeEnd = ed + 1;
if (!CountryNames.ContainsKey(cn))
                            CountryNames.Add(cn, p[6]);
                    }
catch (ArgumentException aex)
                    {
                        dupData.AppendLine($"Duplicated {dupInfo}: {aex.Message}");
                    }
                }
                IPRanges = IP2CN.Select(o => o.Key).OrderBy(o => o).ToArray();
            }
catch (Exception ex)
            {
thrownew ApplicationException($"CSV parsing error: {ex.Message}");
            }
            DupDataInfo = dupData.ToString();
        }
publicuint GetIPAddrDec(string ipAddr)
        {
byte[] b = IPAddress.Parse(ipAddr).GetAddressBytes();
            Array.Reverse(b);
return BitConverter.ToUInt32(b, 0);
        }
 
publicstring GetCountryCode(string ipAddr)
        {
uint ip = GetIPAddrDec(ipAddr);
int idx = Array.BinarySearch(IPRanges, ip);
if (idx < 0)
            {
int idxNearest = ~idx;
if (idxNearest > 0) idxNearest--;
                idx = idxNearest;
            }
return IP2CN[IPRanges[idx]];
        }
 
publicstring ConvertCountryCodeToName(string cnCode)
        {
if (CountryNames.ContainsKey(cnCode))
return CountryNames[cnCode];
return cnCode;
        }
 
publicstring GetCountryName(string ipAddr)
        {
return ConvertCountryCodeToName(GetCountryCode(ipAddr));
        }
    }
}

以相同資料重新測試 BinarySearch 引擎版本。

薑!薑!薑!薑~ 1024 筆只花了 9ms!!平均每一筆耗時 0.01 ms,比 LINQ 查詢版快了450 倍,比呼叫 API 快了 9 萬倍!速度快到嚇我一大跳,雖然大部分是 Array.BinarySearch 的功勞,但我很滿意。

完整程式及單元測試已放上 Github,有興趣玩玩的同學請自取。

呼口號時間:

C# 真棒,.NET 好威呀!


Viewing all articles
Browse latest Browse all 2311

Trending Articles