你知道的越多,你不知道的越多
点赞再看,养成习惯
如果您有疑问或者见解,欢迎指教:
企鹅:869192208
文章目录
- 前言
- 引入jar包
- 实现思路
- 代码实现
- 验证 Guava工具类找出两个 Map 集合的差异数据
- 筛选残联区划和全国区划差异
- 组装完整的区划名称方法
- 区划名称相似度匹配,筛选出可以建立关联关系的区划数据并建立关联关系
- 对筛选出来的相似数据进行去重,保证映射关系的准确性
- 附件
前言
前面的工作中,获取了全国 2022 年行政区划代码的数据,这些数据最终要结合一份第三方公司的区划数据,筛选整合,最终做出同地区不同区划代码的映射表,以下记录相关过程。
- 数据来源
国家统计局 - 爬取好的数据
2023年中国全国5级行政区划(省、市、县、镇、村)
2023全国五级行政区划
数据现状:
- 2022年全国行政区划数据
- 残联区划数据
引入jar包
首先,在 springboot 项目依赖的基础上,针对这次的需求,需要引入 easyexcel 包和 guava 包,还有 hutool 工具,需要在 pom 文件新增以下内容:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.1-jre</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
实现思路
-
将全国区划数据和残联区划存量数据进行筛选匹配,分别找出
- 区划代码和区划名称完全一致的数据
- 区划代码一致,区划名称不一致的数据
- 区划代码和区划名称都不一致的数据
- 全国区划不匹配
- 残联区划不匹配
-
前面筛选出来的数据,需要处理的部分为“区划代码和区划名称都不一致的数据”,将这部分数据中的两个筛选后的文件(全国区划不匹配和残联区划不匹配),单个级别的区划名称,转成完整的区划名称,如:沙太社区居委会–>广东省广州市天河区兴华街道沙太社区居委会
-
将需要对比的国家区划数据和残联区划数据进行相似度匹配,筛选出相似的区划名,这部分数据可以近似看做是能建立关联的数据,但是因为是相似度匹配出来的结果,为了保证筛选出的数据的准确性,需要对匹配结果数据中的区划代码进行去重,宁缺毋滥
代码实现
验证 Guava工具类找出两个 Map 集合的差异数据
引入的类:
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* <h2>验证 Guava工具类找出两个 Map 集合的差异数据</h2>
* @author xymy
* @date 2023-08-02 17:55:31
* @return void
**/
public static void testMapDifferenceTypical() {
Map<Integer, String> left = ImmutableMap.of(1, "a", 2, "b", 3, "c", 4, "d", 5, "e");
Map<Integer, String> right = ImmutableMap.of(1, "a", 3, "f", 5, "g", 6, "z");
MapDifference<Integer, String> diff1 = Maps.difference(left, right);
log.info("两个数据是否相同:{}",diff1.areEqual());
log.info("只有left有的key数据:{}",diff1.entriesOnlyOnLeft());
log.info("只有right有的key数据:{}",diff1.entriesOnlyOnRight());
log.info("left和right的key和value完全相同的数据:{}",diff1.entriesInCommon());
log.info("key相同value不同的数据:{}",diff1.entriesDiffering());
Map<Integer, MapDifference.ValueDifference<String>> integerValueDifferenceMap = diff1.entriesDiffering();
integerValueDifferenceMap.keySet().forEach(f -> {
MapDifference.ValueDifference<String> stringValueDifference = integerValueDifferenceMap.get(f);
log.info("key:{},left的value:{}", f, stringValueDifference.leftValue());
log.info("key:{},right的value:{}", f, stringValueDifference.rightValue());
});
}
输出:
两个数据是否相同:false
只有left有的key数据:{2=b, 4=d}
只有right有的key数据:{6=z}
left和right的key和value完全相同的数据:{1=a}
key相同value不同的数据:{3=(c, f), 5=(e, g)}
key:3,left的value:c
key:3,right的value:f
key:5,left的value:e
key:5,right的value:g
可以看出,全国区划数据和A部门存量数据进行筛选匹配时,只需要将区划代码作为 key ,将区划名称作为 value,就能筛选出想要的数据集合。
筛选残联区划和全国区划差异
- 新建 AreaDifferenceLevelDto 类,用来存储解析出来的残联区划数据和全国区划数据
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.ExcelIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* <h2>2022全国区划和残联区划差异-分层级</h2>
*
* @author xymy
* @date 2023/7/10 15:28
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class AreaDifferenceLevelDto {
@ExcelProperty("ID")
private String id;
@ExcelProperty("DEPT_NAME")
private String deptName;
@ExcelProperty("DEPT_LEVEL")
private String deptLevel;
@ExcelProperty("DEPT_CODE")
private String deptCode;
@ExcelProperty("PARENT_ID")
private String parentId;
@ExcelIgnore
private List<AreaDifferenceLevelDto> childAreaDifferenceLevels;
@ExcelProperty("ALL_NAME")
private String allName;
@ExcelProperty("P_ID")
//为了一层层往前补充完整区划存储的临时父id,处理到哪个层级的完整区划,就存储这个层级上一级的父id
private String pId;
}
- 新建 AreaDifferenceDto 类,用来存储残联区划和全国区划的对比数据
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* <h2>2022全国区划和残联区划差异</h2>
*
* @author xymy
* @date 2023/7/10 15:28
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class AreaDifferenceDto {
//区划代码
private String deptCode;
//区划名称
private String deptName;
}
- 将残联区划数据和全国区划数据做匹配,将其匹配结果输出到 4 个 excel 文件中
/**
* <h2>将全国区划和A单位区划进行筛选,找出区划代码和区划名称都不同的数据,存入两个excel中,分别命名为</h2>
* @author xymy
* @date 2023-08-02 18:22:39
* @return void
**/
public static void areaDifferenceTypical() {
//存储读取到的残联区划信息
List<AreaDifferenceLevelDto> areaClList = new ArrayList<>();
//存储读取到的全国区划信息
List<AreaDifferenceLevelDto> areaQgList = new ArrayList<>();
//读取残联区划数据
EasyExcel.read("D:/area_data/area_code_cl.xlsx", AreaDifferenceLevelDto.class,new AnalysisEventListener<AreaDifferenceLevelDto>() {
@Override
public void invoke(AreaDifferenceLevelDto clArea, AnalysisContext arg1) {
// 读取每条数据
areaClList.add(clArea);
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 读取到列头
log.info(JSON.toJSONString(headMap));
}
@Override
public void doAfterAllAnalysed(AnalysisContext arg0) {
// 读取完毕
log.info("读取残联区划excel结束,数量:{}", areaClList.size());
//读取全国区划数据
EasyExcel.read("D:/area_data/area_code_2023.xlsx", AreaDifferenceLevelDto.class,new AnalysisEventListener<AreaDifferenceLevelDto>() {
@Override
public void invoke(AreaDifferenceLevelDto qgArea, AnalysisContext arg1) {
// 读取每条数据
areaQgList.add(qgArea);
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 读取到列头
log.info(JSON.toJSONString(headMap));
}
@Override
public void doAfterAllAnalysed(AnalysisContext arg0) {
// 读取完毕
log.info("读取全国区划excel结束,数量:{}", areaQgList.size());
//使用 Stream 将列表对象的两个字段抽取出来形成一个 Map,并且在键冲突时将字段值进行合并。
//Collectors.toMap() 方法,传递了三个参数:
// 第一个参数 AreaDifferenceLevelDto::getDeptCode 是用于指定作为键的字段名;
// 第二个参数 AreaDifferenceLevelDto::getDeptName 是用于指定作为值的字段名;
// 第三个参数 (value1, value2) -> value1 + ","+value2 是一个合并函数,用于处理键冲突的情况。在这里,我们将两个字段值通过逗号连接起来。
Map<String, String> areaClMap = areaClList.parallelStream().collect(Collectors.toMap(AreaDifferenceLevelDto::getDeptCode, AreaDifferenceLevelDto::getDeptName, (value1, value2) -> value1 + ","+value2));
Map<String, String> areaQgMap = areaQgList.parallelStream().collect(Collectors.toMap(AreaDifferenceLevelDto::getDeptCode, AreaDifferenceLevelDto::getDeptName, (value1, value2) -> value1 + ","+value2));
MapDifference<String, String> mapDifference = Maps.difference(areaClMap, areaQgMap);
log.info("残联区划独有的数据数量:{}",mapDifference.entriesOnlyOnLeft().size());
log.info("全国区划独有的数据数量:{}",mapDifference.entriesOnlyOnRight().size());
log.info("区划代码和区划名称完全相同的数据数量:{}",mapDifference.entriesInCommon().size());
log.info("区划代码相同,区划名称不同的数据数量:{}",mapDifference.entriesDiffering().size());
// 使用 Stream 将 Map 转换为列表对象
List<AreaDifferenceDto> entriesOnlyOnLeftList = mapDifference.entriesOnlyOnLeft().entrySet().stream()
.map(entry -> new AreaDifferenceDto(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
List<AreaDifferenceDto> entriesOnlyOnRightList = mapDifference.entriesOnlyOnRight().entrySet().stream()
.map(entry -> new AreaDifferenceDto(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
List<AreaDifferenceDto> entriesInCommonList = mapDifference.entriesInCommon().entrySet().stream()
.map(entry -> new AreaDifferenceDto(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
List<AreaDifferenceDto> entriesDifferingList = mapDifference.entriesDiffering().entrySet().stream()
.map(entry -> new AreaDifferenceDto(entry.getKey(), entry.getValue().leftValue()+","+entry.getValue().rightValue()))
.collect(Collectors.toList());
//残联区划独有的数据,将其从所有的残联区划数据中找出来
Set<String> clDeptCodes = entriesOnlyOnLeftList.parallelStream().map(x -> x.getDeptCode()).collect(Collectors.toSet());
List<AreaDifferenceLevelDto> clAreaDifferenceLevelList = areaClList.parallelStream().filter(f -> clDeptCodes.contains(f.getDeptCode()) ).collect(Collectors.toList());
log.info("残联区划独有的数据数量:{}", clAreaDifferenceLevelList.size());
//全国区划独有的数据,将其从所有的全国区划数据中找出来
Set<String> qgDeptCodes = entriesOnlyOnRightList.parallelStream().map(x -> x.getDeptCode()).collect(Collectors.toSet());
List<AreaDifferenceLevelDto> qgAreaDifferenceLevelList = areaQgList.parallelStream().filter(f -> qgDeptCodes.contains(f.getDeptCode()) ).collect(Collectors.toList());
log.info("全国区划独有的数据数量:{}", qgAreaDifferenceLevelList.size());
//调用组装完整的区划名称方法补充完整的区划名称
clAreaDifferenceLevelList = setTopLevelAreaName(clAreaDifferenceLevelList, areaClList);
qgAreaDifferenceLevelList = setTopLevelAreaName(qgAreaDifferenceLevelList, areaQgList);
log.info("补充完整区划名称后的残联区划独有的数据数量:{},补充完整区划名称后的全国区划独有的数据数量:{}", clAreaDifferenceLevelList.size(), qgAreaDifferenceLevelList.size());
String fileName1 = "D:/area_data/2023/全国区划和残联区划差异-残联区划比全国区划多出来的数据" + ".xlsx";
EasyExcel.write(fileName1, AreaDifferenceLevelDto.class).sheet("sheet1").doWrite(clAreaDifferenceLevelList);
String fileName2 = "D:/area_data/2023/全国区划和残联区划差异-全国区划比残联区划多出来的数据" + ".xlsx";
EasyExcel.write(fileName2, AreaDifferenceLevelDto.class).sheet("sheet1").doWrite(qgAreaDifferenceLevelList);
String fileName3 = "D:/area_data/2023/全国区划和残联区划差异-区划代码和区划名称完全相同的数据" + ".xlsx";
EasyExcel.write(fileName3, AreaDifferenceDto.class).sheet("sheet1").doWrite(entriesInCommonList);
String fileName4 = "D:/area_data/2023/全国区划和残联区划差异-区划代码相同区划名称不同的数据" + ".xlsx";
EasyExcel.write(fileName4, AreaDifferenceDto.class).sheet("sheet1").doWrite(entriesDifferingList);
}
}).sheet().doRead();
}
}).sheet().doRead();
}
输出日志
读取残联区划excel结束,数量:681266
读取全国区划excel结束,数量:664487
残联区划代码独有的数据数量:261755
全国区划代码独有的数据数量:244980
区划代码和区划名称完全相同的数据数量:257947
区划代码相同,区划名称不同的数据数量:161560
残联区划独有的数据数量:261759
全国区划独有的数据数量:244980
日志打印发现“残联区划代码独有的数据数量:261755”和“09:37:15.344 残联区划独有的数据数量:261759”这两个数值不一致,查看残联区划excel数据发现这份数据的DEPT_CODE字段有重复数据,不影响后续的业务,这里不用处理。
组装完整的区划名称方法
将前面得到的“全国区划和残联区划差异-残联区划比全国区划多出来的数据”和“全国区划和残联区划差异-全国区划比残联区划多出来的数据”这两份数据,分别将其完整的区划名称组装出来,调用的代码已经在上面给出。
//补充完整的区划名称
clAreaDifferenceLevelList = setTopLevelAreaName(clAreaDifferenceLevelList, areaClList);
qgAreaDifferenceLevelList = setTopLevelAreaName(qgAreaDifferenceLevelList, areaQgList);
log.info("补充完整区划名称后的残联区划独有的数据数量:{},补充完整区划名称后的全国区划独有的数据数量:{}", clAreaDifferenceLevelList.size(), qgAreaDifferenceLevelList.size());
一开始写递归方法来实现,但是加上递归之后,就会报“java.lang.StackOverflowError”,最后折腾许久发现是残联区划有几条错误数据,id和parentId的值是一样的,导致递归进入死循环,于是加了判断条件,记录一下。
- 异常的方法
/**
* <h2>将区划补充完整(省市区镇街村居五级)</h2>
* @author xymy
* @date 2023-08-03 14:27:27
* @param areaList : 需要补充区划名称的数据
* @param allAreaList : 完整的区划数据
* @return java.util.List<com.minstone.appr.projorder.projorder.init.model.dto.AreaDifferenceLevelDto>
**/
public static List<AreaDifferenceLevelDto> setTopLevelAreaName(List<AreaDifferenceLevelDto> areaList, List<AreaDifferenceLevelDto> allAreaList) {
//使用 Collectors.toMap() 方法将流中的对象转换为一个 Map。toMap() 方法接受两个参数,第一个参数是键的提取函数,这里使用 id 作为键;第二个参数是值的提取函数,这里使用 Function.identity() 表示将对象本身作为值。
//这样就得到了一个以 id 为键、AreaDifferenceLevelDto 对象为值的 Map,可以用于快速查找和访问对象。
Map<String, AreaDifferenceLevelDto> allAreaMap = allAreaList.stream()
.collect(Collectors.toMap(AreaDifferenceLevelDto::getId, Function.identity()));
areaList.forEach(area ->{
setParentName(area, allAreaMap);
});
return areaList;
}
/**
* <h2>递归设置完整区划名称</h2>
* @author xymy
* @date 2023-08-03 15:22:44
* @param area :
* @param allAreaMap :
* @return void
**/
private static void setParentName(AreaDifferenceLevelDto area, Map<String, AreaDifferenceLevelDto> allAreaMap) {
if (!"root_node_id".equals(area.getPId())) {
AreaDifferenceLevelDto parentArea = allAreaMap.get(StringUtils.isBlank(area.getPId()) ? area.getParentId() : area.getPId());
if (parentArea != null) {
area.setPId(parentArea.getParentId());
area.setAllName(parentArea.getDeptName() + "-" + (StringUtils.isBlank(area.getAllName()) ? area.getDeptName() : area.getAllName()));
setParentName(area, allAreaMap);
}
}
}
- 修复后的方法
/**
* <h2>将区划补充完整(省市区镇街村居五级)</h2>
* @author chendw
* @date 2023-08-03 14:27:27
* @param areaList : 需要补充区划名称的数据
* @param allAreaList : 完整的区划数据
* @return java.util.List<com.minstone.appr.projorder.projorder.init.model.dto.AreaDifferenceLevelDto>
**/
public static List<AreaDifferenceLevelDto> setTopLevelAreaName(List<AreaDifferenceLevelDto> areaList, List<AreaDifferenceLevelDto> allAreaList) {
//使用 Collectors.toMap() 方法将流中的对象转换为一个 Map。toMap() 方法接受两个参数,第一个参数是键的提取函数,这里使用 id 作为键;第二个参数是值的提取函数,这里使用 Function.identity() 表示将对象本身作为值。
//这样就得到了一个以 id 为键、AreaDifferenceLevelDto 对象为值的 Map,可以用于快速查找和访问对象。
Map<String, AreaDifferenceLevelDto> allAreaMap = allAreaList.stream()
.collect(Collectors.toMap(AreaDifferenceLevelDto::getId, Function.identity()));
areaList.forEach(area ->{
setParentName(area, allAreaMap);
});
return areaList;
}
/**
* <h2>递归设置完整区划名称</h2>
* @author xymy
* @date 2023-08-03 15:22:44
* @param area :
* @param allAreaMap :
* @return void
**/
private static void setParentName(AreaDifferenceLevelDto area, Map<String, AreaDifferenceLevelDto> allAreaMap) {
if (!"root_node_id".equals(area.getPId()) && !area.getId().equals(area.getParentId())) {
AreaDifferenceLevelDto parentArea = allAreaMap.get(StringUtils.isBlank(area.getPId()) ? area.getParentId() : area.getPId());
if (parentArea != null) {
area.setPId(parentArea.getParentId());
area.setAllName(parentArea.getDeptName() + "-" + (StringUtils.isBlank(area.getAllName()) ? area.getDeptName() : area.getAllName()));
setParentName(area, allAreaMap);
}
}
}
区划名称相似度匹配,筛选出可以建立关联关系的区划数据并建立关联关系
- 相似度匹配方法
需要对 261759 条残联区划数据,循环对比 244980 条国家区划数据,如果直接嵌套循环将进行 261759 * 261759 = 64125719820 次迭代,这是一个非常大的数量级。于是决定先将残联区划的 261759 条数据存到redis的 list 中,然后借助其 rightPop 方法,一个个取出来与国家区划数据进行循环比对。
@Resource
RedisTemplate redisTemplate;
/**
* <h2>根据相似度建立残联区划和全国区划的映射关系</h2>
* @author xymy
* @date 2023-08-04 10:41:36
* @return void
**/
public void getClAndQgAreaNear() {
//存储读取到的残联区划比全国区划多出来的数据
List<AreaDifferenceLevelDto> areaClList = new ArrayList<>();
//存储读取到的全国区划比残联区划多出来的数据
List<AreaDifferenceLevelDto> areaQgList = new ArrayList<>();
//读取残联区划数据
EasyExcel.read("D:/area_data/2023/全国区划和残联区划差异-残联区划比全国区划多出来的数据.xlsx", AreaDifferenceLevelDto.class,new AnalysisEventListener<AreaDifferenceLevelDto>() {
@Override
public void invoke(AreaDifferenceLevelDto clArea, AnalysisContext arg1) {
// 读取每条数据
areaClList.add(clArea);
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 读取到列头
log.info(JSON.toJSONString(headMap));
}
@Override
public void doAfterAllAnalysed(AnalysisContext arg0) {
// 读取完毕
log.info("读取残联区划比全国区划多出来的数据excel结束,数量:{}", areaClList.size());
//读取全国区划数据
EasyExcel.read("D:/area_data/2023/全国区划和残联区划差异-全国区划比残联区划多出来的数据.xlsx", AreaDifferenceLevelDto.class,new AnalysisEventListener<AreaDifferenceLevelDto>() {
@Override
public void invoke(AreaDifferenceLevelDto qgArea, AnalysisContext arg1) {
// 读取每条数据
areaQgList.add(qgArea);
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 读取到列头
log.info(JSON.toJSONString(headMap));
}
@Override
public void doAfterAllAnalysed(AnalysisContext arg0) {
// 读取完毕
log.info("读取全国区划比残联区划多出来的数据excel结束,数量:{}", areaQgList.size());
//存储筛选出相似的数据
List<ApprAreaNear> apprAreaNears = new ArrayList<>();
ListOperations<String, AreaDifferenceLevelDto> listOperations = redisTemplate.opsForList();
ListOperations<String, ApprAreaNear> listOperationsNear = redisTemplate.opsForList();
//数据量太大,直接循环遍历耗时太长,先将全国区划部分的数据放到redis中
listOperations.leftPushAll(AREA_QG_REDIS_KEY, areaQgList);
redisTemplate.expire(AREA_QG_REDIS_KEY, 180L, TimeUnit.DAYS);
log.info("全国区划部分的数据放到redis完成,数量:{}", listOperations.size(AREA_QG_REDIS_KEY));
StopWatch stopWatch = DateUtil.createStopWatch();
// 开始计时
stopWatch.start();
Long size = listOperations.size(AREA_QG_REDIS_KEY);
for (int i=0; i < size; i++){
AreaDifferenceLevelDto qgAreaDifference = listOperations.rightPop(AREA_QG_REDIS_KEY);
String qgAllName = qgAreaDifference.getAllName();
String qgDeptLevel = qgAreaDifference.getDeptLevel();
//只有区划名称互相包含,完整名称不为空和数据层级相同的数据才需要对比
areaClList.parallelStream().forEach(cl -> {
if ((qgAreaDifference.getDeptName().contains(cl.getDeptName()) || cl.getDeptName().contains(qgAreaDifference.getDeptName()))
&& StringUtils.isNotBlank(qgAllName) && StringUtils.isNotBlank(cl.getAllName())
&& qgDeptLevel.equals(cl.getDeptLevel())) {
double v = isNearby(StrUtil.replace(qgAllName, "-", ""), StrUtil.replace(cl.getAllName(), "-", ""));
//这里先初筛相似度大于0.85的数据,并且记录下相似度的值,后面可以在此基础上按需精选
if (v >= 0.85) {
ApprAreaNear apprAreaNear = new ApprAreaNear();
apprAreaNear.setId(IdUtil.simpleUUID());
apprAreaNear.setClDeptName(cl.getDeptName());
apprAreaNear.setClDeptCode(cl.getDeptCode());
apprAreaNear.setClLevel(cl.getDeptLevel());
apprAreaNear.setClName(cl.getAllName());
apprAreaNear.setQgDeptCode(qgAreaDifference.getDeptCode());
apprAreaNear.setQgDeptName(qgAreaDifference.getDeptName());
apprAreaNear.setQgLevel(qgDeptLevel);
apprAreaNear.setQgName(qgAllName);
apprAreaNear.setClData(JSON.toJSONString(cl));
apprAreaNear.setQgData(JSON.toJSONString(qgAreaDifference));
apprAreaNear.setNearLever(String.valueOf(v));
//相似,存入相似表和redis
listOperationsNear.leftPush(AREA_NEAR_REDIS_KEY, apprAreaNear);
areaDao.saveApprAreaNear(apprAreaNear);
apprAreaNears.add(apprAreaNear);
}
}
});
}
// 停止计时
stopWatch.stop();
log.info("全部数据对比完成,相似度大于0.85的数据数量:{}条,耗费时间:{}秒", apprAreaNears.size(), stopWatch.getTotalTimeSeconds());
String fileName = "D:/area_data/2023/全国区划和残联区划映射-相似度大于0.85的数据" + ".xlsx";
EasyExcel.write(fileName, ApprAreaNear.class).sheet("sheet1").doWrite(apprAreaNears);
}
}).sheet().doRead();
}
}).sheet().doRead();
}
/**
* 判断两个字符串的相似
* @param str1
* @param str2
* @return
*/
public static double isNearby(String str1, String str2) {
Map<Character, int[]> vectorMap = new HashMap<>();
char[] chArray1 = str1.toCharArray();
for (char c : chArray1) {
if (vectorMap.containsKey(c)) {
vectorMap.get(c)[0]++;
} else {
int[] arr = new int[2];
arr[0] = 1;
vectorMap.put(c, arr);
}
}
char[] chArray2 = str2.toCharArray();
for (char c : chArray2) {
if (vectorMap.containsKey(c)) {
vectorMap.get(c)[1]++;
} else {
int[] arr = new int[2];
arr[1] = 1;
vectorMap.put(c, arr);
}
}
double vector1Modulo = 0;
double vector2Modulo = 0;
double vectorProduct = 0;
for (Map.Entry<Character, int[]> entry : vectorMap.entrySet()) {
int[] arr = entry.getValue();
vector1Modulo += arr[0] * arr[0];
vector2Modulo += arr[1] * arr[1];
vectorProduct += arr[0] * arr[1];
}
vector1Modulo = Math.sqrt(vector1Modulo);
vector2Modulo = Math.sqrt(vector2Modulo);
if (vector1Modulo == 0 || vector2Modulo == 0) {
return 0d;
} else {
double v = vectorProduct / (vector1Modulo * vector2Modulo);
return v;
}
}
全部数据对比完成,相似度大于0.85的数据数量:156137条,耗费时间:8362.1870015秒
可以看出,这种方式耗时还是很长的,所以为了防止中途出现啥异常,可以把数据放到redis之后,一个个拿出来对比,并且把对比的结果存在redis和数据库中
并且筛选出的数据多达156137条,显然还需要进一步的筛选,通过将相似度值的从大到小的排序,我们还需要对数据进行进一步的筛选,去掉区划代码重复的数据
- 省略建表语句,只保留插入mybatis的sql写法
<insert id="saveApprAreaNear">
insert into appr_area_near(
ID,
CL_DEPT_NAME,
CL_DEPT_CODE,
CL_LEVEL,
QG_DEPT_CODE,
QG_DEPT_NAME,
QG_LEVEL,
CL_DATA,
QG_DATA,
NEAR_LEVER)
values(#{id},#{clDeptName},#{clDeptCode},#{clLevel},#{qgDeptCode},#{qgDeptName},#{qgLevel},
#{clData},#{qgData},#{nearLever})
</insert>
- 另一种方式
//方法二
stopWatch.start();
List<ApprAreaNear> apprAreaNears = areaClList.parallelStream()
.flatMap(cl -> areaQgList.stream()
.filter(qg -> (qg.getDeptName().contains(cl.getDeptName()) || cl.getDeptName().contains(qg.getDeptName()))
&& StringUtils.isNotBlank(qg.getAllName()) && StringUtils.isNotBlank(cl.getAllName())
&& qg.getDeptLevel().equals(cl.getDeptLevel())
&& isNearby(StrUtil.replace(qg.getAllName(), "-", ""), StrUtil.replace(cl.getAllName(), "-", "")) > 0.85)
.map(qg -> new ApprAreaNear(IdUtil.simpleUUID(),
cl.getDeptName(),cl.getDeptCode(),qg.getDeptName(), qg.getDeptCode(),
cl.getDeptLevel(), qg.getDeptLevel(),JSON.toJSONString(cl), JSON.toJSONString(qg),
String.valueOf(isNearby(StrUtil.replace(qg.getAllName(), "-", ""), StrUtil.replace(cl.getAllName(), "-", ""))),
cl.getAllName(), qg.getAllName()
))
)
.collect(Collectors.toList());
// 停止计时
stopWatch.stop();
log.info("全部数据对比完成,相似度大于0.85的数据数量:{}条,耗费时间:{}秒", apprAreaNears.size(), stopWatch.getTotalTimeSeconds());
读取残联区划比全国区划多出来的数据excel结束,数量:261759
读取全国区划比残联区划多出来的数据excel结束,数量:244980
全部数据对比完成,相似度大于0.85的数据数量:156132条,耗费时间:758.028423秒
数量有误差,后续还要找一下原因。先记录一下。
对筛选出来的相似数据进行去重,保证映射关系的准确性
对前面筛选出来的数据,还不能满足映射的要求,需要对数据进行处理
- 只提取同层级,区划名称互相包含的数据,比如:西南村-西南村民委员会
- 对数据进行区划代码的去重,保证映射表中区划代码的唯一
- 筛选精确的映射关系数据
@Resource
private SqlSessionFactory sqlSessionFactory;
/**
* <h2>筛选精确的映射关系数据</h2>
* @author xymy
* @date 2023-08-06 21:51:15
* @return void
**/
public void AccurateFiltrateArea(){
ListOperations<String, ApprAreaNear> listOperationsNear = redisTemplate.opsForList();
// 需要处理的数据量大,一次获取会导致redis超时等异常,通过分页获取整个List对象
long pageSize = 10000L;
long totalItems = listOperationsNear.size(AREA_NEAR_REDIS_KEY);
long totalPages = (long) Math.ceil((double) totalItems / pageSize);
List<ApprAreaNear> apprAreaNearAll = new ArrayList<>();
for (long pageIndex = 1; pageIndex <= totalPages; pageIndex++) {
long start = (pageIndex - 1) * pageSize;
long end = pageIndex * pageSize - 1;
List<ApprAreaNear> pageDataList = redisTemplate.opsForList().range(AREA_NEAR_REDIS_KEY, start, end);
apprAreaNearAll.addAll(pageDataList);
}
log.info("获取存储在redis中的相似度>0.85的区划List总数:{}", apprAreaNearAll.size());
//clDeptCode残联区划代码和qgDeptCode全国区划代码去重后再次筛选出apprAreaNearList
List<ApprAreaNear> apprAreaNearList = apprAreaNearAll.parallelStream().collect(Collectors.groupingBy(ApprAreaNear :: getClDeptCode))
.values()
.parallelStream()
.filter(s -> s.size() == 1)
.flatMap(ss -> ss.parallelStream())
.sorted(Comparator.comparing(ApprAreaNear :: getClDeptCode).reversed())
.collect(Collectors.toList());
apprAreaNearList = apprAreaNearList.parallelStream().collect(Collectors.groupingBy(ApprAreaNear :: getQgDeptCode))
.values()
.parallelStream()
.filter(s -> s.size() == 1)
.flatMap(ss -> ss.parallelStream())
.sorted(Comparator.comparing(ApprAreaNear :: getQgDeptCode).reversed())
.collect(Collectors.toList());
Set<String> clDeptCode = apprAreaNearList.parallelStream().map(ApprAreaNear::getClDeptCode).collect(Collectors.toSet());
Set<String> qgDeptCode = apprAreaNearList.parallelStream().map(ApprAreaNear::getQgDeptCode).collect(Collectors.toSet());
log.info("apprAreaNearList:{},clDeptCode:{}, qgDeptCode:{}", apprAreaNearList.size(),clDeptCode.size(), qgDeptCode.size());
//将apprAreaNearList的数据存入数据库的残联区划和全国区划映射表
//数据量大,如果使用循环插入的方式,耗时会很长,使用mybatis的批量操作提高插入性能
SqlSession sqlSession=sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try{
AreaDao mapper=sqlSession.getMapper(AreaDao.class);
apprAreaNearList.forEach(apprAreaNear->{
ApprClArea apprClArea = new ApprClArea();
apprClArea.setSeq(IdUtil.simpleUUID());
apprClArea.setClAreaCode(apprAreaNear.getClDeptCode());
apprClArea.setClAreaName(apprAreaNear.getClDeptName());
apprClArea.setMcOrgAreaCode(apprAreaNear.getQgDeptCode());
apprClArea.setMcOrgAreaName(apprAreaNear.getQgDeptName());
apprClArea.setCreateTime(new Date());
mapper.addApprClArea(apprClArea);
});
sqlSession.clearCache();
sqlSession.commit();
}catch(Exception e){
log.error("插入数据出错:{}", e.getMessage(), e);
}finally{
sqlSession.close();
}
}
获取存储在redis中的相似度>0.85的区划List总数:156157
apprAreaNearList:147350,clDeptCode:147350, qgDeptCode:147350
下面给出 addApprClArea(ApprClArea apprClArea) 的dao方法
<insert id="addApprClArea" parameterType="com.minstone.appr.projorder.projorder.init.model.ApprClArea">
insert into appr_cl_area(
SEQ,
MC_ORG_AREA_CODE,
MC_ORG_AREA_NAME,
CL_AREA_CODE,
CL_AREA_NAME,
CREATE_TIME)
values(#{seq},#{mcOrgAreaCode},#{mcOrgAreaName},#{clAreaCode},#{clAreaName},#{createTime})
</insert>
@ApiModel("残联行政区划信息映照表")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApprClArea implements Serializable {
private static final long serialVersionUID = 1L;
private String seq;
private String mcOrgAreaCode;
private String mcOrgAreaName;
private String clAreaCode;
private String clAreaName;
private Date createTime;
}
最终存储到映射表的数据为147350条。
至此,该需求完成。
附件
处理的区划数据和处理后的区划数据
2022统计局全国区划数据.xlsx
A部门区划数据.xlsx
2022统计局全国区划数据和A部门区划数据映射相似度 > 0.85 的数据.xlsx