JAVA/소박한그룹 프로젝트

Solvedac API를 이용한 해결한 문제 리스트 만들기

kth990303 2021. 6. 1. 22:35
반응형

이번 포스팅의 목표

userName에 백준 id를 입력하면 solvedac api를 호출하여 그 유저가 해결한 문제 목록들을 가져오기

kth990303이 해결한 문제 목록들을 api 호출로 가져와보았다.


실행환경

Java 11

Gradle

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
    implementation 'org.apache.httpcomponents:httpclient:4.5'
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'org.json:json:20190722'
}

우리 그룹원들을 위한 임시 홈페이지는 백준 그룹 홈페이지 외에도 아래 홈페이지가 있다.

https://kth990303.github.io/BaekjoonStudy/test/solvedInfo.html

 

건국대 백준 스터디

건국대 백준 스터디 kth990303's 백준 프로필 kth990303's solved 프로필 알고리즘 기초 난이도인 Bronze부터 숙달된 난이도인 Diamond까지 DFS, BFS 의 기본적인 알고리즘부터 TreeDP, Segtree 의 중급 알고리즘을

kth990303.github.io

당시 그룹원이 총 5명 내외였을 때 사용한 홈페이지이다.

2020년 쯤에 사용하던 홈페이지

이 홈페이지의 아쉬운 점은 백엔드가 없다는 점도 한 몫하지만,

유저가 해결한 문제인지 아닌지에 따른 표시가 되지 않는다는 점이었다.

이 점을 해결하기 위해선 BOJ api를 이용하던지, 아니면 크롤링을 하던지 해야 하는데, 다행히 solved.ac api를 발견하였고, 이걸 이용해 여러가지를 만드는 시도를 진행중이다.

 


주 의!

 

(2021.11. 추가)

현재 SolvedAC API는 v3이며, 아래 글에서는 v2였을 때 api 통신을 이용했습니다.

v3 환경에선 제대로 작동되지 않을 수 있으며, 

HttpClient api 통신방법 참고용으로 봐주시면 감사하겠습니다.

 

(또한, api 통신방법에는 HttpClient도 있지만, httpUrlConnection, RestTemplate도 있습니다.)


 

solvedac api는 아래와 같다.

https://solvedac.github.io/unofficial-documentation/

 

Swagger UI

 

solvedac.github.io

또한 참고용으로, solvedac 개발자분께서 어떻게 solvedac를 개발하셨는지 포스팅을 구경하는 것 또한 재밌으므로 여기에 남겨본다.

https://blog.shift.moe/2019/09/14/developing-solved-ac-1/

 

solved.ac 개발기 1: 학회에서 사용할 서비스 만들기 – Shifted

병역특례 인원이 축소된다는 분위기입니다. 알 수 없는 위기감이 맴돕니다. Sogang ICPC Team에서 (학회장 한다는 사람이 없으면) 제가 졸업할 때까지 학회장을 하려고 했는데, 지금 병역 문제를 해

blog.shift.moe

 

현재의 목표는 아래 홈페이지처럼 건대생들이 풀지 않은 문제 리스트를 만들어보는 것이다.

http://dgful.com/

 

DGIST BOJ

2021-05-31 18:19:08 18133 가톨릭대학교에 워터 슬라이드를??

dgful.com

 

java로 크롤링 및 rest api를 호출해본 경험이 없던 나에겐 하나의 큰 과제였다.


Github

현재까지 진행상황 및 코드리뷰를 해주고 싶다면 아래 깃헙으로 가서 보면 된다.

https://github.com/kth990303/bojUnsolved

 

kth990303/bojUnsolved

use solvedac api. Contribute to kth990303/bojUnsolved development by creating an account on GitHub.

github.com


HttpClient를 이용한 Rest API 호출

처음부터 스프링 부트를 이용하여 만들어보기엔 조금 버겁지 않을까 싶어 기본 자바프로젝트로 만들어보았다.

이번 포스팅에선 자세한 원리는 다루지 않고 목표를 달성하는 데에만 초점을 둘 것이다.

왜냐 하면 나도 아직 공부를 안했기 때문이다...

 

먼저 solvedac api 중에 회원이 해결한 문제 목록들을 json 형태로 제공하는 api가 있다. 아래 api는 kth990303이 해결한 문제 목록들에 대한 api이다.

https://api.solved.ac/v2/search/problems.json?query=solved_by:kth990303&page=11 

11페이지 중 마지막 페이지 부분이다.

보다시피, 해결한 문제 목록이 1100문제나 되기 때문에 여러 문제점들이 발생한다.

  1. 한 URI만 api 호출하는 것은 불가능하다.
  2. 중첩된 객체를 파싱할 수 있는 능력이 필요하다. (객체 안에 객체)

사실상 1번은 그렇게 어렵지 않고, 2번 역시 gson이든 objectMapper든, JAVA를 이용한 json 파싱을 잘 다룰 줄 알면 해결되는 문제이다. 그러나 나는 이번에 처음으로 파싱, api 호출을 해보는 것이었기 때문에 굉장히 헤맸다.

동기 한명이 간단한 코드를 보내주었다.

파싱을 하기 위해 HttpClient를 이용해야 하며,

HttpClient를 이용하기 위해선 HttpClientBuilder가 build할 수 있도록 해줘야했다.

위 과정은 딱 한번만 필요하다. (마치 jpa에서 EntityManagerFactory 같은 느낌인듯?)

 

url은 위 api 링크를 이용하면 된다.

 

먼저 kth990303이 총 해결한 문제 페이지는 11페이지로 이루어져 있기 때문에, 

위 링크 api 호출한 json 데이터에서 총 페이지 수 정보를 파싱하는 메소드를 만들었다.

public String getPagesJson(String url) throws IOException {
        CloseableHttpClient client = HttpClientBuilder.create().build();
        HttpGet request = new HttpGet(url);
        request.setHeader("Content-Type", "application/json");

        CloseableHttpResponse response = client.execute(request);
        HttpEntity entity = response.getEntity();
        if(response.getStatusLine().getStatusCode()==200){
            System.out.println("connect success");
            System.out.println(response.getStatusLine().getStatusCode());
            ResponseHandler<String> handler = new BasicResponseHandler();
            String body = handler.handleResponse(response);
            return body;
        }
        else{
            System.out.println("connect fail");
            System.out.println(response.getStatusLine().getStatusCode());
            return "fail";
        }
    }

HttpClientBuilder에서 create, build해줌으로써 httpclient를 사용할 수 있게 만들어주었고,

HttpGet으로 solved api를 사용하도록 하였다.

json 데이터를 주고받을 것이므로 헤더설정을 위와 같이 해주고,

 

이제 api 호출을 시작하자! client.execute(request (api 주소));를 하면 된다.

나는 page=? 부분의 ?를 비워두어서 아래 json 데이터만 받아오게 하였다. 어차피 총 페이지수만 알면 되기 때문이다.

(총 페이지수를 모르고 파싱하면 푼문제 리스트를 언제까지 파싱해야될지 모르기 때문)

 

만약 호출이 정상적으로 돼서 http code가 200이 반환된다면,

BasicResponseHandler 클래스의 handleResponse 메소드로 json 데이터를 받아오면 된.

그럼 위와 같은 결과가 나온다.

 

사실 잘한건지도 감이 안온다.

내일 공부를 해봐야 알것같고, 스프링에 적용하면 또 다를 수도 있을 듯하다.


GSON을 이용한 푼문제 목록 파싱

자, 이제 본격적으로 푼 문제 목록들을 파싱해보자.

json 데이터를 얻었으니 파싱을 하면 되는데, 나는 GSON 으로 파싱할 것이다.

JsonParser parser = new JsonParser();
    public int getPage(String json){
        JsonElement element = parser.parse(json);

        JsonElement result = element.getAsJsonObject().get("result");
        int page = result.getAsJsonObject().get("total_page").getAsInt();
        return page;
    }
    public int getCnt(String json){
        JsonElement element = parser.parse(json);

        JsonElement result = element.getAsJsonObject().get("result");
        int cnt = result.getAsJsonObject().get("total_problems").getAsInt();
        return cnt;
    }

총 페이지 수, 총 해결한 문제수 정보가 모두 result 객체 안에 있으므로 result 객체를 먼저 얻어와야 한다.

result 객체 데이터를 얻기 위해 getAsJsonObject()로 객체 정보를 얻어온다.

객체 안에 페이지 수, 문제수 등의 정보들이 객체로 쌓여있으므로 다시 result.getAsJsonObject()를 통해 객체에 접근한 후, get("total_page")로 총 페이지 정보에 접근한다. 이것을 Int로 parse하면 된다.

 

총 해결한 문제수도 마찬가지 방법으로 이용한다.

 

아마 위와 같이 짜면 JsonParser(), parser.parse(json); 와 같이 밀줄이 뜰 것이다.

이것은 GSON이 업데이트를 하면서 굳이 위와 같이 인스턴스화할 필요 없다는 경고로 된 것인데, 실제로 실행해보면 경고만 뜨고 아무 문제 없이 잘 돌아가긴 한다.

그럼에도 불구하고, 위 코드를 사용하는 이유는 이상하게 정적 메소드를 사용하는 방법으로 바꿨더니 실행속도가 좀 느렸기 때문이다.

 

만약 정적 메소드를 사용하는 방법으로 고치고 싶으면 아래 블로그를 참고하면 된다.

https://hong00.tistory.com/42

 

[gson] JsonParser 사용법

gson의 JsonParser를 사용하는 방법을 정리한다. Object를 String, JsonObject로 변경하는 방법이다. 먼저 아래와 같이 임시 class를 정의한다. String 형태의 key는 "drink", Gson의 JsonArray value는 "coffee",..

hong00.tistory.com

 

이제 총 페이지 수에 대한 정보도 얻었으므로 문제리스트들을 쭉 뽑아보자.

public List<Integer> getProblems(int page, String url) throws IOException {
        List<Integer> list=new ArrayList<>();
        for(int i=1;i<=page;i++){
            String json = getPagesJson(url + Integer.toString(i));
            JsonElement element = parser.parse(json);

            JsonElement result = element.getAsJsonObject().get("result");
            JsonArray problems = result.getAsJsonObject().getAsJsonArray("problems");
            for(int j=0;j<problems.size();j++){
                int problemId = problems.get(j).getAsJsonObject().get("id").getAsInt();
                list.add(problemId);
            }
        }
        return list;
    }

나는 List에 집어넣어서 그 list를 리턴하는 방식을 이용했다.

 

* 그 외에 GSON은 파싱 뿐만 아니라 Java Object <-> JSON Object 로 요리조리 변환하는 기능도 가지고 있다. 나중에 스프링 부트에서 실행할 때, 도메인으로 db에 insert할 때 꼭 필요할 듯 하여 도움이 된 블로그 링크를 남겨두겠다.

https://soft.plusblog.co.kr/61

 

[Gson 사용법] 자바 객체와 JSON을 다루는 쉬운 방법, 몇 가지 예제

Java 클래스를 다른 곳으로 전송할 때 사용할 수 있는 직렬화(Serialize) 포맷으로 JSON이나 XML 같은 텍스트 포맷을 사용하는 경우가 많다. 문자열을 사용하는 만큼 데이터를 처리하는데 상당히 높은

soft.plusblog.co.kr


메인 메소드 및 실행

흠... 메인 메소드가 정말 개판이다.

뭐 ㅎㅎ 돌아가기만 하면 된다는 마인드로 실행해보았다.

import userProblem.userApi;

import java.io.IOException;
import java.util.List;

public class HelloMain {
    public static void main(String[] args) throws IOException {
        String userName="kth990303";
        String URL
                ="https://api.solved.ac/v2/search/problems.json?query=solved_by:"+userName+"&page=";

        userApi userApi=new userApi();
        String pagesJson = userApi.getPagesJson(URL);
        int page = userApi.getPage(pagesJson);
        int cnt = userApi.getCnt(pagesJson);
        List<Integer> problemsList = userApi.getProblems(page, URL);
        System.out.println(userName+"'s solve problems ': "+cnt+" questions");
        for (int i=0;i<problemsList.size();i++) {
            System.out.print(problemsList.get(i)+" ");
            if(i%15==14)
                System.out.println();
        }
    }
}

총 11페이지이므로 1+11 = 12번 쿼리를 날림을 확인
실행 속도가 11초 정도임을 확인

결과가 잘 나옴을 확인할 수 있다.


이제 다음에는 백준 전체문제 목록을 api로 가져오고,  (api: https://api.solved.ac/v2/search/problems.json?query=tier:b5ornottier:b5 )

건국대생 명단을 jsoup로 크롤링으로 가져와볼 생각이다. (... 문제되면 포기할 수밖에 없겠지만)

 

그렇게 하여 건대생이 해결하지 않은 문제 리스트들을 출력해볼 생각이고,

이후에 스프링부트로 업그레이드하여 웹사이트로 만들어보고 싶다.

반응형