[Java] Jsoup을 이용한 웹 크롤링(하나은행 환율 정보)

seoyoon·2023년 2월 16일
0
post-thumbnail

0. 개요

이번 포스팅은 Java에서 Jsoup을 이용하여 웹 크롤링을 해보겠습니다.

현재 저희는 환율 정보를 기업은행 E-branch를 사용 중인데 e-branch의 단점은
최종 고시 시각이 11시 55분을 넘어가면 정상적으로 데이터가 들어오지 않는 다는 것이였습니다.
마찬가지로 하나은행 Open API를 사용하려고 했으나
최종 고시가 20시 경에 이루어져서 정확한 최종 고시 시각의 환율이 아니라는 점이였습니다.
하나은행 OPEN API

그래서 웹 스크롤링을 하기로 결정하였고 저는 JAVA로 주로 개발을 하기 때문에 JSoup을
사용하기로 하였습니다.

1. Jsoup 추가

1) 정의

jsoup은 java에서 사용할 수 있는 html parser 패키지입니다.

2) 설치 및 라이브러리 추가

JSoup 사이트에 접속ㅇ해서 jar 파일을 다운 받습니다.
다운 받은 jar 파일을 라이브러리에 추가합니다.
프로젝트의 외부 라이브러리에 추가하면 됩니다.
혹은 Maven 기반이라면

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.14.1</version>
</dependency>

Gradle 기반이라면

implementation group: 'org.jsoup', name: 'jsoup', version: '1.14.1'

을 추가해줍니다.

2. 하나은행 환율 가져오기

먼저 하나은행 환율 정보는 아래 사이트에서 확인할 수 있습니다.
하나은행 환율 조회
당일 최초 고시 회차일 때 환율 정보와 전날 최종 고시 회차일 때 환율 정보를 DB에 저장해보겠습니다.

먼저 DTO를 생성합니다.

1) KEBExchangeRateDto.java

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class KEBExchangeRateDto {
	private String basicDate; // 등록년월일
	private int basicSeq; // 고시회차
	private String noticeTime; // 고시 시각
	private String currCD; // 통화
	private String basicRatio; // 단위
	private Double cashBuy; // 현찰 살 때
	private Double cashSell; // 현찰 팔 때
	private Double ttSell; // 송금 보낼 때
	private Double ttBuy; // 송금 받을 때
	private Double tcBuy; // T/C 살 때
	private Double fcSell; // 외화 수표 팔 때
	private Double basicRate; // 매매  기준율
	private Double exhgCmmssn; // 환가 료율
	private Double usaCnvrsn; // 미화 환산율
}

2) KEBExchangeRate.java

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.ibatis.session.SqlSession;
import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import cis.batch.dto.KEBExchangeRateDto;
import cis.batch.util.CmnBatchService;
import cis.connectDB.ConnectDB;

public class KEBExchangeRate implements CmnBatchService{
	Logger logger = Logger.getLogger(KEBExchangeRate.class);
	public void run() throws Exception {
		String tmpInpStrDt = "";
		String pbldDvCd = "";
		String date = "";
		String URL = "https://www.kebhana.com/cms/rate/wpfxd651_01i_01.do";
		String currCD = ""; // 통화
		String ratio = ""; // 단위
		double cashBuy = 0.0; // 현찰 살 때
		double cashSell = 0.0; // 현찰 팔 때
		double ttSell = 0.0; // 송금 보낼 때
		double ttBuy = 0.0; // 송금 받을 때
		double tcBuy = 0.0; // T/C 살 때
		double fcSell = 0.0; // 외화 수표 팔 때
		double basicRate = 0.0; // 매매 기분율
		double exhgCmmssn = 0.0; // 환가 료율
		double usaCnvrsn = 0.0; // 미화 환산율

		ConnectDB conn = new ConnectDB();
		SqlSession session = conn.getSession();
		logger.info("[KEBExchange] is start:: " + new Date());
		try {
			LocalDate today = LocalDate.now();
			LocalDate yesterday = LocalDate.now().minusDays(1);
			int nowTime = LocalTime.now().getHour();
			SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd");
			SimpleDateFormat output = new SimpleDateFormat("yyyyMMdd");
			Date newdt = new Date();

			if (nowTime == 6) {// 전일 최종
				pbldDvCd = "0";
				tmpInpStrDt = yesterday.toString();
				newdt = input.parse(yesterday.toString());

			} else { // 당일 최초
				pbldDvCd = "1";
				tmpInpStrDt = today.toString();
				newdt = input.parse(today.toString());
			}

			date = output.format(newdt);

			Document doc = Jsoup.connect(URL).data("ajax", "true").data("tmpInpStrDt", tmpInpStrDt)
					.data("pbldDvCd", pbldDvCd).data("inqStrDt", date).data("inqKindCd", "1")
					.data("requestTarget", "searchContentDiv").timeout(3000).post();

			Elements header = doc.select("div.printdiv p>span>strong");
			Elements table = doc.select("div.printdiv tbody>tr");

			int seq = Integer.parseInt(getOnlyDigit(header.get(1).text()));
			String time = getOnlyDigit(header.get(2).text());

			ArrayList<KEBExchangeRateDto> lists = new ArrayList<>();

			for (int i = 0; i < table.size(); i++) {
				Element row = table.get(i);
				int idx = row.child(0).text().indexOf(" ");

				if (row.child(0).text().contains("(")) {
					ratio = "100";
				} else {
					ratio = "1";
				}

				currCD = row.child(0).text().substring(idx + 1, idx + 4);
				cashBuy = Double.parseDouble(getRemoveComma(row.child(1).text()));
				cashSell = Double.parseDouble(getRemoveComma(row.child(3).text()));
				ttSell = Double.parseDouble(getRemoveComma(row.child(5).text()));
				ttBuy = Double.parseDouble(getRemoveComma(row.child(6).text()));
				tcBuy = Double.parseDouble(getRemoveComma(row.child(7).text()));
				fcSell = Double.parseDouble(getRemoveComma(row.child(8).text()));
				basicRate = Double.parseDouble(getRemoveComma(row.child(9).text()));
				exhgCmmssn = Double.parseDouble(getRemoveComma(row.child(10).text()));
				usaCnvrsn = Double.parseDouble(getRemoveComma(row.child(11).text()));

				KEBExchangeRateDto exchangeDto = KEBExchangeRateDto.builder().basicDate(date).basicSeq(seq).noticeTime(time)
						.currCD(currCD).basicRatio(ratio).cashBuy(cashBuy).cashSell(cashSell).ttSell(ttSell)
						.ttBuy(ttBuy).tcBuy(tcBuy).fcSell(fcSell).basicRate(basicRate).exhgCmmssn(exhgCmmssn)
						.usaCnvrsn(usaCnvrsn).build();

				lists.add(exchangeDto);
			}

			session.insert("setExchageList", lists);

			session.commit();

			logger.info("[KEBExchange] is start:: + insert success!");

		} catch (Exception e) {
			e.getMessage();
			e.printStackTrace();
			logger.info("[KEBExchange] is start:: + insert failed!");
		} finally {
			if (session != null)
				session.close();
			conn = null;
		}
	}
	public static String getOnlyDigit(String str) {

		StringBuffer sb = new StringBuffer();

		if (str != null && str.length() != 0) {
			Pattern p = Pattern.compile("[0-9]");
			Matcher m = p.matcher(str);
			while (m.find()) {
				sb.append(m.group());
			}
		}
		return sb.toString();
	}

	public static String getRemoveComma(String str) {
		str = str.replace(",", "");
		return str;
	}
}

저는 배치 서비스를 활용해 자동으로 오전 6시 40분에는 전날 최종 고시 환율이,
오전 8시 40분에는 당일 최초 고시 환율이 Insert되는 로직을 구현하기 위해
인터페이스로 CmnBatchService을 추가해주었습니다.


URL에는 스크롤링을 할 웹페이지를 적어줍니다.

예를 들어 2023년 2월 15일 최초 고시 회차는

pdbldDvCd = 1

2023년 2월 15일 최종 고시 회차는

pdbldDvCd = 0

2023년 2월 15일 특정 회차(ex 100 회차)는

pdbldDvCd = 2, pbldSqn = 특정회차
이런 식으로 페이로드가 구성되어 있습니다.

doc에서 select할 태그들은 개발자도구(F12) 클릭 후 요소에서 확인 할 수 있습니다.

3) sqlmapper.xml

<insert id="setExchageList" parameterType="java.util.List">
	<foreach collection="list" item="item" index="index" separator=" " open="INSERT ALL" close="SELECT * FROM DUAL">
		INTO KEB_COM_EXHG
				    (BASIC_DATE
		           , BASIC_SEQ
		           , CURR_CD
		           , NOTICE_TIME
		           , BASIC_RATIO
		           , CASH_BUY
		           , CASH_SELL
		           , TT_SELL
		           , TT_BUY
		           , TC_BUY
		           , FC_SELL
		           , BASIC_RATE
		           , EXHG_CMMSSN
		           , USA_CNVRSN
		           , INS_DATETIME)
		VALUES
		(#{item.basicDate}
	   , #{item.basicSeq}
	   , #{item.currCD}
	   , #{item.noticeTime}
       , #{item.basicRatio}
	   , #{item.cashBuy}
	   , #{item.cashSell}
	   , #{item.ttSell}
       , #{item.ttBuy}
	   , #{item.tcBuy}
	   , #{item.fcSell}
	   , #{item.basicRate}
	   , #{item.exhgCmmssn}
	   , #{item.usaCnvrsn}
	   , SYSDATE)
		</foreach>
	</insert>
profile
Backend Developer

0개의 댓글