springboot

[springboot] java로 크롤링 하기 - Jsoup , 네이버 주식 가격 조회-02

vmpo 2021. 9. 17. 17:19

자바로 네이버 주식가격 크롤링하기 두번째 포스팅입니다.

기본 개발 환경은 아래 포스팅 확인해주시면 됩니다.

 

[springboot] java로 크롤링 하기 - Jsoup , 네이버 주식 가격 조회 -01 - 오오코딩 (tistory.com)

 

[springboot] java로 크롤링 하기 - Jsoup , 네이버 주식 가격 조회 -01

Java로 크롤링 쉽게 하기 Java에서도 Python 처럼 쉽게 크롤링이 가능합니다. Jsoup 라이브러리를 활용해서 네이버 주식 가격을 조회 해보도록 하겠습니다. 전체 소스는 제일 하단에 공유해드렸습니

vmpo.tistory.com

 

이번 포스팅에선 실제 로컬 서버를 띄우고 브라우저에서 크롤링한 데이터를 출력해보도록 하겠습니다.

기본 패키지 구조 입니다.

아래 구조로 구성해두었습니다.

 

Controller - url정보 셋팅

Service - 컨트롤러에서 호출할 메소드 구현

domain - 리턴할 dto 경로

component - 실제 크롤링할 로직 처리

 

#Controller

아래와 같이  localhost:8080/kospi/all 을 호출했을때 처리를 위한 컨트롤러 생성. Json형태로 리턴함.

package com.oogo.api.controller.stock;

import com.oogo.api.domain.dto.stock.KospiStockDto;
import com.oogo.api.service.stock.StockService;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class StockController {

  private final StockService stockService;

  @GetMapping("/kospi/all")
  public List<KospiStockDto> getKosPiStockList(HttpServletRequest request) {
    return stockService.getKosPiStockList();

  }
}

 

#Service

- 컨트롤러에서 호출할 메소드로 component 메소드를 호출해서 결과값을 컨트롤러로 전달함 (중간 다리 역할)

package com.oogo.api.service.stock;

import com.oogo.api.component.stock.JsoupComponent;
import com.oogo.api.domain.dto.stock.KospiStockDto;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class StockService {

  private final JsoupComponent jsoupComponent;

  public List<KospiStockDto> getKosPiStockList() {
    return jsoupComponent.getKosPiStockList();
  }
}

 

#domain

크롤링할 결과 값을 저장할 Dto 클래스 토론방url(discussionRoomUrl) 은 뒷 uri만 크롤링되므로 앞에 도메인까지 포함 될 수 있도록 getter를 새로 추가해준다.

package com.oogo.api.domain.dto.stock;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KospiStockDto {

  private String no;
  private String stockName;
  private String price;                // 현재가
  private String diffAmount;           // 전일비
  private String dayRange;             // 등락률
  private String parValue;             // 액면가
  private String marketCap;            // 시가총액
  private String numberOfListedShares; // 상장 주식 수
  private String foreignOwnRate;       // 외국인 비율
  private String turnover;             // 거래량
  private String per;                  // per
  private String roe;                  // roe
  private String discussionRoomUrl;    // 토론방 url

  public String getDiscussionRoomUrl() {
    return "https://finance.naver.com"+discussionRoomUrl;
  }
}

 

#Component

실제 크롤링 로직 처리를 하는 클래스

package com.oogo.api.component.stock;

import com.oogo.api.domain.dto.stock.KospiStockDto;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
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.stereotype.Component;

@Component
public class JsoupComponent {

  public List<KospiStockDto> getKosPiStockList() {
    final String stockList = "https://finance.naver.com/sise/sise_market_sum.nhn?&page=1";
    Connection conn = Jsoup.connect(stockList);
    try {
      Document document = conn.get();
      return getKosPiStockList(document);
    } catch (IOException ignored) {
    }
    return null;
  }

  public List<KospiStockDto> getKosPiStockList(Document document) {
    Elements kosPiTable = document.select("table.type_2 tbody tr");
    List<KospiStockDto> list = new ArrayList<>();
    for (Element element : kosPiTable) {
      if (element.attr("onmouseover").isEmpty()) {
        continue;
      }
      list.add(createKosPiStockDto(element.select("td")));
    }
    return list;
  }

  public KospiStockDto createKosPiStockDto(Elements td) {
    KospiStockDto kospiStockDto = KospiStockDto.builder().build();
    Class<?> clazz = kospiStockDto.getClass();
    Field[] fields = clazz.getDeclaredFields();

    for (int i = 0; i < td.size(); i++) {
      String text;
      if(td.get(i).select(".center a").attr("href").isEmpty()){
        text = td.get(i).text();
      }else{
        text = "https://finance.naver.com" + td.get(i).select(".center a").attr("href");
      }
      fields[i].setAccessible(true);
      try{
        fields[i].set(kospiStockDto,text);
      }catch (Exception ignored){
      }
    }
    return kospiStockDto;
  }
}

 --Component 상세 설명--

 

1. getKosPiStockList() 

- 서비스에서 호출할 메소드로 

- 타겟 URL을 기준으로 크롤링하기 위한 커넥션을 생성하고 document 객체를 생성한다.

- getKosPiStockList(document) 를 호출해 결과값을 받아 리턴한다.

  public List<KospiStockDto> getKosPiStockList() {
    final String stockList = "https://finance.naver.com/sise/sise_market_sum.nhn?&page=1";
    Connection conn = Jsoup.connect(stockList);
    try {
      Document document = conn.get();
      return getKosPiStockList(document);
    } catch (IOException ignored) {
    }
    return null;
  }

2. getKosPiStockList(Document document)

- 전체 결과값을 1차적으로 전달받는다. (document.select() 메소드로 태그 접근함)

- 이제 전달 받은 결과값 kosPiTable 변수에 담긴 데이터를 for loop를 통해 하나씩 KospiStockDto 객체로 변환해준다.

- KospiStockDto 객체 변환은 createKosPiStockDto() 메소를 활용한다.

- 변환된 객체는 List<KospiStockDto> list; 에 저장한다. 

- 저장한 list를 리턴한다.

  public List<KospiStockDto> getKosPiStockList(Document document) {
    Elements kosPiTable = document.select("table.type_2 tbody tr");
    List<KospiStockDto> list = new ArrayList<>();
    for (Element element : kosPiTable) {
      if (element.attr("onmouseover").isEmpty()) {
        continue;
      }
      list.add(createKosPiStockDto(element.select("td")));
    }
    return list;
  }

 

3. createKosPiStockDto(Elements td)

- 전달 받은 객체를 dto 객체로 변환한다.

- java 의 리플렉션을 활용해 객체로 변환해준다.

- KospiStockDto kospiStockDto = KospiStockDto.builder().build(); 기본 객체를 하나 생성해준다.

  -> KospiStockDto kospiStockDto = new KospiStockDto() 와 동일한 의미이다.

- kospiStockDto.getClass();  해당객체의 Class 타입으로 접근한다.

- clazz.getDeclaredFields();  전체 필드를 배열로 가져온다.

 -> KospiStockDto 에서 생성한 필드 순서로 (위에서 부터 차례로) 배열을 생성한다.

- 전달받은 파라미터인 "Element td" 는 앞서 크롤링시 <tr> 태그 하위에 있는 td들을 모두 갖는 리스트라고 생각하면 된다.

- 아래 캡처화면의 <tr></tr> 영역 정보라고 보면된다.

          <tr>

              <td>..</td>

              <td></td>

          </tr>

- Element td 를 기준으로 for문을 돌리며 i=0 부터 순서대로 Field[] 배열의 0번째 데이터부터 변경해준다.

- field[i].setAccessible(true); private 변수에 대한 접근을 허용하도록 한다.(KospiStockDto 필드가 private 이기 때문)

- fields[i].set(kospiStockDto,text); set 메소드로 필드의 값을 셋팅한다. 

   파라미터로 set(셋팅할 원본 객체, 변경할 값) 을 전달하면 된다. 

- 셋팅된 KospiStockDto 객체를 리턴한다.

  public KospiStockDto createKosPiStockDto(Elements td) {
    KospiStockDto kospiStockDto = KospiStockDto.builder().build();
    Class<?> clazz = kospiStockDto.getClass();
    Field[] fields = clazz.getDeclaredFields();

    for (int i = 0; i < td.size(); i++) {
      String text;
      if(td.get(i).select(".center a").attr("href").isEmpty()){
        text = td.get(i).text();
      }else{
        text = "https://finance.naver.com" + td.get(i).select(".center a").attr("href");
      }
      fields[i].setAccessible(true);
      try{
        fields[i].set(kospiStockDto,text);
      }catch (Exception ignored){
      }
    }
    return kospiStockDto;
  }

 

이제 빌드해서 실제 브라우저에서 어떤결과가 출력되는지 확인해보도록 하겠습니다.

 

http://localhost:8080/kospi/all 호출 결과

아래와 같이 json array 결과를 확인 할 수 있습니다.

이 데이터를 db에 저장할 수도 있고 html을 만들어서 예쁘게 화면으로 출력 할 수도 있을 것 같습니다. 

다음 포스팅에선 db 저장 및 html 화면 출력까지 한 번 해보도록 하겠습니다.

 

 

전체소스는 아래 Github 참고 부탁드립니다.

build99k/spring-boot-buildup (github.com)

 

GitHub - build99k/spring-boot-buildup

Contribute to build99k/spring-boot-buildup development by creating an account on GitHub.

github.com

 

LIST