최근 테니스에 흥미가 생겨 꽤 자주 테니스를 치고 있다. 회사 테니스장은 꽤 편하게 예약할 수 있었지만 서울시에서 운영하는 테니스장 예약은 쉽지 않더라... 특히 예약 페이지가 너무 불편함~ 그래서 예약 페이지의 예약 현황을 자동으로 크롤링하는 아이디어가 떠올랐다.
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Coat {
@Id
Integer id;
String name;
Integer coatNumber;
LocalDate date;
String reservation;
String link;
LocalDateTime modifyDate;
}
Selenium 라이브러리를 그래이들에 추가하니깐 build가 실패하는 이슈가 생겼다. 메일을 전송하는 서비스 쪽에서 JavaMainSenderImpl을 만들지 못했다. 아마 메일 라이브러리와 Selenium 라이브러리가 충돌하는 것 같다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.seleniumhq.selenium:selenium-java'
https://github.com/spring-projects/spring-boot/issues/33452
링크를 참고해서 아래처럼 수정하니 해결됐다. 정확한 원인은 알아봐야 할 것 같다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation("org.seleniumhq.selenium:selenium-java") {
exclude group: "com.sun.activation", module: "jakarta.activation"
}
크롤링을 수행하는 전용 클래스를 만들었다.
CaotRepository 의존성을 추가@Scheduled 를 통해 해당 메서드가 30분마다 수행되도록 구현Coat 엔티티를 만듬Coat 엔티티 리스트를 DB에 저장
@Slf4j
@Component
@RequiredArgsConstructor
public class WebDriverUtil {
private final CoatRepository coatRepository;
@Value("${webdriver.path}")
private String WEB_DRIVER_PATH;
private static final String WEB_DRIVER_ID = "webdriver.chrome.driver";
private static final String RESERVATION_HOME_URL = "https://yeyak.seoul.go.kr/web/main.do";
private static final String TENNIS_RESERVATION_POST_URL = "https://yeyak.seoul.go.kr/web/reservation/selectReservView.do?rsv_svc_id=";
private static final List<String> COAT_ID_LIST = Arrays.asList(
"S201030105206531192", "S201030105601087749", "S201030145802586611", "S210302233348803598",
"S210302233656019242", "S210319185351345647", "S210319192158999179");
private static final String YEBIGUN_HOME_URL = "https://www.yebigun1.mil.kr/dmobis/index_main.do";
public ChromeOptions getDefaultOption() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
return options;
}
@Scheduled(cron = "0 0/30 * * * ?") // 30분 마다
public void getBoramaeCoatInfo() throws InterruptedException {
log.info("reservation information of Boramae tennis coat crawling start");
System.setProperty(WEB_DRIVER_ID, WEB_DRIVER_PATH);
WebDriver driver = new ChromeDriver(getDefaultOption());
driver.get(RESERVATION_HOME_URL);
Thread.sleep(1000); // 브라우저 로딩 대기
driver.manage().window().maximize();
WebElement lang = driver.findElement(By.cssSelector(".language"));
lang.click();
WebElement kor = driver.findElement(By.cssSelector(".language a")); // 한국어로 변경
kor.click();
// 크롤링 로직 시작
List<Coat> coatInfoList = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
for (int i = 0; i < COAT_ID_LIST.size(); i++) {
int coatNumber = i + 1;
String coatId = COAT_ID_LIST.get(i);
driver.get(TENNIS_RESERVATION_POST_URL + coatId); // n번 코트 예약 페이지로 이동
Thread.sleep(1000); // 브라우저 로딩 대기
WebElement pop_x = driver.findElement(By.className("pop_x")); // 팝업 닫기
pop_x.click();
List<WebElement> trList = driver.findElements(By.cssSelector(".tbl_cal tbody td"));
for (WebElement element : trList) {
String date = element.getAttribute("id");
if (date.contains("calendar")) { // 존재하는 일자이면
date = date.substring(9); // 날짜만 파싱
WebElement num = element.findElement(By.className("num"));
if (num.getText().equals(" "))
continue; // 평일은 넘어감
coatInfoList.add(Coat.builder()
.id(Integer.parseInt(coatNumber + date))
.coatNumber(coatNumber)
.date(LocalDate.parse(date, formatter))
.reservation(num.getText())
.name("보라매")
.link(TENNIS_RESERVATION_POST_URL + coatId)
.modifyDate(LocalDateTime.now())
.build());
}
}
}
// 크롤링 로직 끝
driver.close(); // 탭 닫기
driver.quit(); // 브라우저 닫기
coatRepository.saveAll(coatInfoList);
log.info("reservation information of Boramae tennis coat crawling end");
}
}
간혹 크롤링하고 싶은 페이지가 로그인이 필요한 경우일 수 있다. 이때 웹브라우저가 인증되고 유효한 쿠키를 가지고 있으면 좋겠다고 생각했다.
로그인 과정은 웹 사이트에 맞게 구현하면 된다.
public class WebDriverUtil {
...(생략)...
// 쿠키 저장
public void saveCookie(WebDriver driver) {
driver.get(URL);
File file = new File("Cookies.data");
try {
file.delete();
file.createNewFile();
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fileWriter);
for (Cookie ck : driver.manage().getCookies()) {
bw.write(ck.getName() + ";" + ck.getValue() + ";" + ck.getDomain() + ";" + ck.getPath() + ";"
+ ck.getExpiry() + ";" + ck.isSecure());
bw.newLine();
}
bw.close();
fileWriter.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 쿠키 사용
public void getCookie(WebDriver driver) {
try {
File file = new File("Cookies.data");
FileReader fileReader = new FileReader(file);
BufferedReader br = new BufferedReader(fileReader);
String strLine;
while ((strLine = br.readLine()) != null) {
StringTokenizer st = new StringTokenizer(strLine, ";");
while (st.hasMoreTokens()) {
String name = st.nextToken();
String value = st.nextToken();
String domain = st.nextToken();
String path = st.nextToken();
Date expiry = null;
String val;
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH);
if (!(val = st.nextToken()).equals("null")) {
expiry = dateFormat.parse(val);
}
Boolean isSecure = new Boolean(st.nextToken()).booleanValue();
Cookie ck = new Cookie(name, value, domain, path, expiry, isSecure);
log.info(ck.toString());
driver.manage().addCookie(ck);
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}