본문으로 건너뛰기

공공 API 크롤러 개발기

· 약 5분
배정석
백엔드팀

안녕하세요, 비브로스 백엔드 팀에서 일하고 있는 배정석이라고 합니다. 저희 백엔드 팀은 신규 입사자가 들어왔을 때, 기본적인 입사 가이드를 진행한 후 마지막으로 사내 템플릿을 이용하여 간단한 토이 프로젝트를 진행하는데요. 본래 근무일로 5일 예정이던 토이 프로젝트를 8일째 하고 있는 것에 대한 변명으로 제 토이 프로젝트에 대한 글을 적기로 하였습니다.

배경​

왜 크롤러를 만들게 되었나요?​

비브로스에서는 공공 API 정보를 활용하고 있습니다. 공공 API는 2년마다 갱신을 해줘야 하는데, 만기에 대한 사항은 따로 알림이 있지 않아서 사람이 일일이 들어가서 확인해줘야 하는 불편함이 있습니다.

이 불편함을 해소하고자 개발을 시작하게 되었습니다.

API를 제공하는 포털인데, 구독중인 API 목록을  리턴해주는 API를 쓰면 되지 않나요?​

inquiry 그러게요. 사실 날로 먹고 싶어서 있는지 문의 넣어봤습니다. 없다고 합니다.

이제부턴 정말 크롤링뿐이야!!​

study

여전히 날먹에 대한 꿈은 버리지 않았어요.

보통은 ssr이니 뭐니 한다고 사이트 내에 정보를 넣어주잖아요. 보통 이런식으로...

mr_porter

data_go_doc

...근데 왜 넌 document에 모든 정보가 들어있니?

wappalyzer

확인차 wappalyzer를 통해 뽑은 공공데이터포털의 라이브러리 목록을 봐도 initial_state를 넣어줄만한 친구는 없었어요.

사실 제가 간과한 게 있었죠. SSR은 검색 엔진 최적화가 필요할 때나 하는 건데, 공공데이터포털은 굳이 그럴 이유가 없죠.

결국 DOM 정보를 파싱하기로 했어요.


작업 과정​

사용한 라이브러리​

  • puppeteer-core
    • 브라우저 제어 라이브러리
  • chrome-aws-lambda
    • 브라우저 바이너리
  • nodemailer
    • 메일 보내기

프로세스​

  1. eventBridge를 통한 람다 호출

  2. 브라우저 생성 및 설정

    1. request 이벤트 발생 시, 불필요한 리소스 font, image, stylesheet 등은 요청 차단
    await page.setRequestInterception(true);
    page.on('request', req => {
    const resourceType = req.resourceType();
    typesToBlock.includes(resourceType) ? req.abort() : req.continue();
    })
    이미지·폰트·스타일시트 요청을 가로채 중단시키는 puppeteer 코드
    1. alert 발생 시 dismiss 처리
    page.on('dialog', async dialog => {
    await dialog.dismiss();
    });
  3. 로그인

  4. 구독 중인 API 목록 크롤링

  5. 이 달 내로 만료되는 API 목록을 제외하고 필터링

  6. 잔디로 알림 메시지 발신

    1. 잔디 webhook에 대하여 잘 정리된 글이 있으므로 이 글로 대체합니다. (https://boostbrothers.github.io/experience/2021/05/11/잔디-날씨-알림-봇-개발기.html)
  7. 알림 이메일 발신

const transporter = nodemailer.createTransport(
new smtpTransport({
host: HOST,
secure: true,
requireTLS: false,
auth: { user: emailSender.address, pass: emailSender.pass },
})
);

await transporter.sendMail({
from: EMAIL_SENDER_ALIAS,
to: EMAIL_RECIPIENT,
subject: '이번 달에 만료되는 공공 `API` 목록입니다.',
text: 'content',
});

브라우저/페이지 설정을 제외한 브라우저를 이용한 작업 내용은 생략하였습니다. selector 를 집어와서 element 값을 파싱하고 값을 넣는게 전부라서요.

배포​

배포 과정에서 배포가 실패하는 이슈가 있었습니다.

저희는 lambda 또는 batch 작업을 배포할 때 serverless 프레임워크를 사용합니다.

serverless 를 통해 배포를 하면 cloudformation 을 생성해 주는데, 생성이 실패할 경우 serveless cli 가 알려주는 에러 메시지는 해당 아이디의 스택이 없다는 것만 알려줍니다. 그래서 이리저리 둘러보다가 cloudformation의 이벤트 내역을 확인해보고서야 구체적으로 왜 생성이 안 되었는지 알 수 있었습니다.

Resource handler returned message: "Unzipped size must be smaller than 262144000 bytes (Service: Lambda, Status Code: 400, Request ID: ----)" (RequestToken: ----, HandlerErrorCode: InvalidRequest)

패키지 사이즈가 너무 큰 것이 문제였기에, 가볍게 가기 위해 puppeteer 에서 puppeteer-core, chrome-aws-lambda 를 사용하기로 하였습니다.

배포 이후​

배포 후에도 브라우저 생성 실패 이슈가 두 번 있었습니다.

Error: Failed to launch the browser process!
TROUBLESHOOTING: <https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md> at onClose (/var/task/node_modules/puppeteer-core/lib/cjs/puppeteer/node/BrowserRunner.js:241:20)
at Interface.<anonymous> (/var/task/node_modules/puppeteer-core/lib/cjs/puppeteer/node/BrowserRunner.js:231:68)
at Interface.emit (events.js:326:22)
at Interface.EventEmitter.emit (domain.js:483:12)
at Interface.close (readline.js:416:8)
at Socket.onend (readline.js:194:10)
at Socket.emit (events.js:326:22)
at Socket.EventEmitter.emit (domain.js:483:12)
at endReadableNT (_stream_readable.js:1241:12)
at processTicksAndRejections (internal/process/task_queues.js:84:21)

위 케이스는 브라우저를 띄우기에 람다에 할당된 메모리가 부족해서 생겼던 이슈였어요.

구체적으로 메모리가 부족하다고 말을 안 해줘서 구글링을 통해 알아냈습니다.

Error: Failed to launch the browser process!
[0407/145744.782156:WARNING:resource_bundle.cc(405)] locale_file_path.empty() for locale
[0407/145744.782481:FATAL:zygote_host_impl_linux.cc(117)] No usable sandbox!
Update your kernel or see <https://chromium.googlesource.com/chromium/src/+/master/docs/linux/suid_sandbox_development.md> for more information on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox.
...

사용 가능한 샌드박스가 없다는 문제가 터지기도 했었구요. 샌드박스를 안 쓰도록 처리했습니다 (브라우저 실행시 --no-sandbox 플래그).

결과​

jandi_res email_res

순서대로 잔디 메시지, 수신한 이메일입니다.


추후 개선 가능한 점​

국가에서 공익을 위해 오픈한 포털이니만큼 접근이 막힐 것 같지는 않지만, 행여나 막힌다고 한다면 아래의 사항들을 개선할 수 있을 것 같습니다.

  1. puppeteer-stealth-plugin 적용
  2. anti-captcha 적용
  3. user-agent 랜덤화 or 구글 웹 크롤러의 user-agent 이용하기

마치며​

긴 글 읽어주셔서 감사합니다. 변명으로 시작했지만, 적다 보니 굉장히 재밌었네요.

제 글이 조금이라도 유익했고, 제 헛소리에 조금이라도 즐거우셨길 바라면서 이만 줄이겠습니다.


참고할만한 문서​