Skip to content

Instantly share code, notes, and snippets.

@appkr
Last active Jun 30, 2021
Embed
What would you like to do?
주소정제기 초벌
package com.vroong.neogeo.support.address.refiner;
import static com.vroong.neogeo.domain.AdditionalInfo.*;
import static com.vroong.neogeo.domain.RegionType.*;
import static com.vroong.neogeo.support.address.Regex.*;
import com.vroong.neogeo.domain.AddressEntry;
import com.vroong.neogeo.domain.AddressEntry.AddressEntryBuilder;
import com.vroong.neogeo.domain.Refinable;
import com.vroong.neogeo.support.address.Regex;
import com.vroong.neogeo.support.address.dictionary.SidoDictionary;
import com.vroong.neogeo.support.address.refiner.preprocessor.PreprocessorFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 주소를 정제합니다
*
* @see Refinable
* @see PreprocessorFactory
*/
@Component
@Slf4j
public class HomemadeAddressRefiner implements Refinable {
private String data = "";
private String preprocessed = "";
private Progress progress = new Progress();
public AddressEntryBuilder refine(String uglyAddress) {
progress.leave("주소 정제 시작; 원본 주소=" + uglyAddress);
this.data = PreprocessorFactory.create(uglyAddress).process();
this.preprocessed = this.data;
progress.leave("전처리 완료; 전처리된 주소=" + preprocessed);
AddressEntryBuilder builder = AddressEntry.builder()
.regionType(RAW);
// 가설: 모든 주소는 "시도" 요소를 가지고 있다
// 가설: "시도"는 항상 추출할 수 있다
handleDepth1(builder);
// 가설: (세종특별자치시를 제외한) 모든 주소는 "시군구" 요소를 가지고 있다
// 가설: "시군구"는 항상 추출할 수 있다 -> WRONG
handleDepth2(builder);
if (Regex.isRoadAddress(data)) {
handleRoadAddress(builder);
} else {
handleJibunAddress(builder);
}
progress.leave("주소 정제 완료; 정제된 주소=" + builder.build().getRawAddress());
if (progress.getStatus() == Status.FAIL) {
builder.rawAddress(uglyAddress);
}
log.info("주소 정제 결과 {}", progress);
return builder;
}
private void handleDepth1(AddressEntryBuilder builder) {
String depth1 = "";
Matcher matcher = SIDO_PATTERN.matcher(data);
if (matcher.find()) {
// "시도"를 추출하고 원본 문자열에서 삭제
depth1 = matcher.group("sido");
data = data.replaceFirst(depth1, "").trim();
depth1 = SidoDictionary.convertSido(depth1);
} else {
progress.leave("시도 추출 실패; 남은 주소=" + data);
progress.fail();
}
builder.region1DepthName(depth1.trim());
}
private void handleDepth2(AddressEntryBuilder builder) {
String depth2 = "";
Matcher matcher = SIGUNGU_PATTERN.matcher(data);
if (!builder.build().getRegion1DepthName().contains("세종")) {
// "세종(특별자치시)"는 시군구가 없음
if (matcher.find()) {
// "시군구"를 추출하고 원본 문자열에서 삭제
depth2 = matcher.group(0);
data = data.replaceFirst(depth2, "").trim();
} else {
progress.leave("시군구 추출 실패; 남은 주소=" + data);
progress.fail();
}
}
builder.region2DepthName(depth2.trim());
}
private void handleRoadAddress(AddressEntryBuilder builder) {
// 도로명 주소이면
String captured = "", depth3 = "", depth4 = "";
Matcher matcher = ROAD_NAME_PATTERN.matcher(data);
if (matcher.find()) {
try {
captured = matcher.group(0);
if (captured != null) { // e.g. " 까치산로2가길 14"
captured = captured.trim();
}
final String[] part = captured.split(" ");
if (part[0].matches(".*(읍|면)$")) { // e.g. 남원읍 태위로 12-11
depth3 = part[0];
depth4 = part[1];
} else { // e.g. 가작로113번길 1, 통일로 1
depth3 = "";
depth4 = part[0];
}
decorateBuildingNumber(builder, matcher);
data = data.replaceFirst(captured, "").trim();
} catch (Exception e) {
progress.leave("도로명 추출 실패; 남은 주소=" + data);
progress.fail();
}
} else {
progress.leave("도로명 추출 실패; 남은 주소=" + data);
progress.fail();
}
builder
.regionType(ROAD_NAME)
.region3DepthName((depth3 != null) ? depth3.trim() : "")
.region4DepthName((depth4 != null) ? depth4.trim() : "");
}
private void handleJibunAddress(AddressEntryBuilder builder) {
// 지번 주소이면
String captured = "", depth3 = "", depth4 = "";
Matcher matcher = JIBUN_PATTERN.matcher(data);
if (matcher.find()) {
try {
captured = matcher.group(0);
depth3 = matcher.group("dong");
depth4 = matcher.group("ri");
decorateJibunNumber(builder, matcher);
data = data.replaceFirst(captured, "").trim();
} catch (Exception e) {
progress.leave("행정동/법정동 추출 실패; 남은 주소=" + data);
progress.fail();
}
} else {
progress.leave("행정동/법정동 추출 실패; 남은 주소=" + data);
progress.fail();
}
builder
.regionType(LEGAL)
.region3DepthName((depth3 != null) ? depth3.trim() : "")
.region4DepthName((depth4 != null) ? depth4.trim() : "");
}
private void decorateBuildingNumber(AddressEntryBuilder builder, Matcher matcher) {
String num1 = "", num2 = "", underground = "", air = "";
try {
air = matcher.group("air");
underground = matcher.group("underground");
num1 = matcher.group("num1");
num2 = matcher.group("num2");
if (num2 != null && num2.contains("0")) {
num2 = "";
}
} catch (Exception e) {
progress.leave("건물번호 추출 실패; 남은 주소=" + data);
progress.fail();
}
if (air != null && air.trim().equals("공중")) {
builder.additionalInfo(AIR);
}
if (underground != null && underground.trim().equals("지하")) {
builder.additionalInfo(UNDER_GROUND);
}
builder
.mainRegionNumber((num1 != null) ? num1.trim() : "")
.subRegionNumber((num2 != null) ? num2.trim() : "");
}
private void decorateJibunNumber(AddressEntryBuilder builder, Matcher matcher) {
String num1 = "", num2 = "", mountain = "";
try {
mountain = matcher.group("mountain");
num1 = matcher.group("num1");
num2 = matcher.group("num2");
if (num2 != null && num2.contains("0")) {
num2 = "";
}
} catch (Exception e) {
progress.leave("지번/건물번호 추출 실패; 남은 주소=" + data);
progress.fail();
}
if (mountain != null && mountain.trim().equals("")) {
builder.additionalInfo(MOUNTAIN);
}
builder
.mainRegionNumber((num1 != null) ? num1.trim() : "")
.subRegionNumber((num2 != null) ? num2.trim() : "");
}
enum Status {
SUCCESS, FAIL
}
@Data
class Progress {
Status status = Status.SUCCESS;
List<String> logs = new ArrayList<>();
void fail() {
this.status = Status.FAIL;
}
void leave(String entry) {
this.logs.add(entry);
}
}
}
package com.vroong.neogeo.support.address.refiner;
import com.vroong.neogeo.domain.AddressEntry;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Slf4j
class HomemadeAddressRefinerTest {
HomemadeAddressRefiner refiner = new HomemadeAddressRefiner();
@ParameterizedTest
@ArgumentsSource(UglyAddressProvider.class)
public void refine(String ugly, String refined) {
final AddressEntry entry = refiner.refine(ugly).build();
log.info("{}", entry);
assertEquals(refined, entry.getRawAddress());
}
}
package com.vroong.neogeo.support.address;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.regex.Pattern.MULTILINE;
public class Regex {
public static boolean isRoadAddress(String query) {
// NOTE. 대구광역시 중구 "남성로"는 도로명/동 이름으로 지번과 도로명을 식별할 수 없음; 아래 두 주소는 같은 위치임
// - 도로명: 대구광역시 중구 남성로 35
// - 지번: 대구광역시 중구 남성로 62
if (query.matches(".*\\b(시장북로|세종로)\\b.*")) {
return false;
}
final Matcher matcher = ROAD_NAME_PATTERN.matcher(query);
return matcher.find();
}
public static boolean isJibunAddress(String query) {
if (query.matches(".*\\b(시장북로|세종로)\\b.*")) {
return true;
}
final Matcher matcher = JIBUN_PATTERN.matcher(query);
return matcher.find();
}
public static final Pattern SIDO_PATTERN = Pattern.compile(
"(?<sido>"
+ "[가-힣]+도|" // e.g. 경기도, 충청북도
+ "([가-힣]+(?:특별시|광역시|자치시|자치도))|" // e.g. 서울특별시, 대구광역시, 세종특별자치시, 제주특별자치도
+ "(충북|충남|경북|경남|전북|전남|강원|경기|제주)|"
+ "(서울|부산|대구|대전|광주|인천|울산|세종)시?"
+ ")", MULTILINE);
public static final Pattern SIGUNGU_PATTERN = Pattern.compile(
"(?<sigungu>"
+ ".+시\\s.+구\\b|" // e.g. (경기도) 성남시 중원구
+ ".+시\\b|" // e.g. (충청북도) 청주시
+ ".+구\\b|" // e.g. (서울특별시) 강남구
+ ".+군\\b" // e.g. (대구광역시) 달성군
+ ")", MULTILINE);
public static final Pattern ROAD_NAME_PATTERN = Pattern.compile(
"(?<road>"
+ "계변고개|" // e.g. (울산광역시 중구) 계변고개
+ "[가-힣0-9]+거리\\b|" // e.g. (울산광역시 중구) 만남의거리
+ "[가-힣]+[읍|면]\\s?[가-힣0-9·.]+로\\b|" // e.g. (제주특별자치도 서귀포시) 남원읍 태위로
+ "[가-힣]+[읍|면]\\s?[가-힣0-9·.]+길\\b|" // e.g. (울산광역시 울주군) 산남면 동향교1길
+ "[가-힣0-9·.]+로\\b|" // e.g. (서울특별시 중랑구) 동일로
+ "[가-힣0-9·.]+길\\b" // e.g. (서울특별시 종로구) 경교장1길
+ ")"
+ "\\s?"
+ "(?<underground>지하|지하 )?"
+ "(?<air>공중|공중 )?"
+ "(?<num1>[0-9]{1,4})"
+ "(?:-)?"
+ "(?<num2>[0-9]{1,4})?", MULTILINE);
public static final Pattern JIBUN_PATTERN = Pattern.compile(
"(?<dong>[가-힣0-9.]+[0-9]{0,2}[읍|면|동|가])" // e.g. (서울특별시 종로구) 종로5가
+ "\\s"
+ "(?<ri>[가-힣0-9]+리(?:\\([一-龥]+\\))?)?" // e.g. (경기도 남양주시 화도읍) 녹촌리, (경북 경산시 진량읍) 평사리(平沙)
+ "(?:[가-힣a-zA-Z0-9()\\s]*?)?"
+ "\\b(?<mountain>산|산 )?"
+ "(?<num1>[0-9]{1,4})"
+ "(?:-)?"
+ "(?<num2>[0-9]{1,4})?\\b", MULTILINE);
}
package com.vroong.neogeo.support.address.refiner;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import java.util.stream.Stream;
public class UglyAddressProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
return Stream.of(
Arguments.of("못생긴 주소", "정제된 주소")
);
}
}
@appkr
Copy link
Author

appkr commented Jun 30, 2021

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment