동시성 프로그래밍으로 데이터 수집하기

정재욱·2022년 2월 16일

Back End

목록 보기
3/3

본 포스트는 인프런의 "파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI)" 강의를 듣고 회고 목적으로 작성하였습니다.

📌 서버와 클라이언트, HTTP, API 이해

사용자가 웹 브라우저에 접속을 해서 url을 입력해서 엔터를 눌렀을 때 어떤 과정을 거치게 될까?

위 그림을 참고하자. 먼저 사용자가 웹 브라우저에 url을 입력하면 요청 데이터와 함께 http 규칙을 통해서 데이터가 보내진다. http는 하나의 미리 정의해 놓은 규칙이라 생각하면 된다. http에 대한 자세한 내용은 여기서 확인하자.
이후 도메인 주소가 DNS 라는 곳으로 보내지는데, DNS 라는 곳은 도메인 네임을 IP 주소로 변환해주는 곳이다. 도메인 네임은 사용자들이 알기 쉽게 www.naver.com 과 같이 정의해 놓은 곳이고, 실제로 서버 주소에 찾아가기 위해서는 IP 주소가 필요하다. DNS는 이러한 도메인네임을 IP주소로 바꿔주는 역할을 한다.
이후 인터넷에서 IP주소에 해당하는 서버를 찾고, 서버에 요청 데이터를 보낸다. 그러면 서버는 요청 데이터에 대한 응답 데이터를 반환하고, 이가 웹 브라우저로 나타나는 것이다.

조금더 추상화 해보자면 아래 그림과 같다.

여기서 클라이언트는 웹 브라우저가 될 수도 있고, 파이썬 프로그램일수도 있고, 자바 프로그램일수도 있다. 클라이언트는 서버에 요청을 하고, 서버는 그 요청에 대한 응답을 반환한다.

추가적으로 HTTPAPI에 대한 내용을 한 번씩 읽어보자.



📌 웹 크롤링, 스크래핑과 법적 주의사항

웹 크롤링이란 "검색 엔진의 구축 등을 위하여 특정한 방법으로 웹 페이지를 수집하는 프로그램"을 말하며, 웹 스크래핑이란 "웹에서 데이터를 수집하는 프로그램"을 말한다. 하지만 요즘에는 두 용어가 비슷한 용도로 사용되는 듯 하다.

웹 크롤링 및 웹 스크래핑을 하는데 있어서 사용할 패키지는 beatifulsoup4 이고 pip install beatifulsoup4를 사용하여 미리 설치하자. (참고 : beatifulsoup4 공식문서)

이제 코드로 실습을 해보자. 먼저 다음과 같이 html_doc이 있다고 가정해보자.

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

이제 beatifulsoup4를 사용해서 html 문서를 분석해보자.
다음과 같이 beatifulsoup4를 import 해오고 soup 객체를 만들자. BeatifulSoup의 인자로는 분석할 html문서인 html_doc와 "html.parser"를 넣어줬는데, "html.parser"의 의미는 BeatifulSoup 객체에게 HTML의 관점에서 인자로 받은 문자열을 분석하라고 알려주는 것이다. prettify() 메소드는 BeatifulSoup에서 파싱한 결과를 유니코드 형태로 리턴한다.

from bs4 import BeatifulSoup


soup = BeatifulSoup(html_doc, "html.parser")
print(soup.prettify())

# 출력결과
# <html>
#  <head>
#   <title>
#    The Dormouse's story
#   </title>
#  </head>
#  <body>
#   <p class="title">
#    <b>
#     The Dormouse's story
#    </b>
#   </p>
#   <p class="story">
#    Once upon a time there were three little sisters; and their names were 
#    <a class="sister" href="http://example.com/elsie" id="link1">
#     Elsie
#    </a>
#    ,
#    <a class="sister" href="http://example.com/lacie" id="link2">
#     Lacie
#    </a>
#    and
#    <a class="sister" href="http://example.com/tillie" id="link3">
#     Tillie
#    </a>
#    ;
# and they lived at the bottom of a well.
#   </p>
#   <p class="story">
#    ...
#   </p>
#  </body>
# </html>

만약 html에 포함된 특정한 속성의 값을 추출하고 싶다면 어떻게 해야할까?
아래 코드를 참고하자!

print(soup.title) # title 속성 추출
# <title>The Dormouse's story</title>

print(soup.p) # p태그 추출
# <p class="title"><b>The Dormouse's story</b></p>

print(soup.find("p","story") # p태그 중에서 클래스가 story인 데이터 추출
# <p class="story">Once upon a time there were three little sisters; and their names were
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,   
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>; 
# and they lived at the bottom of a well.</p>

print(soup.find("p","story").text)
# Once upon a time there were three little sisters; and their names were    
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.


다음으로 웹에서 데이터를 수집할 때 주의사항을 알아보자.
거의 모든 웹에는 robots.txt 라는 크롤링 규칙이 있다. 구글에서의 설명에 따르면 "robots.txt 파일은 크롤러가 사이트에서 엑세스할 수 있는 URL을 검색엔진 크롤러에 알려준다. 이 파일은 주로 요청으로 인해 사이트가 오버로드되는것을 방지하기 위해 사용한다." 라고 쓰여있다. (참고 : Robots.txt 소개)

구글의 robots.txt 파일을 확인하고 싶다면, 주소창에 google.com 뒤에 /robots.txt를 추가하여 검색해보자. 검색을 하면 다음과 같은 창이 나온다.

간단히 알아보자.
User-agent 는 접속하는 주체(크롤러)이다. * 표시가 되어 있는 것은 모든 것에 대해서 아래와 같은 규칙을 적용한다는 뜻 이다.
Disallow: /search 는 google.com/search 와 같이 google.com 다음에 /search가 오는 정보들을 크롤러의 접근을 허용하지 않겠다는 뜻이다.
Allow: /search/about 은 /search/about에 한정해서 크롤러의 접근을 허용하겠다는 뜻이다.
이런식으로 거의 모든 사이트에는 robots.txt가 있어서 크롤링을 하기 전에 미리 확인하자.

📌 동시성 프로그래밍으로 웹 크롤링, 스크래핑 성능 극대화

이어서 bjpublic.tistory의 robots.txt를 확인하면 다음과 같다.

robots.txt 파일에 제한되지 않게 비제이퍼블릭 사이트에서 카테고리 항목(전체 출간 도서)를 크롤링 해보자.
전체 출간 도서를 클릭하면 다음과 같은 화면을 볼 수 있다.

전체 출간 도서 창은 여러 페이지로 이루어져 있고, 1page의 주소창을 보면 "https://bjpublic.tistory.com/category/전체%20출간%20도서" 라고 되어있는데, 2페이지를 클릭하면 뒤에 "?page=2"가 붙는다. "?page=1"를 뒤에 붙여도 1페이지가 나온다.

우리의 목적은 전체 페이지의 모든 텍스트 데이터를 모집하는 것이다. 이를 위해 지난 포스트에서 실행했던 aiohttp를 이용해서 html 데이터를 가져오고, beautifulsoup4를 사용하여 원하는 데이터만 뽑을 것이다. 또한 각각의 페이지에 대해서 동시에 데이터를 뽑는 동시성 프로그래밍을 사용할 것이다. 기본 코드는 다음과 같다.

from bs4 import BeautifulSoup
import aiohttp
import asyncio


async def fetch(session, url):
    async with session.get(url) as response:
    	html = await response.text()
        print(html)

async def main():
    BASE_URL = "https://bjpublic.tistory.com/category/%EC%A0%84%EC%B2%B4%20%EC%B6%9C%EA%B0%84%20%EB%8F%84%EC%84%9C?"
    urls = [f"{BASE_URL}?page={i}" for i in range(1, 10)]
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(*[fetcher(session, url) for url in urls])


if __name__ == "__main__":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main())

먼저 main함수는 코루틴 함수 이기 때문에 asyncio.run() 메소드로 실행을 하게 된다.
BASE_URL은 기본이 되는 url인데, 전체 출간 도서를 클릭했을때 나오는 첫 화면의 url이다. 그리고 urls는 BASE_URL 뒤에 페이지 주소를 붙여서 만든 리스트이다.

이후 async with aiohyyp.ClientSession() as session: 구문을 통해 세션을 열어주고 fetch함수를 실행하는데, 각각의 url에 대해서 데이터를 가져오는 함수이다. 여기서 *이 붙은 이유는 언패킹을 위해서이다.

fetch함수를 더 살펴보면, 세션에 대해서 get이라는 메소드를 사용한 reponse context를 만들고, reponsetext라는 메소드를 이용해서 html 데이터를 가져온다.

코드를 실행시켜 보면 html 데이터가 출력되는 것을 확인할 수 있다. 하지만 우리는 이러한 데이터 중에서도 도서에 대한 정보를 나타내는 text데이터만 가져오고 싶다. 이를 위해 BeautifulSoup를 사용해보자.

async with session.get(url) as response:
        html = await response.text()
        soup = BeautifulSoup(html, "html.parser")
        cont_thumb = soup.find_all("div", "cont_thumb")
        for cont in cont_thumb:
            title = cont.find("p", "txt_thumb")
            if title is not None:
                print(title.text)

먼저 위에서 이미 실습했던 것 처럼 soup 객체를 만들자. (복습 : BeatifulSoup의 인자로는 분석할 html문서인 html_doc와 "html.parser"를 넣어줬는데, "html.parser"의 의미는 BeatifulSoup 객체에게 HTML의 관점에서 인자로 받은 문자열을 분석하라고 알려주는 것이다.)
필요한 데이터를 파싱하려면 해당하는 클래스를 알아야 한다. 이는 다음과 같이 웹 페이지의 개발자도구(f12) 로 들어가 화살표 모양을 누르고 원하는 텍스트에 마우스를 올리면 해당 클래스를 표시해준다.

확인해보면 우리가 원하는 텍스트는 "cont_thumb"라는 클래스를 가진 div 태그 element 요소 안에 "txt_thumb" 클래스를 가지고 있는 p 태그에 해당하는 텍스트와 그 바로 밑에 있는 "txt_thumb" 클래스의 텍스트이다.

먼저 "cont_thumb"를 가져와 보자. 위에서는 find 메소드를 사용했는데 여기서는 find_all 메소드를 사용할 것 이다. find_all 메소드는 해당하는 클래스를 가진 모든 태그들을 리스트에 담아준다.

```python
from bs4 import BeautifulSoup
import aiohttp
import asyncio


async def fetch(session, url):
    async with session.get(url) as response:
    	html = await response.text()
        soup = BeatifulSoup(html, "html.parser")
        cont_thumb = soup.find_all("div", "cont_thumb")  # 추가
        print(cont_thumb)   # print 하여 확인해보자

async def main():
    BASE_URL = "https://bjpublic.tistory.com/category/%EC%A0%84%EC%B2%B4%20%EC%B6%9C%EA%B0%84%20%EB%8F%84%EC%84%9C?"
    urls = [f"{BASE_URL}?page={i}" for i in range(1, 10)]
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(*[fetcher(session, url) for url in urls])


if __name__ == "__main__":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main())

코드를 실행시키면 위 사진과 같이 결과가 나올텐데, class="cont_thumb"인 div element를 리스트에 담아 출력한다. 이제 이 안에 있는 텍스트만 뽑아보자.

# fetch 함수에 추가.
for cont in cont_thumb:
    title = cont.find("p", "txt_thumb")
    if title is not None:
		print(title.text)

div element들이 list에 담겨있기 때문에 for문을 이용해 리스트의 각 요소들을 순회하며 find 메소드를 이용해 p태그 안의 "txt_thumb"를 추출하여 title에 저장하자. 그리고 title.text를 print하여 결과를 확인해 보자.



📌 포스트맨 셋업, 네이버, 카카오 오픈 API 사용하기

먼저 포스트맨을 다운로드 받자. 포스트맨은 API를 테스트 할 수 있는 소프트웨어다.

다운로드를 받은 후 실행하면 회원가입을 하라는 문구가 나온다. 무시하고 스킵면 다음과 같은 창이 뜬다. 여기서 + 버튼을 누르자.

이제 네이버 API와 카카오 API를 사용해보자!
먼저 NAVER Developers에 들어가서 "서비스 API"를 클릭한다. 이후 검색을 누르자. 검색 API를 사용하면 파이썬으로 검색을 실행하여 데이터를 수집할 수 있다. "오픈 API 이용 신청"을 누르자. 약관동의 및 계정 정보 등록을 하고나면 "애플리케이션 등록"으로 이동할 것이다. 여기서 애플리케이션 이름은 아무거나 작성하고 사용 API는 "검색", 환경 추가는 "WEB 설정", 웹 서비스 URL에는 http://localhost:8080를 적어주자.

이렇게 성공적으로 등록을 완료하면 Client ID와 Client Secret이 나오는데 이를 본인만 볼 수 있는 메모장 등에 적어두도록 하자.

이제 우리는 네이버 API를 사용할 수 있는 ID와 Secret Key를 부여 받았다. 간단하게 뉴스 검색을 해보자.
NAVER Developers Documents에 들어가보면 아래와 같이 예시가 나와있다.

[호출]란에 나와 있는 https://openapi.naver.com/v1/search/news.xml?query=%EC%A3%BC%EC%8B%9D&display=10&start=1&sort=sim 을 복사해서 포스트맨의 요청 URL 란에 복사 붙여넣기하고, .xml과 query= 이후에 나오는 것을 다 지우자. Send를 눌러보자.

그러면 위 사진과 같이 "인증에 실패했다"라는 문구가 나타난다. query=cat 으로 바꿔주고, 우리가 애플리케이션을 등록하면서 받은 Client ID와 Client Secret을 "Headers"를 누른 후 VALUE에 넣어주고 Send를 다시 눌러보자.

그러면 위와 같이 인증에 성공한 것을 볼 수 있다. 그리고 사진의 아래 부분을 보면 우리가 네이버에 검색을 한 그 결과가 JSON 파일로 출력되는 것을 확인할 수 있다. display가 10개인데, 이는 URL의 뒤에 &display=50이라고 하면 50개가 출력이되는 것을 확인할 수 있다.

만약 cat에 대한 이미지를 검색하고 싶다면? URL을 https://openapi.naver.com/v1/search/news?query=cat&display=50 에서 https://openapi.naver.com/v1/search/image?query=cat&display=50 로 바꾸기만 하면 된다!!! (신기...)



네이버 API 사용법을 알아봤으니, 이제 카카오 API 사용법을 알아보자!
Kakao Developers에 들어가서 제품 전체 보기를 클릭하자. 그리고 인공지능 API의 검색을 클릭하고 시작하기를 누르자. 그러면 애플리케이션 추가하기가 보일거다. 그걸 누르면 다음과 같은 화면이 나온다.

앱 이름과 사업자명은 아무거나 작성하면 된다. 작성을 완료하고 저장 버튼을 누르면 애플리케이션 생성이 완료된다.

생성된 애플리케이션을 눌러서 들어가보자.

들어가면 위 사진과 같이 여러 앱 키가 있는데 우리는 REST API 키를 사용할것이다. 미리 복사해두자
이후 개발가이드에 들어가서 보면 어떻게 사용하라는지 예시가 나와있다.

마찬가지로 포스트맨에 들어가서 새로운 Tab을 추가한 후 url에 https://dapi.kakao.com/v2/search/web?query=cat 을 붙여넣자.
당연하게도 key를 입력해야겠죠?

Headers에 들어간 후 KEY에는 Authorization을, VALUE에는 KakaoAK {아까 복사한 REST API 키}를 작성해줍니다. 이때 KakaoAK 와 REST API 키는 한 칸 띄고 작성해주셔야 합니다. 그러면 다음과 같이 JSON파일이 뜨는것을 확인할 수 있습니다.

추가적으로 "내 애플리케이션"에 들어가서 "플랫폼 설정" -> "Web 플랫폼 등록"을 해주시는게 좋습니다.

나중에는 웹사이트 도메인이 서버에 올라가서 등록이 되면 그 사이트를 입력해 주면 된다!
세부적인 사용법은 개발 가이드을 참고하자!!



📌 오픈 API를 활용한 이미지 데이터 수집



📌 동시성 프로그래밍으로 이미지 다운로더 개발 (feat. aiofiles)

profile
AI 서비스 엔지니어를 목표로 공부하고 있습니다.

0개의 댓글