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에 아주 잘 저장이 되었다.