본문 바로가기

Backend/SpringBoot

SpringBoot의 Jsoup을 이용해 코로나 현황 크롤링하기 프로젝트

안녕하세요!

이번 포스팅에서는 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이라는 주제를 가지고 포스팅하기 좋을 것 같아 포스팅을 하게 되었습니다! 😎

혹시 위 글을 읽으면서 잘못된 내용이 있거나 보충하고 싶은 내용이 있으면 댓글로 알려주시면 감사하겠습니다!

읽어주셔서 감사합니다!