리팩토링
데이터를 추출하고 크롤러가 완성된 것처럼 보일 수 있지만, 사실 이것은 시작에 불과합니다. 간단한 설명을 위해 오류 처리, 프록시, 로깅, 아키텍처, 테스트, 문서화 등 신뢰할 수 있는 소프트웨어가 갖춰야 할 많은 요소들을 생략했습니다. 다행히도 오류 처리는 대부분 Crawlee가 자체적으로 처리하므로, 특별한 처리가 필요한 경우가 아니라면 걱정하지 않으셔도 됩니다.
이제 좋은 코딩 관행을 위해 Router
를 사용하여 크롤러 코드를 더 잘 구조화하는 방법을 살펴보겠습니다.
라우팅
다음 코드에서는 몇 가지 변경사항을 적용했습니다:
- 코드를 여러 파일로 분리했습니다.
console.log
대신 더 보기 좋은 컬러 로그를 위해 Crawlee 로거를 사용했습니다.if
문 없이 더 깔끔한 라우팅을 위해Router
를 추가했습니다.
main.mjs
파일에는 크롤러의 기본 구조를 배치했습니다:
import { PlaywrightCrawler, log } from 'crawlee';
import { router } from './routes.mjs';
// CRAWLEE_LOG_LEVEL 환경 변수나 설정 옵션으로
// 설정하는 것이 더 좋습니다. 이것은 예시일 뿐입니다 😈
log.setLevel(log.LEVELS.DEBUG);
log.debug('크롤러 설정 중.');
const crawler = new PlaywrightCrawler({
// if 문이 있는 긴 requestHandler 대신
// router 인스턴스를 제공합니다.
requestHandler: router,
});
await crawler.run(['https://warehouse-theme-metal.myshopify.com/collections']);
그리고 별도의 routes.mjs
파일에는:
import { createPlaywrightRouter, Dataset } from 'crawlee';
// createPlaywrightRouter()는 더 나은 인텔리센스와
// 타입을 얻기 위한 헬퍼입니다. Router.create()도 사용 가능합니다.
export const router = createPlaywrightRouter();
// if 문의 request.label === DETAIL 부분을 대체합니다.
router.addHandler('DETAIL', async ({ request, page, log }) => {
log.debug(`데이터 추출 중: ${request.url}`);
const urlPart = request.url.split('/').slice(-1);
const manufacturer = urlPart[0].split('-')[0];
const title = await page.locator('.product-meta h1').textContent();
const sku = await page
.locator('span.product-meta__sku-number')
.textContent();
const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();
const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));
const inStockElement = page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();
const inStock = (await inStockElement.count()) > 0;
const results = {
url: request.url,
manufacturer,
title,
sku,
currentPrice: price,
availableInStock: inStock,
};
log.debug(`데이터 저장 중: ${request.url}`);
await Dataset.pushData(results);
});
router.addHandler('CATEGORY', async ({ page, enqueueLinks, request, log }) => {
log.debug(`페이지네이션 인큐 중: ${request.url}`);
// 현재 카테고리 페이지에 있습니다. 이를 통해 페이지네이션을 하면서 모든 제품과
// 발견하는 후속 페이지를 인큐할 수 있습니다
await page.waitForSelector('.product-item > a');
await enqueueLinks({
selector: '.product-item > a',
label: 'DETAIL', // <= 다른 라벨 사용
});
// 이제 "다음" 버튼을 찾아서 다음 결과 페이지를 인큐해야 합니다(있는 경우)
const nextButton = await page.$('a.pagination__next');
if (nextButton) {
await enqueueLinks({
selector: 'a.pagination__next',
label: 'CATEGORY', // <= 같은 라벨 사용
});
}
});
// 시작 URL과 LIST 라벨이 지정된 URL을 처리하는
// 기본 라우트입니다.
router.addDefaultHandler(async ({ request, page, enqueueLinks, log }) => {
log.debug(`페이지에서 카테고리 인큐 중: ${request.url}`);
// 이는 라벨이 없는 시작 페이지에 있다는 의미입니다.
// 이 페이지에서는 모든 카테고리 페이지만 인큐하면 됩니다.
await page.waitForSelector('.collection-block-item');
await enqueueLinks({
selector: '.collection-block-item',
label: 'CATEGORY',
});
});
이러한 변경사항들을 자세히 살펴보겠습니다. 이러한 수정으로 크롤러의 가독성과 관리가 더욱 쉬워질 것입니다.
코드를 여러 파일로 분리하기
코드를 여러 파일로 분리하고 로직을 분리하는 것은 매우 중요합니다. 한 파일의 코드가 적을수록 한 번에 생각해야 할 코드가 줄어들며, 이는 좋은 방법입니다. 실제로는 라우트도 별도의 파일로 더 분리하는 것이 좋을 것입니다.
Crawlee log
사용하기
Crawlee의 log
객체에 대해서는 문서에서 자세히 읽어보실 수 있지만, 여기서는 로그 레벨에 대해 강조하고 싶습니다.
Crawlee log
는 log.debug
, log.info
, log.warning
등 여러 로그 레벨을 제공합니다. 이는 로그를 더 읽기 쉽게 만들 뿐만 아니라, log.setLevel()
함수나 CRAWLEE_LOG_LEVEL
환경 변수를 통해 특정 레벨을 선택적으로 끌 수 있습니다. 이를 통해 필요할 때는 디버그 로그를 활용하고, 필요하지 않을 때는 로그를 깔끔하게 유지할 수 있습니다.
Router를 사용한 크롤링 구조화
처음에는 단순한 if/else
문을 사용하여 페이지 유형에 따라 다른 로직을 선택하는 것이 더 읽기 쉬워 보일 수 있습니다. 하지만 페이지 유형이 두 개 이상이 되고, 각 페이지의 로직이 수십 또는 수백 줄로 늘어나면 이 방식은 복잡해질 수 있습니다.
모든 프로그래밍 언어에서 로직을 읽기 쉽고 이해하기 쉬운 작은 단위로 나누는 것이 좋은 관행입니다. 모든 것이 서로 연결되어 있고 변수를 어디서나 사용할 수 있는 천 줄짜리 requestHandler()
를 스크롤하면서 디버깅하는 것은 아름답지 않습니다. 그래서 우리는 라우트를 별도의 파일로 분리하는 것을 선호합니다.
다음 단계
다음이자 마지막 단계에서는 Crawlee 프로젝트를 클라우드에 배포하는 방법을 살펴보겠습니다. CLI를 사용하여 프로젝트를 시작했다면 이미 Dockerfile이 준비되어 있으며, 다음 섹션에서는 Apify 플랫폼에 쉽게 배포하는 방법을 보여드리겠습니다.