강좌 Course 2. part 5. 실전프로젝트
강의에서는 2023년 4월 기준으로 시도별 일일 코로나 발생 현황 기록이 사이트에 있었으나, 2023년 11월 현재는 없어졌다. 사이트가 현재는 실시간으로 집계가 안되어서, 그냥 내가 크롤링 하고 싶던 빌보드 차트와 월드와이드 박스오피스 중에서 고민하다가 일단 박스 오피스로 마음을 먹었다. 이미지까지 같이 있었으면 좋았을테지만, 거기까지는 안나오길래 그냥 표만 찾은 것으로 만족했다.
요구사항:
1. 일일 코로나 바이러스 감염 현황을 크롤링한다.
2. 크롤링한 데이터를 CovidStatus 클래스 객체로 저장한다.
3. CovidStatus 객체를 사용하여 콘솔에 시도별 일일코로나 발생 현황을 출력한다. (출력 형식: 시도, 합계, 국내발생, 해외유입, 확진환자, 사망자, 발생률)
4. 크롤링한 데이터를 엑셀 파일로 저장한다. (파일 이름: covidstatus<날짜>_<시간>.xlsx)
5. 크롤링한 데이터를 PDF 파일로 저장한다. (파일 이름: 엑셀과 동일)
클래스 설계:
1. CovidScraper: 웹 크롤링 및 결과 출력 (main)
2. CovidStatus: 코로나 현황 데이터를 저장
3. ExcelExporter: 엑셀 파일로 데이터를 저장
4. PdfExporter: PDF 파일로 데이터를 저장
이 사이트를 크롤링하기로 마음먹었다. 이렇게 하면 설계는 동일하되 코로나 현황 데이터가 아니라 영화 데이터를 담게 된다.
200개의 행이 있는 표여서 처음에는 영화 하나씩 순위, 제목, 성적을 영화 객체에 담으려고 했지만 Element 관계가 애매해서 그냥 영화 순위 따로, 제목 따로, 성적 따로 Elements로 긁어 객체에 저장했다. 또, 미국 내 성적과 해외 성적 비율은 직접 계산할 수도 있으므로 따로 객체에 저장하지 않았다.
BoxOfficeMovie 객체들을 담은 리스트를 반환하는 scrapMovies(), 이 리스트를 받아 엑셀로 저장하는 excelExport(), PDF로 저장하는 pdfExport()를 작성하였다.
// BoxOfficeMovie - 편의상 생성자, setter, getter, toString 생략
package kr.movies.boxoffice;
public class BoxOfficeMovie {
private String rank;
private String title;
private String worldWide;
private String domestic;
private String foreign;
...
}
이 다음으로, 실제 스크랩을 해오는 scrapMovies()를 가지고 있는 메인 클래스의 코드이다. 영화 성적은 전세계, 미국, 해외가 전부 같은 class를 가지고 있어서 우선 한번에 긁어온 후, 한 영화 당 3개의 성적 정보가 가지는 규칙을 활용해 객체에 저장하였다.
package kr.movies.boxoffice;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class BoxOfficeScraper {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
ExcelExporter excelExporter = new ExcelExporter();
PdfExporter pdfExporter = new PdfExporter();
String url = "https://www.boxofficemojo.com/year/world/";
while (true){
System.out.println("---숫자를 입력하세요---");
System.out.println("1. 2023년도 박스 오피스 엑셀로 저장");
System.out.println("2. 2023년도 박스 오피스 PDF로 저장");
System.out.println("3. 종료");
int num = scanner.nextInt();
scanner.nextLine();
if (num == 1) {
List<BoxOfficeMovie> movies = scrapMovies(url);
excelExporter.excelExport(movies);
System.out.println("엑셀 파일을 생성하였습니다.");
} else if (num==2){
List<BoxOfficeMovie> movies = scrapMovies(url);
pdfExporter.pdfExport(movies);
System.out.println("PDF 파일을 생성하였습니다.");
} else if (num == 3){
System.out.println("종료합니다.");
return;
} else {
System.out.println("잘못된 입력입니다.");
}
}
}
public static List<BoxOfficeMovie> scrapMovies(String url) {
List<BoxOfficeMovie> movies = new ArrayList<>();
try {
// 403 오류때문에 추가.
Document document = Jsoup.connect(url).userAgent("Mozilla").get();
// 영화 순위
Elements ranks = document.getElementsByClass("a-text-right mojo-header-column mojo-truncate mojo-field-type-rank mojo-sort-column");
// 영화 제목
Elements titles = document.getElementsByClass("a-text-left mojo-field-type-release_group");
// 영화 전세계, 미국, 해외 성적
Elements w_d_fs = document.getElementsByClass("a-text-right mojo-field-type-money");
List<String> movieRanks = new ArrayList<>();
List<String> movieTitles = new ArrayList<>();
List<String> movieGross = new ArrayList<>();
// 문자열로 저장
for (Element rank : ranks) {
movieRanks.add((String.valueOf(rank.text())));
}
// 문자열로 저장
for (Element title : titles) {
movieTitles.add(title.text());
}
// 문자열로 저장
for (Element w_d_f : w_d_fs) {
movieGross.add(String.valueOf(w_d_f.text()));
}
// 총 200개의 영화;
// 영화 당 성적 정보가 3개 정보 활용하여 객체에 정보 저장
for (int i = 0; i < ranks.size(); i++) {
String rank = movieRanks.get(i);
String title = movieTitles.get(i);
String worldWide = movieGross.get(i * 3);
String domestic = movieGross.get(i * 3 + 1);
String foreign = movieGross.get(i * 3 + 2);
BoxOfficeMovie movie = new BoxOfficeMovie(rank, title, worldWide, domestic, foreign);
movies.add(movie);
}
} catch (Exception e) {
e.printStackTrace();
}
return movies;
}
}
아래는 BoxOfficeMovie 객체 리스트를 받아 엑셀로 저장해주는 코드이다.
// ExcelExporter
package kr.movies.boxoffice;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
public class ExcelExporter {
private Workbook workbook;
public ExcelExporter(){
workbook = new XSSFWorkbook();
}
public void excelExport(List<BoxOfficeMovie> movieList) {
Sheet sheet = workbook.createSheet("Worldwide Box Office");
// 헤더
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("순위");
headerRow.createCell(1).setCellValue("영화 제목");
headerRow.createCell(2).setCellValue("전세계 성적");
headerRow.createCell(3).setCellValue("미국 내 성적");
headerRow.createCell(4).setCellValue("해외 성적");
// 영화 정보 입력
for (int i = 1; i <= movieList.size(); i++) {
Row movieRow = sheet.createRow(i);
movieRow.createCell(0).setCellValue(movieList.get(i-1).getRank());
movieRow.createCell(1).setCellValue(movieList.get(i-1).getTitle());
movieRow.createCell(2).setCellValue(movieList.get(i-1).getWorldWide());
movieRow.createCell(3).setCellValue(movieList.get(i-1).getDomestic());
movieRow.createCell(4).setCellValue(movieList.get(i-1).getForeign());
try {
FileOutputStream fos = new FileOutputStream("Worldwide Box Office.xlsx");
workbook.write(fos);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
PDF로 저장하는 PdfExporter이다.
package kr.movies.boxoffice;
import com.itextpdf.kernel.font.*;
import com.itextpdf.kernel.pdf.*;
import com.itextpdf.layout.*;
import com.itextpdf.layout.element.*;
import com.itextpdf.layout.property.*;
import java.io.IOException;
import java.util.List;
public class PdfExporter {
public PdfExporter(){}
public void pdfExport(List<BoxOfficeMovie> movieList) {
try {
PdfWriter writer = new PdfWriter("Worldwide Box Office.pdf");
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
// 폰트 설정
PdfFont font = PdfFontFactory.createFont("KyoboHandwriting2020pdy.ttf", "Identity-H", true);
document.setFont(font);
document.setFontSize(10);
// 문서 타이틀 생성
Paragraph titleParagraph = new Paragraph("Worldwide Box Office");
titleParagraph.setFontSize(16);
titleParagraph.setTextAlignment(TextAlignment.LEFT);
titleParagraph.setBold();
document.add(titleParagraph);
// 영화 정보 테이블 생성
float[] columnWidth = new float[]{2, 3, 2, 2, 2};
Table table = new Table(UnitValue.createPointArray(columnWidth));
table.setWidth(UnitValue.createPercentValue(100));
table.setMarginTop(20);
// 표 헤더 생성
table.addHeaderCell(new Cell().add(new Paragraph("순위").setFont(font)));
table.addHeaderCell(new Cell().add(new Paragraph("영화 제목").setFont(font)));
table.addHeaderCell(new Cell().add(new Paragraph("전세계 매출").setFont(font)));
table.addHeaderCell(new Cell().add(new Paragraph("미국 매출").setFont(font)));
table.addHeaderCell(new Cell().add(new Paragraph("해외 매출").setFont(font)));
// 영화 정보 입력
for (BoxOfficeMovie movie : movieList) {
table.addCell(new Paragraph(movie.getRank()).setFont(font));
table.addCell(new Paragraph(movie.getTitle()).setFont(font));
table.addCell(new Paragraph(movie.getWorldWide()).setFont(font));
table.addCell(new Paragraph(movie.getDomestic()).setFont(font));
table.addCell(new Paragraph(movie.getForeign()).setFont(font));
}
document.add(table);
document.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
또, 처음에는 영화 제목을 제외한 나머지 숫자들을 전부 정수와 실수로 받았지만, 3자리마다 콤마 표시 및 $ 표시 등이 가독성이 훨씬 낫다고 생각해서 전부 통째로 문자열로 받았다. pdf는 따로 수정할 일이 없고 엑셀은 작업할 때 쉽게 숫자 형태로 바꿀 수 있기 때문에 큰 문제가 없다고 판단했다.
우여곡절 끝에 완성한 결과,
콘솔로 키보드 입력을 받아 엑셀 및 pdf로 저장한다.
00개의 영화 박스오피스 성적 정보가 엑셀로 저장된 것을 볼 수 있다.
PDF로는 좋아하는 폰트도 적용했다.(ㅎ)
추후 추가할 점:
1. 파일명에 현재 날짜 넣기 >> 날짜 api 사용
2. 백분율 계산 포함