Hyunseok
현재 사이트는 2024년 11월 이후로 업데이트 되지 않습니다. 새 글은 블로그로 확인해주세요. 블로그로 이동
프로그래밍 [BCrawler] 특가를 크롤링 해서 모아서 보자
2024. 5. 19. 15:40

현재 사이트는 2024년 11월 이후로 업데이트 되지 않습니다. 새 글은 블로그로 확인해주세요. 블로그로 이동

마지막 글을 올린지도 근 3달이 다되어 가고 있다 

 

결론부터 말하면 .. 바뀐 환경에 적응한다고 에너지를 다 쓰는 것일까

막상 자취방에서는 개발이 잘 되질 않는다 

 

뭐, 여하튼 그건 그거고 그거랑 별개로 항상 하던

"내가 하는 일 자동화하기"

에 대해서 사이드로 만들어낸 껀덕지가 생겨서 이번에도 작성해보자 

 

 

만들기 전에 .. 

나는 매일 핫딜을 긁어 보는 습관이 있다

실제로 이런식으로 100달러가 넘는 허브가 50달러에 풀리는 경우가 있는데 

 

모바일겜 가챠를 하지 않고 딱히 어디 현질하는 요소도 없는 나에게는

요런 것들을 사 모으는 것이 소소한 취미이자 스트레스해소이다 

 

이러한 사이트들을 긁는 데에 있어서 필요한 사이트가 하나 있긴하다 

 

https://www.algumon.com/

 

알구몬

뽐뿌 제대로 넣어드립니다

www.algumon.com

 

알구몬이라는 사이트가 있긴한데 UI가 너무 작고 카테고리 구분없이

(정확히 말하면 들어가서 그냥 카테고리 설정하고 보면 된다)

나오는 감도 있고 광고도 나오기도하고 .. 

 

그냥 이러는 것 보다는 공부도 할 겸 내가 하나 만들기로한다 

 

초기 기획은 퀘존, 펨, 뽐 이렇게 3개인데

펨은 봇정지를 먹은 것인지 아니면 aws를 광역 밴을 때린 것인지 .. (아니 1시간에 1회 긁는 건데 굳이 ;;)

그냥 개발단계에서 패스하고 현재 퀘존, 뽐 이렇게 2군데만 한시간에 한 번씩 긁고 있다 

 

프로젝트 세팅

이번에는 간단하게 Nextjs + prisma로 스택을 잡았다

UI는 1년내내 내리 쓰고 있는 shadcn 으로 결정했다 

nextjs와 prisma세팅은 공식 홈페이지에 나와있고

shadcn도 그냥 커맨드라인 몇개면 설치 되어버리니 .. 그냥 바로 세팅해주자 

sql는 이미 aws에 돌아가고있는 mysql8서버를 사용했다

 

방법

fetch로 html을 긁고 html에서 정규식을 이용하여 테이블을 긁고

그것들을 object화, db에 각각 정보를 규격화 하여서 넣으려했는데 ..

 

아니나 다를까 가장 쉽겠지 하던 정규식 부분에서 막히기 시작한다 

gpt에게 물어봤지만 이상한 답변만 하면서 빡친 나는 결국 3자 라이브러리를 하나 가져와서 쓰기 시작했다

 

https://cheerio.js.org/

 

The industry standard for working with HTML in JavaScript | cheerio

The fast, flexible & elegant library for parsing and manipulating HTML and XML.

cheerio.js.org

내  github repository에도 적어놨지만.. cheerio는 신이다 무조건 애용해주자

 

뽐 로직은 깃허브에 놔두고 간단한 예시로 퀘존핫딜을 긁는 로직을 가져와보았다

 

import * as cheerio from 'cheerio'
const qdDataMapping = {
    is_closed: 'label',
    url: 'subject-link',
    category: 'category',
    price: 'text-orange',
    shipping: 'shipping',
    title: 'ellipsis-with-reply-cnt',
    date: 'date',
    img_src: 'maxImg',
} as { [key: string]: string }

const parseQuasar = (rawHtml: string): Article[] => {
    const $ = cheerio.load(rawHtml)
    const marketInfoLists = $('.market-info-list')

    return marketInfoLists
        .map((_, element) => {
            const entries = Object.entries(qdDataMapping).map(([key, className]) => {
                switch (key) {
                    case 'img_src':
                        return [key, $(element).find('div.thumb-wrap a img').attr('src') || '']
                    case 'url':
                        return [key, $(element).find(`.${className}`).attr('href') || '']
                    case 'shipping':
                        const spanText = $(element)
                            .find('span')
                            .filter((_, el) => Object.keys(el.attribs).length === 0)
                            .text()
                            .split('배송비')
                            .pop()
                            ?.trim()
                        return [key, spanText || '']
                    default:
                        return [key, $(element).find(`.${className}`).text().trim()]
                }
            })
            return Object.fromEntries(entries)
        })
        .get()
}

export { parseQuasar }

 

밤 새면서 멍때리면서 쓴 코드라 좀 더럽긴한데 뭐.. 간단히 설명하면

cheerio가 던져주는 object와 사전정의 object로

값을 치환/가공 하고 parsing을 끝내는 로직이다 

 

 

 

import prisma from '@/prisma/db'
import { parseQuasar } from '@/util/parse/qs'
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
export const POST = async () => {
    const latestRecord = await prisma.lastupdate.findFirst({
        where: { type: 'QS' },
        orderBy: { lastupdate: 'desc' },
    })
    const isAbleToUpdate = !latestRecord || Date.now() - latestRecord.lastupdate.getTime() > 3600000 / 2
    if (!isAbleToUpdate)
        return NextResponse.json({ status: `Please retry after 1 hour from the last update. \n Last update: ${latestRecord.lastupdate}` })

    const qsRawHtml = await fetch('https://quasarzone.com/bbs/qb_saleinfo').then((res) => res.text())
    const qsJSON = parseQuasar(qsRawHtml).filter((qs) => qs.title)
    qsJSON.forEach((qs) => {
        qs.id = qs.url.split('/').pop() || ''
        qs.is_closed = qs.is_closed === '종료'
    })
    const qsIds = qsJSON.map((qs) => qs.id)

    const qsInDb = await prisma.qs.findMany({
        where: { id: { in: qsIds } },
        orderBy: { id: 'desc' },
    })
    const qsInDbMap = new Map(qsInDb.map((qs) => [qs.id, qs]))
    const toUpdate = qsJSON.filter((qs) => qsInDbMap.has(qs.id))
    const toCreate = qsJSON.filter((qs) => !qsInDbMap.has(qs.id))

    await prisma.$transaction([
        ...toUpdate.map((qs) =>
            prisma.qs.update({
                where: { qsid: qs.id },
                data: {
                    is_closed: qs.is_closed as boolean,
                    url: qs.url,
                    category: qs.category,
                    price: qs.price,
                    shipping: qs.shipping,
                    title: qs.title,
                    date: qs.date,
                    img_src: qs.img_src,
                },
            }),
        ),
        ...toCreate.map((qs) =>
            prisma.qs.create({
                data: { qsid: qs.id, ...qs, is_closed: qs.is_closed as boolean },
            }),
        ),
    ])

    await prisma.lastupdate.create({
        data: { type: 'QS' },
    })
    return NextResponse.json({ status: 'OK' })
}

 

위의 코드와 같이 nextjs 의 api 기능과 파싱로직을 맞물어서 ..

혹시모를 오류를 대비해서 transaction으로 insert하는 api도 작성해두고 .. 

 

list도 불러오는 로직도 대충 저렇게 api로 작성해두고 서버를 띄우면....!! 

 

 

대충 깔끔하게 작성이 완료된다 

 

하루에 1~2시간? 한 이틀정도 잡고 실사용 3일정도 하면서

실제 데이터를 사용하면서 버그 조금 잡은 기간 합하면

한 1주일 정도 안정화까지 걸린 것 같다

(실제 개발시간은 3시간도 안될듯하다)

 

너무 초급적인 내용이지만 너무 유용한..

왜 작년에 만들지 않았나라는 생각이 드는

간단한 사이드 프로젝트이다 

 

소스코드는 아래에서 확인가능하다

https://github.com/B-HS/BCrawler

 

GitHub - B-HS/BCrawler: Hotdeal crawler for several sites.

Hotdeal crawler for several sites. Contribute to B-HS/BCrawler development by creating an account on GitHub.

github.com

 

이 글을 읽으시는 여러분들도 뭔가 긁어서 할 일이 있다면

cheerio를 사용하여 쌈@뽕한 사이트를 잘 만들어보자 

 

또한 잡담은 거의 개인 사이트에서 하고있으니

우당탕탕 개발자의 일기가 보고싶다면

(일기가 몇개 없긴 하지만..) 아래의 사이트로 ..

 

https://blog.gumyo.net/

 

BBlog

BBlog

blog.gumyo.net

 

 

 


프로그래밍의 다른 글