前言
近期有個項目需要用到號碼歸屬查詢,歸屬地數據庫可能比不上ip138,淘寶上也有賣的-,-! 文本提供一個279188條記錄並壓縮成562KB的歸屬地數據。
我在互聯網上搜索了相關文章,要不是數據庫查詢或者是訪問網上的api,到底有沒有更好的方式,我想各大手機軟件的歸屬地都是屬於本地查詢的。
當我發現了Android Jni 使用C++對二進制文件查詢 這篇文章,發現效率真是高,作者的算法也相當出色。
於是直接把它用C#來實現了一個版本,並且加上號碼的類型,效率上沒相差太多,起碼我們的項目已經夠用了。
這是原文的一段話:
隨便去網絡上搜索一個號碼歸屬地數據庫下載,你可能會找到各種格式,access,txt,db等。除了用insert sql語句外,你還可以用CSV文件格式來互相轉換。因為SQLite Expert 支持CSV文件導入,導出。
數據最佳存放方式如上圖中的表1CallerLoc和表2LocationInfo。這樣用一條連表sql語句查詢即可。類似這樣的sql語句:select number, area from CallerLoc join LocationInfo on CallerLoc.location = LocationInfo.location。
假設你有了這樣的xx.db文件,可以把該文件放在Android項目的assets文件下,然后在自定義的ContentProvider中的query方法中,嘗試把xx.db 復制到手機的/data/data/你的項目包名/databases中,查詢用上面提到的sql語句就行了。
這是一個解決方案,但是db文件太大了,280,000條記錄差不多有8MB大小。 別人解壓你的apk,dat文件一下子就被別人竊取走了。
有什么方式可以解決這個問題?分析表1,感覺數據還可以壓縮(用自定義的格式),把數據寫入到一個文件中,通過打開文件來搜索,寫入方式用二進制的話,別人就竊取不了了。Java處理速度慢的話,還可以改用C++,通過JNI橋梁來處理。
相關技術和理論請參考原作者地址:
Android 號碼,來電歸屬地 Jni 使用C++對二進制文件查詢(一) 理論篇
Android 號碼,來電歸屬地 Jni 使用C++對二進制文件查詢(二) C++實現篇
Android 號碼,來電歸屬地 Jni 使用C++對二進制文件查詢(三) APK 實現篇
提供本文所修改過的源代碼下載。
簡單說下修改過的類庫
areacode.dat(562KB)
內嵌的資源文件,此文件是根據areacode.txt(9,522KB)生成而來。(279188條數據)
NumberInfoCompress
號碼壓縮的結構體,和原文C++版本的基本一致,只是增加了號碼類型的儲存;(占用8個字節)
PhoneInfo
號碼的結構信息,分別有號碼段、地區、類型。
PhoneWriter
壓縮號碼歸屬地並生成二進制文件。
1 public void DoWriter(Stream stream, Encoding encoding) { 2 if (_data == null || _data.Count == 0) 3 return; 4 5 BinaryWriter bw = new BinaryWriter(stream, encoding); 6 7 //設置偏移量在開頭預留寫入NumberInfoCompress的總數 8 this.WriteCount(bw, 0, _phoneInfoCompressCount); 9 10 //設置偏移量在開頭預留號碼類型的總數 11 this.WriteCount(bw, 4, 0); 12 13 //先讀取第一條號碼數據 14 var enumerator = this._data.GetEnumerator(); 15 16 if (!enumerator.MoveNext()) 17 return; 18 19 //為什么要預先讀取一條數據呢?獲取第一條數據是為了和下一條進行對比 20 var phoneInfo = enumerator.Current; 21 22 //增加城市信息,並且返回集合所在索引位置 23 var cityIdx = this.AddCity(phoneInfo.City); 24 //增加號碼類型信息,並且返回集合所在索引位置 25 var cardIdx = this.AddCard(phoneInfo.CardType); 26 27 //構造一個8字節存儲的結構體 28 var pre = new NumberInfoCompress(phoneInfo.Code, 0, cityIdx, cardIdx); 29 30 while (enumerator.MoveNext()) { 31 //讀取下一條數據,准備和上一條比較 32 phoneInfo = enumerator.Current; 33 cityIdx = this.AddCity(phoneInfo.City); 34 cardIdx = this.AddCard(phoneInfo.CardType); 35 36 //和上個號碼對比是否連續的,比如 1370875 1370876 1370877。 37 //1370875開頭有3個,表示13708 375:從75開始有3個連續的號碼 38 if (phoneInfo.Code - (pre.GetBegin() + pre.GetSkip()) == 1 && cityIdx == pre.GetCityIndex()) { 39 //設置號碼段連續位置 40 pre.SetSkip((ushort)(phoneInfo.Code - pre.GetBegin())); 41 } else { 42 //遞增一個 43 ++_phoneInfoCompressCount; 44 45 //寫入13708號碼段的數據 46 this.Write(bw, pre); 47 48 //繼續構造一個8字節存儲的結構體等待下次循環比較 49 pre = new NumberInfoCompress(phoneInfo.Code, 0, cityIdx, cardIdx); 50 } 51 } 52 53 //寫入最后的號碼數據 54 this.Write(bw, pre); 55 ++_phoneInfoCompressCount;//記錄總數 56 57 //寫入NumberInfoCompress的總數 58 this.WriteCount(bw, 0, _phoneInfoCompressCount); 59 60 //寫入號碼類型的總數 61 this.WriteCount(bw, 4, (uint)(_listCard.Count)); 62 63 //結尾寫入城市地區數據 64 this.WriteCity(bw, encoding); 65 66 //結尾寫入號碼類型數據 67 this.WriteCard(bw, encoding); 68 69 bw.Close(); 70 bw.Dispose(); 71 }
PhoneReader
用來讀取areacode.dat,比如查詢號碼歸屬地。
1 public PhoneInfo GetPhoneInfo(Stream stream, Encoding encoding, int number) { 2 PhoneInfo result = new PhoneInfo(); 3 4 result.Code = number; 5 6 BinaryReader br = new BinaryReader(stream, encoding); 7 8 //獲取索引總數 9 int phoneInfoCompressCount = br.ReadInt32(); 10 //號碼類型總數 11 int cardCount = br.ReadInt32(); 12 int left = 0, right = phoneInfoCompressCount - 1; 13 14 var per = new NumberInfoCompress(); 15 var perSize = Marshal.SizeOf(per); 16 17 //使用折半查詢(二分法) 18 while (left <= right) { 19 //折半 20 int middle = (left + right) / 2; 21 //索引總數8字節 + middle * NumberInfoCompress字節數 22 stream.Position = sizeof(int) * 2 + middle * perSize; 23 24 //讀取NumberInfoCompress數據 25 per.Before = br.ReadUInt16(); 26 per.After = br.ReadUInt16(); 27 per.CityIndex = br.ReadUInt16(); 28 per.CardIndex = br.ReadUInt16(); 29 30 //判斷號碼是否匹配 31 if (number < per.GetBegin()) { 32 right = middle - 1;//在左半區間找 33 } else if (number > (per.GetBegin() + per.GetSkip())) { 34 left = middle + 1;//在右半區間找 35 } else { 36 //已找到,直接查詢城市和號碼類型 37 result.City = DoFindCityThing(br, phoneInfoCompressCount, per); 38 result.CardType = DoFindCardThing(br, cardCount, per); 39 return result; 40 } 41 } 42 br.Close(); 43 br.Dispose(); 44 return result; 45 } 46 47 private string DoFindCityThing(BinaryReader br, int phoneInfoCompressCount, NumberInfoCompress infoMiddle) { 48 //計算城市區域信息位置 49 //sizeof(int) * 2 開頭位置儲存了一個4字節的NumberInfoCompress總數和類型總數 50 //phoneInfoCompressCount NumberInfoCompress總數 51 //Marshal.SizeOf(infoMiddle) NumberInfoCompress占用空間 52 //infoMiddle.GetCityIndex() 城市的所在位置 53 //_maxCityLength 城市總數 54 //偏移量 = 索引總數8字節 + 索引總數 * NumberInfoCompress字節數 + 城市的所在位置 * 城市大小 55 long totalOffset = sizeof(int) * 2 + phoneInfoCompressCount * Marshal.SizeOf(infoMiddle) 56 + infoMiddle.GetCityIndex() * this._maxCityLength; 57 58 br.BaseStream.Position = totalOffset;//設置偏移量 59 char[] charCity = br.ReadChars(this._maxCityLength); 60 return new string(charCity, 0, Array.IndexOf(charCity, '\0')); 61 } 62 63 private string DoFindCardThing(BinaryReader br, int cardCount, NumberInfoCompress infoMiddle) { 64 //號碼類型存儲在尾端 65 //所以偏移量 = (流的總長度 - 類型總數 * 類型大小) + 所在位置 * 類型大小 66 long totalOffset = (br.BaseStream.Length - cardCount * this._maxCardLength) + infoMiddle.GetCardIndex() * this._maxCardLength; 67 68 br.BaseStream.Position = totalOffset;//設置偏移量 69 char[] charCard = br.ReadChars(this._maxCardLength); 70 return new string(charCard, 0, Array.IndexOf(charCard, '\0')); 71 }
AreaCode
封裝了手機歸屬地查詢函數。
FrmAreaCode
用來演示如何查詢電話號碼歸屬地以及把文本文件生成為壓縮過的二進制文件(areacode.dat)。
結語
原作者的壓縮算法我們也可以稍作改變,但是用這種算法的前提條件是必須有序且有規律,最后用二分法才會提高查詢速度。
項目資源里面的文本文件是每行一個號碼段,如:號碼,區域,類型;讀者可以自行存儲到任何數據庫等地方,方便日后管理。