본문 바로가기
코딩/자동화

Go 언어(golang)로 파이썬보다 속도가 n배 빠른 웹스크레이퍼(크롤러) 만들기

by 나홀로코더 2022. 3. 7.
반응형

목차

1. 주제 소개

2. 웹스크레이퍼 파이썬 버전 소개

3. golang으로 웹스크레이퍼 빠르게 만들기


 

1. 주제 소개

 

꽤 시간이 지난 일이지만 필자는 이 블로그에 파이썬을 이용한 웹스크레이핑 방법을 소개했었다.

 

파이썬으로 웹 페이지에서 정보 추출하기(웹스크레이핑, 웹크롤링)/Request와 Beautifulsoup 이용하기

 

파이썬으로 웹 페이지에서 정보 추출하기(웹스크레이핑, 웹크롤링)/Request와 Beautifulsoup 이용하기

서론 파이썬 입문 콘텐츠에서 가장 흔히 보이는 것이 바로 웹페이지에 게시된 정보들을 추출하여 활용하는 웹스크레이핑 방법인 것 같다. 이미 이에 대해 잘 설명하고 있는 수많은 자료들이 있

codealone.tistory.com

파이썬을 이용한 웹스크레이핑(웹크롤링) 예제/requests와 beautifulsoup로 웹페이지 정보 추출하기

 

파이썬을 이용한 웹스크레이핑(웹크롤링) 예제/requests와 beautifulsoup로 웹페이지 정보 추출하기

1. 시작하기 전에 앞서 requests와 beautifulsoup의 기본적인 사용법을 소개했다. 여기에서는 이를 활용한 실제 웹스크레이핑 예제를 다룬다. 참고로 앞선 글과 여기에서 소개하는 방법은 URL 주소를 통

codealone.tistory.com

 

이 글에서는 파이썬이 아닌 Go 언어를 이용한 웹스크레이핑 방법을 소개한다. 

 

 

반응형

 

2. 웹스크레이퍼 파이썬 버전 소개

 

먼저 Go 언어로 만들 웹스크레이퍼와 비교할 파이썬 버전의 웬스크레이퍼를 소개한다.

 

위에 링크된 글에 소개한 것과는 다른 것인데, 필자가 이 블로그를 운영하면서 각 게시글이 구글 검색 결과에 노출되는지를 확인하고 싶어서 만든 것이다.

 

import requests 
import bs4 
import sys

for i in range(int(sys.argv[1]), int(sys.argv[2])):
    
    if i in [12, 13, 29, 37, 61]: # exclude
        pass
        
    else:
        url = 'https://google.com/search?q=site:https://codealone.tistory.com/' + str(i) 
        request_result=requests.get( url )
        soup = bs4.BeautifulSoup(request_result.text,"html.parser") 
        result = soup.find("h3")
        
        if result == None:
            print(i, result)

        else:
            print(i, "OK")

 

매우 간단하다. 1~3줄은 관련 라이브러리를 불러오는 것이다.

(sys는 커맨드라인에서 스크립트를 실행할 때 검색할 글 번호를 넘겨주기 위한 용도이다.)

 

5~6번 줄의 if문은 이 블로그에 존재하지 않는 글 번호를 제외하기 위한 코드이다.

 

그 아래로는 구글에서 "site:https://codealone.tistory.com/2" 등 검색어로 검색한 다음, 검색 결과에서 제목을 추출하고, 그 결과가 None일 경우와 None이 아닐 경우를 구별해서 결과를 출력해주는 코드이다.

 

코드를 쓰는 것도, 사용하는 것도 간편하지만, 게시글의 수가 60개가 넘기 때문에 60번 이상의 HTTP 요청을 하고 그 결과를 받는 데에 시간이 오래 걸린다.

 

이것과 똑같은 작업을 Go 언어를 이용해서 더 빠르게 해 보자.

 

 

반응형

 

3. golang으로 웹스크레이퍼 빠르게 만들기

 

golang은 go 루틴이라는 기능을 제공한다.

 

go 루틴은 아래 페이지에서 "Concurrency"라는 제목 아래에 소개되고 있다. 동시성이나 병행성을 뜻한다.

 

https://go.dev/doc/effective_go#concurrency

 

Effective Go - The Go Programming Language

Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straightforward translation of a C

go.dev

 

완전히 같은지는 모르겠으나 비슷한 표현으로는 비동기성(Asynchronous) 프로그래밍, 멀티 스레딩 등의 용어가 있는 것 같다.

 

동시성이든 비동기성이든 일상적으로 사용되는 표현은 아니어서 언뜻 와닿지는 않는데, 한 마디로 여러 작업을 동시에 실행하는 방식을 말한다.

 

이 글에서 예제로 삼고 있는 스크립트를 보면, HTTP 요청을 보내고, 응답을 받고, 그다음 요청을 보내고, 또 응답을 받고, 또 그다음 요청을 보내는 일을 순서대로 반복하고 있는데, 이런 경우에 각 요청과 응답 사이에 걸리는 시간 동안은 순전히 대기 시간으로 사용되게 되고, 각 요청과 응답에 걸린 시간의 총합계가 전체 프로그램의 실행 시간이 된다.

 

요청-> 대기-> 응답-> 요청-> 대기-> 응답-> 요청-> 대기-> 응답-> ...

 

그러나 동시성, 비동기성 프로그램은 HTTP 요청을 보내고 응답이 오기까지 기다리지 않고 다른 요청을 바로 보내는 방식이다. 이렇게 되면 전체 프로그램의 실행 시간은 각 요청과 응답에 걸린 시간 중 가장 오래 걸린 1건의 시간과 같게 된다. 

 

요청-> 대기-> 응답
요청-> 대기-> 응답
요청-> 대기-> 응답
요청-> 대기-> 응답
요청-> 대기-> 응답

 

go 루틴은 이러한 동시성 프로그램을 아주 간편하게 짤 수 있도록 해준다.

(파이썬에서도 이와 비슷한 프로그램을 짤 수 있지만 go가 더 간단해 보인다.)

 

3.1 golang 설치

 

Go 언어를 사용하기 위해 Go를 설치하여야 한다. 방법이 매우 간단하니 아래 링크를 참조하고, 여기에서는 설명을 생략한다.

 

https://go.dev/doc/install

 

Download and install - The Go Programming Language

Download and install Download and install Go quickly with the steps described here. For other content on installing, you might be interested in: 1. Go download. Click the button below to download the Go installer. Download Go Don't see your operating syste

go.dev

 

코드 편집기로는 비주얼스튜디오코드를 사용한다. Go를 설치하고 나서 VS Code를 실행하면 Go와 관련한 이런저런 툴을 설치하라는 알림이 뜬다. 전부 Yes를 눌러 설치해주고, Go 언어 확장도 설치해주자.

 

 

반응형

 

3.2 기본적인 사용법

 

기본적인 사용법을 살펴보자. 늘 그렇듯 Hello Wolrd 문자열을 출력해본다.

 

Go 파일을 새로 만든다. Go 스크립트의 확장자는 .go이다. main.go를 이름으로 파일을 만들어 보자.

 

첫 줄에는 package main이라고 써준다. 위의 설치 절차를 잘 따랐다면 p만 써도 코드 조각이 추천될 것이다.

 

 

그런 다음 main 함수를 만들어 준다.

 

 

그 안에다 Hello Wolrd를 출력해주는 코드를 적어보자.

 

fmt.P까지 적으면 아래처럼 코드 조각이 추천된다. 

 

 

그리고 fmt.Println을 선택하면 fmt 패키지를 import 하는 코드까지 자동으로 입력된다.

 

 

Println 함수에 "Hello World!"를 입력한 뒤 파일을 저장하고, 터미널을 열어 go run main.go를 입력해보자. 아래처럼 실행이 된다.

 

 

 

3.3 파이썬 버전과 같은 코드 작성해 보기

 

자 이제는 위에서 본 파이썬 버전의 웹스크레이퍼를 Go 언어로 만들어보자.

 

3.3.1 관련 패키지 설치

 

웹스크레이퍼를 만들기 위해서는 관련 패키지를 불러와야 하는데, 위에서 본 fmt 같은 내장 패키지가 아닌 외부 패키지는 불러오기가 조금 더 복잡하다.

 

먼저 go mod init {내 모듈명} 명령어를 터미널에 입력해서 go.mod 파일을 생성한다. go.mod 파일은 현재 작업 중인 프로그램의 의존성(dependency)에 관한 정보를 담고 있다. 이 글에 따라 작업하고 나면 go.mod 파일이 아래와 같이 작성된다.

 

module github.com/codealone/scraper

go 1.17

require (
	github.com/PuerkitoBio/goquery v1.8.0 // indirect
	github.com/andybalholm/cascadia v1.3.1 // indirect
	golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 // indirect
)

 

그다음으로는 터미널에 go get github.com/PuerkitoBio/goquery 명령어를 입력해서 파이썬의 Beautifulsoup와 비슷한 역할을 하는 goquery 패키지를 설치해 준다. 코드를 작성할 때 위 위 패키지명으로 import해줄 것이다.

 

3.3.2 게시글 1건을 체크하는 프로그램

 

먼저 특정 블로그 게시글 1건이 검색에 노출되는지 체크하는 코드를 작성해 보자. 아래와 같이 작성한다.

 

package main

import (
	"fmt"
	"net/http"

	"github.com/PuerkitoBio/goquery"
)

func main() {
	url := "https://google.com/search?q=site:https://codealone.tistory.com/65"
	res, _ := http.Get(url) 
    // 파이썬의 requests와 같은 역할을 한다. HTTP 요청을 보내고 그 결과를 res에 저장한다. Get 메서드는 response와 함께 error도 반환하는데, 여기에서는 이 부분은 무시하기 위해서 언더바(_) 표시를 했다.
	doc, _ := goquery.NewDocumentFromResponse(res)
    // 파이썬의 beautifulsoup와 같은 역할을 한다. HTML 문서를 doc에 저장한다.
	fmt.Println(doc.Find("h3").Text())
    // Find 메서드로 h3 태그를 찾은 다음, Text 메서드로 h3 태그의 텍스트를 반환해 준다.
}

 

http 패키지는 위에서 fmt 패키지를 사용한 것처럼 코드만 쓰면 자동으로 import 부분이 작성된다. 반면 goquery는 불러와 줘야 한다.

 

자세한 설명은 주석으로 대체한다.

 

3.3.2 반복문으로 여러 건을 체크하는 프로그램

 

다음으로는 반복문을 추가해본다. 여기까지 하면 위의 파이썬 버전과 같은 기능을 한다. 실행해 보면 파이썬보다 조금 더 빠르지만, 큰 차이는 없다.

 

package main

import (
	"fmt"
	"net/http"
	"strconv"

	"github.com/PuerkitoBio/goquery"
)

func main() {
	url := "https://google.com/search?q=site:https://codealone.tistory.com/"
	for i := 2; i < 5; i++ {
    // Go 언어는 파이썬과 for문의 문법이 다르다. 이 부분을 말로 표현하면 "i를 2로 두고; i가 5보다 작은 동안에; i를 1씩 늘리면서 순회하라"는 것이다.
		res, _ := http.Get(url + strconv.Itoa(i))
        // i는 숫자이고 url은 문자열이기 때문에 strconv 패키지를 이용해서 i를 문자열로 변환해 준다.
		doc, _ := goquery.NewDocumentFromResponse(res)
		fmt.Println(doc.Find("h3").Text())
	}

}

 

자세한 설명은 주석으로 대체한다.

 

반응형

 

3.4 파이썬 버전보다 빠른 코드 작성해 보기

 

자 이제 이 글의 주인공인 go 루틴을 볼 차례이다. 위에서 만든 코드에 go 루틴을 적용해보자. 완성본을 먼저 보면 아래와 같다. 주석으로 표시한 번호 순서에 따라 작성 방법을 살펴보자.

 

package main

import (
	"fmt"
	"net/http"
	"strconv"

	"github.com/PuerkitoBio/goquery"
)

func main() {
	c := make(chan string) // (3)
	start_num := 10
	end_num := 16
	for i := start_num; i < end_num; i++ {
		go check(i, c) // (2)
	}
	for j := start_num; j < end_num; j++ {
		fmt.Println(<-c) // (6)
	}
}

// (1)
func check(i int, c chan<- string) { // (4)
	url := "https://google.com/search?q=site:https://codealone.tistory.com/"
	index := strconv.Itoa(i)
	res, _ := http.Get(url + index)
	doc, _ := goquery.NewDocumentFromResponse(res)
	c <- index + doc.Find("h3").Text() // (5)
}

 

(1) 먼저 HTTP 요청과 응답에 관련된 코드를 따로 빼서 함수로 정의한다. 

 

(2) main 함수에서는 check 함수가 실행되도록 수정한다. 이때 함수 앞에 go를 붙인다. 이 부분이 go 루틴이 되는 것이다. check 함수에 넘겨준 c는 다음 (3)항에서 설명한다. 조금 헷갈릴 수 있지만 중요한 개념이다.

 

(3) main 함수 내에서 Go 루틴을 실행하는 데 필요한 channel을 변수로 선언한다. 여기에서 선언한 c는 (2)에서 Go 루틴을 실행할 때 인자로 넘겨준다. 변수명은 c가 아니라 무엇이어도 상관이 없다.

    채널은 Go 루틴이 완료되었을 때 그 신호를 수신하기 위한 경로이다. 채널을 통해 수신되는 신호는 True/False 같은 불(bool) 값일 수도 있고, Go 루틴이 실행된 결과 얻어진 결과나, 또는 임의의 숫자 등 아무것이나 될 수 있다. 여기에서는 check 함수를 실행한 결과 얻어진 게시글의 제목을 채널을 통해 수신할 것이다.

 

(4) check 함수는 main 함수의 for문에서 넘겨받을 인덱스 번호 i와 채널 c를 인자로 갖는다. 우리는 게시글 제목을 채널을 통해 송신할 것이므로 <-string이라고 표시한다. 말로 표현하면 "채널 c를 인자로 갖는데 c의 타입은 문자열이다"라는 뜻이다.

 

(5) check 함수를 실행한 결과 얻어진 게시글의 제목과 해당 게시글의 번호를 c로 넘겨준다.

 

(6) 이 부분까지 작성이 되어야 Go 루틴과 채널이 역할을 다 할 수 있다. Go 루틴에서 수신한 시그널을 처리해주는 부분이다. 채널이 신호를 수신하기 위한 경로라고 하면 개념이 조금 모호하게 들리지만, <-c 자체를 하나의 변수와 같이 보면 된다. 여기에서 <-c는 각 채널에서 보내온 게시글 제목에 해당한다.

    유의할 부분은 Go 루틴이 실행된 횟수만큼 이 부분에서 받아줘야 한다는 것이다. 그렇기 때문에 Go 루틴을 실행할 때와 동일한 반복수만큼 for문을 통해 <-c를 출력하고 있다.

 

3.5 실행 시간 확인

 

스크립트가 완성되었으니 얼마나 빠르게 작동하는지 보자. 시간을 측정하기 위해서 13번째 줄과 23번째 줄에 관련 코드를 추가하고, 이 블로그 게시글 전부(2번부터 68번까지)에 대해서 검색 결과를 체크해본 결과 1.129355초가 소요되었다.

 

굳이 해보진 않았지만 파이썬으로 한다면 1건당 1초씩만 잡더라도 1분 이상은 소요되었을 것이다.

 

반응형

댓글