중고물품 가격을 정리해서 보여주는 토이 프로젝트를 만들려고 Selenium 으로 크롤링하는 프로그램을 만드는 중입니다.
JAVA + Selenium 으로 진행했습니다.
코드 부분은 부분 코드만 기입했습니다.
동적인 자료를 수집할 때 주로 사용하는 오픈소스 프레임워크입니다.
자바스크립트로 동적으로 변하는 웹 페이지를 크롤링 작업을 할 수 있습니다.
다양한 웹 브라우저를 지원하여 여러 브라우저에서 동작이 가능합니다.
실제 브라우저를 실행하여 테스트를 진행하기 때문에 속도 측면에서 다소 느린 부분이 있습니다.
JavaScript 의 동적 로딩을 대기해야해서 대기시간을 추가해야 합니다.
또한 Python, Java 등 다양한 언어를 지원해서 호환성이 좋습니다.
크롤링을 Chrome 으로 진행하기 때문에 ChromeDriver 가 필요합니다.
private ChromeOptions getChromeOptions() {
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("disable-extensions");
chromeOptions.addArguments("no-sandbox");
chromeOptions.addArguments("--remote-allow-origins=*");
chromeOptions.addArguments("headless");
return chromeOptions;
}
private void setProperty() throws IOException{
System.setProperty("webdriver.chrome.driver", getChromeDriverExe().getFile().getAbsolutePath());
}
크롤링 진행 시 설정을 지정하고, ChromeDriver 를 실행할 수 있게 경로를 설정해야 합니다.
WebDriver 객체를 이용해 페이지 내에 요소에 접근하여 WebElement 객체로 반환합니다.
WebElement 객체로 클릭, 입력 등 요소의 동작을 지정할 수 있습니다.
private void searchKeywordAndMovePage() {
// 입력 상자 찾기
WebElement searchBox = driver.findElement(By.id(cssValueContainer.getSearchBoxCss()));
// 검색어 입력
searchBox.sendKeys(keyword);
// 검색 버튼 클릭
searchBox.submit();
}
WebDriverWait 을 사용하여 요소가 로딩이 완료되어 사용이 가능할 때까지 대기합니다.
private List<WebElement> crawlingAndWaitForLoadingComplete() {
return waitDrvier.until(d -> waitForLoadingElement(d.findElements(By.xpath(cssValueContainer.getItemPostCssWithReplaceKeyword(keyword)))));
}
private List<WebElement> waitForLoadingElement(List<WebElement> itemPostElements) {
try {
// 모든 요소가 화면에 표시될 때까지 기다립니다.
return (!itemPostElements.isEmpty() && itemPostElements.stream().allMatch(WebElement::isDisplayed)) ? itemPostElements : null;
} catch (StaleElementReferenceException e) {
return crawlingAndWaitForLoadingComplete();
}
}
요소 검색은 찾고자 하는 부분에 특정 class, tag, 속성값 등을 이용해서 특정 요소를 가져와야합니다.
가져온 요소에서 필요한 내용 ( 저같은 경우는 게시글 이름, 중고 가격, 올린 시간 ) 을 css 기준으로 뽑아내어 객체로 만들어서 반환하고 있습니다.
private List<UsedItem> consolidateCrawlingData(List<WebElement> elements) {
return elements.stream().map(this::createJoongnaItemList).filter(Objects::nonNull).toList();
}
private UsedItem getItem(WebElement element) {
String url = getItemLink(element);
String title = getItemTitle(element);
String[] priceAndUploadTime = getItemPriceAndUploadTime(element);
return new UsedItem(title, url, priceAndUploadTime[0], priceAndUploadTime[1]);
}
private String getItemTitle(WebElement element) {
WebElement contentTag = element.findElement(By.tagName("img"));
return contentTag.getAttribute("alt");
}
private String getItemLink(WebElement element) {
return element.getAttribute("href");
}
private String[] getItemPriceAndUploadTime(WebElement element) {
WebElement content = element.findElement(By.cssSelector(cssValueContainer.getPriceContainerCss()));
return new String[]{getItemPrice(content), getItemUploadTime(content)};
}
private String getItemPrice(WebElement content) {
return content.findElement(By.cssSelector(cssValueContainer.getItemPriceTagCss())).getText();
}
private String getItemUploadTime(WebElement content) {
List<WebElement> elements = content.findElements(By.cssSelector(cssValueContainer.getItemUploadTextCss()));
String uploadTime = elements.get(elements.size() - 1).getText();
// 광고 게시글 제외
isAdvertisement(uploadTime);
return uploadTime;
}
Selenium 은 요소가 동적으로 로딩되는 것을 대기하여 동작이 진행되기 때문에 접근하는 요소가 로딩이 덜 되었거나, 사용이 불가능한 경우 ( 웹 페이지 크기 등으로 인해 요소가 화면에 안보이는 경우 등 ) 요소에 접근하지 못해 에러가 발생하게 됩니다.
적절한 대기 시간을 주거나, 해당 상황에 예외처리로 잘 대처하여 재요청을 진행하는 등 고려할 상황이 많았습니다.
또한 4k 화면에서 크롤링이 되던 것이 노트북 화면에서는 크롤링이 안되고 그런게 있어서 추가 수정이 필요합니다.
이후 개발은 ScheduleTask 를 추가해서 일정시간마다 자동으로 크롤링하여 DB에 저장하는 로직까지 추가가 남아있습니다.