Project

[stock] 배당금 저장

sangyunpark 2023. 9. 15. 15:29

Scrapper를 확장하기 위해(야후 Finance 뿐만아니라 Naver Finance 등등에서 사용하기 위해)

interface화

package com.example.stock.scrapper;

import com.example.stock.model.Company;
import com.example.stock.model.ScrapedResult;

public interface Scrapper { // 확장성 있게 interface 사용 - 다른 웹에서도 사용할 수 있도록 -
    public Company scrapCompanyByTicker(String ticker);
    public ScrapedResult scrap(Company company);
}

 

Scrapper를 구현한 YahooFinaceScrapper

package com.example.stock.scrapper;

import com.example.stock.model.Company;
import com.example.stock.model.Dividend;
import com.example.stock.model.ScrapedResult;
import com.example.stock.model.constants.Month;
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;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Component
public class YahooFinanceScrapper implements Scrapper{

    private static final String URL = "https://finance.yahoo.com/quote/%s/history?period1=%d&period2=%d&interval=1mo"; // heap 영역에 url 할
    private static final long START_TIME = 86400; // 60 * 60 * 24
    private static String SUMMARY_URL = "https://finance.yahoo.com/quote/%s?p=%s";

    @Override
    public ScrapedResult scrap(Company company) {

        ScrapedResult scrapedResult = new ScrapedResult();
        scrapedResult.setCompany(company);

        try {

            long now = System.currentTimeMillis() / 1000;

            String url = String.format(URL, company.getTicker(), START_TIME, now); // formatting

            // 데이터 가져오기
            Connection connection = Jsoup.connect(url);
            Document document = connection.get();

            Elements parsingDivs = document.getElementsByAttributeValue("data-test", "historical-prices");
            Element table = parsingDivs.get(0);

            Element tbody = table.children().get(1); // tbody 값 가져오기

            List<Dividend> dividends = new ArrayList<>();
            for (Element e : tbody.children()) {
                String txt = e.text();
                System.out.println(txt);
                if (!txt.endsWith("Dividend")) { // dividend로 끝나지 않는 문자열
                    continue;
                }

                String[] splits = txt.split(" ");
                int month = Month.strToNumber(splits[0]);
                int day = Integer.parseInt(splits[1].replace(",", ""));
                int year = Integer.parseInt(splits[2]);
                String dividend = splits[3];

                if (month < 0) {
                    throw new RuntimeException("Unexpected Month enum value -> " + splits[0]);
                }


                dividends.add(Dividend.builder()
                        .date(LocalDateTime.of(year, month, day, 0, 0))
                        .dividen(dividend)
                        .build());
            }

            scrapedResult.setDividends(dividends);

        } catch (IOException e) {
            e.printStackTrace();
        }

        return scrapedResult;
    }

    @Override
    public Company scrapCompanyByTicker(String ticker){

        String url = String.format(SUMMARY_URL,ticker,ticker);

        try{

            Document document = Jsoup.connect(url).get();
            Element titleEle = document.getElementsByTag("h1").get(0);
            
            StringBuilder title = new StringBuilder();

            for (int i = 0; i < titleEle.text().length(); i++) {

                char value = titleEle.text().charAt(i);

                if(value == '('){
                    break;
                }

                title.append(value);
            }

            return Company.builder()
                    .ticker(ticker)
                    .name(title.toString())
                    .build();

        }catch(IOException e){
            e.printStackTrace();
        }

        return null;
    }
}

try ~ catch를 사용해서 스크래핑을 하지 못하는 경우를 생각해주어야 한다.

받아온 데이터를 년,월,일 중 월에 대한 부분을 잘 정의해주기 위해서 Month enum을 사용한다.

package com.example.stock.model.constants;

public enum Month {

    JAN("Jan",1),
    FEB("Feb",2),
    MAR("Mar",3),
    APR("Apr",4),
    MAY("May",5),
    JUN("Jun",6),
    JUL("Jul",7),
    AUG("Aug",8),
    SEP("Sep",9),
    OCT("Oct", 10),
    NOV("Nov",11),
    DEC("Dec",12);

    private String s;
    private int number;

    Month(String s, int n){
        this.s = s;
        this.number = n;
    }

    public static int strToNumber(String s){
        for(Month m : Month.values()){
            if(m.s.equals(s)){
                return m.number;
            }
        }

        return -1;
    }
}

 

Controller

package com.example.stock.controller;

import com.example.stock.model.Company;
import com.example.stock.service.CompanyService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/company")
@AllArgsConstructor
public class CompanyController {

    private final CompanyService companyService;

    @GetMapping("/autocomplete")
    public ResponseEntity<?> autocomplete(@RequestParam String keyword){
        return null;
    }

    @GetMapping // 회사 리스트 조회
    public ResponseEntity<?> searchCompany(){
        return null;
    }

    @PostMapping // 회사 저장
    public ResponseEntity<?> addCompany(@RequestBody Company request){
        String ticker = request.getTicker().trim();
        if(ObjectUtils.isEmpty(ticker)){
            throw new RuntimeException("ticker is empty");
        }

        Company company = this.companyService.save(ticker);

        return ResponseEntity.ok(company);
    }

    @DeleteMapping // 배당금 정보 삭제
    public ResponseEntity<?> deleteCompany(){
        return null;
    }
}

회사의 정보를 저장하기 위한 addCompany Controller로직 구현

ticker를 이용하여 회사에 대한 정보를 저장해준다.(배당금  포함)

 

 

addCompany에서 사용되는 companyService의 save 메소드를 보자!

 

 

Service

CompanyService

package com.example.stock.service;

import com.example.stock.entity.CompanyEntity;
import com.example.stock.entity.DividenEntity;
import com.example.stock.model.Company;
import com.example.stock.model.ScrapedResult;
import com.example.stock.repository.CompanyRepository;
import com.example.stock.repository.DividenRepository;
import com.example.stock.scrapper.YahooFinanceScrapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.List;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class CompanyService {

    private final YahooFinanceScrapper yahooFinanceScrapper;

    private final CompanyRepository companyRepository; // repository 불러오기
    private final DividenRepository dividenRepository;

    public Company save(String ticker) { // 긁어온 데이터 저장

        boolean exists = this.companyRepository.existsByTicker(ticker);

        if(exists){;
            throw new RuntimeException("already exists");
        }

        return this.storeCompanyAndDividend(ticker);
    }

    private Company storeCompanyAndDividend(String ticker){

        // ticker를 기준으로 회사를 스크래핑
        Company company = this.yahooFinanceScrapper.scrapCompanyByTicker(ticker);

        if(ObjectUtils.isEmpty(company)){
            // ObjectUtils.isEmpty() -> null 체크와 빈 문자열 체크를 동시에 할 수 있다.
            throw new RuntimeException("failed to scrap ticker -> " + ticker);
        }

        // 해당 회사가 존재할 경우, 회사의 배당금 정보를 스크래핑
        ScrapedResult scrapedResult = this.yahooFinanceScrapper.scrap(company);

        // 스크래핑 결과
        CompanyEntity companyEntity = this.companyRepository.save(new CompanyEntity(company));

        List<DividenEntity> dividenEntities = scrapedResult.getDividends().stream()
                .map(e -> new DividenEntity(companyEntity.getId(), e))
                .collect(Collectors.toList()); // collect를 호출해야 map이 마무리 된다.

        this.dividenRepository.saveAll(dividenEntities);

        return company;
    }
}

h2 Database에 데이터가 존재하는지 여부를 먼저 살펴보고 존재하지 않을 경우에만 데이터를 저장해주는 것을 볼 수 있다.

Service내부에 scrapper 메소드를 호출한후, repository를 사용해서 h2 database에 저장해주는 방식으로 구현했다.

 

 

요청 보내기

 

결과

 

H2 Database에 아주 잘 저장이 되었다.