BackEnd/Spring Boot

Jsoup을 활용한 웹 크롤링

연향동큰손 2025. 9. 19. 00:38

 

크롤링이란

크롤링(crawling)이란  페이지의 소스를 그대로 가져와서 그 안에서 원하는 데이터를 추출해 내는 행위이고, 이러한 과정을 수행하는 소프트웨어를 크롤러(Crawler)라고 한다.

 

인터넷 상의 방대한 양의 데이터를 빠르고 효율적으로 수집할 수 있기 때문에 크롤링은 매우 유용하게 활용된다.

 

크롤링을 통해 정적 데이터를 수집할 경우 데이터를 한번만 수집해와도 되지만 동적 데이터(ex 뉴스,주식 시세, 날씨...)를 수집해야하는 경우에는 주기적으로 크롤러를 실행하여 데이터베이스를 업데이트 해줘야 한다.

 

 

 

관련 라이브러리

Python에서는 Beautiful Soup, Selenium등의 라이브러리가 사용되고, Java에서는 Jsoup을 통해 크롤링을 구현할 수 있다. 

 

 


Jsoup을 활용한 웹 크롤링 

 

Spring Boot + Jsoup을 이용해 고용24의 채용정보를 크롤링 해서 데이터베이스에 저장 해보자.

 

 

1. Jsoup 의존성 추가

//Jsoup
implementation 'org.jsoup:jsoup:1.17.2'

 

 

2.  엔드포인트 정의

// 워크넷 기본 도메인
private static final String BASE = "https://www.work24.go.kr";

// 채용공고 "목록" 페이지 엔드포인트
private static final String LIST_URL = BASE + "/wk/a/b/1200/retriveDtlEmpSrchList.do";

// 채용공고 "상세" 페이지 기본 경로
private static final String DETAIL_PATH = "/wk/a/b/1500/empDetail.do";

// 채용공고 "상세" 페이지 정식 경로
private static final String AUTH_VIEW_PATH = "/wk/a/b/1500/empDetailAuthView.do";

 

 

3. 채용 정보 목록 가지고 오기

 

고용24 채용정보 검색 페이지를 보면 검색 결과칸에 여러개의 채용정보가 존재하는데 이 목록의 전체 HTML을 가지고 와야한다. 

이때 Jsoup을 사용할 수 있다.

 

우선 Jsoup으로 요청 옵션과 파라미터를 설정하여 Connection을 생성한다.

나는 일반채용과 최근 1달 동안의 데이터만 수집할것이기 때문에 요청 파라미터로 조건을 넣어줬다.

            Connection conn = Jsoup.connect(LIST_URL)
                    .userAgent("Mozilla/5.0 (compatible; YouthJobBot/1.0)")
                    .referrer(LIST_URL) //목록 페이지
                    .header("Accept-Language", "ko-KR,ko;q=0.9")
                    .timeout(15000)
                    .method(Connection.Method.GET) //HTTP GET요청
                    .data("searchMode","Y")
                    .data("siteClcd","all")
                    .data("empTpGbcd","1") //고용형태 (1=일반채용)
                    .data("currentPageNo", String.valueOf(page))
                    .data("pageIndex", String.valueOf(page))
                    .data("resultCnt", String.valueOf(PAGE_SIZE)) //페이지 당 결과 수
                    .data("sortField","DATE")
                    .data("sortOrderBy","DESC")
                    .data("termSearchGbn","M-1") //최근 1개월 정보만
                    .data("regDateStdtParam", regFrom.format(YMD))
                    .data("regDateEndtParam", regTo.format(YMD))
                    .data("cloTermSearchGbn","M-1") //마감일 검색 범위 -> 최근 1개월
                    .data("cloDateStdtParam", ddlFrom.format(YMD))
                    .data("cloDateEndtParam", ddlTo.format(YMD))
                    .data("moreButtonYn","Y");

 

이제 세팅된 Connection 객체를 실행시켜 HTTP GET요청을 보내고 응답 HTML 문서를 Document 객체로 파싱하여 반환 해주면 된다.

Document doc = conn.get();
return doc;

 

 

4. 상세 페이지 순회 하면서 HTML의 데이터 추출 

상세 페이지 링크를 통해 상세정보 페이지로 넘어오게 되면 회사명, 근로형태, 임금, 주소등과 같은 정보를 확인할 수 있다.

나는 DB에 저장될 모든 정보를 상세 페이지에서 수집하도록 구현 했기 때문에 모든 검색결과에 대한 상세 페이지를 조회하도록 했다.

 

따라서, 설정했던 검색 옵션의 결과가 몇개의 페이지에 걸쳐서 데이터가 넘어오는지 Document를 통해 확인하고, 

계산된 페이지 수 만큼 순회 하면서 상세 정보 링크를 수집하여 상세 정보 페이지의 데이터를 파싱 해오면 된다.

private int parseTotalCount(Document doc) { //검색결과가 몇 페이지인지 검사
    Matcher m = TOTAL_RE.matcher(doc.text());
    if (m.find()) {
        try { return Integer.parseInt(m.group(1).replace(",", "")); } //페이지 수 계산
        catch (Exception ignore) {}
    }
    return -1;
}

 

 

파싱할때는 HTML에서 태그,라벨을 기준으로 데이터를 추출했다.

 

상세페이지의 소스코드의 일부분을 확인하면 다음과 같다.

<!-- 채용정보 -->
<div class="emp_sumup_wrp">
    <div class="tit_area">
        <div>
            <p class="corp_info">
                <strong>(주)지성큐앤텍</strong>
            </p>
            <strong class="title">전산입력 사무원 모집</strong>
        </div>
    </div>

    <div class="column">
        <strong class="b1_sb">근무조건</strong>
        <ul class="list">
            <li>
                <em class="tit">임금</em>
                <p>
                    연봉 3,540만원 이상
                </p>
            </li>
            <li>
                <em class="tit">지역</em>
                <p>
                    경상남도 창원시 성산구 성주로81번길 6, (주)지성큐앤텍 (남산동)
                </p>
            </li>
        </ul>
    </div>

    <div class="column">
        <strong class="b1_sb">고용형태</strong>
        <ul class="list">
            <li>
                <em class="tit">고용형태</em>
                <p>
                    기간의 정함이 없는 근로계약
                </p>
            </li>
            <li>
                <em class="tit">근무형태</em>
                <p>
                    주 5일 근무
                    (주 소정근로시간: 40시간)
                </p>

 

위 구조를 보면 회사명, 채용 제목, 임금, 지역등의 정보가 태그와 라벨을 통해 구분되어 있다.

 

회사명은 Jsoup 라이브러리의 selectFirst() 함수를 이용해 추출하였다.

selectFirst()는 Jsoup 라이브러리에서 제공하는 함수로, CSS 선택자(CSS Selector) 를 기준으로 첫 번째로 매칭되는 요소(Element)를 반환한다.

 

HTML 코드에서 회사명은 <p class="corp_info"><strong>회사명</strong></p> 구조로 되어 있기 때문에,
corp_info 라벨 내부의 <strong> 태그를 선택하여 텍스트 값을 가져오도록 했다.

Element s = d.selectFirst(".emp_sumup_wrp .corp_info > strong, .corp_info > strong");

 

급여 정보는 <th scope="row">임금 조건</th> <td>연봉 3,540만원 이상</td> 과 같이 구성 되어있고

"임금 조건" 옆에 td값 "~ 만원 이상"을 추출하기위한 함수 pickByLabel을 정의 했다.

    private static final String RX_SALARY =
            "임금" + WS + "조건|급여" + WS + "조건|임금|급여|연봉|월급|시급|일급";
            
    private String pickByLabel(Document d, String rx) {
        Element td = d.selectFirst("th:matchesOwn(^\\s*(?:" + rx + ")\\s*$) + td");
        if (td != null && !td.text().isBlank()) return td.text().trim();
        Element dd = d.selectFirst("dt:matchesOwn(^\\s*(?:" + rx + ")\\s*$) + dd");
        if (dd != null && !dd.text().isBlank()) return dd.text().trim();
        return null;
    }

임금 조건, 급여 조건, 임금, 급여, 연봉, 월급, 시급, 일급 와 같은 다양한 라벨 이름을 모두 매칭하기 위한 정규식 RX_SALARY를 정의하고, pickByLabel 함수에서 rx값으로 넘겨줘서 급여 정보를 추출할 수 있다.

 

 

크롤링 실행 결과 데이터베이스에 정보가 잘 저장된것을 확인 가능했다.