카카오, 네이버 등의 플랫폼에서 웹툰을 크롤링하고 DB에 저장까지 성공했다.
이번에는 Genre 모듈을 통해 웹툰의 키워드를 변환하고 삭제하는 작업을 거쳐야 한다. 세부적인 목표는 다음과 같다.
카카오와 네이버 웹툰을 전부 합하면 5000개가 넘는다. 이 데이터들을 전부 크롤링하는데는 시간이 꽤 소요된다. 즉, 만약에 장르 키워드를 변환 또는 삭제하는 작업에서 예상치 못한 일이 발생할 경우에 매번 크롤링을 다시 하는 것은 시간낭비이다.
따라서, 현재 크롤링이 완료된 테이블을 백업시켜놓는다. 만약 코드를 잘못 작성했거나 혹은 예상과는 다른일이 발생할 경우 테이블을 다시 백업시키면 그만이다.
$ mysqldump -h [mysql계정] -u [사용자아이디] -p [데이터베이스명] [테이블명] > [백업파일명].sql
mysql -h [mysql계정] -u [사용자아이디] -p [데이터베이스명] < [백업파일명].sql
이전에 장르 학습 및 통일 글에서 필요없는 키워드를 제거, 변환이 필요한 키워드 또한 Genres 테이블에 반영했다. 따라서 모든 웹툰의 장르 키워드를 Genres 테이블과 비교해가며 처리하면 된다.
하지만 주의해야 할 점들이 몇가지 있다.
1. 카카오는 키워드의 기준이기 때문에 카카오 키워드만을 기준으로 처리한다.
2. 네이버는 변환되는 키워드이기 때문에 네이버 뿐 아니라 카카오 키워드도 포함해서 처리한다.
3. 네이버는 로판 장르가 모두 로맨스에 포함되어 있기 때문에 따로 분류해줘야 한다.
4. 장르 키워드가 변동하기 때문에 장르 개수도 다시 체크한다.
// Transform 파일을 통해 해당 서비스의 웹툰 장르 변환 및 삭제
async updateWebtoonGenreForTransform(service: string, referService: boolean) {
const webtoons = await this.webtoonService.getAllWebtoonForOption({ service });
for (let webtoon of webtoons) {
let keywords: string[] = JSON.parse(webtoon.genres);
let updateDto: UpdateWebtoonDto = { webtoonId: webtoon.webtoonId };
for (let [idx, keyword] of keywords.entries()) {
console.log(idx, keyword);
const genre = await this.getGenre({ keyword });
// 네이버의 로판 키워드일 경우 카테고리로 바꾸고 장르는 삭제
if (keyword === "로판" && service === "naver") {
keywords[idx] = "delete";
updateDto.category = "로판";
continue;
}
// 장르 키워드가 DB에 없다면 삭제
if ( !genre || ( !referService && genre && genre.service !== service )) {
console.log(`장르 키워드 ${keyword} 삭제 완료`);
keywords[idx] = "delete";
continue;
}
// 장르 변환
if (genre.transformed) {
keywords[idx] = genre.transformed;
console.log(`장르 키워드 ${keyword} => ${genre.transformed} 변환 완료`);
} else {
continue;
}
}
// 장르 삭제 및 개수 다시 카운트
keywords = keywords.filter((keyword) => { return keyword !== "delete" });
const genreCount = keywords.length;
console.log(keywords);
// 변경사항에 저장
updateDto.genres = JSON.stringify(keywords);
updateDto.genreCount = genreCount;
// DB 장르 및 장르 개수 업데이트
await this.webtoonService.updateWebtoonForOption(updateDto);
}
}
장르 키워드는 숫자가 많고 그 의미가 중요하기 때문에 학습 및 통일하는 과정을 gpt를 이용했지만 카테고리는 숫자가 적고 변환도 5개만 필요하기 때문에 직접 json파일을 작성했다.
다음은 네이버 카테고리를 카카오 카테고리로 변환해야 할 키워드들이다.
{"무협/사극":"무협","스릴러":"드라마","스포츠":"드라마","일상":"드라마","개그":"드라마","감성":"드라마"}
async updateWebtoonCategoryForTransform(service: string) {
// 변환 파일 불러오기
const filePath = path.join(TRANSOFRM_FOLDER, `${service}CategoryTransform.json`);
const categoryTransform: { [category: string]: string } = require(filePath);
// 해당 플랫폼 웹툰 전부 불러오기
const webtoons = await this.webtoonService.getAllWebtoonForOption({ service });
for (let webtoon of webtoons) {
const { webtoonId, category } = webtoon;
const genres = JSON.parse(webtoon.genres);
let updateDto: UpdateWebtoonDto = { webtoonId };
// 카테고리 변환
if (category in categoryTransform) {
const newCategory = categoryTransform[category];
updateDto.category = newCategory;
// 카테고리가 드라마일 경우 기존의 카테고리를 장르에 더한다.
if (newCategory === "드라마" && !genres.includes(category)) {
const newGenres = [category, ...genres];
updateDto.genres = JSON.stringify(newGenres);
updateDto.genreCount = newGenres.length;
console.log(`${category} => ${newGenres}`);
}
// DB업데이트
await this.webtoonService.updateWebtoonForOption(updateDto);
console.log(`카테고리 ${category} => ${newCategory} 변환 완료`);
} else {
console.log(`카테고리 ${category} 유지`);
}
}
}
위에서 updateWebtoonGenreForTransform 메서드에서 반복문은 원래 다음과 같은 형태였다.
for (let [idx, keyword] of keywords.entries()) {
...
keywords.splice(idx,1);
...
}
하지만 저 반복문에서 장르 키워드가 몇개씩 제거가 안되는 문제가 발생했다.
원인은 entries 반복문중에 splice를 하면 연속적으로 삭제를 해야할 경우 건너뛰어 버리게 된다. 예를 들어, 2번째와 3번쨰 인덱스를 삭제해야 하는데 2번째를 지워버리면 3번째가 2번째가 되어버리기 때문에 원래 3번째 인덱스는 건너뛰어버리고 반복문을 실행한다.
따라서 다음과 같이 변경했다.
for (let [idx, keyword] of keywords.entries()) {
...
if (keyword === "로판" && service === "naver") {
keywords[idx] = "delete";
updateDto.category = "로판";
continue;
}
...
keywords = keywords.filter((keyword) => { return keyword !== "delete" });
const genreCount = keywords.length;
console.log(keywords);
...
}
삭제해야할 키워드는 delete로 바꿔두고 나중에 filter 메서드를 통해 일괄로 없애는 것이다.
옛날에 처음 데이터 작업을 할 때에는 백업, 복원을 할 수 있는 사실조차 몰랐기 때문에
크롤링 => 데이터 작업 => 실패 => 다시 크롤링 => 데이터 작업 => 실패 => 다시 크롤링 ......
무한 반복의 굴레였다.. 하지만 이번에는 테이블을 미리 복원해두고 만약 데이터 작업 도중 예상치 못한 일이 생기거나 코드를 잘못 작성한 경우 바로바로 복원이 가능해서 시간을 매우 절약할 수 있었다.
프로젝트를 1차로 끝낸 후에 다른 플랫폼의 웹툰을 추가할 경우가 생길것이다. 리팩토링 전의 코드는 미래를 생각하지 않고 당장의 카카오, 네이버만 처리할려는 마음으로 일회용 코드를 작성했다. 카카오와 네이버만 한정해서 작업을 처리하는 코드였다.
하지만 리팩토링 후의 코드는 만약 카카오 또는 네이버가 아닐 경우에도 작업을 수행할 수 있게끔 재사용을 생각하면서 코드를 작성했다.
if ( !genre || ( !referService && genre && genre.service !== service )) {
console.log(`장르 키워드 ${keyword} 삭제 완료`);
keywords[idx] = "delete";
continue;
}
위의 코드에서 삭제하는 조건을 보면 다음과 같다.
1. DB에 키워드가 아예 존재하지 않는경우 (카카오외 다른 플랫폼)
2. referService가 false이고(변환하지않음) 키워드가 존재하긴 하지만 서비스가 다른 경우 (카카오)
1번은 간단하지만 2번은 꽤 어렵다. 이것은 카카오와 다른 플랫폼을 겹치지 않기 위해 작성한 것으로 카카오는 기준 키워드이기 때문에 다른 플랫폼의 키워드는 참고하면 안된다.
예를 들어 네이버장르에 "퓨전사극"이라는 키워드가 존재하더라도 카카오 웹툰 중 "퓨전사극" 키워드가 있으면 삭제해야 한다. 왜? 카카오는 "퓨전사극" 키워드가 존재하지 않기 때문이다.
위와 같이 카카오와 네이버를 비교할 뿐 아니라 다른 플랫폼을 넣어도 실행이 되게끔 작성하는 것이 재사용성을 강조한 코드이다.