티스토리 뷰

 

KOSTA에서 Java 기반 DevOps 과정을 들으며 Final 프로젝트에서 사용한 크롤링을 정리하려고 한다.

파이썬을 사용했으면 더 편하긴 했겠지만, 우리는 Java 를 배웠기 때문에 Java를 사용하여 크롤링을 진행한다.

모르는 사람이 봐도 금방 이해할 수 있도록 진짜 최대한 쉽고 간단하게 작성하려고 노력했다.!!

 


 

🛠️ 환경 설정

 

우선 개발환경은 이클립스를 사용하고 Maven 으로 빌드를 진행합니다.

아래 3개의 라이브러리를 <dependencies> 태그 안에다 넣어주고 빌드를 진행합니다. (저장!)

 

<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.26.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.github.bonigarcia/webdrivermanager -->
<dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>5.9.2</version>
</dependency>

<dependency>  
    <groupId>edu.stanford.nlp</groupId>
    <artifactId>stanford-corenlp</artifactId>
    <version>4.4.0</version>
    <classifier>models</classifier>
</dependency>

 

혹시나 시간이 흐르고 나서 버전이 업데이트 될 수도 있기 때문에 위에 주석을 남겨놓았으니 주석에 적혀있는 링크를 통하여 최신버전을 다운받아 주시면 되겠습니다.

 

좀 더 친절하게 한번 해볼까요?

 

https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java 

위 링크에 접속해 봅니다.

 

 

최신 버전으로 하는게 제일 좋은것으로 알고 있습니다만 원하는 버전을 선택해주세요.

 

그러면 아래와 같은 화면이 나오는데, 아래 <dependency> 부분을 긁어서 pom.xml 설정 파일에 넣어주시면 완료입니다.

 

Maven 외에도 Gradle 로 진행하시는 분들을 위해서 Gradle 탭도 있으니 눌러서 해당하는 코드를 각자의 설정 파일에 넣어주시면 되겠습니다.

 

 

셀레니움(Selenium) 으로 크롤링하는 법을 찾다보니 관련된 블로그들에 내용들이 서로 다르다보니 헷갈리더라구요.

혹시나 제가 진행하는 방법으로 안되시는 분들을 위해 아래에 다른 블로그 링크를 걸어두었습니다.

 

https://velog.io/@rednada1486/Java-Selenium-%ED%99%9C%EC%9A%A9-%EB%8F%99%EC%A0%81%EC%9B%B9%ED%81%AC%EB%A1%A4%EB%A7%81

 

 


 

 

✌️ 네이버 지도 크롤링 하기

 

네이버 지도 크롤링 시나리오를 간단하게 정해놓고 크롤링을 진행하겠습니다.

 

(1) 네이버 지도에 접속합니다.

(2) 종각역에 존재하는 파스타집을 검색하기 위해 검색창에 "종각역 파스타" 를 검색합니다.

(3) 매장을 클릭하여 상세페이지를 불러온다.

(4) 해당 매장의 여러 정보들을 크롤링한다.

(5) 상위 3개의 매장에 대해 위 과정을 반복한다.

 

우선 아래 코드는 1번과 2번을 진행한 코드입니다.

public class Crawler {
    public static void main(String[] args) {
        WebDriver driver = new ChromeDriver();
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        WebDriverManager.chromedriver().setup();  

        // 네이버 지도 페이지로 접속
        driver.get("https://map.naver.com");

        WebElement searchBox = wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("input.input_search")));
        searchBox.sendKeys("종각역 파스타");
        searchBox.sendKeys(Keys.ENTER);
        
        //종각역 파스타를 입력한 후 페이지가 로딩 되는것을 기다림
        Thread.sleep(2000);
	
        JavascriptExecutor js = (JavascriptExecutor) driver;
        while (!(Boolean) js.executeScript("return document.querySelector('iframe#searchIframe') !== null;")) {;
            Thread.sleep(500); // 짧은 시간 대기 후 반복 확인
        }
        
        driver.switchTo().frame("searchIframe");
    }
}

 

 

📑 코드 설명

 

셀레니움은 크롬 브라우저를 사용하여 크롤링을 진행합니다.

 

간단하게 WebDriver는 브라우저를 제어하기 위한 인터페이스이고 ChromeDriver 는 실제로 크롬 브라우저를 열고 해당 브라우저를 제공해주는 드라이버 정도로만 생각하시면 될 것 같습니다. (크롤링을 진행하려면 그냥 한줄 붙인다고 생각!!)

WebDriver driver = new ChromeDriver();

 

 

WebDriverWait 은 웹 페이지에서 특정 요소가 나타날 때까지 기다리는 작업을 담당합니다.

 

크롤링을 할 때, 주변 환경이나 네트워크 혹은 변덕으로 인해 사용자가 원하는 요소를 즉시 찾을 수 없을 때가 많습니다.

 

쉽게 얘기하면 컴퓨터마다 로딩 시간이 다르니까요?!

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

 

로딩이 되지도 않았는데, 어떤 행위를 하려고 하면 에러가 발생합니다.

이러한 문제를 해결하기 위해 WebDriverWait을 정의하고 사용합니다.

사용 예는 아래에서 자세히 설명합니다.

그리고 인자값으로는 driver와 최대 기다리는 시간을 넣어줍니다. 현재는 10초를 넣어주었습니다.

 

 

셀레니움 라이브러리 버전과 사용자의 크롬 브라우저의 버전이 맞지 않으면 충돌이 나 문제가 생길 수 있습니다.

이를 방지하기 위해 WebDriverManager를 사용함으로써 드라이버의 버전을 체크한 후 알맞는 버전으로 알아서 설치해줍니다.

WebDriverManager.chromedriver().setup();

 

 

네이버 지도에 들어가기 위해 driver.get() 을 사용합니다.

get 메서드의 인자값으로 접속하려는 URL을 넣어주면 됩니다.

// 네이버 지도 페이지로 접속
driver.get("https://map.naver.com");

 

 

그리고 "종각역 파스타"를 검색하기 위해서는 검색창에 입력 후에 엔터키를 눌러 검색을 해야합니다.

 

하지만 위에서도 설명했듯이 네이버지도에 들어가고 나서 검색창이 로딩되지도 않았는데, 검색을 하려면 에러가 발생합니다.

 

이를 위해 위에서 정의해둔 wait 을 사용하여 기다리다가 검색창이 로딩되었다면 WebElement 객체로 요소를 받아옵니다.

WebElement searchBox = wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("input.input_search")));
searchBox.sendKeys("종각역 파스타"); 
searchBox.sendKeys(Keys.ENTER);

 

셀레니움에서는 HTML, CSS 구조로 요소를 가져올 수 있습니다. (HTML, CSS를 잘 모르신다면 어려울지도..?)

 

그렇기 때문에 크롬에서 '개발자 도구' 를 열어서 가지고 오려는 요소의 태그, 클래스명, ID 값 혹은 속성값들을 확인하고 작성하시면 됩니다. 

 

아래 사진에서 볼 수 있듯 검색창의 HTML 구조를 보시면 input 태그의 input_search 클래스명을 가진것을 볼 수 있습니다.

 

By.cssSelector() 사용하여 해당 요소의 정보를 넣어주면 해당 요소를 WebElement 객체로 받아올 수 있습니다.

//input 태그의 input_search 클래스명을 가진 요소
By.cssSeletor("input.input_search") 

//test 라는 ID 값을 가진 요소
By.cssSeletor("#test") 

//div태그 밑에 div태그 밑에 div태그 밑에 hello 라는 클래스값을 가진 요소
By.cssSeletor("div>div>div>.hello") 

// test 라는 클래스요소 하위의 a라는 클래스 값을 가진 요소 혹은 b라는 클래스 값을 가진 요소 (콤마로 구분 가능)
By.cssSeletor(".test .a , .test .b")

 

 

검색이 되었다면 아래와 같이 매장들이 로딩 되었을것입니다.

 

네이버 지도를 크롤링 할때 어려움을 겪는 부분이 이제 나타나는데, 바로 iframe 입니다.

 

셀레니움은 iframe 을 자동으로 인식하지 않기 때문에 "iframe이 나타났다! 여기를 바라봐!" 라고 개발자가 얘기해주어야 합니다. 왜냐하면 iframe 내부에 우리가 원하는 매장들의 정보들이 담겨져 있기 때문입니다.

 

아래 코드가 위 내용을 진행합니다.

 

JavascriptExecutor 는 셀레니움이 제공하는 인터페이스로 JavaScript 코드를 직접 실행할 수 있는 도구입니다.

 

갑자기 자바스크립트?? 라고 하실 수 있는데, 사실 자바스크립트를 사용하지 않고 크롤링을 진행 할 수 있습니다.

 

하지만 제가 약 3~4주동안 크롤링을 진행해본 결과 자바스크립트를 이용하면 더 편리하고, 강제적으로 실행해야하는 부분이 있을때는(추후에 알게되실겁니다..)

 

자바스크립트를 사용해야 했기 때문에 그냥 따라서 사용하시면 될거같습니다.

JavascriptExecutor js = (JavascriptExecutor) driver;
while (!(Boolean) js.executeScript("return document.querySelector('iframe#searchIframe') !== null;")) {;
    Thread.sleep(500); // 짧은 시간 대기 후 반복 확인
}

driver.switchTo().frame("searchIframe");

 

document.querySelector('iframe#searchIframe') : iframe 태그 중 ID가 searchIframe인 요소를 찾습니다.

 

만약 해당 요소가 존재하면 true, 존재하지 않으면 false를 반환합니다.

 

반복문을 통해 찾을때까지 반복하고 만약 찾아서 true를 반환해서 반복문을 빠져 나왔다면 

 

dirver.switchTo().frame 을 통해 프레임을 전환해 줍니다.

(셀레니움에서 이제 여기를 바라봐! 부분입니다)


 

다음 아래 코드는 (3) 매장을 클릭하여 상세페이지를 불러온다. 부분의 코드입니다.

 

그리고 제가 설명하지 않는 부분은 그냥 외우거나 긁어다 사용하십시오.. 

전부다 이해하려고 하면 다칩니다..

wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy 이런거 등등..

//가게 이름 요소를 두 가지 경우에 맞게 찾기
List<WebElement> shopLinks = wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(By.cssSelector("div>.place_bluelink>.TYaxT, div.place_bluelink>span.YwYLL")));

// 최대 2개의 상세 페이지 URL 가져오기
for (int i = 0; i < Math.min(1, shopLinks.size()); i++) {
    WebElement shop = shopLinks.get(i);     

    // JavaScript로 클릭 실행
    ((JavascriptExecutor) driver).executeScript("arguments[0].click();", shop);
    while (!driver.getCurrentUrl().contains("/place/")) {
        ((JavascriptExecutor) driver).executeScript("arguments[0].click();", shop);
        Thread.sleep(1000); // 재시도 후 대기	       
    }

    while (true) {
        try {
            // 원래 검색 페이지로 돌아가기
            driver.switchTo().defaultContent();
            
            // iframe 전환
            wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#entryIframe")));
            break; // 성공하면 반복 종료
        } catch (NoSuchFrameException e) {
            Thread.sleep(500); // 잠시 대기 후 다시 시도
        }
    }
}

 

 

📑 코드 설명

 

매장의 상세페이지에 들어가기 위해서는 매장 이름을 눌러야합니다.

그렇다면 매장 이름부분을 요소로 가져오면 되겠죠?

요소로 가져온 후 클릭하면 상세페이지에 들어갈 수 있으니까요.

//가게 이름 요소를 두 가지 경우에 맞게 찾기
List<WebElement> shopLinks = wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(By.cssSelector("div>.place_bluelink>.TYaxT, div.place_bluelink>span.YwYLL")));

 

아래 사진을 보시면 매장 이름은 span 태그에 TYaxT 클래스 값을 가지고 있습니다.

 

그래서 위에 매장 이름 요소를 가져올 때 By.cssSelector("div>.place_bluelink>.TYaxT,  ....span.YwYLL") 으로 작성하였는데, 왜 YwYLL 부분을 추가하였냐면 크롤링을 진행하다 보면 매장마다 조금씩 HTML 구조가 다른것을 확인했기 때문입니다.

 

대부분은 TYaxT 라는 클래스 값을 가지고 있었지만 간혹가다 매장 이름의 요소가 span 태그의 YwYLL 클래스값을 가지고 있었기 때문에 콤마(,)를 사용하여 둘 중 하나를 찾도록 하였습니다.

 

그리고 아까와는 다르게 List<WebElement> 처럼 컬렉션으로 값을 받았는데, 이유가 무엇이냐면 TYaxT 라는 요소를 가진 요소가 한개가 아닌 여러개가 존재하기 때문입니다. 

 

셀레니움은 우리가 작성한 조건에 해당하는 요소들을 전부 가져오기 때문에 List를 사용하여 요소들 받아옵니다.

 

어떻게 여러개 받는지 아냐?? 라는 질문에는 개발자 도구를 켜서 확인해보시면 됩니다.

 

위 케이스 같은 경우는 가게의 이름을 가져오는 코드이며, 우리가 보통 네이버지도에 검색하면 검색결과가 1개만 나오지는 않잖아요?

 

여러개 나오니까 우리가 찾으려는 요소도 여러개 존재하겠구나 라고 생각하면 됩니다.

 

쉽게 얘기해 지금은 가게의 이름을 여러개 가져옵니다.

 

 

shopLinks.size() 는 10개가 출력되게 됩니다. (shopLinks는 위에서 받아온 매장 이름의 개수)

네이버 지도는 스크롤을 할때마다 새로운 매장이 로드되기 때문에 스크롤을 진행하지 않았을 때는 10개의 매장을 보여줍니다.

 

지금은 10개중 1번만 크롤링을 진행할 것이기 때문에 Math.min() 메서드를 사용하여 크롤링 진행 개수를 제어했습니다.

shopLinks.get() 을 통해 순서대로 매장 이름의 요소를 가져옵니다. 가져올때는 WebElement를 사용합니다.

// 최대 2개의 상세 페이지 URL 가져오기
for (int i = 0; i < Math.min(1, shopLinks.size()); i++) {
    WebElement shop = shopLinks.get(i);     
		
    ...
    ...
}

 

 

가져온 요소(매장 이름)를 클릭하는 코드입니다. 

 

이번에도 자바스크립트를 사용하였습니다.

자바스크립트를 사용하지 않고 click() 을 사용할 수 있지만, 위에서도 얘기했듯이 강제적으로 눌러줘야 하는 상황이 생길 수 있고 경험한 바에 따르면 click() 메서드가 잘 동작하지 않는 경우가 있어 안전하게 자바스크립트를 사용하였습니다.

 // JavaScript로 클릭 실행
((JavascriptExecutor) driver).executeScript("arguments[0].click();", shop);
while (!driver.getCurrentUrl().contains("/place/")) {
    ((JavascriptExecutor) driver).executeScript("arguments[0].click();", shop);
    Thread.sleep(1000); // 재시도 후 대기	       
}

 

매장 이름을 클릭을 했을 때 상세페이지가 잘 나오는지 확인해야 하는데, 아래 사진을 보시면 상세페이지가 잘 로딩되었다는 것은 주소창에  "/place/" 가 붙으면 확인할 수 있었습니다. 

 

그렇기에 while (!driver.getCurrentUrl().contains("/place/")) 부분을 추가해 만약 현재 URL에 "place"가 붙어 있지 않다면 재시도를 진행하였습니다.

 

 

 

처음에 "종각역 파스타"를 검색하고 iframe을 전환하던것을 기억하시나요?

 

이번에도 동일하게 상세페이지로 이동해야한다는것을 셀레니움에게 알려주어야 합니다.

 

아까는 driver.switchTo().frame 문법을 사용했으나 이번에는 다른 방법으로 프레임을 전환했습니다.

 

그냥 여러가지 방법이 있다고만 알아두고, 이 방법은 entryIframe 의 로딩을 기다리고 프레임을 변환하는 코드입니다.

while (true) {
    try {
        // 원래 검색 페이지로 돌아가기
        driver.switchTo().defaultContent();

        // iframe 전환
        wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#entryIframe")));
        break; // 성공하면 반복 종료
    } catch (NoSuchFrameException e) {
        Thread.sleep(500); // 잠시 대기 후 다시 시도
    }
}

 

 

 


 

이제 드디어 (4) 해당 매장의 여러 정보들을 크롤링한다. 부분입니다.

현재까지 잘 따라오셨다면 상세페이지까지 로딩이 완료되고 entryIframe 으로 전환까지 완료되었을것입니다.

 

모든 정보를 크롤링하기는 코드가 길어지고 글이 길어질테니 몇가지만 해보도록 하겠습니다.

사용 방법 한 두개만 알아둔다면 방식은 똑같아서 응용하기 쉬울것입니다.

 

1. 매장 이름 크롤링 해보기

 

아래 코드 한줄이 끝입니다. 쉽죠?

해당 요소의 텍스트(text)를 가져오려면 getText() 메서드를 사용하여 쉽게 가져올 수 있습니다.

 

WebElement 는 <span class= .... >콩지POT지</span> 을 가져오기 때문에 텍스트만 가져와야 합니다.

WebElement name = driver.findElement(By.cssSelector("span.GHAhO"));
System.out.println("1. 매장이름 : " + name.getText());

 

 

 

2. 매장의 첫 번째 이미지 가져오기

 

 

매장 이름을 가져오는 것과는 다르게 속성부분의 데이터를 가져와야하기 때문에 getText()가 아닌 getDomAttribute() 를 사용하였습니다.

 

우리는 "src" 속성의 값을 가져오기 때문에 아래와 같이 적었고, 나중에 응용할 때 다른 속성을 가져오고 싶다면 src 대신에 다른 속성을 적어주면 되겠죠?

String mainImg = driver.findElement(By.cssSelector(".fNygA>a>img")).getDomAttribute("src");
System.out.println("2. 이미지 URL : " + mainImg);

 

 

 

3. 매장 주소 크롤링

 

요소를 가져올 때는 findElement() 를 사용하여 WebElement 요소를 가져옵니다.

 

혹시 List<> 처럼 여러개의 요소를 가져와야 한다면 findElements() 를 사용하면 됩니다.

 

간단합니다.

WebElement address = driver.findElement(By.cssSelector("span.LDgIH"));
System.out.println("3. 매장 주소 : " + address.getText());

 

 

다음은 심화버전으로 들어가보겠습니다.

 

"정보" 탭을 클릭하고 데이터를 크롤링을 진행해보겠습니다.

 

위 사진을 보면 "정보" 를 클릭하고 싶은데 옆으로 넘기는 화살표에 가려서 클릭이 되지않습니다.

 

이럴 때를 대비해서 아까 위에서 자바스크립트의 강제 클릭을 사용했습니다.

 

우선 순서대로 코드를 보겠습니다. 아래 코드는 정보탭을 클릭하고 활성화를 기다리는 코드입니다.

상세페이지를 보면 알겠지만, 메뉴바에 탭이 여러개 존재합니다.

"홈", "소식" , "메뉴", "예약", "리뷰", "사진", "정보" 등등..

우리는 저 요소들을 가져와서 "정보" 부분에 해당하는 요소만 가져오려고 합니다.

그리고 그 요소를 클릭하면 정보탭이 클릭되는것이지요.

// 정보 탭 클릭..
List<WebElement> menuItems = driver.findElements(By.cssSelector(".flicking-camera>a"));
WebElement infoTab = null;

//여러개의 탭 중에 "정보" 탭 찾기
for (WebElement menuItem : menuItems) {
    String menuText = menuItem.getText().trim();
    if (menuText.equals("정보")) 
        infoTab = menuItem;
}

// 탭 활성화 확인
((JavascriptExecutor) driver).executeScript("arguments[0].click();", infoTab);

while (!infoTab.getAttribute("aria-selected").equals("true")) {
    ((JavascriptExecutor) driver).executeScript("arguments[0].click();", infoTab);
    Thread.sleep(500); // 재시도 후 대기
}

 

우선 flicking-camera 클래스 값을 가진 요소에 존재하는 하위 a 태그 요소들을 가져옵니다.

지금은 7개의 a태그들이 존재하죠?

그 a 태그들 하위에 탭의 이름이 적혀져 있습니다.

우리는 그 이름을 하나씩 반복문을 통해 꺼내본 후 "정보" 에 해당하는 부분이 발견되면 그 요소를 가져옵니다.

 

 

정보 탭을 찾은 후 아래 코드를 실행합니다.

"정보" 탭이 어떤 특정한 요소에 가려져 클릭이 되지않을 수 있기 때문에 자바스크립트를 사용하여 강제 클릭을 유도합니다.

// 탭 활성화 확인
((JavascriptExecutor) driver).executeScript("arguments[0].click();", infoTab);

while (!infoTab.getAttribute("aria-selected").equals("true")) {
    ((JavascriptExecutor) driver).executeScript("arguments[0].click();", infoTab);
    Thread.sleep(500); // 재시도 후 대기
}

 

아래 사진을 보면 정보 탭이 정상적으로 클릭되었다면 "정보" 텍스트를 가진 a 태그의 "aria-selected" 속성의 값이 true 로 변하는 것을 볼 수 있습니다. 

 

우린 이 정보를 통해 "정보" 탭이 정상적으로 잘 눌렸는지 확인할 수 있습니다.

그리고 true가 아니라면(잘 눌리지 않았다면) 반복문을 통해 재시도를 요청할 수 있습니다.

 

 

 

 

4,. 매장 소개 크롤링

 

이후 데이터를 크롤링하는것은 매우 쉽습니다.

 

WebElement info = driver.findElement(By.cssSelector("div.T8RFa"));
System.out.println("4. 매장 소개 : " + info.getText());

 

 

 

5. 매장 편의시설 크롤링 

 

편의시설 및 서비스 같은 경우 서비스 하나랑 li 태그가 하나 존재하는 것을 확인할 수 있습니다.

지금은 3개의 서비스가 존재해서 3개의 li 태그 (c7TR6 클래스값)가 존재합니다.

 

 

이번에는 쪼~금 다른방법으로 크롤링을 해보겠습니다.

상위 요소를 가져온 후 그 내부에서 li 태그 요소들을 가져오는 방법입니다.

 

거의 다를건 없지만 findElement를 사용하여 "편의시설 및 서비스" 부분을 가져온 후에 그 안에서 findElements() 를 사용하여 c7TR6 클래스값을 가진 요소들을 가져오는 방법입니다.

 

굳이 왜 이렇게 코드를 작성했냐면, 일단 간혹가다 매장의 "편의시설 및 서비스" 부분이 존재하지 않을 수도 있습니다.

그럴 경우 우선 있는지 없는지 체크를 한 후에 예외 처리를 진행 하기 위함입니다.

아래 코드는 편의시설 및 서비스 레이아웃 자체가 존재하지 않다면 NoSuchElementException 이 발생하고 예외를 처리하고 있습니다.

try {
    WebElement service = driver.findElement(By.cssSelector(".place_section.no_margin.VMtyJ"));
    List<WebElement> servicese = service.findElements(By.cssSelector(".c7TR6 div.owG4q"));
    System.out.print("5. 매장 편의 시설 및 서비스 : ");
    for (WebElement s : servicese) 
        System.out.print(s.getText() + ", ");
    System.out.println();
    } catch (NoSuchElementException e) {
        System.out.println("편의시설 및 서비스 요소가 존재하지 않습니다.");
    }	
}

 


 

 

"(5) 상위 3개의 매장에 대해 위 과정을 반복한다."  부분은 아래의 전체 코드로 대체합니다.

 

// 최대 3개의 상세 페이지 URL 가져오기

for (int i = 0; i < Math.min(3, shopLinks.size()); i++)

이 부분에서 3이 3번 반복한다는 뜻입니다. (3개의 매장을 가져온다.)

최대 10까지 가능하고, 10개 이상을 가져오고 싶다면 스크롤을 통해 추가 로드 후 매장을 더 가져와야 한다.

그 부분은 추후에 고급편에서 설명하겠다. (쓰게 된다면...)

 

그리고 반복문 맨 아래에

driver.switchTo().defaultContent(); 
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#searchIframe")));

 

위 코드를 작성해 주어야 합니다.

 

상세 페이지에서 크롤링을 진행하느라 셀레니움이 현재 "entryIframe" (상세페이지)를 보고 있는데, 다시 새로운 매장의 크롤링을 진행하기 위해 "searchIframe" (검색결과창) 으로 돌아가야 하기 때문이다.

 

 

전체 코드

import java.time.Duration;
import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NoSuchFrameException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import io.github.bonigarcia.wdm.WebDriverManager;

public class Crawler {
    public static void main(String[] args) throws InterruptedException {
        WebDriver driver = new ChromeDriver();
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        WebDriverManager.chromedriver().setup();  
	     
	     // 네이버 지도 페이지로 접속
         driver.get("https://map.naver.com");
         
         // JavaScript로 검색어를 입력
         WebElement searchBox = wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("input.input_search")));
         searchBox.sendKeys("종각역 파스타");
         searchBox.sendKeys(Keys.ENTER);
         
         Thread.sleep(2000);
	
        JavascriptExecutor js = (JavascriptExecutor) driver;
        while (!(Boolean) js.executeScript("return document.querySelector('iframe#searchIframe') !== null;")) {;
            Thread.sleep(500); // 짧은 시간 대기 후 반복 확인
        }
        
        driver.switchTo().frame("searchIframe");
                
        //가게 이름 요소를 두 가지 경우에 맞게 찾기
        List<WebElement> shopLinks = wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(By.cssSelector("div>.place_bluelink>.TYaxT, div.place_bluelink>span.YwYLL")));
        
        // 최대 3개의 상세 페이지 URL 가져오기
        for (int i = 0; i < Math.min(3, shopLinks.size()); i++) {
        	WebElement shop = shopLinks.get(i);     
        	
        	// JavaScript로 클릭 실행
	        ((JavascriptExecutor) driver).executeScript("arguments[0].click();", shop);
	        while (!driver.getCurrentUrl().contains("/place/")) {
	            ((JavascriptExecutor) driver).executeScript("arguments[0].click();", shop);
	            Thread.sleep(1000); // 재시도 후 대기	       
	        }
	        
	        while (true) {
	            try {
	            	// 원래 검색 페이지로 돌아가기
	     	       driver.switchTo().defaultContent();
	     	        
	     	       // iframe 전환
	     	       wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#entryIframe")));
	               break; // 성공하면 반복 종료
	            } catch (NoSuchFrameException e) {
	                Thread.sleep(500); // 잠시 대기 후 다시 시도
	            }
	        }
	        
	        WebElement name = driver.findElement(By.cssSelector("span.GHAhO"));
	        System.out.println("1. 매장이름 : " + name.getText());
	        
	        String mainImg = driver.findElement(By.cssSelector(".fNygA>a>img")).getDomAttribute("src");
	        System.out.println("2. 이미지 URL : " + mainImg);
	        
	        WebElement address = driver.findElement(By.cssSelector("span.LDgIH"));
	        System.out.println("3. 매장 주소 : " + address.getText());
	        
	        // 정보 탭 클릭..
	        List<WebElement> menuItems = driver.findElements(By.cssSelector(".flicking-camera>a"));
	        WebElement infoTab = null;
            
	        for (WebElement menuItem : menuItems) {
	            String menuText = menuItem.getText().trim();
	            if (menuText.equals("정보")) 
	            	infoTab = menuItem;
	        }
	        
	        // 탭 활성화 확인
	        ((JavascriptExecutor) driver).executeScript("arguments[0].click();", infoTab);
	    	
	        while (!infoTab.getAttribute("aria-selected").equals("true")) {
	            ((JavascriptExecutor) driver).executeScript("arguments[0].click();", infoTab);
	            Thread.sleep(500); // 재시도 후 대기
	        }        
	        
	       WebElement info = driver.findElement(By.cssSelector("div.T8RFa"));
	       System.out.println("4. 매장 소개 : " + info.getText());
	       
           try {
               WebElement service = driver.findElement(By.cssSelector(".place_section.no_margin.VMtyJ"));
               List<WebElement> servicese = service.findElements(By.cssSelector(".c7TR6 div.owG4q"));
               System.out.print("5. 매장 편의 시설 및 서비스 : ");
               for (WebElement s : servicese) 
                   System.out.print(s.getText() + ", ");
               System.out.println();
           } catch (NoSuchElementException e) {
               System.out.println("편의시설 및 서비스 요소가 존재하지 않습니다.");
           }	       
	        
            // 원래 검색 페이지로 돌아가기
            driver.switchTo().defaultContent(); //driver.navigate().back(); // 이전 페이지로 돌아가기

            // iframe 전환
            wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.cssSelector("iframe#searchIframe")));
        }
    }
}

출력 결과 중 일부