유튜브 채널 / 컨텐츠 데이터를 수집하여 관리하는 큰 틀의 시스템을 만들어보고 싶어 해당 시스템을 러프하게 설계하고 이 중 하나의 작은 서비스인 채널 목록 수집 크롤러를 어떻게 동작하도록 설계하고 구현 했는지, 이후 개발할 전체 시스템은 어떤 구조인지 공유 드려보겠습니다:)
흐름
채널 목록을 검색하는 페이지에서 특정 키워드로 검색을 하면 관련 채널이 위 이미지와 같이 검색됩니다. 검색되는 데이터들을 n번 스크롤 다운 하면서 채널 상세에 접근할 수 있는 URI(subscriber name)을 수집하게 됩니다.
- Client — gRPC 클라이언트
- Channel Collector — 채널검색 페이지 크롤링 서비스
- Youtube Channel Page — 유튜브 채널 검색 웹 페이지
제어
유튜브 에서는 공식적으로 제공하는 Youtube Data API 를 이용한 데이터 수집 이외에는 공식 제공하지 않기 때문에 요청량을 제어하지 않으면 IP Block 이 될 가능성이 있습니다. 그래서 아래와 같은 제어 조건을 통하여 IP Block 이 되지 않도록 조정하고 있습니다.
- 데이터 추출 loop 마지막 부분에 스레드를 1초 단위로 쉬면서(sleep) 스크롤 다운 -> 데이터 추출 로직을 수행 하도록 제어
// uri path 추출 로직
func (c *Collector) extractSubscriberNames(
numScrolls int,
ctx context.Context,
ch chan []string,
) error {
for i := 0; i < numScrolls; i++ {
channelIds, err := c.extractSubscriberNameByIndex(ctx, i)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to scroll down by n %d times\n", i+1))
}
// 스크롤당 추출한 List 를 Golang Channel을 통하여 전송하는 로직
ch <- channelIds
// 추출 동작이 끝나는 시점에 1초간 Thread Sleep 하여 요청량 제어
time.Sleep(1 * time.Second)
}
fmt.Printf("total %d '#subscribers' nodes were collected\n", numScrolls)
close(ch)
return nil
}
- gRPC 요청이 동시에 들어올 경우를 대비하여 두개의 mutex(사이트 접근, 스크롤 다운)를 활용하여 초당 유튜브 홈페이지 접근량 제어
func (c *Collector) accessWebsite(ctx context.Context, keyword string) error {
// mutex 를 활용하여 동시 요청 발생 시 하나씩만 실행하도록 제어하는 예시
c.accessMutex.Lock()
defer c.accessMutex.Unlock()
fmt.Printf("start to access website by keyword: %s\n", keyword)
defer fmt.Printf("end to access website by keyword: %s\n", keyword)
uri := fmt.Sprintf("%s?search_query=%s&sp=%s", baseUrl, url.QueryEscape(keyword), "EgIQAg%253D%253D")
err := chromedp.Run(ctx,
chromedp.Navigate(uri),
)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to navigate to %s", uri))
}
// 사이트 접근 후 1초간 Thread Sleep
time.Sleep(1 * time.Second)
return nil
}
구현
크롤링 라이브러리
해당 서비스는 Golang 을 이용하여 구현한 작은 서비스입니다. Golang 에서는 웹사이트를 크롤링 하기 위해 두가지 선택지가 있는것 같은데요, 보통 colly 나 chromedp 를 이용하는것 같습니다.
유튜브 페이지를 크롤링 하려면 웹사이트를 스크롤 다운하여 pagination 해야 하기 때문에 브라우저를 직접 활용하여 접근하는 chromedp 를 사용해야 했습니다.
통신방식
Service 간 통신은 보통 Rest API / GraphQL / gRPC 이렇게 세가지 선택지가 있다고 생각 하는데요
이 중 gRPC 를 선택한 이유는 일반적인 단방향 통신부터 양방향 통신 그리고 stream 방식의 통신 등 Server — Client 간 여러 통신방식을 지원하고 요청 페이지만큼의 데이터를 다 추출할때까지 기다리지 않고도 response stream 을 통하여 1번의 요청(request) 에도 여러번의 응답(response) 데이터를 반환할 수 있기 때문에 gRPC 를 선택하게 되었습니다.
해당 서비스 구현 코드는 아래 링크에서 확인해보실 수 있습니다.
Conclusions
유튜브 채널검색 페이지를 크롤링 하여 채널 상세에 접근할 수 있는 URI 목록을 추출하는 서비스를 구현해보았고 어떤 흐름으로 동작 하는지, 크롤링을 지속적으로 하기 위해 어떤 제어 장치들을 두었는지 설명해보았습니다.
아마 채널 목록 페이지를 크롤링 하는 니즈 보다는 컨텐츠 위주로 크롤링 하는 분들이 많을거라 생각이 드는데요, 원하는 목적이 다르더라도 유튜브 크롤링을 구현함에 있어 해당 포스팅이 참고 용도로 도움을 드릴 수 있을것 같습니다.
이후 제가 생각하는 전체 수집 시스템이 어떻게 되는지 전체 아키텍처 및 서비스 흐름에 대한 설명을 다음 포스팅에서 진행해보도록 하겠습니다.