-
[Java/Spring] Selenium과 Kakao Map API를 활용하여 좌표얻기Java, Spring 2024. 3. 14. 21:53
이번 포스팅에서는 카카오 맵 API를 활용하여 비인기 여행지의 위도와 경도를 얻어 최종적으로 데이터베이스에 저장하는 과정을 포스팅 하겠다. 도로명 주소가 있는 경우 해당 장소의 좌표를 얻는 건 그리 어려운 건 아니지만, 우리가 다룰 전국의 비인기 여행지는 약 1000여개이므로 이를 모두 수작업으로 한다는 건, 사실상 불가능이다.
위 과정을 자동화 해보자
먼저 이전 포스팅에서 크롤링을 통한 비인기 여행지의 데이터에는 도로명 주소가 포함되어 있다. 우리는 이 비인기 여행지 객체를 저장하기 직전에 도로명 주소롤 카카오 맵에 검색을 하고 검색 결과 중 1번에 기록 되어 있는 여행지를 클릭하도록 그리고 클릭할 경우 해당 장소의 위도와 경도를 다시 Selenium을 통해 크롤링하여 정보를 취합해 데이터 베이스에 저장하도록 설계하였다.
아래 코드를 보자.
@Component @RequiredArgsConstructor public class SearchWithRoadAddresses { private WebDriver driver; public void connectToUrl() { System.setProperty("webdriver.chrome.driver", "C:/Users/voslr/study/kakaoMap/src/main/java/liveinsoha/instagram/location/driver/chromedriver.exe"); driver = new ChromeDriver(); driver.get("http://localhost:9090"); } public Coordinate searchWithAddress(String roadAddress) { // 검색창에 도로명 주소를 하나씩 입력하고 검색하기 System.out.println("address = " + roadAddress); // 검색창에 도로명 주소를 입력하고 검색하기 버튼 클릭 WebElement keywordInput = driver.findElement(By.id("keyword")); keywordInput.clear(); // 이전 입력값을 지우기 waitFor(500); keywordInput.sendKeys(roadAddress); WebElement searchButton = driver.findElement(By.xpath("//button[@type='submit']")); searchButton.click(); // 알림이 뜰 때까지 대기 //검색 결과 없을 경우 null리턴, 검색 결과 있을 경우 Coordinate 리턴 return waitForAlert(driver); } public void quitDriver() { driver.quit(); } // 알림이 뜰 때까지 대기하는 메서드. 검색결과 있을 경우 좌표객체 리턴 private Coordinate waitForAlert(WebDriver driver) { WebDriverWait wait = new WebDriverWait(driver, Duration.ofMillis(500)); try { //검색 결과가 없을 경우 alert 확인. wait.until(ExpectedConditions.alertIsPresent()).accept(); System.out.println("검색결과 없음."); //그리고 다음 검색으로.. } catch (org.openqa.selenium.TimeoutException e) { //Alert가 발생하지 않았으므로 검색결과 있음. TimeOutException 발생 System.out.println("검색결과 있음."); //검색결과가 있을 경우 return handleSearchResult(driver); } return null; //검색결과가 없을 경우 null리턴. } // 검색 결과를 처리하고 알림이 있는지 확인하여 처리하는 메서드 private Coordinate handleSearchResult(WebDriver driver) throws NoSuchElementException { // 검색 결과 확인 WebElement placesList = driver.findElement(By.id("placesList")); List<WebElement> searchResults = placesList.findElements(By.tagName("li")); // 검색 결과가 존재하는 경우 // 검색 결과 중 1번을 클릭 WebElement searchResultItem = searchResults.get(0); searchResultItem.click(); // 페이지 새로고침 대기 waitFor(700); WebElement latitudeElement = driver.findElement(By.id("latitude")); WebElement longitudeElement = driver.findElement(By.id("longitude")); String mapX = latitudeElement.getText(); String mapY = longitudeElement.getText(); System.out.println("mapX, mapY = " + mapX + ", " + mapY); // 좌표 객체로 반환 return new Coordinate(Double.parseDouble(mapX), Double.parseDouble(mapY)); } // 대기 메서드 public static void waitFor(int milliseconds) { try { Thread.sleep(milliseconds); }catch (InterruptedException e){ e.printStackTrace(); } } }
약간 복잡하다. 크롤링 하면서 발생하는 예외 경우를 모두 처리해 주기위해 많은 분기문이 있다. 먼저 searchWithAddress함수는 도로명 주소를 매개변수로 받아 검색에 나선다.
한국관광 데이터랩에서 제공하는 도로명 주소가 정확하지 않은 경우도 있어서 검색결과가 존재하지 않는 경우도 있었다. 따라서 그 떄의 경우에는 '검색결과가 없습니다'라는 Alert가 발생하고 확인을 클릭하여 다음 과정으로 넘어가도록 하였다.
검색 결과가 있는 경우 Alert를 예측하였지만 발생하지 않아 500ms내에 TimeOutException이 발생하고 catch문에 왼쪽 검색 목록 중 가장 정확도가 높은 맨 앞의 결과를 클릭하도록 코드를 작성했다.
또한 클릭할 경우 해당 장소의 좌표가 지도위에 표시되도록 JavaScript코드도 작성하였다.
// 목록 아이템을 클릭했을 때 해당 장소의 좌표 정보를 표시합니다 itemEl.onclick = function (idx) { return function () { var selectedMarker = markers[idx]; var selectedPlace = places[idx]; // 마커가 지도 영역 안에 있는지 확인합니다 if (selectedMarker.getMap()) { // 클릭한 장소의 마커가 지도에 표시되고 있는 경우 var content = '<div id="locationInfo" style="padding:5px;">' + '장소명: ' + selectedPlace.place_name + '<br>' + '위도: <span id="latitude">' + selectedPlace.y + '</span><br>' + '경도: <span id="longitude">' + selectedPlace.x + '</span></div>'; // 인포윈도우에 표시할 내용을 설정합니다 infowindow.setContent(content); // 클릭한 마커의 위치로 인포윈도우를 옮깁니다 infowindow.open(map, selectedMarker); } else { alert('마커가 지도 영역 밖에 있어 좌표 정보를 표시할 수 없습니다.'); } } }(i);
위 코드 블럭을 보면 카카오 맵에서 제공하는 위도와 경도를 표시하고, 해당 HTML태그에 id속성을 지정해주어, id선택자를 통해 크롤링 해올 수 있도록 하였다.
@RequiredArgsConstructor @Component public class ParseSaveApplication { private final NotFamousPlaceRepository notFamousPlaceRepository; private final SearchWithRoadAddresses searchWithRoadAddresses; public void run() throws IOException { String directoryPath = "C:/Users/voslr/Downloads/소멸 위험 지역 csv 파일들"; ReadLineContext<NotFamousPlace> context = new ReadLineContext<>(new NotFamousPlaceParser()); searchWithRoadAddresses.connectToUrl(); //localhost:9090에 띄워진 카카오 맵 url에 연결. List<NotFamousPlace> nonFamousPlaces = context.readByLine(directoryPath); for (NotFamousPlace place : nonFamousPlaces) { // 검색 건수가 5000 이상 10000 미만인 경우에만 저장 if (place.getSearchCount() >= 5000 && place.getSearchCount() < 10000) { // 비인기 여행지에 해당하는 NonFamousPlace 객체들을 출력 Coordinate coordinate; try { coordinate = searchWithRoadAddresses.searchWithAddress(place.getRoadAddress()); if (coordinate != null) { //좌표가 존재할 경우만 저장 place.setCoordinate(coordinate.getMapX(), coordinate.getMapY()); System.out.println(place);//출력 notFamousPlaceRepository.save(place); } } catch (NoSuchElementException e){ //예외 발생시 건너 뜀. } } } searchWithRoadAddresses.quitDriver(); } }
위의 SearchWithRoadAddress 클래스는 ParseSaveApplication에서 참조하고 있고 도로명 주소로 검색한 결과 좌표가 있는 경우에만 유효한 비인기 여행지 객체로서 인정하고 저장하기로 하였다.
위 어플리케이션을 돌려보면 아래와 같은 영상을 확인할 수 있다.
위 과정을 거치고 데이터베이스에 위도 경도와 함께 우리의 비인기 여행지들이 저장된 것을 확인할 수 있다.
우리가 인기있게 만들어 줄게~~~
'Java, Spring' 카테고리의 다른 글
[Java/Spring] 여행지의 인스타그램 해시태그 수 크롤링 (1) 2024.03.15 [Java/Spring] csv파일 Parsing하여 데이터베이스에 저장하기 (0) 2024.03.14 [Java/Spring] Selenium으로 데이터 크롤링하기 (1) 2024.03.14 Spring Security와 소셜 로그인 (0) 2024.03.09 HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법 (1) 2024.01.26