近期,国内多平台上线了 IP 归属地功能,国内显示到省份/地区,国外显示到国家。

该功能在技术层面上有两种实现方案:

  1. 手机设备端开启 GPS 定位功能,APP 端调用设备能力获取到定位信息。
  2. 根据用户访问的 IP 地址获取归属地。

本文主要介绍根据 IP 地址获取归属地的功能实现。

ip2region

Ip2region (2.0 - xdb) 是一个离线 IP 地址管理框架和定位器,支持数十亿数据段,十微秒的搜索性能。提供多种编程语言的 xdb 引擎实现。

GitHub 地址:https://github.com/lionsoul2014/ip2region

maven 依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- org.lionsoul/ip2region -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.6</version>
</dependency>
<!-- commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

拷贝 xdb 数据文件

xdb 数据文件位于 https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb。下载后将其拷贝至项目的 resources 目录下的 ip2region 文件夹内。

编写工具类

完整用法可查看 https://github.com/lionsoul2014/ip2region/tree/master/binding/java。下面提供缓存整个 xdb 数据的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package io.github.llnancy.mojian.log.util;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;

/**
* ip2region utils
*
* @author sunchaser admin@lilu.org.cn
* @since JDK8 2022/12/1
*/
@Slf4j
public class Ip2regionUtils {

/**
* ip2region.xdb 文件位置
*/
private static final String IP2REGION_LOCATION = "ip2region/ip2region.xdb";

/**
* 内网 IP
*/
private static final String LOCALHOST_REGION = "内网IP";

/**
* 未知 IP
*/
private static final String UNKNOWN_REGION = "未知";

/**
* 查询器
*/
private static Searcher searcher;

static {
loadIp2regionXdb();
initSearcher();
}

/**
* 加载 ip2region.xdb 文件
*/
private static void loadIp2regionXdb() {
if (Objects.nonNull(searcher)) {
return;
}
InputStream is = null;
try {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources(IP2REGION_LOCATION);
if (ArrayUtils.isEmpty(resources)) {
log.warn("ip2region.xdb resource file does not exist.");
return;
}
Resource resource = resources[0];
is = resource.getInputStream();
File dest = new File(IP2REGION_LOCATION);
// 注意,使用者需要在 .gitignore 中忽略此处写入到项目下的 ip2region 文件夹
FileUtils.copyInputStreamToFile(is, dest);
} catch (IOException e) {
log.warn("init ip2region.xdb error.", e);
} finally {
try {
if (Objects.nonNull(is)) {
is.close();
}
} catch (IOException ignore) {
// ignore
}
}
}

/**
* 初始化 {@link Searcher} 查询器
*/
private static void initSearcher() {
try {
byte[] buf = Searcher.loadContentFromFile(IP2REGION_LOCATION);
searcher = Searcher.newWithBuffer(buf);
} catch (IOException e) {
log.warn("init ip2region's searcher error.", e);
}
}

/**
* eg. 中国|0|浙江省|杭州市|电信
*
* @param ip ip address
* @return region
*/
@SneakyThrows
public static String searchRegion(String ip) {
return searcher.search(ip);
}

/**
* 查询友好的地理位置
*
* @param ip ip address
* @return friendly region
*/
public static String searchFriendlyRegion(String ip) {
return friendlyRegion(searchRegion(ip));
}

/**
* 返回友好的地理位置
*
* @param region 源地理位置
* @return 友好的地理位置
*/
public static String friendlyRegion(String region) {
if (region.contains(LOCALHOST_REGION)) {
return LOCALHOST_REGION;
}
region = region.replace("|0", StringUtils.EMPTY)
.replace("0|", StringUtils.EMPTY)
.replace("省", StringUtils.EMPTY);
String[] address = region.split("\\|");
if (ArrayUtils.isEmpty(address)) {
return UNKNOWN_REGION;
}
// 国内仅展示省份/地区
if (Objects.equals("中国", address[0])) {
if (address.length > 1) {
return address[1];
}
} else {
// 国外展示国家-省份/地区
if (address.length > 1) {
return buildAddress1(address).toString();
}
}
return buildAddress0(address).toString();
}

private static StringBuilder buildAddress1(String[] address) {
return buildAddress0(address).append(address[1]);
}

private static StringBuilder buildAddress0(String[] address) {
return new StringBuilder().append(address[0]);
}

@SneakyThrows
public static void closeSearcher() {
searcher.close();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testIp2region() {
// 国内 IP
String ip = "183.136.182.140";
String region = Ip2regionUtils.searchRegion(ip);
String friendlyRegion = Ip2regionUtils.searchFriendlyRegion(ip);
System.out.println(region);
System.out.println(friendlyRegion);

// 国外 IP
ip = "67.220.90.13";
region = Ip2regionUtils.searchRegion(ip);
friendlyRegion = Ip2regionUtils.searchFriendlyRegion(ip);
System.out.println(region);
System.out.println(friendlyRegion);
}

运行结果:

1
2
3
4
中国|0|浙江省|杭州市|电信
浙江
美国|0|犹他|盐湖城|0
美国犹他