회사에서 당장 사용해야 해서 급하게 만들었던 “크롤링을 통한 데이터 이전 자동화 프로그램”
하나가 있었는데 이렇게 끝내기는 아쉬워서 해당 프로그램을 확장시키고 좀 더
유연하게 만들고 싶어서 현재 작성일을 기준으로 작업 중에 있었습니다.
자세한 내용은 완성되고 포트폴리오 게시판에 작성 할 예정이긴 하지만 그 중 제가 프로그램을 만들면서
신기 했었던 기능 중 하나를 설명하고 그에 해당하는 샘플 코드를 작성 해 두려고 합니다
참고로 해당 코드는 샘플 코드이며 조금만 수정하면 사용할 수 있습니다.
본문
자바에서 유명한 크롤링 라이브러리 두개가 있는데 jsoup, selenium 있는데 jsoup는 프로그램이 완성되면 자세히 설명할 예정입니다.
이번 게시 주 내용은 selenium 인데
일반적인 html 파일을 읽어와서 요소를 추출하는 것 외에 셀레니움은 크롬이나 파이어폭스같은 드라이버를 통해서 클릭이벤트를 발생시켜 파일 다운로드 까지 연계할 수 있었습니다.
이렇게 되면 이론상 로그인까지 진행해서 토큰이나 세션값까지 얻어 올 수 있을 것 같은데 이건 나중에 다뤄보겠습니다.
각설하고 코드 먼저 보여드리겠습니다.
코드에 앞서 저는 먼저 프로젝트를 gradle로 관리하고 있습니다.
plugins {
id 'java'
}
group = 'com.nhcb'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.postgresql:postgresql:42.7.2' // PostgreSQL JDBC 드라이버
implementation 'org.yaml:snakeyaml:2.0' // SnakeYAML
implementation 'org.jsoup:jsoup:1.15.3' // Jsoup
implementation 'org.apache.commons:commons-lang3:3.12.0' // Apache Commons Lang, html 태그 변환용
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
compileOnly 'org.projectlombok:lombok:1.18.30'
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.1'
// Selenium Java binding
implementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '4.23.0'
// Add WebDriver manager to automatically manage browser drivers
implementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '5.7.0'
}
test {
useJUnitPlatform()
}
참고해주세요
먼저 전체 코드를 설명 해드리면
1. 크롬 드라이버를 셋업 시킨다
2. 크롬드라이버를 셋업하면서 옵션을 넣어줍니다
3. Driver 에서 find element 를 통해 class 이름이 a.view_file_download 인 요소를 찾습니다. (id도 가능s)
4. 찾은 요소의 링크를 추출해 내고 click() 메소드를 통해 클릭 이벤트를 발생시킵니다.
5. 해당하는 file data를 받아와 다운로드 진행합니다. (저는 서버에 바로 저장될 데이터여서 UUID를 사용했습니다.)
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 파일을 다운로드 할때 경로 및 설정을 담당하는 클래스
* */
public class FileManager {
public static void downloadAllFiles(String pageUrl,String fileGroupId) throws IOException {
WebDriver driver = null;
try {
// WebDriverManager를 사용하여 ChromeDriver 설정
WebDriverManager.chromedriver().setup();
// Chrome 옵션 설정
ChromeOptions options = new ChromeOptions();
// 다운로드 경로 설정
Map<String, Object> prefs = new HashMap<>();
prefs.put("download.default_directory", "sampleFilePath"); // 파일경로를 꼭 지정해주세요
prefs.put("download.prompt_for_download", false);
prefs.put("download.directory_upgrade", true);
prefs.put("safebrowsing.enabled", true);
options.setExperimentalOption("prefs", prefs);
// 기타 Chrome 옵션 설정
options.addArguments("--disable-popup-blocking");
options.addArguments("--disable-extensions");
options.addArguments("--disable-gpu");
options.addArguments("--no-sandbox");
options.addArguments("--headless"); // headless 모드 (브라우저 UI 없이 실행)
driver = new ChromeDriver(options);
// 페이지로 이동
driver.get(pageUrl);
// 다운로드 링크들 찾기
List<WebElement> downloadLinks = driver.findElements(By.cssSelector("a.view_file_download"));
// 파일 그룹 채번
int sersNo = 1;
// 각 링크 클릭하여 다운로드
for (WebElement downloadLink : downloadLinks) {
String href = downloadLink.getAttribute("href");
System.out.println("다운로드 링크 클릭: " + href);
downloadLink.click();
String originalFileName = downloadLink.getText();
// 새로운 파일 이름 생성 (UUID 사용)
// 파일 다운로드 대기 (필요에 따라 시간 조정)
Thread.sleep(1000);
// 다운로드된 파일 확인
File downloadedFile = getLatestFileFromDir("sampleFilePath");
if (downloadedFile != null) {
// UUID를 사용하여 새 파일 이름 생성
String newFileName = generateRandomFileName(originalFileName);
File renamedFile = new File("sampleFilePath/" + newFileName);
// 파일 이름 변경
if (downloadedFile.renameTo(renamedFile)) {
System.out.println("원본 파일 이름 1 : " + originalFileName);
System.out.println("파일 다운로드가 완료되었습니다 2 : " + renamedFile.getName());
sersNo++; // 저장후 채번 증가
} else {
System.out.println("파일 이름 변경에 실패했습니다.");
}
} else {
System.out.println("파일 다운로드에 실패했습니다.");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt(); // InterruptedException 발생 시 현재 스레드 상태를 복구
} finally {
if (driver != null) {
driver.quit();
}
// 모든 크롬 프로세스를 강제 종료
killChromeDriverProcess();
}
}
// 원래 파일 이름을 기반으로 랜덤한 파일 이름 생성
private static String generateRandomFileName(String originalFileName) {
// 파일 확장자 추출
String extension = "";
int dotIndex = originalFileName.lastIndexOf('.');
if (dotIndex > 0) {
extension = originalFileName.substring(dotIndex); // 파일 확장자
}
// 랜덤 UUID 생성 및 확장자 추가
String randomFileName = UUID.randomUUID().toString() + extension;
return randomFileName;
}
// 다운로드된 파일 중 가장 최근 파일을 반환하는 메서드
private static File getLatestFileFromDir(String dirPath) {
Path dir = Paths.get(dirPath);
try {
return Files.list(dir)
.filter(f -> !Files.isDirectory(f))
.max((f1, f2) -> Long.compare(f1.toFile().lastModified(), f2.toFile().lastModified()))
.map(Path::toFile)
.orElse(null);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static void killChromeDriverProcess() {
try {
// ChromeDriver 프로세스를 명시적으로 종료
Runtime.getRuntime().exec("taskkill /F /IM chromedriver.exe");
Runtime.getRuntime().exec("taskkill /F /IM chrome.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
}
주의할 점은 크롬드라이버를 실행할 때 마다 백그라운드에서 따로 죽이지 않으면 계속 돌아가면서 메모리를 잡아먹어 렉을 발생시킨 다는 점 입니다.
사실 샘플 코드처럼 매번 드라이버를 실행 , 강제종료 할 필요 없이 딱 한번만 구동후 해당 객체를 계속 돌려가면서 쓰다가 마지막에만 kill시켜주면 됩니다.
저번에 그것도 모르고 크롬만 15개 정도 키고 맥이 뻣을뻔했습니다... 이점만 주의하면 문제없이 사용할 수 있을 것 같습니다!
마지막 말
데이터를 이전하면 크롤링 코드 자체는 어렵지 않으나 몇가지 까다로운 점들이 있었습니다.
그 중 하나가 연관된 file 이였고 저는 셀레니움 라이브러리를 통해서 파일 다운로드를 해결할 수 있었습니다.
자세한 내용은 토이 프로젝트가 완성되면 올리겠습니다.