├── .java-version ├── version_explain.png ├── src ├── main │ ├── resources │ │ ├── phone.dat │ │ └── log4j2.properties │ └── java │ │ └── me │ │ └── ihxq │ │ └── projects │ │ └── pna │ │ ├── PhoneNumberInfo.java │ │ ├── Attribution.java │ │ ├── algorithm │ │ ├── LookupAlgorithm.java │ │ ├── SequenceLookupAlgorithmImpl.java │ │ ├── BinarySearchAlgorithmImpl.java │ │ ├── AnotherBinarySearchAlgorithmImpl.java │ │ └── ProspectBinarySearchAlgorithmImpl.java │ │ ├── ISP.java │ │ └── PhoneNumberLookup.java └── test │ ├── resources │ └── log4j2-test.properties │ └── java │ └── me │ └── ihxq │ └── projects │ └── pna │ ├── AlgorithmResultDifferTest.java │ ├── benchmark │ └── BenchmarkRunner.java │ └── PhoneNumberLookupAlgorithmTest.java ├── .travis.yml ├── .gitignore ├── CHANGLOG ├── LICENSE ├── .github └── workflows │ └── maven.yml ├── README.md └── pom.xml /.java-version: -------------------------------------------------------------------------------- 1 | 1.8 2 | -------------------------------------------------------------------------------- /version_explain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EeeMt/phone-number-geo/HEAD/version_explain.png -------------------------------------------------------------------------------- /src/main/resources/phone.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EeeMt/phone-number-geo/HEAD/src/main/resources/phone.dat -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | cache: 3 | directories: 4 | - $HOME/.m2 5 | jdk: 6 | - openjdk8 7 | script: 8 | - mvn clean install -B -V 9 | - mvn test jacoco:report coveralls:report -DrepoToken=${repoToken} -B 10 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | name=PropertiesConfig 2 | appenders = console 3 | 4 | appender.console.type = Console 5 | appender.console.name = STDOUT 6 | appender.console.layout.type = PatternLayout 7 | appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n 8 | 9 | rootLogger.level = info 10 | rootLogger.appenderRefs = stdout 11 | rootLogger.appenderRef.stdout.ref = STDOUT -------------------------------------------------------------------------------- /src/test/resources/log4j2-test.properties: -------------------------------------------------------------------------------- 1 | name=PropertiesConfig 2 | appenders = console 3 | 4 | appender.console.type = Console 5 | appender.console.name = STDOUT 6 | appender.console.layout.type = PatternLayout 7 | appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n 8 | 9 | rootLogger.level = debug 10 | rootLogger.appenderRefs = stdout 11 | rootLogger.appenderRef.stdout.ref = STDOUT -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # output 26 | target 27 | out 28 | 29 | # idea 30 | /.idea 31 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/PhoneNumberInfo.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * 电话号码信息 8 | * 9 | * @author xq.h 10 | * 2019/10/18 20:54 11 | **/ 12 | @Data 13 | @AllArgsConstructor 14 | public class PhoneNumberInfo { 15 | /** 16 | * 号码 17 | */ 18 | private String number; 19 | /** 20 | * 归属地信息 21 | */ 22 | private Attribution attribution; 23 | /** 24 | * 运营商 25 | */ 26 | private ISP isp; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/Attribution.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | /** 8 | * 归属信息 9 | * 10 | * @author xq.h 11 | * 2019/10/18 20:58 12 | **/ 13 | @Data 14 | @AllArgsConstructor 15 | @Builder 16 | public class Attribution { 17 | /** 18 | * 省份 19 | */ 20 | private String province; 21 | /** 22 | * 城市 23 | */ 24 | private String city; 25 | /** 26 | * 邮政编码 27 | */ 28 | private String zipCode; 29 | /** 30 | * 区号 31 | */ 32 | private String areaCode; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/algorithm/LookupAlgorithm.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna.algorithm; 2 | 3 | import me.ihxq.projects.pna.PhoneNumberInfo; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * 查找算子 9 | * 10 | * @author xq.h 11 | * on 2019/10/18 21:24 12 | **/ 13 | public interface LookupAlgorithm { 14 | 15 | /** 16 | * 装载数据. 17 | * 18 | * @param data 来自phone.dat 19 | */ 20 | void loadData(byte[] data); 21 | 22 | /** 23 | * 根据电话号码查找归属地 24 | * @param phoneNumber 电话号码, 11位或前7位 25 | * @return 电话号码归属信息 26 | */ 27 | Optional lookup(String phoneNumber); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /CHANGLOG: -------------------------------------------------------------------------------- 1 | # 版本变更 2 | 3 | 4 | ## 1.0.0-201911 5 | 6 | - 初始版本 7 | 8 | 9 | ## 1.0.1-201911 10 | 11 | - 增加文档注释 12 | - 删除无用类 13 | 14 | ## 1.0.2-201911 15 | 16 | - 增加算法ProspectBinarySearch 17 | 18 | ## 1.0.3-201911 19 | 20 | - 修复jar包引用phone.dat无法找到的问题 21 | 22 | ## 1.0.4-201911 23 | 24 | - 修复打成jar包后数据读取异常的问题 25 | - 更新junit版本 26 | 27 | ## 1.0.5-201911 28 | 29 | - 修复打成jar包后数据读取异常的问题 30 | 31 | ## 1.0.5-202004 32 | 33 | - 更新数据版本到2020.04 34 | 35 | ## 1.0.6-202004 36 | 37 | - 修复一个bug https://github.com/EeeMt/phone-number-geo/issues/1 38 | - 优化单元测试 39 | 40 | ## 1.0.7-202004 41 | - 修复1300000xxxx无法查询的问题 42 | - 修复单元测试缺陷 43 | 44 | ## 1.0.8-202004 45 | - InputStream没有关闭问题 46 | 47 | ## 1.0.8-202106 48 | - 升级数据版本 49 | 50 | ## 1.0.8-202108 51 | - 升级数据版本 52 | - 升级依赖包版本 53 | 54 | ## 1.0.9-202108 55 | - 修复编译问题, 使用jdk8重新编译 56 | 57 | ## 1.0.9-202302 58 | - 升级数据版本到2023年2月 -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/ISP.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.Arrays; 6 | import java.util.Optional; 7 | 8 | /** 9 | * 运营商 10 | * 11 | * @author xq.h 12 | * on 2019/10/18 20:57 13 | **/ 14 | @SuppressWarnings("SpellCheckingInspection") 15 | public enum ISP { 16 | CHINA_MOBILE("中国移动", 1), 17 | CHINA_UNICOM("中国联通", 2), 18 | CHINA_TELECOM("中国电信", 3), 19 | CHINA_TELECOM_VIRTUAL("中国电信虚拟运营商", 4), 20 | CHINA_UNICOM_VIRTUAL("中国联通虚拟运营商", 5), 21 | CHINA_MOBILE_VIRTUAL("中国移动虚拟运营商", 6), 22 | UNKNOWN("未知", -1), 23 | ; 24 | /** 25 | * 中文名 26 | */ 27 | @Getter 28 | private final String cnName; 29 | private final int value; 30 | 31 | ISP(String cnName, int value) { 32 | this.cnName = cnName; 33 | this.value = value; 34 | } 35 | 36 | public static Optional of(int value) { 37 | return Arrays.stream(values()) 38 | .filter(v -> v.value == value) 39 | .findAny(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EeeMt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "master" ] 14 | pull_request: 15 | branches: [ "master" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up JDK 8 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '8' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | - name: Update dependency graph 35 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 36 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/PhoneNumberLookup.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.algorithm.BinarySearchAlgorithmImpl; 5 | import me.ihxq.projects.pna.algorithm.LookupAlgorithm; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.InputStream; 9 | import java.util.Arrays; 10 | import java.util.Optional; 11 | 12 | import static java.util.Objects.requireNonNull; 13 | 14 | /** 15 | * 电话号码归属信息查询 16 | * 17 | * @author xq.h 18 | * 2019/10/18 21:25 19 | **/ 20 | @Slf4j 21 | public class PhoneNumberLookup { 22 | private static final String PHONE_NUMBER_GEO_PHONE_DAT = "phone.dat"; 23 | private final LookupAlgorithm lookupAlgorithm; 24 | /** 25 | * 数据版本hash值, 版本:202302 26 | */ 27 | private static final int DATA_HASH = 1990190400; 28 | 29 | private void init() { 30 | try { 31 | byte[] allBytes; 32 | try (final InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(PHONE_NUMBER_GEO_PHONE_DAT); 33 | final ByteArrayOutputStream output = new ByteArrayOutputStream()) { 34 | int n; 35 | byte[] buffer = new byte[1024 * 4]; 36 | while (-1 != (n = requireNonNull(inputStream, "PhoneNumberLookup: Failed to get inputStream.").read(buffer))) { 37 | output.write(buffer, 0, n); 38 | } 39 | allBytes = output.toByteArray(); 40 | } 41 | int hashCode = Arrays.hashCode(allBytes); 42 | log.debug("loaded datasource, size: {}, hash: {}", allBytes.length, hashCode); 43 | if (hashCode != DATA_HASH) { 44 | throw new IllegalStateException("Hash of data not match, expect: " + DATA_HASH + ", actually: " + hashCode); 45 | } 46 | lookupAlgorithm.loadData(allBytes); 47 | } catch (Exception e) { 48 | log.error("failed to init PhoneNumberLookUp", e); 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | /** 54 | * 使用默认算子 55 | */ 56 | public PhoneNumberLookup() { 57 | this(new BinarySearchAlgorithmImpl()); 58 | } 59 | 60 | /** 61 | * @param lookupAlgorithm 算子 62 | */ 63 | public PhoneNumberLookup(LookupAlgorithm lookupAlgorithm) { 64 | this.lookupAlgorithm = lookupAlgorithm; 65 | init(); 66 | } 67 | 68 | /** 69 | * @param phoneNumber 电话号码, 11位, 或前7位 70 | * @return 电话号码归属信息 71 | */ 72 | public Optional lookup(String phoneNumber) { 73 | return lookupAlgorithm.lookup(phoneNumber); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/me/ihxq/projects/pna/AlgorithmResultDifferTest.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.algorithm.LookupAlgorithm; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.platform.commons.util.ReflectionUtils; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.concurrent.ThreadLocalRandom; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | /** 18 | * @author xq.h 19 | * on 2019/10/18 22:30 20 | **/ 21 | @Slf4j 22 | public class AlgorithmResultDifferTest { 23 | 24 | private boolean isDiff(List> candidates) { 25 | boolean allPresent = false; 26 | boolean allAbsent = false; 27 | PhoneNumberInfo temp = null; 28 | for (Optional candidate : candidates) { 29 | if (candidate.isPresent()) { 30 | allPresent = true; 31 | if (allAbsent) { 32 | return false; 33 | } 34 | if (temp == null) { 35 | temp = candidate.get(); 36 | } else { 37 | if (!temp.equals(candidate.get())) { 38 | return false; 39 | } else { 40 | temp = candidate.get(); 41 | } 42 | } 43 | } else { 44 | allAbsent = true; 45 | if (allPresent) { 46 | return false; 47 | } 48 | } 49 | } 50 | return false; 51 | } 52 | 53 | /** 54 | * test all {@link LookupAlgorithm} implements result whether the same or not 55 | */ 56 | @Test 57 | public void testDiff() { 58 | List lookups = ReflectionUtils.findAllClassesInPackage("me.ihxq.projects.pna", v -> Arrays.asList(v.getInterfaces()).contains(LookupAlgorithm.class), v -> true) 59 | .stream() 60 | .map(c -> { 61 | try { 62 | LookupAlgorithm algorithm = (LookupAlgorithm) c.newInstance(); 63 | return new PhoneNumberLookup(algorithm); 64 | } catch (Exception e) { 65 | throw new RuntimeException(e); 66 | } 67 | }) 68 | .collect(Collectors.toList()); 69 | boolean noneMatch = Stream.generate(() -> { 70 | long phoneNumber = (long) (ThreadLocalRandom.current().nextDouble(1D, 2D) * 1000_000_000_0L); 71 | return String.valueOf(phoneNumber); 72 | }).limit(2_000) 73 | .parallel() 74 | .noneMatch(v -> { 75 | List> results = lookups.stream() 76 | .map(l -> l.lookup(v)) 77 | .collect(Collectors.toList()); 78 | return isDiff(results); 79 | }); 80 | assertTrue(noneMatch); 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/algorithm/SequenceLookupAlgorithmImpl.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna.algorithm; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.Attribution; 5 | import me.ihxq.projects.pna.ISP; 6 | import me.ihxq.projects.pna.PhoneNumberInfo; 7 | 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.Optional; 11 | 12 | /** 13 | * @author xq.h 14 | * 2019/10/19 00:12 15 | **/ 16 | @Slf4j 17 | public class SequenceLookupAlgorithmImpl implements LookupAlgorithm { 18 | private ByteBuffer originalByteBuffer; 19 | private int indicesOffset; 20 | 21 | @Override 22 | public void loadData(byte[] data) { 23 | originalByteBuffer = ByteBuffer.wrap(data).asReadOnlyBuffer(); 24 | originalByteBuffer.order(ByteOrder.LITTLE_ENDIAN); 25 | //noinspection unused 26 | int dataVersion = originalByteBuffer.getInt(); 27 | indicesOffset = originalByteBuffer.getInt(4); 28 | } 29 | 30 | @SuppressWarnings("DuplicatedCode") 31 | @Override 32 | public Optional lookup(String phoneNo) { 33 | ByteBuffer byteBuffer = originalByteBuffer.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); 34 | log.trace("try resolve attribution of: {}", phoneNo); 35 | if (phoneNo == null) { 36 | log.debug("phoneNo is null"); 37 | return Optional.empty(); 38 | } 39 | int phoneNoLength = phoneNo.length(); 40 | if (phoneNoLength < 7 || phoneNoLength > 11) { 41 | log.debug("phoneNo {} is not acceptable, length invalid, length should range 7 to 11, actual: {}", 42 | phoneNo, phoneNoLength); 43 | return Optional.empty(); 44 | } 45 | 46 | int attributionIdentity; 47 | try { 48 | attributionIdentity = Integer.parseInt(phoneNo.substring(0, 7)); 49 | } catch (NumberFormatException e) { 50 | log.debug("phoneNo {} is invalid, is it numeric?", phoneNo); 51 | return Optional.empty(); 52 | } 53 | 54 | for (int i = indicesOffset; i < byteBuffer.limit(); i = i + 8 + 1) { 55 | 56 | byteBuffer.position(i); 57 | int phonePrefix = byteBuffer.getInt(); 58 | int infoStart = byteBuffer.getInt(); 59 | byte ispMark = byteBuffer.get(); 60 | if (phonePrefix == attributionIdentity) { 61 | ISP isp = ISP.of(ispMark).orElse(ISP.UNKNOWN); 62 | byteBuffer.position(infoStart); 63 | //noinspection StatementWithEmptyBody 64 | while ((byteBuffer.get()) != 0) { 65 | } 66 | int infoEnd = byteBuffer.position() - 1; 67 | byteBuffer.position(infoStart); 68 | int length = infoEnd - infoStart; 69 | byte[] bytes = new byte[length]; 70 | byteBuffer.get(bytes, 0, length); 71 | String oriString = new String(bytes); 72 | String[] split = oriString.split("\\|"); 73 | Attribution build = Attribution.builder() 74 | .province(split[0]) 75 | .city(split[1]) 76 | .zipCode(split[2]) 77 | .areaCode(split[3]) 78 | .build(); 79 | return Optional.of(new PhoneNumberInfo(phoneNo, build, isp)); 80 | } 81 | } 82 | return Optional.empty(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/me/ihxq/projects/pna/benchmark/BenchmarkRunner.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna.benchmark; 2 | 3 | import me.ihxq.projects.pna.PhoneNumberLookup; 4 | import me.ihxq.projects.pna.algorithm.AnotherBinarySearchAlgorithmImpl; 5 | import me.ihxq.projects.pna.algorithm.BinarySearchAlgorithmImpl; 6 | import me.ihxq.projects.pna.algorithm.ProspectBinarySearchAlgorithmImpl; 7 | import me.ihxq.projects.pna.algorithm.SequenceLookupAlgorithmImpl; 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.Mode; 10 | import org.openjdk.jmh.annotations.Scope; 11 | import org.openjdk.jmh.annotations.State; 12 | import org.openjdk.jmh.runner.Runner; 13 | import org.openjdk.jmh.runner.RunnerException; 14 | import org.openjdk.jmh.runner.options.Options; 15 | import org.openjdk.jmh.runner.options.OptionsBuilder; 16 | import org.openjdk.jmh.runner.options.TimeValue; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.concurrent.ThreadLocalRandom; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.Stream; 24 | 25 | /** 26 | * @author xq.h 27 | * 2020/2/4 11:38 28 | **/ 29 | public class BenchmarkRunner { 30 | public static void main(String[] args) throws RunnerException { 31 | Options opt = new OptionsBuilder() 32 | .include(BenchmarkRunner.class.getSimpleName()) 33 | .warmupForks(0) 34 | .forks(1) 35 | .warmupIterations(5) 36 | .measurementIterations(5) 37 | .warmupTime(TimeValue.seconds(5)) 38 | .measurementTime(TimeValue.seconds(5)) 39 | .timeout(TimeValue.minutes(1)) 40 | .mode(Mode.AverageTime) 41 | .timeUnit(TimeUnit.NANOSECONDS) 42 | .threads(3) 43 | .build(); 44 | 45 | new Runner(opt).run(); 46 | } 47 | 48 | @State(Scope.Thread) 49 | public static class PhoneNumbers { 50 | private final List phoneNumbers; 51 | private int index = 0; 52 | 53 | public PhoneNumbers() { 54 | this.phoneNumbers = Stream.generate(() -> { 55 | long phoneNumber = (long) (ThreadLocalRandom.current().nextDouble(1D, 2D) * 1000_000_000_0L); 56 | return String.valueOf(phoneNumber); 57 | }).limit(2_000_000) 58 | .collect(Collectors.toCollection(ArrayList::new)); 59 | } 60 | 61 | public String getPhoneNumber() { 62 | if (index == phoneNumbers.size()) { 63 | index = 0; 64 | } 65 | return phoneNumbers.get(index++); 66 | } 67 | } 68 | 69 | 70 | private static final PhoneNumberLookup binarySearchLookup = new PhoneNumberLookup(new BinarySearchAlgorithmImpl()); 71 | private static final PhoneNumberLookup anotherBinarySearchLookup = new PhoneNumberLookup(new AnotherBinarySearchAlgorithmImpl()); 72 | private static final PhoneNumberLookup sequenceLookup = new PhoneNumberLookup(new SequenceLookupAlgorithmImpl()); 73 | private static final PhoneNumberLookup prospectBinarySearchLookup = new PhoneNumberLookup(new ProspectBinarySearchAlgorithmImpl()); 74 | 75 | @Benchmark 76 | public void binarySearchLookup(PhoneNumbers phoneNumbers) { 77 | binarySearchLookup.lookup(phoneNumbers.getPhoneNumber()); 78 | } 79 | 80 | @Benchmark 81 | public void anotherBinarySearchLookup(PhoneNumbers phoneNumbers) { 82 | anotherBinarySearchLookup.lookup(phoneNumbers.getPhoneNumber()); 83 | } 84 | 85 | @Benchmark 86 | public void sequenceLookup(PhoneNumbers phoneNumbers) { 87 | sequenceLookup.lookup(phoneNumbers.getPhoneNumber()); 88 | } 89 | 90 | @Benchmark 91 | public void prospectBinarySearchLookup(PhoneNumbers phoneNumbers) { 92 | prospectBinarySearchLookup.lookup(phoneNumbers.getPhoneNumber()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 手机归属地查询 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/me.ihxq.projects/phone-number-geo?color=FE9A2E&label=maven)](https://maven-badges.herokuapp.com/maven-central/me.ihxq.projects/phone-number-geo) 4 | [![Build Status](https://travis-ci.com/EeeMt/phone-number-geo.svg?branch=master)](https://travis-ci.com/EeeMt/phone-number-geo) 5 | [![javadoc](https://javadoc.io/badge2/me.ihxq.projects/phone-number-geo/javadoc.svg)](https://javadoc.io/doc/me.ihxq.projects/phone-number-geo) 6 | [![Coverage Status](https://coveralls.io/repos/github/EeeMt/phone-number-geo/badge.svg?branch=master&service=github&kill_cache=1)](https://coveralls.io/github/EeeMt/phone-number-geo?branch=master) 7 | ![GitHub](https://img.shields.io/github/license/eeemt/phone-number-geo) 8 | 9 | ## 简介 10 | 根据手机号确定手机号运营商即归属地, 支持包括虚拟运营商的中国大陆手机号查询. 11 | 12 | ## 数据源 13 | 14 | 数据源`dat`文件来自[xluohome/phonedata](https://github.com/xluohome/phonedata)提供的数据库, 会不定时同步更新数据库 15 | 16 | 当前数据源版本: `202302` 17 | ## maven 18 | ```xml 19 | 20 | me.ihxq.projects 21 | phone-number-geo 22 | x.x.x-xxxxxx 23 | 24 | ``` 25 | [这里](https://maven-badges.herokuapp.com/maven-central/me.ihxq.projects/phone-number-geo)获取最新版号. 26 | 27 | 28 | 版本号解释: 29 | ![](./version_explain.png) 30 | 31 | ## 示例 32 | ```java 33 | class Demo1{ 34 | public static void main(String[] args){ 35 | PhoneNumberLookup phoneNumberLookup = new PhoneNumberLookup(); 36 | PhoneNumberInfo found = phoneNumberLookup.lookup("18798896741").orElseThrow(RuntimeException::new); 37 | } 38 | } 39 | ``` 40 | ```java 41 | class Demo2{ 42 | public static void main(String[] args){ 43 | PhoneNumberLookup phoneNumberLookup = new PhoneNumberLookup(); 44 | String province = phoneNumberLookup.lookup("130898976761") 45 | .map(PhoneNumberInfo::getAttribution) 46 | .map(Attribution::getProvince) 47 | .orElse("未知"); 48 | } 49 | } 50 | ``` 51 | ```java 52 | class Demo3{ 53 | public static void main(String[] args){ 54 | PhoneNumberLookup phoneNumberLookup = new PhoneNumberLookup(); 55 | PhoneNumberInfo found = phoneNumberLookup.lookup("18798896741").orElseThrow(RuntimeException::new); 56 | found.getNumber(); // 18798896741 57 | found.getAttribution().getProvince(); // 贵州 58 | found.getAttribution().getCity(); // 贵阳 59 | found.getAttribution().getZipCode(); // 550000 60 | found.getAttribution().getAreaCode(); // 0851 61 | found.getIsp(); // ISP.CHINA_MOBILE 62 | } 63 | } 64 | ``` 65 | 66 | ## 对比`libphonenumber` 67 | 对比[libphonenumber](https://github.com/google/libphonenumber), `libphonenumber`有更多功能, 包括验证号码格式, 格式化, 时区等, 68 | 但基于[xluohome/phonedata](https://github.com/xluohome/phonedata)提供的`dat`数据库能囊括包含虚拟运营商号段的更多号段. 69 | 70 | 至于速度, 未做比较, 但本仓库实现已足够快, 选择时建议更多权衡易用性, 功能和数据覆盖范围. 71 | 72 | ## Benchmark 73 | 74 | 工程里已内置四种算法, 跑分情况如下: 75 | ``` 76 | Benchmark Mode Cnt Score Error Units 77 | BenchmarkRunner.anotherBinarySearchLookup avgt 5 390.483 ± 3.544 ns/op 78 | BenchmarkRunner.binarySearchLookup avgt 5 386.357 ± 3.739 ns/op 79 | BenchmarkRunner.prospectBinarySearchLookup avgt 5 304.622 ± 1.899 ns/op 80 | BenchmarkRunner.sequenceLookup avgt 5 1555265.227 ± 48814.379 ns/op 81 | ``` 82 | 性能测试源码位于`me.ihxq.projects.pna.benchmark.BenchmarkRunner`, 基于`JMH` 83 | 84 | 测试样本在每次启动时生成, 供所有算子测试使用, 所以每次测试结果有差异, 结果可用于横向比较, 不适用于纵向比较. 85 | 86 | 默认使用的是`me.ihxq.projects.pna.algorithm.BinarySearchAlgorithmImpl`, 87 | 可以通过`new PhoneNumberLookup(new AlgorithmYouLike());`使用其他算法; 88 | 89 | 也可自行实现算法, 实现`me.ihxq.projects.pna.algorithm.LookupAlgorithm`即可. 90 | 91 | ## 感谢 92 | - 感谢[xluohome/phonedata](https://github.com/xluohome/phonedata)共享的数据库 93 | - 也参考了@fengjiajie 的java实现[fengjiajie/phone-number-geo](https://github.com/fengjiajie/phone-number-geo) 94 | 95 | 96 | ## Todo 97 | - [x] 发布到`maven`中央仓库 98 | -------------------------------------------------------------------------------- /src/test/java/me/ihxq/projects/pna/PhoneNumberLookupAlgorithmTest.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.algorithm.*; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | 8 | import java.util.concurrent.ThreadLocalRandom; 9 | import java.util.stream.Stream; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | /** 14 | * @author xq.h 15 | * on 2019/10/18 22:30 16 | **/ 17 | @Slf4j 18 | public class PhoneNumberLookupAlgorithmTest { 19 | 20 | private PhoneNumberLookup constructLookup(LookupAlgorithm algorithm) { 21 | PhoneNumberLookup phoneNumberLookup; 22 | if (algorithm == null) { 23 | phoneNumberLookup = new PhoneNumberLookup(); 24 | } else { 25 | phoneNumberLookup = new PhoneNumberLookup(algorithm); 26 | } 27 | return phoneNumberLookup; 28 | } 29 | 30 | @ParameterizedTest 31 | @MethodSource("algorithms") 32 | public void invalid(LookupAlgorithm algorithm) { 33 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 34 | assertFalse(phoneNumberLookup.lookup(null).isPresent()); 35 | assertFalse(phoneNumberLookup.lookup("000").isPresent()); 36 | assertFalse(phoneNumberLookup.lookup("136800O0000").isPresent()); // it's 'O', a letter, which should be '0' 37 | assertFalse(phoneNumberLookup.lookup("-1").isPresent()); 38 | assertFalse(phoneNumberLookup.lookup("130898976761").isPresent()); 39 | } 40 | 41 | @ParameterizedTest 42 | @MethodSource("algorithms") 43 | public void lookup(LookupAlgorithm algorithm) { 44 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 45 | PhoneNumberInfo found = phoneNumberLookup.lookup("18798896741").orElseThrow(RuntimeException::new); 46 | assertNotNull(found.getAttribution()); 47 | assertEquals(found.getNumber(), "18798896741"); 48 | assertEquals(found.getAttribution().getProvince(), "贵州"); 49 | assertEquals(found.getAttribution().getCity(), "贵阳"); 50 | assertEquals(found.getAttribution().getZipCode(), "550000"); 51 | assertEquals(found.getAttribution().getAreaCode(), "0851"); 52 | assertEquals(found.getIsp(), ISP.CHINA_MOBILE); 53 | assertNotNull(found.getIsp().getCnName()); 54 | } 55 | 56 | @ParameterizedTest 57 | @MethodSource("algorithms") 58 | public void lookupVirtual(LookupAlgorithm algorithm) { 59 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 60 | PhoneNumberInfo found = phoneNumberLookup.lookup("17048978123").orElseThrow(RuntimeException::new); 61 | assertNotNull(found.getAttribution()); 62 | assertEquals(found.getNumber(), "17048978123"); 63 | assertEquals(found.getAttribution().getProvince(), "陕西"); 64 | assertEquals(found.getAttribution().getCity(), "西安"); 65 | assertEquals(found.getAttribution().getZipCode(), "710000"); 66 | assertEquals(found.getAttribution().getAreaCode(), "029"); 67 | assertEquals(found.getIsp(), ISP.CHINA_UNICOM_VIRTUAL); 68 | } 69 | 70 | @ParameterizedTest 71 | @MethodSource("algorithms") 72 | public void concurrencyLookup(LookupAlgorithm algorithm) { 73 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 74 | //noinspection ResultOfMethodCallIgnored 75 | Stream.generate(() -> { 76 | long phoneNumber = (long) (ThreadLocalRandom.current().nextDouble(1D, 2D) * 1000_000_000_0L); 77 | return String.valueOf(phoneNumber); 78 | }).limit(2_000) 79 | .parallel() 80 | .forEach(v -> phoneNumberLookup.lookup(v).isPresent()); 81 | } 82 | 83 | @ParameterizedTest 84 | @MethodSource("algorithms") 85 | public void lookupFirst(LookupAlgorithm algorithm) { 86 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 87 | assertTrue(phoneNumberLookup.lookup("13000000000").isPresent()); 88 | } 89 | 90 | @ParameterizedTest 91 | @MethodSource("algorithms") 92 | public void lookupLast(LookupAlgorithm algorithm) { 93 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 94 | assertTrue(phoneNumberLookup.lookup("19999790000").isPresent()); 95 | } 96 | 97 | @ParameterizedTest 98 | @MethodSource("algorithms") 99 | public void lookupEqual(LookupAlgorithm algorithm) { 100 | PhoneNumberLookup phoneNumberLookup = constructLookup(algorithm); 101 | assertEquals(phoneNumberLookup.lookup("19999790000"), phoneNumberLookup.lookup("19999790000")); 102 | } 103 | 104 | public static Stream algorithms() { 105 | return Stream.of( 106 | null, 107 | new BinarySearchAlgorithmImpl(), 108 | new ProspectBinarySearchAlgorithmImpl(), 109 | new SequenceLookupAlgorithmImpl(), 110 | new AnotherBinarySearchAlgorithmImpl() 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/algorithm/BinarySearchAlgorithmImpl.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna.algorithm; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.Attribution; 5 | import me.ihxq.projects.pna.ISP; 6 | import me.ihxq.projects.pna.PhoneNumberInfo; 7 | 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.Optional; 11 | 12 | /** 13 | * 二分查找算子 14 | * 15 | * @author xq.h 16 | * 2019/10/19 00:12 17 | **/ 18 | @Slf4j 19 | @SuppressWarnings("DuplicatedCode") 20 | public class BinarySearchAlgorithmImpl implements LookupAlgorithm { 21 | private ByteBuffer originalByteBuffer; 22 | private int indicesStartOffset; 23 | private int indicesEndOffset; 24 | 25 | @Override 26 | public void loadData(byte[] data) { 27 | originalByteBuffer = ByteBuffer.wrap(data) 28 | .asReadOnlyBuffer() 29 | .order(ByteOrder.LITTLE_ENDIAN); 30 | //noinspection unused 31 | int dataVersion = originalByteBuffer.getInt(); // dataVersion not valid, don't know why 32 | indicesStartOffset = originalByteBuffer.getInt(4); 33 | indicesEndOffset = originalByteBuffer.capacity(); 34 | } 35 | 36 | /** 37 | * 对齐 38 | */ 39 | private int alignPosition(int pos) { 40 | int remain = (pos - indicesStartOffset) % 9; 41 | if (pos - indicesStartOffset < 9) { 42 | return pos - remain; 43 | } else if (remain != 0) { 44 | return pos + 9 - remain; 45 | } else { 46 | return pos; 47 | } 48 | } 49 | 50 | private boolean isInvalidPhoneNumber(String phoneNumber) { 51 | if (phoneNumber == null) { 52 | log.debug("phone number is null"); 53 | return true; 54 | } 55 | int phoneNumberLength = phoneNumber.length(); 56 | if (phoneNumberLength < 7 || phoneNumberLength > 11) { 57 | log.debug("phone number {} is not acceptable, length invalid, length should be 11 or 7(for left 7 numbers), actual: {}", 58 | phoneNumber, phoneNumberLength); 59 | return true; 60 | } 61 | return false; 62 | } 63 | 64 | @Override 65 | public Optional lookup(String phoneNumber) { 66 | log.trace("try to resolve attribution of phone number: {}", phoneNumber); 67 | ByteBuffer byteBuffer = originalByteBuffer.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); 68 | if (isInvalidPhoneNumber(phoneNumber)) { 69 | return Optional.empty(); 70 | } 71 | int attributionIdentity; 72 | try { 73 | attributionIdentity = Integer.parseInt(phoneNumber.substring(0, 7)); 74 | } catch (NumberFormatException e) { 75 | log.debug("phone number {} is invalid, is it numeric?", phoneNumber); 76 | return Optional.empty(); 77 | } 78 | int left = indicesStartOffset; 79 | int right = indicesEndOffset; 80 | int mid = (left + right) / 2; 81 | mid = alignPosition(mid); 82 | while (mid >= left && mid <= right) { 83 | if (mid == right) { 84 | return Optional.empty(); 85 | } 86 | int compare = compare(mid, attributionIdentity, byteBuffer); 87 | if (compare == 0) { 88 | return extract(phoneNumber, mid, byteBuffer); 89 | } else if (mid == left) { 90 | return Optional.empty(); 91 | } else if (compare > 0) { 92 | int tempMid = (mid + left) / 2; 93 | right = mid; 94 | mid = alignPosition(tempMid); 95 | } else { 96 | int tempMid = (mid + right) / 2; 97 | left = mid; 98 | mid = alignPosition(tempMid); 99 | } 100 | } 101 | return Optional.empty(); 102 | } 103 | 104 | private Optional extract(String phoneNumber, int indexStart, ByteBuffer byteBuffer) { 105 | byteBuffer.position(indexStart); 106 | //noinspection unused 107 | int prefix = byteBuffer.getInt(); // it is necessary 108 | int infoStartIndex = byteBuffer.getInt(); 109 | byte ispMark = byteBuffer.get(); 110 | ISP isp = ISP.of(ispMark).orElse(ISP.UNKNOWN); 111 | 112 | byte[] bytes = new byte[determineInfoLength(infoStartIndex, byteBuffer)]; 113 | byteBuffer.get(bytes); 114 | String oriString = new String(bytes); 115 | Attribution attribution = parse(oriString); 116 | 117 | return Optional.of(new PhoneNumberInfo(phoneNumber, attribution, isp)); 118 | } 119 | 120 | private int determineInfoLength(int infoStartIndex, ByteBuffer byteBuffer) { 121 | byteBuffer.position(infoStartIndex); 122 | //noinspection StatementWithEmptyBody 123 | while ((byteBuffer.get()) != 0) { 124 | // just to find index of next '\0' 125 | } 126 | int infoEnd = byteBuffer.position() - 1; 127 | byteBuffer.position(infoStartIndex); //reset to info start index 128 | return infoEnd - infoStartIndex; 129 | } 130 | 131 | private Attribution parse(String ori) { 132 | String[] split = ori.split("\\|"); 133 | if (split.length < 4) { 134 | throw new IllegalStateException("content format error"); 135 | } 136 | return Attribution.builder() 137 | .province(split[0]) 138 | .city(split[1]) 139 | .zipCode(split[2]) 140 | .areaCode(split[3]) 141 | .build(); 142 | } 143 | 144 | private int compare(int position, int key, ByteBuffer byteBuffer) { 145 | byteBuffer.position(position); 146 | int phonePrefix = byteBuffer.getInt(); 147 | return Integer.compare(phonePrefix, key); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/algorithm/AnotherBinarySearchAlgorithmImpl.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna.algorithm; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.Attribution; 5 | import me.ihxq.projects.pna.ISP; 6 | import me.ihxq.projects.pna.PhoneNumberInfo; 7 | 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.Arrays; 11 | import java.util.Optional; 12 | 13 | /** 14 | * @author xq.h 15 | * 2019/10/19 00:12 16 | **/ 17 | @Slf4j 18 | public class AnotherBinarySearchAlgorithmImpl implements LookupAlgorithm { 19 | private ByteBuffer originalByteBuffer; 20 | private int indicesStartOffset; 21 | private int indicesEndOffset; 22 | 23 | @Override 24 | public void loadData(byte[] data) { 25 | originalByteBuffer = ByteBuffer.wrap(data).asReadOnlyBuffer(); 26 | originalByteBuffer.order(ByteOrder.LITTLE_ENDIAN); 27 | //noinspection unused 28 | int dataVersion = originalByteBuffer.getInt(); 29 | indicesStartOffset = originalByteBuffer.getInt(4); 30 | indicesEndOffset = originalByteBuffer.limit(); 31 | } 32 | 33 | /** 34 | * 对齐 35 | */ 36 | private int alignPosition(int pos) { 37 | int remain = (pos - indicesStartOffset) % 9; 38 | if (pos - indicesStartOffset < 9) { 39 | return pos - remain; 40 | } else if (remain != 0) { 41 | return pos + 9 - remain; 42 | } else { 43 | return pos; 44 | } 45 | } 46 | 47 | @SuppressWarnings("DuplicatedCode") 48 | @Override 49 | public Optional lookup(String phoneNo) { 50 | log.trace("try to resolve attribution of: {}", phoneNo); 51 | ByteBuffer byteBuffer = originalByteBuffer.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); 52 | if (phoneNo == null) { 53 | log.debug("phoneNo is null"); 54 | return Optional.empty(); 55 | } 56 | int phoneNoLength = phoneNo.length(); 57 | if (phoneNoLength < 7 || phoneNoLength > 11) { 58 | log.debug("phoneNo {} is not acceptable, length invalid, length should range 7 to 11, actual: {}", 59 | phoneNo, phoneNoLength); 60 | return Optional.empty(); 61 | } 62 | 63 | int attributionIdentity; 64 | try { 65 | attributionIdentity = Integer.parseInt(phoneNo.substring(0, 7)); 66 | } catch (NumberFormatException e) { 67 | log.debug("phoneNo {} is invalid, is it numeric?", phoneNo); 68 | return Optional.empty(); 69 | } 70 | int left = indicesStartOffset; 71 | int right = indicesEndOffset; 72 | int mid = (left + right) / 2; 73 | mid = alignPosition(mid); 74 | while (mid >= left && mid <= right) { 75 | if (mid == right) { 76 | return Optional.empty(); 77 | } 78 | int compare = compare(mid, attributionIdentity, byteBuffer); 79 | if (compare == 0) { 80 | break; 81 | } 82 | if (mid == left) { 83 | return Optional.empty(); 84 | } 85 | 86 | if (compare > 0) { 87 | int tempMid = (mid + left) / 2; 88 | tempMid = alignPosition(tempMid); 89 | right = mid; 90 | int remain = (tempMid - indicesStartOffset) % 9; 91 | if (tempMid - indicesStartOffset < 9) { 92 | mid = tempMid - remain; 93 | continue; 94 | } 95 | if (remain != 0) { 96 | mid = tempMid + 9 - remain; 97 | } else { 98 | mid = tempMid; 99 | } 100 | } else { 101 | int tempMid = (mid + right) / 2; 102 | tempMid = alignPosition(tempMid); 103 | left = mid; 104 | int remain = (tempMid - indicesStartOffset) % 9; 105 | if (tempMid - indicesStartOffset < 9) { 106 | mid = tempMid - remain; 107 | continue; 108 | } 109 | if (remain != 0) { 110 | mid = tempMid + 9 - remain; 111 | } else { 112 | mid = tempMid; 113 | } 114 | } 115 | } 116 | 117 | byteBuffer.position(mid); 118 | //noinspection unused 119 | int prefix = byteBuffer.getInt(); 120 | int infoStartIndex = byteBuffer.getInt(); 121 | byte ispMark = byteBuffer.get(); 122 | Optional isp = ISP.of(ispMark); 123 | byteBuffer.position(infoStartIndex); 124 | int resultBufferSize = 200; 125 | int increase = 100; 126 | byte[] bytes = new byte[resultBufferSize]; 127 | byte b; 128 | int i; 129 | for (i = 0; (b = byteBuffer.get()) != 0; i++) { 130 | bytes[i] = b; 131 | if (i == resultBufferSize - 1) { 132 | resultBufferSize = resultBufferSize + increase; 133 | bytes = Arrays.copyOf(bytes, resultBufferSize); 134 | } 135 | } 136 | String oriString = new String(bytes, 0, i); 137 | String[] split = oriString.split("\\|"); 138 | Attribution build = Attribution.builder() 139 | .province(split[0]) 140 | .city(split[1]) 141 | .zipCode(split[2]) 142 | .areaCode(split[3]) 143 | .build(); 144 | return Optional.of(new PhoneNumberInfo(phoneNo, build, isp.orElse(ISP.UNKNOWN))); 145 | } 146 | 147 | private int compare(int position, int key, ByteBuffer byteBuffer) { 148 | byteBuffer.position(position); 149 | int phonePrefix; 150 | try { 151 | phonePrefix = byteBuffer.getInt(); 152 | } catch (Exception e) { 153 | throw new RuntimeException(e); 154 | } 155 | return Integer.compare(phonePrefix, key); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/me/ihxq/projects/pna/algorithm/ProspectBinarySearchAlgorithmImpl.java: -------------------------------------------------------------------------------- 1 | package me.ihxq.projects.pna.algorithm; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import me.ihxq.projects.pna.Attribution; 5 | import me.ihxq.projects.pna.ISP; 6 | import me.ihxq.projects.pna.PhoneNumberInfo; 7 | 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.Optional; 11 | 12 | /** 13 | * 二分查找算子 14 | * 15 | * @author xq.h 16 | * 2019/10/19 00:12 17 | **/ 18 | @Slf4j 19 | @SuppressWarnings("DuplicatedCode") 20 | public class ProspectBinarySearchAlgorithmImpl implements LookupAlgorithm { 21 | private ByteBuffer originalByteBuffer; 22 | private int indicesStartOffset; 23 | private int indicesEndOffset; 24 | 25 | @Override 26 | public void loadData(byte[] data) { 27 | originalByteBuffer = ByteBuffer.wrap(data) 28 | .asReadOnlyBuffer() 29 | .order(ByteOrder.LITTLE_ENDIAN); 30 | //noinspection unused 31 | int dataVersion = originalByteBuffer.getInt(); // dataVersion not valid, don't know why 32 | indicesStartOffset = originalByteBuffer.getInt(4); 33 | indicesEndOffset = originalByteBuffer.capacity(); 34 | } 35 | 36 | /** 37 | * 对齐 38 | */ 39 | private int alignPosition(int pos) { 40 | int remain = (pos - indicesStartOffset) % 9; 41 | if (pos - indicesStartOffset < 9) { 42 | return pos - remain; 43 | } else if (remain != 0) { 44 | return pos + 9 - remain; 45 | } else { 46 | return pos; 47 | } 48 | } 49 | 50 | private boolean isInvalidPhoneNumber(String phoneNumber) { 51 | if (phoneNumber == null) { 52 | log.debug("phone number is null"); 53 | return true; 54 | } 55 | int phoneNumberLength = phoneNumber.length(); 56 | if (phoneNumberLength < 7 || phoneNumberLength > 11) { 57 | log.debug("phone number {} is not acceptable, length invalid, length should be 11 or 7(for left 7 numbers), actual: {}", 58 | phoneNumber, phoneNumberLength); 59 | return true; 60 | } 61 | return false; 62 | } 63 | 64 | @Override 65 | public Optional lookup(String phoneNumber) { 66 | log.trace("try to resolve attribution of phone number: {}", phoneNumber); 67 | ByteBuffer byteBuffer = originalByteBuffer.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); 68 | if (isInvalidPhoneNumber(phoneNumber)) { 69 | return Optional.empty(); 70 | } 71 | int attributionIdentity; 72 | try { 73 | attributionIdentity = Integer.parseInt(phoneNumber.substring(0, 7)); 74 | } catch (NumberFormatException e) { 75 | log.debug("phone number {} is invalid, is it numeric?", phoneNumber); 76 | return Optional.empty(); 77 | } 78 | int left = indicesStartOffset; 79 | int right = indicesEndOffset; 80 | int attributionIdentityPrefix = attributionIdentity / 100_000; 81 | int mid = indicesStartOffset + ((indicesEndOffset - indicesStartOffset) / 7 * (attributionIdentityPrefix - 13)); 82 | mid = alignPosition(mid); 83 | while (mid >= left && mid <= right) { 84 | if (mid == right) { 85 | return Optional.empty(); 86 | } 87 | int compare = compare(mid, attributionIdentity, byteBuffer); 88 | 89 | if (compare == 0) { 90 | return extract(phoneNumber, mid, byteBuffer); 91 | } else if (mid == left) { 92 | return Optional.empty(); 93 | } else if (compare > 0) { 94 | int tempMid = (mid + left) / 2; 95 | right = mid; 96 | mid = alignPosition(tempMid); 97 | } else { 98 | int tempMid = (mid + right) / 2; 99 | left = mid; 100 | mid = alignPosition(tempMid); 101 | } 102 | } 103 | return Optional.empty(); 104 | } 105 | 106 | private Optional extract(String phoneNumber, int indexStart, ByteBuffer byteBuffer) { 107 | byteBuffer.position(indexStart); 108 | //noinspection unused 109 | int prefix = byteBuffer.getInt(); // it is necessary 110 | int infoStartIndex = byteBuffer.getInt(); 111 | byte ispMark = byteBuffer.get(); 112 | ISP isp = ISP.of(ispMark).orElse(ISP.UNKNOWN); 113 | 114 | byte[] bytes = new byte[determineInfoLength(infoStartIndex, byteBuffer)]; 115 | byteBuffer.get(bytes); 116 | String oriString = new String(bytes); 117 | Attribution attribution = parse(oriString); 118 | 119 | return Optional.of(new PhoneNumberInfo(phoneNumber, attribution, isp)); 120 | } 121 | 122 | private int determineInfoLength(int infoStartIndex, ByteBuffer byteBuffer) { 123 | byteBuffer.position(infoStartIndex); 124 | //noinspection StatementWithEmptyBody 125 | while ((byteBuffer.get()) != 0) { 126 | // just to find index of next '\0' 127 | } 128 | int infoEnd = byteBuffer.position() - 1; 129 | byteBuffer.position(infoStartIndex); //reset to info start index 130 | return infoEnd - infoStartIndex; 131 | } 132 | 133 | private Attribution parse(String ori) { 134 | String[] split = ori.split("\\|"); 135 | if (split.length < 4) { 136 | throw new IllegalStateException("content format error"); 137 | } 138 | return Attribution.builder() 139 | .province(split[0]) 140 | .city(split[1]) 141 | .zipCode(split[2]) 142 | .areaCode(split[3]) 143 | .build(); 144 | } 145 | 146 | private int compare(int position, int key, ByteBuffer byteBuffer) { 147 | byteBuffer.position(position); 148 | int phonePrefix = byteBuffer.getInt(); 149 | return Integer.compare(phonePrefix, key); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | me.ihxq.projects 8 | phone-number-geo 9 | 1.0.9-202302 10 | jar 11 | phone-number-geo 12 | https://github.com/EeeMt/phone-number-geo 13 | find geo info for phone numbers which belongs to china. 14 | 15 | 16 | major 17 | eeemt 18 | hxq@live.com 19 | http://www.ihxq.me 20 | +8 21 | 22 | 23 | 24 | 25 | MIT License 26 | http://www.opensource.org/licenses/mit-license.php 27 | repo 28 | 29 | 30 | 31 | scm:git:https://github.com/EeeMt/phone-number-geo.git 32 | scm:git:git@github.com:EeeMt/phone-number-geo.git 33 | https://github.com/EeeMt/phone-number-geo 34 | 35 | 36 | UTF-8 37 | UTF-8 38 | 1.8 39 | 1.8 40 | 41 | 42 | 43 | 44 | org.openjdk.jmh 45 | jmh-core 46 | test 47 | 1.32 48 | 49 | 50 | org.openjdk.jmh 51 | jmh-generator-annprocess 52 | test 53 | 1.32 54 | 55 | 56 | org.projectlombok 57 | lombok 58 | 1.18.20 59 | true 60 | provided 61 | 62 | 63 | org.junit.jupiter 64 | junit-jupiter 65 | 5.8.0 66 | test 67 | 68 | 69 | org.apache.logging.log4j 70 | log4j-api 71 | 2.20.0 72 | 73 | 74 | org.apache.logging.log4j 75 | log4j-slf4j-impl 76 | 2.20.0 77 | true 78 | 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-compiler-plugin 85 | 3.8.1 86 | 87 | 8 88 | 8 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-source-plugin 94 | 3.2.1 95 | 96 | 97 | attach-sources 98 | 99 | jar 100 | 101 | 102 | 103 | 104 | 105 | org.eluder.coveralls 106 | coveralls-maven-plugin 107 | 4.3.0 108 | 109 | 110 | org.jacoco 111 | jacoco-maven-plugin 112 | 0.8.5 113 | 114 | 115 | default-prepare-agent 116 | 117 | prepare-agent 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-javadoc-plugin 125 | 3.1.1 126 | 127 | UTF-8 128 | UTF-8 129 | 130 | 131 | 132 | attach-java-docs 133 | 134 | jar 135 | 136 | 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-surefire-plugin 143 | 2.22.2 144 | 145 | junit:junit 146 | UTF-8 147 | 148 | ${argLine} -Dfile.encoding=UTF-8 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | default 157 | 158 | true 159 | 160 | 161 | 162 | publish 163 | 164 | 165 | 166 | org.simplify4u.plugins 167 | sign-maven-plugin 168 | 0.3.1 169 | 170 | 171 | sign-artifacts 172 | verify 173 | 174 | sign 175 | 176 | 177 | 178 | 179 | 180 | org.sonatype.plugins 181 | nexus-staging-maven-plugin 182 | 1.6.8 183 | true 184 | 185 | sonatype_releases 186 | https://oss.sonatype.org/ 187 | true 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | sonatype_snapshots 198 | https://oss.sonatype.org/content/repositories/snapshots 199 | 200 | 201 | sonatype_releases 202 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 203 | 204 | 205 | 206 | --------------------------------------------------------------------------------