1. 개요
Tripot 프로젝트 중 축제 정보를 가져와 보여줄 기능을 구현하고자 했었다. 이에 대한민국 구석구석 사이트에서 정보를 크롤링해와 이를 보여주는 기능을 구현해보자. 후술하겠지만 해당 코드는 프로젝트에 사용하지 않았다.
- 웹 크롤러: 조직적, 자동화된 방법으로 월드 와이드 웹을 탐색하는 컴퓨터 프로그램
- 웹 크롤링: 크롤러가 하는 작
2. Selenium

웹 브라우저 자동화를 위해 만들어진 파이썬, 자바, C# 등 다양한 언어를 지원하는 오픈 소스 프레임워크이다.
@NullMarked
public interface WebDriver extends SearchContext {
void get(String url);
@Nullable String getCurrentUrl();
@Nullable String getTitle();
List<WebElement> findElements(By by);
WebElement findElement(By by);
@Nullable String getPageSource();
void close();
void quit();
Set<String> getWindowHandles();
String getWindowHandle();
TargetLocator switchTo();
Navigation navigate();
Options manage();
@Beta
public interface Window {
Dimension getSize();
void setSize(Dimension targetSize);
Point getPosition();
void setPosition(Point targetPosition);
void maximize();
void minimize();
void fullscreen();
}
public interface Navigation {
void back();
void forward();
void to(String url);
void to(URL url);
void refresh();
}
...
자바 셀레니움의 WebDriver 인터페이스이다. getter로 브라우저 및 접속 사이트의 이름 등을 가져오거나, get() 함수로 url에 요청을 보내거나, findElement()로 해당 페이지의 정보를 가져오는 등 다양한 역할을 수행할 수 있다.
3. ChromeDriver
지금은 Selenium을 크롤러로서 사용한다. 크롬, 사파리, 오페라 등 다양한 브라우저를 지원하지만 지금은 크롬 브라우저를 쓰도록 한다.

우선 설치되어있는 크롬의 버전을 확인한다. 설정 -> Chrome 정보를 통해 확인할 수 있다.
https://developer.chrome.com/docs/chromedriver/downloads?hl=ko
다운로드 | ChromeDriver | Chrome for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 다운로드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 달리 명시되지 않는 한 이 페이지의 콘텐츠
developer.chrome.com


해당 사이트에서 크롬 버전에 맞는 크롬드라이버를 다운로드한다. 버전이 크게 차이나지 않는다면 정상적으로 동작한다.
4. 설정
@Configuration
public class WebDriverConfig {
private static String WEB_DRIVER_ID = "webdriver.chrome.driver";//property 키
private static String WEB_DRIVER_PATH = "D:\\Download\\chromedriver-win64\\chromedriver.exe";
@Bean
public ChromeOptions chromeOptions() {
// webDriver 옵션 설정
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless"); //브라우저 창 X
chromeOptions.addArguments("--lang=ko"); //언어 설정
chromeOptions.addArguments("--no-sandbox"); //리눅스에서 셀레니움이 적절히 동작하지 않을 때 사용할 수 있는 옵션
chromeOptions.addArguments("--disable-dev-shm-usage"); /// dev/shm 사용 안함
chromeOptions.addArguments("--disable-gpu"); //gpu 가속 사용 x
return chromeOptions;
}
@Bean
public WebDriver webDriver(ChromeOptions chromeOptions) {
System.setProperty(WEB_DRIVER_ID, WEB_DRIVER_PATH);
WebDriver driver = new ChromeDriver(chromeOptions);
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
return driver;
}
}
하나의 웹 드라이버를 만들어 사용할 것이므로 Config 클래스를 만들어 빈으로 등록해주었다. 크롬드라이버의 경로를 등록하고, chromeOption 설정을 주입해 크롬드라이버 인스턴스를 만든다.
5. 구현
https://korean.visitkorea.or.kr/kfes/list/festivalCalendar.do
월별 축제 달력 | 대한민국 구석구석 축제
전국 방방곡곡에서 열리는 축제 정보를 소개합니다. 전국의 다채로운 축제와 함께 행복하고 즐거운 여행 되세요!
korean.visitkorea.or.kr:443
우선 크롤링 대상 웹사이트가 어떻게 구성되어있는지 확인해보자.

웹 사이트에 접속하면 달력에 오늘 날짜가 적혀있고, 오늘 진행하는 축제 리스트가 아래에 표시된다.

처음에는 12개까지의 결과만 보여지고, 무한 스크롤이 구현되어있어 스크롤을 맨 아래로 내려야 모든 축제 정보를 받아올 수 있다.
이를 기반으로 서비스 코드를 작성해보자.
private final WebDriver webDriver;
...
public List<FestivalDto> crawl(){
List<FestivalDto> response = new ArrayList<>();
try {
webDriver.get("https://korean.visitkorea.or.kr/kfes/list/festivalCalendar.do");
Thread.sleep(2000);
우선 해당 페이지에 get을 사용하여 요청을 보낸다. 응답을 받아야 다음 과정을 진행할 수 있으므로 2초동안 sleep을 건다.
JavascriptExecutor js = (JavascriptExecutor) webDriver;
Long lastHeight = (Long) js.executeScript("return document.body.scrollHeight");
while (true) {
js.executeScript("window.scrollTo(0, document.body.scrollHeight);");
Thread.sleep(500);
Long newHeight = (Long) js.executeScript("return document.body.scrollHeight");
log.info("lastHeight: {}, newHeight={}", lastHeight, newHeight);
if (lastHeight.equals(newHeight)) {
break;
}
lastHeight = newHeight;
}
그 다음 해당 사이트의 스크롤을 끝까지 내려야 한다. JavascriptExecutor을 사용하여 스크립트를 실행한다. 해당 페이지의 높이를 구하고, 스크롤을 한번 끝까지 내렸을 때 높이가 달라지지 않았다면 축제 정보가 더 로딩되지 않은 것이고, 이때 크롤링을 하면 오늘 열리는 모든 축제 정보를 가져올 수 있다.

F12를 눌러 개발자 도구를 연 후 흰색 박스에 해당하는 버튼을 클릭 후

원하는 요소를 클릭하면

해당 위치로 이동할 수 있다. list라는 id를 가진 ul(unordered list) 내에 list로 축제 정보 요소가 있는 것을 확인할 수 있다.

각 요소에는 하이퍼링크 내에 other_festival_content가 있고, 이 안에 축제 이름, 기간(date), 지역(loc)이 있는 것을 확인할 수 있다.

해당 위치의 cssSelector는 우클릭후 위의 위치의 버튼을 눌러 복사할 수 있다.
List<WebElement> elements = webDriver.findElements(By.cssSelector("#list > li"));
이를 붙여넣고, findElements를 이용해 리스트 내의 요소를 모두 받아올 수 있다. 모든 정보를 확인하려면 뒤에 li가 붙어야 한다.
for (WebElement element : elements) {
String title = element.findElement(By.cssSelector("a > div.other_festival_content > strong")).getText();
String duration = element.findElement(By.cssSelector("a > div.other_festival_content > div.date")).getText();
String place = element.findElement(By.cssSelector("a > div.other_festival_content > div.loc")).getText();
response.add(FestivalDto.builder()
.title(title)
.duration(duration)
.place(place)
.build());
}
각 요소에 대해 동일한 과정을 수행하여 제목, 기간, 위치를 받아 응답 리스트에 추가 후 리턴한다.

정상적으로 받아서 리턴하는 것을 확인할 수 있었다.
@PreDestroy
private void closeWebDriver(){
if (Objects.nonNull(webDriver)) {
webDriver.close();
}
}
물론 프로그램을 종료할 때 웹 드라이버를 꺼주어야 한다.
5. 주의
https://knto.or.kr/helpdeskCopyrightguide
한국관광공사
관광으로 행복한 나라를 만드는 기업, 한국관광공사의 공식 웹사이트입니다.
knto.or.kr:443
해당 html 파일, 축제 정보 등 엄연히 저작물이므로 상업적 목적으로 이용할 수 없다. Tripot 프로젝트는 광고를 통해 수익을 내는 앱이므로 해당 정보를 사용하지 않기로 했다.
일부 사이트는 루트 페이지에 /robots.txt를 붙이면 어떤 경로에서 크롤링이 가능하고, 불가능한지 알 수 있다.
google.com/robots.txt
User-agent: *
Disallow: /search
Allow: /search/about
Allow: /search/static
Allow: /search/howsearchworks
Disallow: /sdch
Disallow: /groups
Disallow: /index.html?
Disallow: /?
Allow: /?hl=
Disallow: /?hl=*&
Allow: /?hl=*&gws_rd=ssl$
Disallow: /?hl=*&*&gws_rd=ssl
Allow: /?gws_rd=ssl$
Allow: /?pt1=true$
Disallow: /imgres
Disallow: /u/
Disallow: /setprefs
Disallow: /default
Disallow: /m?
Disallow: /m/
Allow: /m/finance
Disallow: /mobile?
Disallow: /wml?
...