Skip to main content

공공 API 크롤러 개발기

· 8 min read
배정석

안녕하세요, 비브로스 백엔드 팀에서 일하고 있는 배정석이라고 합니다. 저희 백엔드 팀은 신규 입사자가 들어왔을 때, 기본적인 입사 가이드를 진행한 후 마지막으로 사내 템플릿을 이용하여 간단한 토이 프로젝트를 진행하는데요. 본래 근무일로 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();
    })
    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 이용하기

마치며

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

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


참고할만한 문서