본문 바로가기

Development/Python

venv 개념 정리, 그리고 BeautifulSoup4와 pymongo

들어가며

3주차에서는 파이썬 기초 문법, bs4(BeautifulSoup4)를 사용한 웹 페이지 스크래핑(크롤링), pymongo를 사용한 MongoDB 사용 방법에 대해서 다루었다. 단, 처음 파이썬을 접한 상황이라면 이 강의로는 파이썬 기초를 잡기에는 턱없이 부족하니, wikidocs나 구글링, 유튜브를 통해서 어느정도 기초 지식을 쌓고 수강하는 것을 추천한다. 특히, 문자열 및 리스트의 인덱싱과 슬라이싱, 딕셔너리 정도만 숙지하여도 3주차 강의를 수월하게 이해하고 따라갈 수 있다고 생각한다.

package와 venv

package

이미 누가 짜놓은 소스코드를 현재 내가 진행 중인 프로젝트로 불러와서 사용할 수 있다. 이렇게 다른 사람이 구현해놓은 기능의 소스코드를 패키지, 모듈, 라이브러리라고 한다. 이들은 서로 다른 의미를 나타내는 용어이지만, 통상적으로 라이브러리라고 한다(본 글을 작성할 때에는 패키지라고 칭하겠다). 패키지는 어떠한 이유(브라우저의 버전 업데이트로 인한 호환성 이슈 발생 등)로 인해 새로운 버전으로 업데이트 될 수 있으며, 버전별로 사용법에 차이가 있을 수 있다.

venv(virtual environment)

나는 주로 카페(노트북)나 집(PC)에서 프로젝트를 진행하는데, 이 경우 노트북과 PC에 설치된 패키지의 버전이 맞지 않아서 오류가 발생하곤 했다. 이와 같이 여러 환경에서 프로젝트를 진행하는 경우, 패키지의 버전 관리가 필요한데, 이를 가능케하는 것이 바로 venv이다. venv는 같은 시스템에서 실행되는 다른 파이썬 프로그램들의 동작에 영향을 주지 않기 위해 파이썬 배포 패키지들을 설치 및 업그레이드를 가능하게 하는 실행 환경이다. 즉, 로컬이 아닌 가상 환경에 패키지를 설치함으로써 여러 기기에 설치된 패키지 버전을 관리하지 않아도 되며, 필요에 따라 가상 환경을 불러들여 원하는 버전의 패키지를 사용할 수 있다.

request

request는 파이썬 프로그램이 웹 서버로 http 요청을 할 수 있도록 도와주는 패키지이다. 먼저, 현재 진행 중인 프로젝트 폴더 위치에서 아래 명령어를 사용하여 설치하여야 한다.

$ pip install request

2주차에서 사용한 서울시 실시간 미세먼지 현황 API를 통해 데이터를 받아오는 소스코드는 다음과 같다.

import request

url = 'http://openapi.seoul.go.kr:8088/6d4d776b466c656533356a4b4b5872/json/RealtimeCityAir/1/99'

def request_GET() -> None:
    req = requests.get(url)
    res = req.json()
    rows = res["RealtimeCityAir"]["row"]

    for row in rows:
        name = row['MSRSTE_NM']
        value = row['IDEX_MVL']

        if value > 80:
            print(f'{name} : {value}')


if __name__ == "__main__":
    request_GET()

BeautifulSoup4

BeautifulSoup4(줄여서 bs4)는 웹 페이지의 정보를 수집(스크래핑이라고 하며, 크롤링이라는 용어가 범용적으로 사용된다) 할 수 있도록 도와주는 패키지이다. 마찬가지로 bs4를 사용하기 위해서는 해당 패키지를 설치하여야 한다.

$ pip install BeautifulSoup4

bs4를 사용하는 기본적인 코드는 다음과 같다.

import requests
from bs4 import BeautifulSoup

user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'

headers = {'User-Agent' : user_agent}

url = 'https://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=pnt&date=20200303'

soup = BeautifulSoup(requests.get(url, headers=headers).text, 
                     'html.parser')
  • BeautifulSoup 패키지를 활용하여 HTML을 검색하기 편한 상태로 만든다.
  • url 변수는 네이버 영화랭킹 웹 페이지로 이동하는 주소이다.
  • soup 변수에 파싱하기 좋은 상태의 HTML이 담겨 있으며, 필요한 부분을 추출할 수 있다.

필요한 부분을 추출할 때에는 query selector로 HTML의 특정 요소에 접근할 수 있다. query selector를 쉽게 말하자면 CSS의 선택자라고 할 수 있겠다. 예를 들어, 아래와 같은 HTML에서 '여긴 어디?'라는 텍스트를 나타내는 p 태그에 접근할 때 query selector는 div > a > p.target이 된다.

<body>
    <div>
        <p>환영합니다.</p>
    </div>
    <div>
        <a>
            <p class="target">여긴 어디?</p>
        </a>
    </div>
</body>

query selector를 알아낸 후에는 select 함수를 통해 해당 태그를 선택하여 꺼낼 수 있으며, 태그의 텍스트, 속성 등을 추출할 수 있다.

p_tag = soup.select('div > a > p.target')
p_tag_text = p_tag.text
p_tag_class = p_tag['class']

예를 들어, 아래 코드와 같이 네이버 영화랭킹 웹 페이지에서 순위, 제목, 평점을 추출하고, 이를 출력해보도록 할 수 있다.

import requests
from bs4 import BeautifulSoup

url = "https://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=pnt&date=20200303"


class NaverMovies:
    def __init__(self) -> None:
        self.agents = ["Mozilla/5.0",
                       "(Windows NT 10.0; Win64; x64)AppleWebKit/537.36",
                       "(KHTML, like Gecko)",
                       "Chrome/73.0.3683.86",
                       "Safari/537.36"]
        self.headers = {'User-Agent': " ".join(self.agents)}
        self.data = requests.get(url, headers=self.headers)
        self.soup = BeautifulSoup(self.data.text, 'html.parser')

    def scrap_movies(self) -> None:
        selectors = {"tr": "#old_content > table > tbody > tr",
                     "title": "td.title > div > a",
                     "rank": "td.ac > img",
                     "point": "td.point"}

        trs = self.soup.select(selectors['tr'])

        for tr in trs:
            img_tag = tr.select_one(selectors['rank'])
            a_tag = tr.select_one(selectors['title'])
            td_tag = tr.select_one(selectors['point'])

            if a_tag is not None:
                rank = img_tag.attrs.get('alt')[:2]
                title = a_tag.text
                point = float(td_tag.text)
                print(f"{rank}\t{title}\t{point}")


if __name__ == "__main__":
    crawler = NaverMovies()
    crawler.scrap_movies()

pymongo

pymongo는 파이썬으로 MongoDB를 제어할 수 있도록 도와주는 패키지이다. 마찬가지로 패키지를 사용하기 위해서는 해당 패키지를 설치하여야 한다.

$ pip install pymongo

pymongo를 사용하기 위한 기본 코드는 다음과 같으며, MongoDB가 실행중이어야 한다.

from pymongo import MongoClient

client = MongoClient('localhost', 27017)
db = client.DB이름

pymongo를 사용해서 데이터 입력, 조회, 출력, 삭제를 하기 위한 방법은 공식 페이지를 보면 자세히 알 수 있기 때문에 별도로 정리하지 않고, 이를 응용한 소스코드만 기록하였다.

from pymongo import MongoClient


class MongoDB:
    def __init__(self) -> None:
        self.client = MongoClient('localhost', 27017)
        self.db = self.client.dbsparta

    def insert_one(self, collection: str, doc: {}) -> bool:
        """insert_one('users', {'name': 'bobby', 'age': 28})"""

        try:
            self.db[collection].insert_one(doc)
            return True
        except Exception as error:
            print(error)
            return False

    def insert_many(self, collection: str, docs: []) -> bool:
        """insert_many('users', [{'name': 'bobby', 'age': 28}]"""

        try:
            self.db[collection].insert_many(docs)
            return True
        except Exception as error:
            print(error)
            return False

    def find_many(self, collection: str, search: dict = None, setting: dict = None) -> list:
        """find_many('users', {'name': 'bobby'}, {'_id': False})"""

        search = {} if search is None else search
        setting = {'_id': False} if setting is None else setting

        try:
            return list(self.db[collection].find(search, setting))
        except Exception as error:
            print(error)
            return []

    def find_one(self, collection: str, search: dict = None, setting: dict = None) -> dict:
        """find_one('users', {'name': 'bobby'}, {'_id': False})"""

        search = {} if search is None else search
        setting = {'_id': False} if setting is None else setting

        try:
            return self.db[collection].find_one(search, setting)
        except Exception as error:
            print(error)
            return {}

    def update_one(self, collection: str, search: dict, how: dict) -> bool:
        """update_one('users', {'name': 'bobby'}, {'$set': {'age': 27}})"""

        try:
            self.db[collection].update_one(search, how)
            return True
        except Exception as e:
            print(e)
            return False

    def delete_one(self, collection: str, search: dict) -> bool:
        """delete_one('users', {'name': 'bobby'})"""

        try:
            self.db[collection].delete_one(search)
            return True
        except Exception as e:
            print(e)
            return False


if __name__ == "__main__":
    mongodb = MongoDB()

마치며

강의에서는 위의 내용 외에도 웹 페이지로부터 수집한 결과를 MongoDB에 저장하고, 수정하는 것까지 해보았으나, 이는 기본적인 사용법이라고 생각하여 자세히 기록하지는 않았다. 또한, 지니뮤직 사이트에서 음악 차트의 순위, 제목, 가수를 출력하는 숙제를 내주었는데, bs4 사용법을 제대로 숙지하였다면 어렵지 않게 해결할 수 있다고 생각한다(소스코드).

import requests
from bs4 import BeautifulSoup

url = "https://www.genie.co.kr/chart/top200?ditc=D&ymd=20200403&hh=23&rtm=N&pg=1"


class GenieMusics:
    def __init__(self) -> None:
        self.agents = ["Mozilla/5.0",
                       "(Windows NT 10.0; Win64; x64)AppleWebKit/537.36",
                       "(KHTML, like Gecko)",
                       "Chrome/73.0.3683.86",
                       "Safari/537.36"]
        self.headers = {'User-Agent': " ".join(self.agents)}
        self.data = requests.get(url, headers=self.headers)
        self.soup = BeautifulSoup(self.data.text, 'html.parser')

    def scrap_musics(self) -> None:

        selectors = {"tr": "#body-content > div.newest-list > div > table > tbody > tr",
                     "rank": "td.number",
                     "title": "td.info > a.title",
                     "artist": "td.info > a.artist"}
        trs = self.soup.select(selectors['tr'])
        for tr in trs:
            rank_tag = tr.select_one(selectors['rank'])
            title_tag = tr.select_one(selectors['title'])
            artist_tag = tr.select_one(selectors['artist'])

            rank = "0" + rank_tag.text.strip()[:2].strip()
            rank = rank[-2:]
            title = title_tag.text.strip()
            artist = artist_tag.text.strip()
            print(f"{rank}\t{title}\t{artist}")


if __name__ == "__main__":
    crawler = GenieMusics()
    crawler.scrap_musics()