안녕하세요!
이번 포스팅에서는 SpringBoot의 Jsoup을 이용해서
코로나 현황 통계를 크롤링하는 미니 프로젝트를 작성해보겠습니다!
해당 프로젝트의 코드는 깃허브에서 확인이 가능합니다 🤗
사전 준비
robots.txt 확인작업
robots.txt를 확인하는 이유는 크롤링을 허용해주는 웹사이트인지를 알기 위해 확인을 해주어야합니다.
크롤링할 대상의 웹페이지의 도메인/robots.txt로 들어가면 확인할 수 있습니다. (robots.txt에 대한 자세한 설명)
저희가 크롤링할 페이지는 robots.txt 파일이 존재하지 않아 (...) 크롤링을 허용한다고 판단하고 크롤링을 진행해보겠습니다!
Jsoup
Jsoup이란 HTML을 가져오고 파싱할 수 있게 도와주는 자바 라이브러리입니다.
이 라이브러리를 사용하면 자바에서 간단하게 크롤링 및 파싱이 가능합니다.
프로젝트 설명
현재 시간을 기준으로 코로나 현황을 가져오는 프로젝트입니다.
완성 모습은 아래와 같습니다.
개발 환경
- IntelliJ Ultimate
- Gradle 6.0.1
- Java 1.8
- SpringBoot 2.2.5
- Mac OS
의존성 추가
start.spring.io에서 추가할 수 있는 의존성입니다.
- Lombok
- Tymeleaf
- Web
- Test
구현
1. 의존성 추가
Jsoup을 사용하기 위해 의존성을 추가해줍니다.
build.gradle
compile group: 'org.jsoup', name: 'jsoup', version: '1.11.3'
2. 객체 만들기
위 정보들을 담을 멤버변수(시도명, 전일대비확진환자증감, 확진환자수, 사망자수, 발생률, 일일검사건수)를 포함한 객체들을 만들어줍니다.
KoreaStats.java
@ToString
@Builder
@Getter
public class KoreaStats {
private String country; // 시도명
private int diffFromPrevDay; // 전일대비확진환자증감
private int total; // 확진환자수
private int death; // 사망자수
private double incidence; // 발병률
private int inspection; // 일일 검사환자 수
}
코드의 가독성을 위해 Lombok을 사용했습니다.
값을 가져오기 위해 @Getter를, 콘솔에서 값을 확인하기 위해 @ToString을,
값을 주입하여 객체를 만들기 위해 @Builder 어노테이션을 추가해주었습니다.
(@Setter를 추가해주고 set 방식으로 값을 주입해주어도 상관없습니다.)
3. Service 만들기
이제 객체도 준비되었으니 Jsoup으로 크롤링하는 메소드를 만들 차례입니다.
@Service
public class CoronaVirusDataService {
private static String KOREA_COVID_DATAS_URL = "http://ncov.mohw.go.kr/bdBoardList_Real.do?brdId=1&brdGubun=13";
@PostConstruct
public void getKoreaCovidDatas() throws IOException {
Document doc = Jsoup.connect(KOREA_COVID_DATAS_URL).get();
System.out.println(doc);
}
}
가장 먼저 해당 URL을 GET형식으로 가져온 문서를 모두 가져오는 코드를 작성합니다.
@PostConstruct 어노테이션을 사용해 시작하자마자 메소드를 실행하게 설정해줍니다.
콘솔창을 보면 해당 페이지의 html 전체가 찍히는 것을 확인할 수 있습니다.
하지만 이 데이터들을 가공하기에는 데이터의 양이 너무 많으니 파싱을 통해 원하는 데이터를 추려보겠습니다.
통계가 있는 페이지로 들어가서 확인해보면,
필요한 데이터들은 <table> - <tbody>에 있는 <tr>태그안에 있다는 것을 확인 할 수 있습니다.
Jsoup에서 제공해주는 파싱 메소드인 select()를 이용해서 수정해보겠습니다.
@Service
public class CoronaVirusDataService {
private static String KOREA_COVID_DATAS_URL = "http://ncov.mohw.go.kr/bdBoardList_Real.do?brdId=1&brdGubun=13";
@PostConstruct
public void getKoreaCovidDatas() throws IOException {
Document doc = Jsoup.connect(KOREA_COVID_DATAS_URL).get();
Elements contents = doc.select("table tbody tr");
System.out.println(doc);
}
}
table안의 tbody안의 tr 태그는 하나의 Element가 되고,
Element를 담는 리스트 같은 역할을 하는 것이 Elements입니다.
select()의 반환 타입은 Elements입니다.
이제 이 Element들을 이용해서 객체에 값을 넣어주어야합니다.
시도명은 <th>에, 값들은 <td>에 있다는 것을 알 수 있습니다.
@Service
public class CoronaVirusDataService {
private static String KOREA_COVID_DATAS_URL = "http://ncov.mohw.go.kr/bdBoardList_Real.do?brdId=1&brdGubun=13";
@PostConstruct
public void getKoreaCovidDatas() throws IOException {
Document doc = Jsoup.connect(KOREA_COVID_DATAS_URL).get();
Elements contents = doc.select("table tbody tr");
for(Element content : contents){
Elements tdContents = content.select("td");
KoreaStats koreaStats = KoreaStats.builder()
.country(content.select("th").text())
.diffFromPrevDay(Integer.parseInt(tdContents.get(0).text()))
.total(Integer.parseInt(tdContents.get(1).text()))
.death(Integer.parseInt(tdContents.get(2).text()))
.incidence(Double.parseDouble(tdContents.get(3).text()))
.inspection(Integer.parseInt(tdContents.get(4).text()))
.build();
System.out.println(koreaStats.toString());
}
}
}
위와 같이 코드를 수정해줍니다.
for-each문을 돌려서 추가로 <td>태그를 가지고 있는 요소들을 검색해서 tdContents에 넣습니다.
KoreaStats 객체를 만들고, text()를 이용하여 태그의 값을 가져와서 태그 순서대로 값을 세팅해줍니다.
도시명은 <th>를 가지고 있고, <tr>안의 <th>는 하나밖에 없기 때문에 바로 text()를 이용해서 값을 넣어줍니다.
값이 잘 나오는 것을 확인할 수 있습니다.
CoronaVirusDataService.java
@Service
public class CoronaVirusDataService {
private static String KOREA_COVID_DATAS_URL = "http://ncov.mohw.go.kr/bdBoardList_Real.do?brdId=1&brdGubun=13";
public List<KoreaStats> getKoreaCovidDatas() throws IOException {
List<KoreaStats> koreaStatsList = new ArrayList<>();
Document doc = Jsoup.connect(KOREA_COVID_DATAS_URL).get();
Elements contents = doc.select("table tbody tr");
for(Element content : contents){
Elements tdContents = content.select("td");
KoreaStats koreaStats = KoreaStats.builder()
.country(content.select("th").text())
.diffFromPrevDay(Integer.parseInt(tdContents.get(0).text()))
.total(Integer.parseInt(tdContents.get(1).text()))
.death(Integer.parseInt(tdContents.get(2).text()))
.incidence(Double.parseDouble(tdContents.get(3).text()))
.inspection(Integer.parseInt(tdContents.get(4).text()))
.build();
koreaStatsList.add(koreaStats);
}
return koreaStatsList;
}
}
이제 이 값들을 컨트롤러에서 사용하기 위해 리턴값을 List<KoreaStats>로 수정해주고,
반복문을 돌릴 때 마다 리스트에 값을 추가해서 최종적으로 리스트를 리턴해주는 메소드로 수정해줍니다.
4. Service 동작 테스트하기
방금 만든 메소드가 잘 동작하는지 확인해주는 테스트를 만들어보겠습니다.
CoronaVirusDataServiceTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class CoronaVirusDataServiceTest {
@Autowired
CoronaVirusDataService coronaVirusDataService;
@Test
public void getKoreaCovidDatas_동작테스트() throws IOException {
// given
List<KoreaStats> coronaList = new ArrayList<>();
// when
coronaList = coronaVirusDataService.getKoreaCovidDatas();
// then
assertThat(coronaList.get(0).getCountry()).isEqualTo("합계");
assertThat(coronaList.get(0).getTotal()).isGreaterThan(0);
}
}
테스트를 통과했습니다!
5. 컨트롤러&뷰 만들기
이제 이 서비스를 이용해서 뷰로 값을 넘겨줄 컨트롤러를 만들어보겠습니다.
HomeController.java
@RequiredArgsConstructor
@Controller
public class HomeController {
private final CoronaVirusDataService coronaVirusDataService;
@GetMapping("/korea")
public String korea(Model model) throws IOException {
List<KoreaStats> koreaStatsList = coronaVirusDataService.getKoreaCovidDatas();
model.addAttribute("koreaStats", koreaStatsList);
return "korea";
}
}
korea.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>COVID-19 tracker</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<div class = "container">
<h1 class="display-4">Coronavirus Tracker Application(Korea)</h1>
<p class="lead">한국의 코로나19 현황테이블</p>
<div class="jumbotron">
<h1 class="display-4" th:text="${koreaStats[0].total}"></h1>
<p class="lead">Total cases reported</p>
<hr class="my-4">
<p>
<span>New cases reported since previous day</span>
<span th:text="${koreaStats[0].diffFromPrevDay}">0</span>
</p>
</div>
<table class= "table">
<tr>
<th>지역</th>
<th>확진환자수(전일 대비 증감량)</th>
<th>사망자</th>
<th>일일 검사건수</th>
<th title = "인구 10만 명당">발생률(*)</th>
</tr>
<tr th:each = "stat : ${koreaStats} ">
<td th:text = "${stat.country}"> </td>
<td th:text = "|${stat.total}(${stat.diffFromPrevDay})|">0</td>
<td th:text = "${stat.death}">0</td>
<td th:text = "${stat.inspection}">0</td>
<td th:text = "${stat.incidence}">0</td>
</tr>
</table>
</div>
</body>
</html>
컨트롤러에서는 방금 만든 서비스를 모델에 담아 뷰에 넘겨주고,
뷰에서는 모델로 넘어온 리스트를 가지고 html에 뿌려주고 있습니다.
비주얼적인 도움을 받기 위해 BootStrap을 사용했습니다 😅
컨트롤러와 뷰까지 다 만들고 실행한 후에 /korea로 접속을 하면
아주 잘 적용이 된 것을 볼 수 있습니다!
정리
요즘 이슈가 되는 코로나 웹페이지를 Jsoup으로 크롤링하는 미니 프로젝트를 개인적으로 진행했었는데,
Jsoup이라는 주제를 가지고 포스팅하기 좋을 것 같아 포스팅을 하게 되었습니다! 😎
혹시 위 글을 읽으면서 잘못된 내용이 있거나 보충하고 싶은 내용이 있으면 댓글로 알려주시면 감사하겠습니다!
읽어주셔서 감사합니다!
'Backend > SpringBoot' 카테고리의 다른 글
SpringBoot의 MockHttpSession을 이용해서 JUnit Mock 테스트하기 (0) | 2020.03.26 |
---|---|
SpringBoot의 AOP을 이용해서 로그 남기기 (1) | 2020.03.23 |
SpringBoot의 Querydsl을 이용하여 동적쿼리(Dynamic SQL) 작성하기 (1) | 2020.02.26 |
SpringBoot의 ControllerAdvice를 이용하여 JSON 형태로 예외처리하기 (0) | 2020.02.18 |
SpringBoot의 MockMvc를 사용하여 GET, POST 응답 테스트하기 (6) | 2020.01.31 |