[nodejs] 웹툰 플랫폼 랭킹 순위 크롤링 해보기 - 2

protomothis·2023년 1월 15일
0

지금까지...

지난 시간에는 픽코마에서 제공하는 랭킹 페이지에서 데이터를 긁어오는 서비스를 만들었다. 사실 크롤링이라기보다는 자동화되지 않은 스크랩에 가까운데 추후에는 특정 시간에 데이터를 가져오기, 가져온 데이터를 정제하고 쌓기를 구현한다면 충분히 괜찮은 서비스가 될 것 같다.
이번 시간에는 긁어온 데이터를 엑셀로 말아서 요청이 오면 엑셀 파일로 응답을 해주는 작업을 해볼까 한다.

노드 환경에서 어떻게 엑셀을 만들지?

이전 시간에도 살펴봤듯이, 무수한 라이브러리가 존재한다. 그 중에서 유명하고 쓰기 편한 api를 제공하는 exceljs를 사용해볼까 한다.

https://github.com/exceljs/exceljs

이 친구는 csv부터 microsoft excel과 호환되는 xlsx 엑셀 포맷으로도 워크 시트를 작성할 수 있는 api를 제공한다.
내가 추출한 랭킹 데이터에는 단순 문자열 데이터가 대다수지만, 커버 이미지를 포함하기 때문에 이미지 포맷을 지원하는 xlsx 엑셀 포맷으로 구성해보려고 한다.

Exceljs 적용하기

  public async createDataSheet(list: ResponsePiccomaRankingData[]) {
    const reformatData = await this.refactPiccomaRankingData(list);
    // 새로운 워크북 인스턴스를 생성합니다.
    const wb = new exceljs.Workbook();
    // 워크북에 시트를 추가합니다.
    const ws = wb.addWorksheet('Sheet 1');
	// 데이터를 차트에 담기 편하게 remapping...
    const rows = reformatData.map((row, i) => (['', row.ranking, row.title, row.author, row.likes]));
    // 워크 시트에 테이블을 초기화합니다.
    const table = ws.addTable({
      name: 'rankingTable',
      ref: 'A1',
      headerRow: true,
      style: {
        // exceljs에서 기본으로 제공하는 테마를 사용한다.
        theme: 'TableStyleDark3',
        showRowStripes: true,
      },
      columns: [{
        name: 'cover',
      }, {
        name: 'ranking',
      }, {
        name: 'title',
      }, {
        name: 'author',
      }, {
        name: 'likes',
      }],
      rows,
    });

    // 이미지 버퍼를 준비합니다.
    const responses = await Promise.all(list.map((row) => fetch(`https:${row.imgSrc}`))).then((response) => response.map((r) => r));
    const buffers = await Promise.all(responses.map(async (r) => r.buffer()));

    // 이미지 버퍼를 워크북에 등록합니다.
    buffers.forEach((buffer, index) => {
      if (buffer === null) {
        return;
      }
      const imageId = wb.addImage({ buffer, extension: 'png' });
      // 순차적으로 이미지 추가해주기
      ws.addImage(imageId, {
        tl: { col: 0, row: index + 1 },
        ext: { width: 164, height: 234 },
      });
    });

    // column 내 row width 크기에 맞게 리사이즈를 해주는 역할을 함
    ExcelHelperService.adjustWorkSheetColumnWidth(ws);

    return wb.xlsx.writeBuffer();
  }

// ExcelHelperService.tsx
class ExcelHelperService {
  public static adjustWorkSheetColumnWidth(workSheet: excelJs.Worksheet): void {
    // 전체 컬럼을 대상으로...
    workSheet.columns.forEach(column => {
      // 컬럼 내 모든 로우의 길이를 측정하고
      const lengths = column.values.map(v => v.toString().length);
      // 최대 길이를 뽑아서 최대 길이만큼 컬럼 width 를 맞춰준다
      const maxLength = Math.max(...lengths.filter(v => typeof v === 'number'));
      column.width = maxLength;
    });
  }
}

export default ExcelHelperService;

아직 완전히 정리되지 않았지만, 이처럼 데이터를 엑셀로 구성해주는 서비스 코드를 작성했다. 작성하고 나니 예외 처리에 대해서는 고려가 안되어 있었는데.. 우선 서비스를 호출하는 곳에서 핸들링 하도록 했다.
추후에 테스트 코드를 작성하고, 한번 정리가 필요해 보인다.

그런대 그때, 지인에게 추가적인 요청이 있었다.

"자동으로 뽑아주는 건 좋은데 혹시 컨텐츠 번역도 가능할까? 아마.. 제목만 알아볼 수 있게 우리 회사 작품만이라도 국문화 했으면 좋겠는데"

안될거 없어서 그럼 일본어 제목과 매치되는 번역본을 만들어달라고 했고, 다음과 같이 작업했다.

  private async refactPiccomaRankingData(list: ResponsePiccomaRankingData[]) {
    const wb = new exceljs.Workbook();
    // 번역 파일을 불러옵니다.
    await wb.xlsx.readFile(path.join(getAppRootDir(), './temp/piccoma_jp-ko.xlsx'));
    const helpSheet = wb.getWorksheet(1);
    const translateList = [];

    // 지인이 만들어준 시트를 해석한다... 일본어, 한국어 순으로 컬럼이 구성되어 있었다.
    helpSheet.getColumn(2).eachCell((cell, rowNumber) => {
      const jp = cell.text;
      const kr = helpSheet.getCell(rowNumber, 2).text;
      translateList.push({ jp, kr });
    });

    // 가져온 데이터에서 번역된 데이터와 매치되면 치환하기
    const result = list.map((data, index) => {
      const source = translateList.find((tl) => tl.jp === data.title);
      if (source) {
        data.title = source.kr;
      }
      return data;
    });
    
    return result;
  }

임시로 프로젝트 내에 번역 본을 추가해두고 테스트 해보니 잘 동작했다.
그런데 이건 단순히 임시에 불과하니까 좀 더 용이하게 번역본을 수정하거나 대체하게되는 일이 있을때 편하기 위해 S3와 같은 클라우드 스토리지를 사용하던가 NAS 를 활용해야겠다.
아니면 사실 정적파일을 직접 집어넣는 것 보다는 구글 시트로 구성을 하고 api로 필요한 컬럼만 뽑아오는 것도 방법이다.

profile
frontend dev, home server lover.

0개의 댓글