Back-End/Java(Spring)

[Java] Jsoup를 이용한 웹 페이지 크롤링

siyamaki 2022. 3. 29. 15:57

순서

1. AFK Arena의 홈페이지에서 필요한 쿠폰 코드들을 크롤링하여 DB에 저장(여기서 DB작업은 다 되었다고 가정)

2. 쿠폰 등록 페이지에 들어가서 로그인(쿠키 또는 세션 유지)

3. 로그인 한 상태의 페이지를 파싱하여 requestBody에 쿠폰을 담아 전송


크롤링 하고 싶은 페이지의 URL을 Jsoup객체에 담는다.

import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

final String URL = "https://www.afkarena.net/redemption-codes";
Document doc = Jsoup.connect(URL).get();

get()으로 해당 URL을 불러오면 Document 객체에 해당 URL을 Jsoup가 파싱을 하여 DOM(Document Object Model)형식으로 불러옵니다.(DOM에 대한 자세한 설명은 http://www.tcpschool.com/javascript/js_dom_concept 참조)

 

Elements는 해당 DOM의 요소(태그)들을 저장하는 객체입니다.

Elements couponList = doc.select("input");

doc.select("input")은 input 태그들의 모임을 가지고 있습니다. 배열 형식이며 반복문으로 접근 가능합니다.

for(Element c : couponList) {
    String coupon = c.val();

    if(coupon.equals("")) {
        break;
    } else {
        list.add(coupon);
    }
}

각각의 개별적인 객체는 Element고 해당 태그의 값(실제 화면에 표시되는 값)을 가져올땐 .val()을 이용합니다. 반환형은 String입니다.

 

쿠폰 리스트를 가져왔으면 이 쿠폰과 다른 정보들을 JSON형식으로 만들어 쿠폰을 입력하는 페이지에 전송 할 것입니다.

 

AFK Arena의 쿠폰을 등록하는 URL과 쿠폰을 입력할 때 필요한 인게임 UID가 필요합니다.

final String URL = "https://cdkey.lilith.com/api/verify-afk-code";
String AFKArenaUID = "10001238";
String verifyCode = "1234"

해당 웹 페이지에 접속하여 크롬의 개발자 도구를 실행해 전송 버튼을 눌러 어떠한 파라미터들이 전송되는지 확인합니다.

 

이 페이지에서는 uid, game, code를 요청하고 있습니다. 로그인을 하고 쿠폰을 등록하는 방식이어서 로그인을 한 이후의 페이지를 따로 파싱을 하여야 합니다.

uid는 게임 ID, game은 어떤 게임인지 식별자, code는 로그인 할 때 필요한 인증 코드입니다.

해당 값을 json으로 만들어주어야 합니다.

String에서 따옴표를 쓰기 위해 Escape문자 처리를 하였습니다.

String jsonBody = "{\"uid\":" + AFKArenaUID + ", \"game\":\"afk\", \"code\" : \"" + verifyCode + "\"}";
setSSL();

또한 URL에 Http가 아닌 Https로 연결이 되어 있기 때문에 SSL처리를 따로 해주어야 합니다.

Https에 대한 정보는 https://developer.mozilla.org/ko/docs/Glossary/https 에서 확인하실 수 있습니다.

public static void setSSL() throws NoSuchAlgorithmException, KeyManagementException {
    TrustManager[] trustAllCerts = new TrustManager[] {
        new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() { return null; }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
        }
    };

    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, trustAllCerts, new SecureRandom());

    HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) { return true; }
    }); 
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 
}

해당 메서드를 만들어서 호출만 하면 됩니다.

크롬 개발자 도구를 이용해 어떤 방식으로 통신을 하는지 확인합니다.

로그인 유지를 하기 위해선 쿠키가 필요합니다.

※ 쿠키와 세션의 차이는 쿠키는 Client단(보통 인터넷 프로그램)에 저장되고 세션은 서버단에 저장되며 세션이 보안성이 더 높습니다.

 

SSL까지 설정이 되었으면 로그인하는 URL에 Connection을 설정해야 합니다.

final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36";
Connection.Response getLoginCookie = Jsoup.connect(URL)
.userAgent(userAgent)
.header("Content-Type", "application/json;charset=UTF-8")
.header("Accept", "application/json")
.followRedirects(true)
.ignoreHttpErrors(true)
.ignoreContentType(true)
.header("referer", "https://cdkey.lilith.com/afk-global")
.requestBody(jsonBody)
.method(Connection.Method.POST)
.execute();

 userAgent, Content-Type, Accept, referer 및 requestBody(실제 전송할 데이터), HTTP Method(보통 POST를 많이 사용하며 해당 페이지에서 요청하는 대로 사용) 설정하고 .execute()를 마지막으로 해당 URL에 접속하게됩니다.

 

실제로 로그인을 해 보면 새로운 URL로 이동하게 되는데 이 URL이 우리가 직접 쿠폰 코드를 입력할 URL입니다.

String registURL = "https://cdkey.lilith.com/api/cd-key/consume";

해당 페이지에서는 한번에 1개의 쿠폰만 입력이 가능하니 쿠폰이 담긴 List를 반복문을 돌아 해당 페이지에 접속을 합니다.

for(String coupon : couponList) {
// 로직부분
}

 

개발자 도구를 열어 쿠폰 등록을 하게되면 type, game은 고정값, uid, cdkey가 가변값으로 설정되어 있습니다.

요청에 보낼 새로운 jsonBody를 만들어줍니다.

String jsonBody1 = "{\"type\":\"cdkey_web\", \"game\":\"afk\", \"uid\" : " + AFKArenaUID + ", \"cdkey\" : \""+ coupon +"\"}";

 

 

쿠폰 등록 URL에 Connection을 다시 설정해줍니다. Connection 세팅은 위의 방법과 동일하며 requestBody에는 새로운 파라미터를 보내야 합니다.

for(String coupon : couponList) {
    String jsonBody1 = "{\"type\":\"cdkey_web\", \"game\":\"afk\", \"uid\" : " + AFKArenaUID + ", \"cdkey\" : \""+ coupon +"\"}";
    Connection.Response registCoupon = Jsoup.connect(registURL)
            .userAgent(userAgent)
            .header("Content-Type", "application/json;charset=UTF-8")
            .header("Accept", "application/json")
            .cookies(getLoginCookie.cookies())
            .followRedirects(true)
            .ignoreHttpErrors(true)
            .ignoreContentType(true)
            .header("referer", "https://cdkey.lilith.com/afk-global")
            .requestBody(jsonBody1)
            .method(Connection.Method.POST)
            .execute();
    System.out.println(registCoupon.statusCode());
    int res = registCoupon.statusCode();
    if(res == 200) {
        System.out.println("쿠폰 입력 성공");
    } else {
        System.out.println("사용 기간이 만료되었거나 이미 사용이 완료된 쿠폰입니다.");
    }
}

여기서 이 페이지는 로그인한 쿠키를 검증하고 있기 때문에 위의 로그인 페이지에서 가져온 쿠키를 얻어와서 Connection의 .cookies에 담아야 합니다.

.cookies(getLoginCookie.cookies())

실행을 하게되면 인게임 내로 쿠폰이 사용된 선물이 옵니다.

 

여기서 Connection을 연결해서 statusCode를 받으면 int 리턴값으로 HTTP Status Code가 나오게 됩니다.

(Http Status Code의 자세한 내용은 https://developer.mozilla.org/ko/docs/Web/HTTP/Status 참조)

 

status가 200일 때는 성공이므로 성공 메세지 그 외에는 필요한 내용을 넣어 분기 처리를 해 주면 됩니다.


전체 소스

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.ssafy.backend.coupon.mapper.CouponMapper;
import com.ssafy.backend.coupon.model.GameIDModel;

@Service
public class CouponServiceImpl implements CouponService {
	
	final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36";
	
	@Autowired
	private CouponMapper mapper;
	
	@Override
	public List<String> getAFKArenaCoupon() throws Exception {
		final String URL = "https://www.afkarena.net/redemption-codes";
		
		Document doc = Jsoup.connect(URL).get();
		
		Elements couponList = doc.select("input");
		
		List<String> list = new ArrayList<>();
		
		for(Element c : couponList) {
			String coupon = c.val();
			System.out.println(coupon);
			if(coupon.equals("")) {
				break;
			} else {
				list.add(coupon);
			}
		}
		
		return list;
	}

	@Override
	public int updateAFKArenaUID(GameIDModel model) throws Exception {
		return mapper.updateAFKArenaUID(model);
	}
	
	@Override
	public GameIDModel getGameUID(String userID) throws Exception {
		return mapper.getGameUID(userID);
	}
	
	// 132597005
	@Override
	public void registAFKArenaCoupon(String userID, String verifyCode) throws Exception {
		
		final String URL = "https://cdkey.lilith.com/api/verify-afk-code";
		
		String AFKArenaUID = mapper.selectAFKArenaUID(userID);
		
		String jsonBody = "{\"uid\":" + AFKArenaUID + ", \"game\":\"afk\", \"code\" : \"" + verifyCode + "\"}";
		setSSL();
		
		Connection.Response getLoginCookie = Jsoup.connect(URL)
				.userAgent(userAgent)
				.header("Content-Type", "application/json;charset=UTF-8")
				.header("Accept", "application/json")
                .followRedirects(true)
                .ignoreHttpErrors(true)
				.ignoreContentType(true)
				.header("referer", "https://cdkey.lilith.com/afk-global")
				.requestBody(jsonBody)
				.method(Connection.Method.POST)
				.execute();
		
		System.out.println(getLoginCookie.statusCode());
		
		String registURL = "https://cdkey.lilith.com/api/cd-key/consume";
		List<String> couponList = getAFKArenaCoupon();
		
		for(String coupon : couponList) {
			String jsonBody1 = "{\"type\":\"cdkey_web\", \"game\":\"afk\", \"uid\" : " + AFKArenaUID + ", \"cdkey\" : \""+ coupon +"\"}";
			Connection.Response registCoupon = Jsoup.connect(registURL)
					.userAgent(userAgent)
					.header("Content-Type", "application/json;charset=UTF-8")
					.header("Accept", "application/json")
					.cookies(getLoginCookie.cookies())
	                .followRedirects(true)
	                .ignoreHttpErrors(true)
					.ignoreContentType(true)
					.header("referer", "https://cdkey.lilith.com/afk-global")
					.requestBody(jsonBody1)
					.method(Connection.Method.POST)
					.execute();
			System.out.println(registCoupon.statusCode());
			int res = registCoupon.statusCode();
			if(res == 200) {
				System.out.println("쿠폰 입력 성공");
			} else {
				System.out.println("사용 기간이 만료되었거나 이미 사용이 완료된 쿠폰입니다.");
			}
		}
	}
	
	public static void setSSL() throws NoSuchAlgorithmException, KeyManagementException {
		TrustManager[] trustAllCerts = new TrustManager[] {
			new X509TrustManager() {
				public X509Certificate[] getAcceptedIssuers() { return null; }

				@Override
				public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

				@Override
				public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
			}
		};
		
		SSLContext sc = SSLContext.getInstance("SSL");
		sc.init(null, trustAllCerts, new SecureRandom());
		
		HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
			@Override
			public boolean verify(String hostname, SSLSession session) { return true; }
		}); 
		HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 
	}
}