近期有个项目需要用到号码归属查询,归属地数据库可能比不上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条数据)
号码压缩的结构体,和原文C++版本的基本一致,只是增加了号码类型的储存;(占用8个字节)
号码的结构信息,分别有号码段、地区、类型。
压缩号码归属地并生成二进制文件。
class="code">public void DoWriter(Stream stream, Encoding encoding) { if (_data == null || _data.Count == 0) return; BinaryWriter bw = new BinaryWriter(stream, encoding); //设置偏移量在开头预留写入NumberInfoCompress的总数 this.WriteCount(bw, 0, _phoneInfoCompressCount); //设置偏移量在开头预留号码类型的总数 this.WriteCount(bw, 4, 0); //先读取第一条号码数据 var enumerator = this._data.GetEnumerator(); if (!enumerator.MoveNext()) return; //为什么要预先读取一条数据呢?获取第一条数据是为了和下一条进行对比 var phoneInfo = enumerator.Current; //增加城市信息,并且返回集合所在索引位置 var cityIdx = this.AddCity(phoneInfo.City); //增加号码类型信息,并且返回集合所在索引位置 var cardIdx = this.AddCard(phoneInfo.CardType); //构造一个8字节存储的结构体 var pre = new NumberInfoCompress(phoneInfo.Code, 0, cityIdx, cardIdx); while (enumerator.MoveNext()) { //读取下一条数据,准备和上一条比较 phoneInfo = enumerator.Current; cityIdx = this.AddCity(phoneInfo.City); cardIdx = this.AddCard(phoneInfo.CardType); //和上个号码对比是否连续的,比如 1370875 1370876 1370877。 //1370875开头有3个,表示13708 375:从75开始有3个连续的号码 if (phoneInfo.Code - (pre.GetBegin() + pre.GetSkip()) == 1 && cityIdx == pre.GetCityIndex()) { //设置号码段连续位置 pre.SetSkip((ushort)(phoneInfo.Code - pre.GetBegin())); } else { //递增一个 ++_phoneInfoCompressCount; //写入13708号码段的数据 this.Write(bw, pre); //继续构造一个8字节存储的结构体等待下次循环比较 pre = new NumberInfoCompress(phoneInfo.Code, 0, cityIdx, cardIdx); } } //写入最后的号码数据 this.Write(bw, pre); ++_phoneInfoCompressCount;//记录总数 //写入NumberInfoCompress的总数 this.WriteCount(bw, 0, _phoneInfoCompressCount); //写入号码类型的总数 this.WriteCount(bw, 4, (uint)(_listCard.Count)); //结尾写入城市地区数据 this.WriteCity(bw, encoding); //结尾写入号码类型数据 this.WriteCard(bw, encoding); bw.Close(); bw.Dispose(); }
用来读取areacode.dat,比如查询号码归属地。
public PhoneInfo GetPhoneInfo(Stream stream, Encoding encoding, int number) { PhoneInfo result = new PhoneInfo(); result.Code = number; BinaryReader br = new BinaryReader(stream, encoding); //获取索引总数 int phoneInfoCompressCount = br.ReadInt32(); //号码类型总数 int cardCount = br.ReadInt32(); int left = 0, right = phoneInfoCompressCount - 1; var per = new NumberInfoCompress(); var perSize = Marshal.SizeOf(per); //使用折半查询(二分法) while (left <= right) { //折半 int middle = (left + right) / 2; //索引总数8字节 + middle * NumberInfoCompress字节数 stream.Position = sizeof(int) * 2 + middle * perSize; //读取NumberInfoCompress数据 per.Before = br.ReadUInt16(); per.After = br.ReadUInt16(); per.CityIndex = br.ReadUInt16(); per.CardIndex = br.ReadUInt16(); //判断号码是否匹配 if (number < per.GetBegin()) { right = middle - 1;//在左半区间找 } else if (number > (per.GetBegin() + per.GetSkip())) { left = middle + 1;//在右半区间找 } else { //已找到,直接查询城市和号码类型 result.City = DoFindCityThing(br, phoneInfoCompressCount, per); result.CardType = DoFindCardThing(br, cardCount, per); return result; } } br.Close(); br.Dispose(); return result; } private string DoFindCityThing(BinaryReader br, int phoneInfoCompressCount, NumberInfoCompress infoMiddle) { //计算城市区域信息位置 //sizeof(int) * 2 开头位置储存了一个4字节的NumberInfoCompress总数和类型总数 //phoneInfoCompressCount NumberInfoCompress总数 //Marshal.SizeOf(infoMiddle) NumberInfoCompress占用空间 //infoMiddle.GetCityIndex() 城市的所在位置 //_maxCityLength 城市总数 //偏移量 = 索引总数8字节 + 索引总数 * NumberInfoCompress字节数 + 城市的所在位置 * 城市大小 long totalOffset = sizeof(int) * 2 + phoneInfoCompressCount * Marshal.SizeOf(infoMiddle) + infoMiddle.GetCityIndex() * this._maxCityLength; br.BaseStream.Position = totalOffset;//设置偏移量 char[] charCity = br.ReadChars(this._maxCityLength); return new string(charCity, 0, Array.IndexOf(charCity, '\0')); } private string DoFindCardThing(BinaryReader br, int cardCount, NumberInfoCompress infoMiddle) { //号码类型存储在尾端 //所以偏移量 = (流的总长度 - 类型总数 * 类型大小) + 所在位置 * 类型大小 long totalOffset = (br.BaseStream.Length - cardCount * this._maxCardLength) + infoMiddle.GetCardIndex() * this._maxCardLength; br.BaseStream.Position = totalOffset;//设置偏移量 char[] charCard = br.ReadChars(this._maxCardLength); return new string(charCard, 0, Array.IndexOf(charCard, '\0')); }
封装了手机归属地查询函数。
用来演示如何查询电话号码归属地以及把文本文件生成为压缩过的二进制文件(areacode.dat)。
原作者的压缩算法我们也可以稍作改变,但是用这种算法的前提条件是必须有序且有规律,最后用二分法才会提高查询速度。
项目资源里面的文本文件是每行一个号码段,如:号码,区域,类型;读者可以自行存储到任何数据库等地方,方便日后管理。