코드 보기
import textwrap
import pandas as pd
import requests
from bs4 import BeautifulSoup
from lets_plot import *
LetsPlot.setup_html()이 장에서는 웹페이지에서 웹 스크래핑을 통해 얻거나 API를 통해 인터넷에서 직접 가져온 온라인 데이터를 다루는 방법을 설명합니다. 중요한 원칙은 API가 제공된다면 항상 API를 사용해야 한다는 점입니다. API는 정보를 여러분의 파이썬 세션으로 직접 전달하도록 설계되었기 때문에 많은 노력을 아껴줄 것입니다.
이미 언급했듯이, 가능하면 항상 API를 사용해야 합니다. 하지만 그렇지 못한 상황에서 웹 스크래핑의 규칙은 무엇일까요? 먼저, 그것이 법적으로 허용되는지, 윤리적으로 옳은지에 대해 이야기해야 합니다. 전반적으로 이에 관한 상황은 복잡합니다.
법적 측면은 여러분이 거주하는 지역에 따라 크게 달라집니다. 하지만 일반적인 원칙으로서, 데이터가 공개되어 있고(public), 비개인적이며(non-personal), 비상업적이고(non-commercial), 사실에 근거한(factual) 것이라면 괜찮을 가능성이 큽니다. 이 세 가지 요소는 사이트의 이용 약관, 개인 식별 정보, 저작권과 연결되어 있기 때문에 중요합니다.
데이터가 공개되지 않았거나, 개인적이거나, 사실이 아닌 경우, 또는 수익 창출을 목적으로 하거나 상업적으로만 제공되는 것을 스크래핑하려는 경우 변호사와 상담해야 합니다. 어떤 경우든 페이지를 호스팅하는 서버의 리소스를 존중해야 합니다. 가장 중요한 것은 많은 페이지를 스크래핑할 때 요청 사이에 충분한 대기 시간을 두어야 한다는 것입니다. Tenacity는 이를 위한 훌륭한 패키지로, 실행 시도 사이에 일시 정지하는 기능(및 다른 여러 유용한 기능)을 제공합니다.
자세히 살펴보면 많은 웹사이트 페이지 어딘가에 “이용 약관” 또는 “서비스 약관” 링크가 포함되어 있으며, 해당 페이지를 자세히 읽어보면 사이트에서 웹 스크래핑을 명시적으로 금지하고 있는 것을 종종 발견하게 될 것입니다. 이러한 페이지들은 대개 회사들이 매우 광범위한 권리를 주장하는 법적 구역입니다. 가능하면 이러한 이용 약관을 존중하는 것이 예의이지만, 모든 주장을 곧이곧대로 받아들일 필요는 없습니다.
미국 법원은 웹사이트 하단에 이용 약관을 단순히 기재해 두는 것만으로는 여러분이 그에 구속된다고 보기에 충분하지 않다고 판단해 왔습니다(예: HiQ Labs v. LinkedIn). 일반적으로 이용 약관에 구속되려면 계정을 생성하거나 체크박스를 선택하는 등의 명시적인 행동을 취했어야 합니다. 이것이 데이터가 공개되었는지 여부가 중요한 이유입니다. 접근을 위해 계정이 필요 없다면 이용 약관에 구속될 가능성이 낮습니다. 단, 유럽의 상황은 상당히 다른데, 유럽 법원은 명시적으로 동의하지 않더라도 이용 약관이 집행 가능하다고 판단해 왔습니다.
데이터가 공개되어 있더라도 이름, 이메일 주소, 전화번호, 생년월일 등 개인 식별 정보를 스크래핑하는 것에는 매우 주의해야 합니다. 유럽은 이러한 데이터의 수집 및 저장에 대해 엄격한 법률(GDPR로 알려짐)을 가지고 있으며, 어디에 살든 윤리적 늪에 빠질 가능성이 큽니다. 예를 들어 2016년에 한 연구 그룹이 데이트 사이트인 OkCupid에서 70,000명의 공개 프로필 정보(예: 사용자 이름, 나이, 성별, 위치 등)를 스크래핑하여 익명화 시도 없이 공개했습니다. 연구자들은 데이터가 이미 공개되어 있었으므로 문제가 없다고 생각했지만, 이 연구는 데이터셋에 포함된 사용자들의 식별 가능성에 대한 윤리적 우려로 인해 널리 비난받았습니다. 여러분의 작업이 개인 식별 정보 스크래핑을 포함한다면, OkCupid 사례와 개인 식별 정보의 획득 및 공개와 관련된 유사한 연구 윤리 문제들을 반드시 읽어보기를 강력히 권장합니다.
마지막으로 저작권법에 대해서도 걱정해야 합니다. 저작권법은 복잡합니다. 미국 법은 무엇이 보호되는지 다음과 같이 명시합니다: “[…] 어떤 유형의 표현 매체에 고정된 독창적인 저작물, […]”. 그러고 나서 문학 저작물, 음악 저작물, 영화 등 적용되는 특정 카테고리를 설명합니다. 저작권 보호에서 눈에 띄게 빠진 것은 데이터입니다. 이는 여러분이 스크래핑 대상을 사실(facts)로 한정한다면 저작권 보호가 적용되지 않음을 의미합니다. (단, 유럽에는 데이터베이스를 보호하는 별도의 “sui generis” 권리가 있습니다.)
간단한 예로, 미국에서 재료 목록과 조리 지침은 저작권 보호 대상이 아니므로 저작권을 사용하여 레시피를 보호할 수 없습니다. 하지만 그 레시피 목록에 상당한 양의 새로운 문학적 내용이 수반된다면 그것은 저작권 보호 대상이 됩니다. 이것이 인터넷에서 레시피를 찾을 때 항상 앞부분에 그렇게 많은 내용이 있는 이유입니다.
원본 콘텐츠(텍스트나 이미지 등)를 스크래핑해야 하는 경우, 여전히 공정 이용(fair use) 원칙에 의해 보호받을 수 있습니다. 공정 이용은 딱딱한 규칙이 아니라 여러 요인을 고려합니다. 연구나 비상업적 목적으로 데이터를 수집하고 스크래핑 대상을 필요한 것만으로 제한한다면 적용될 가능성이 큽니다.
이 장에서는 pandas 패키지가 필요합니다. 또한 이미 설치되어 있어야 할 seaborn도 사용할 것입니다. 터미널에서 pixi add beautifulsoup4 및 pixi add pandas-datareader를 실행하여 beautifulsoup와 pandas-datareader 패키지를 설치해야 합니다. 또한 두 가지 내장 패키지인 textwrap과 requests를 사용할 것입니다.
시작하기 위해 필요한 패키지들을 불러오겠습니다(스크립트나 노트북 상단에 필요한 패키지들을 불러오는 것은 항상 좋은 관행입니다).
import textwrap
import pandas as pd
import requests
from bs4 import BeautifulSoup
from lets_plot import *
LetsPlot.setup_html()URL과 파일 타입을 알고 있다면 인터넷에서 데이터를 읽어오는 것은 쉽습니다. 여기 예시로 URL에 저장된 CSV 파일인 ‘storms’ 데이터셋을 읽어보겠습니다 (처음 10개 행만 가져옵니다):
pd.read_csv(
"https://vincentarelbundock.github.io/Rdatasets/csv/dplyr/storms.csv", nrows=10
)| rownames | name | year | month | day | hour | lat | long | status | category | wind | pressure | tropicalstorm_force_diameter | hurricane_force_diameter | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Amy | 1975 | 6 | 27 | 0 | 27.5 | -79.0 | tropical depression | NaN | 25 | 1013 | NaN | NaN |
| 1 | 2 | Amy | 1975 | 6 | 27 | 6 | 28.5 | -79.0 | tropical depression | NaN | 25 | 1013 | NaN | NaN |
| 2 | 3 | Amy | 1975 | 6 | 27 | 12 | 29.5 | -79.0 | tropical depression | NaN | 25 | 1013 | NaN | NaN |
| 3 | 4 | Amy | 1975 | 6 | 27 | 18 | 30.5 | -79.0 | tropical depression | NaN | 25 | 1013 | NaN | NaN |
| 4 | 5 | Amy | 1975 | 6 | 28 | 0 | 31.5 | -78.8 | tropical depression | NaN | 25 | 1012 | NaN | NaN |
| 5 | 6 | Amy | 1975 | 6 | 28 | 6 | 32.4 | -78.7 | tropical depression | NaN | 25 | 1012 | NaN | NaN |
| 6 | 7 | Amy | 1975 | 6 | 28 | 12 | 33.3 | -78.0 | tropical depression | NaN | 25 | 1011 | NaN | NaN |
| 7 | 8 | Amy | 1975 | 6 | 28 | 18 | 34.0 | -77.0 | tropical depression | NaN | 30 | 1006 | NaN | NaN |
| 8 | 9 | Amy | 1975 | 6 | 29 | 0 | 34.4 | -75.8 | tropical storm | NaN | 35 | 1004 | NaN | NaN |
| 9 | 10 | Amy | 1975 | 6 | 29 | 6 | 34.0 | -74.8 | tropical storm | NaN | 40 | 1002 | NaN | NaN |
API(application programming interface)를 사용하는 것은 인터넷에서 정보를 끌어오는 또 다른 방법입니다. 이는 단지 하나의 도구(예: 파이썬)가 다른 도구(예: 서버)와 대화하고 유용하게 정보를 교환하기 위한 방법일 뿐입니다. 전형적인 유스케이스는 API를 통해 특정 쿼리에 맞는 데이터 요청을 게시하고 그 대가로 해당 데이터의 다운로드를 받는 것입니다. (웹사이트를 스크래핑하는 것보다 항상 API를 우선적으로 사용해야 합니다.)
모든 도구와 작동하도록 설계되었기 때문에 API와 상호 작용하는 데 프로그래밍 언어가 반드시 필요한 것은 아니지만, 언어를 사용하는 것이 훨씬 쉽습니다.
일부 API에 접속하려면 API 키(key)가 필요합니다. 사이트에 가입만 하면 되는 경우도 있고, 접속료를 지불해야 하는 경우도 있습니다.
이를 확인하기 위해 API를 직접 사용하여 시계열 데이터를 가져와 보겠습니다. requests 패키지를 사용하여 인터넷에 요청을 보낼 것입니다.
API는 ‘엔드포인트(endpoint)’라고 불리는 기본 URL과 질문을 인코딩하는 URL로 구성됩니다. ONS API를 예로 들어보겠습니다. 엔드포인트는 “https://api.beta.ons.gov.uk/v1/”입니다. 나머지 API 부분은 ’data?uri=’ 형태이며 그 뒤에 시계열 ID(jp9z)와 데이터셋 ID(LMS)가 붙습니다. 이는 영국 서비스 부문의 구인수 데이터입니다.
API에 의해 반환되는 데이터는 일반적으로 JSON 형식으로, 중첩된 파이썬 딕셔너리와 매우 유사하게 생겼으며 그 항목들은 동일한 방식으로 접근할 수 있습니다 - 아래 예제에서 시리즈의 제목을 가져올 때 이 방식이 사용됩니다. JSON은 분석에 좋지 않으므로 pandas를 사용하여 데이터를 가공하겠습니다.
url = "https://api.beta.ons.gov.uk/v1/data?uri=/employmentandlabourmarket/peopleinwork/employmentandemployeetypes/timeseries/jp9z/lms/previous/v108"
# ONS API로부터 데이터 가져오기:
json_data = requests.get(url).json()
# 빠른 플롯을 위한 데이터 준비
title = json_data["description"]["title"]
df = (
pd.DataFrame(pd.json_normalize(json_data["months"]))
.assign(
date=lambda x: pd.to_datetime(x["date"]),
value=lambda x: pd.to_numeric(x["value"]),
)
.set_index("date")
)
df["value"].plot(title=title, ylim=(0, df["value"].max() * 1.2), lw=3.0);/tmp/ipykernel_6103/3260175139.py:11: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
date=lambda x: pd.to_datetime(x["date"]),
우리는 API를 읽는 것에 대해 이야기했습니다. 데이터, 모델 등 원하는 무엇이든 제공하기 위해 직접 API를 만들 수도 있습니다! 이는 고급 주제이므로 다루지 않겠습니다. 하지만 필요하다면 가장 간단한 방법은 Fast API를 사용하는 것입니다. Fast API에 대한 짧은 비디오 튜토리얼은 여기에서 찾을 수 있습니다.
ONS 데이터를 가져오는 데 코드가 많이 들지는 않았지만, 단 한 줄이면 더 좋지 않을까요? 다행히 이를 쉽게 만들어주는 패키지들이 있지만 API에 따라 다릅니다(그리고 API는 시간이 지나면 생기거나 없어지기도 합니다).
추가 API에 접근하기 위한 가장 포괄적인 라이브러리는 pandas-datareader이며, 다음 사이트들에 대한 편리한 접근을 제공합니다:
등등.
FRED(세인트루이스 연방준비은행 경제 데이터 라이브러리)를 사용하는 예제를 보겠습니다. 이번에는 영국의 실업률을 살펴보겠습니다:
import pandas_datareader.data as web
df_u = web.DataReader("LRHUTTTTGBM156S", "fred")
df_u.plot(title="영국 실업률 (퍼센트)", legend=False, ylim=(2, 6), lw=3.0);/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50689 (\N{HANGUL SYLLABLE YEONG}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44397 (\N{HANGUL SYLLABLE GUG}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49892 (\N{HANGUL SYLLABLE SIL}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50629 (\N{HANGUL SYLLABLE EOB}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 47456 (\N{HANGUL SYLLABLE RYUL}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 54140 (\N{HANGUL SYLLABLE PEO}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49468 (\N{HANGUL SYLLABLE SEN}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 53944 (\N{HANGUL SYLLABLE TEU}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50689 (\N{HANGUL SYLLABLE YEONG}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 44397 (\N{HANGUL SYLLABLE GUG}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49892 (\N{HANGUL SYLLABLE SIL}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 50629 (\N{HANGUL SYLLABLE EOB}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 47456 (\N{HANGUL SYLLABLE RYUL}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 54140 (\N{HANGUL SYLLABLE PEO}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 49468 (\N{HANGUL SYLLABLE SEN}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/events.py:82: UserWarning: Glyph 53944 (\N{HANGUL SYLLABLE TEU}) missing from font(s) STIXGeneral.
func(*args, **kwargs)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50689 (\N{HANGUL SYLLABLE YEONG}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44397 (\N{HANGUL SYLLABLE GUG}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49892 (\N{HANGUL SYLLABLE SIL}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50629 (\N{HANGUL SYLLABLE EOB}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 47456 (\N{HANGUL SYLLABLE RYUL}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 54140 (\N{HANGUL SYLLABLE PEO}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49468 (\N{HANGUL SYLLABLE SEN}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 53944 (\N{HANGUL SYLLABLE TEU}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50689 (\N{HANGUL SYLLABLE YEONG}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 44397 (\N{HANGUL SYLLABLE GUG}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49892 (\N{HANGUL SYLLABLE SIL}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 50629 (\N{HANGUL SYLLABLE EOB}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 47456 (\N{HANGUL SYLLABLE RYUL}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 54140 (\N{HANGUL SYLLABLE PEO}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 49468 (\N{HANGUL SYLLABLE SEN}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 53944 (\N{HANGUL SYLLABLE TEU}) missing from font(s) STIXGeneral.
fig.canvas.print_figure(bytes_io, **kw)
그리고 매우 유용한 기능이므로, pandas-datareader를 사용하여 세계은행(World Bank) 데이터에 접근하는 방법도 보겠습니다.
# 세계은행 총 온실가스 배출량 (인당 CO2e 메트릭 톤)
# https://data.worldbank.org/indicator/EN.GHG.ALL.PC.CE.AR5
# 국가 및 지역 코드는 http://api.worldbank.org/v2/country 에서 확인
from pandas_datareader import wb
df = wb.download( # 세계은행으로부터 데이터 다운로드
indicator="EN.GHG.ALL.PC.CE.AR5", # 지표 코드
country=["US", "CHN", "IND", "Z4", "Z7"], # 국가 코드
start=2019, # 시작 연도
end=2019, # 종료 연도
)
df = df.reset_index() # 국가를 인덱스에서 제거
df["country"] = df["country"].apply(lambda x: textwrap.fill(x, 10)) # 긴 이름 줄바꿈
df = df.sort_values("EN.GHG.ALL.PC.CE.AR5") # 재정렬/home/runner/work/python4DS/python4DS/.pixi/envs/default/lib/python3.10/site-packages/pandas_datareader/wb.py:592: UserWarning: Non-standard ISO country codes: Z4, Z7
warnings.warn(
/tmp/ipykernel_6103/4092054377.py:6: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead
df = wb.download( # 세계은행으로부터 데이터 다운로드
(
ggplot(df, aes(x="country", y="EN.ATM.CO2E.PC"))
+ geom_bar(aes(fill="country"), color="black", alpha=0.8, stat="identity")
+ scale_fill_discrete()
+ theme_minimal()
+ theme(legend_position="none")
+ ggsize(600, 400)
+ labs(
subtitle="이산화탄소 (인당 메트릭 톤)",
title="미국이 인당 배출량에서 세계를 선도함",
y="",
)
)때로는 API를 직접 사용하는 것이 편리하며, 예로 OECD API는 직접 접근을 통해 활용할 수 있는 엄청난 복잡성을 가지고 있습니다. OECD API는 JSON과 XML 포맷 모두로 데이터를 제공하며, 우리는 pandasdmx(파이썬 데이터 생태계를 위한 통계 데이터 및 메타데이터 교환(SDMX) 패키지)를 사용하여 XML 포맷 데이터를 가져와 일반적인 pandas 데이터 프레임으로 변환할 것입니다.
OECD API를 사용하는 핵심은 국가, 시간, 리소스 및 시리즈에 대한 수많은 코드를 아는 것입니다. API가 사용하는 코드에 대한 대략적인 안내는 여기에서 찾을 수 있지만 필요한 것을 정확히 찾는 것은 다소 까다로울 수 있습니다. 두 가지 팁은 다음과 같습니다: 1. 찾고 있는 내용이 “QNA”(분기별 국민 계정)와 같이 이름이 지정된 특정 데이터셋에 있다는 것을 안다면, 브라우저에 https://stats.oecd.org/restsdmx/sdmx.ashx/GetDataStructure/QNA/all?format=SDMX-ML를 입력하고 XML 파일을 살펴보세요. 하위 코드와 사용 가능한 국가들을 고를 수 있습니다. 2. https://stats.oecd.org/ 에서 탐색하고 Customise를 사용한 다음 모든 “Use Codes” 상자를 체크하여 탐색 중인 코드 이름을 확인하세요.
이 작업의 예제를 보겠습니다. 2010년 이후 여러 국가의 생산성(시간당 GDP) 데이터를 보고 싶습니다. 생산성 리소스(코드 “PDB_LV”)에 있으며 고용자당 GDP(코드 “T_GDPEMP”)의 USD 현재 가격(코드 “CPC”) 측정을 2010년부터(코드 “startTime=2010”) 원합니다. 생산성 측정이 약간 더 비교 가능할 수 있는 일부 선진국들에 대해 이 데이터를 가져오겠습니다. 아래 주석들은 각 단계에서 일어나는 일을 설명합니다.
import pandasdmx as pdmx
# pdmx에게 OECD 데이터를 원한다고 알림
oecd = pdmx.Request("OECD")
# OECD API에서 지정한 형식으로 요청에 관한 모든 사항 설정
data = oecd.data(
resource_id="PDB_LV",
key="GBR+FRA+CAN+ITA+DEU+JPN+USA.T_GDPEMP.CPC/all?startTime=2010",
).to_pandas()
df = pd.DataFrame(data).reset_index()
df.head()| LOCATION | SUBJECT | MEASURE | TIME_PERIOD | value | |
|---|---|---|---|---|---|
| 0 | CAN | T_GDPEMP | CPC | 2010 | 78848.604088 |
| 1 | CAN | T_GDPEMP | CPC | 2011 | 81422.364748 |
| 2 | CAN | T_GDPEMP | CPC | 2012 | 82663.028058 |
| 3 | CAN | T_GDPEMP | CPC | 2013 | 86368.582158 |
| 4 | CAN | T_GDPEMP | CPC | 2014 | 89617.632446 |
좋습니다, 잘 작동하네요! 아주 깔끔한 형식의 데이터를 얻었습니다.
웹 스크래핑은 브라우저에 표시되도록 의도된 정보를 인터넷에서 긁어오는 방법입니다. 하지만 이는 최후의 수단으로만 사용되어야 하며, 웹사이트의 이용 약관에 의해 허용되는 경우에만 사용해야 합니다.
인터넷에서 데이터를 얻는다면 가능하면 언제나 API를 사용하는 것이 훨씬 좋습니다. 구조화된 방식으로 정보를 가져오는 것이 바로 API가 존재하는 이유이기 때문입니다. API는 또한 자주 바뀔 수 있는 웹사이트보다 더 안정적이어야 합니다. 일반적으로 조직에서 여러분이 자신들의 데이터를 가져가는 것을 환영한다면, 그 목적을 위해 명시적으로 API를 만들었을 것입니다. 웹 스크래핑을 허용하면서도 API가 없는 주요 웹사이트는 거의 없습니다. 이러한 웹사이트의 경우 API가 없다면 스크래핑이 이용 약관에 어긋날 가능성이 큽니다. 그러한 이용 약관은 법적으로 집행 가능할 수 있습니다(국가마다 규칙이 다르며 스크래핑 가능 여부가 모호하지 않은 경우 법적 조언이 정말로 필요합니다).
웹 스크래핑이 좋지 않은 다른 이유들도 있습니다. 예를 들어 과거 데이터(back-run)가 필요한 경우 API를 통해서는 제공되지만 웹페이지에는 표시되지 않을 수 있습니다. (또는 아예 이용 불가능할 수도 있는데, 이 경우 해당 조직에 연락하거나 스냅샷이 찍혀 있을 수도 있는 WaybackMachine을 확인해 보는 것이 최선입니다).
따라서 이 책은 거의 항상 더 나은 해결책이 존재하기 때문에 웹 스크래핑에 대해 다소 부정적입니다. 하지만 그럼에도 불구하고 유용할 때가 있습니다.
스크래핑 상황에 처하게 된다면 그것이 법적으로 허용되는지 반드시 확인하고, 웹사이트의 robots.txt 규칙을 위반하지 않는지도 확인하세요. 이는 거의 모든 웹사이트에 있는 특별한 파일로, (법적 허용을 전제로) 무엇을 크롤링해도 되는지, 로봇이 무엇을 건드려서는 안 되는지를 규정합니다.
파이썬에는 웹 스크래핑과 관련하여 선택할 수 있는 폭이 매우 넓습니다. 사용자 스타일과 필요에 따라 다양한 범위를 커버하는 매우 강력한 5가지 라이브러리가 있습니다: requests, lxml, beautifulsoup, selenium, 그리고 scrapy.
빠르고 간단한 웹 스크래핑을 위해, 제가 주로 사용하는 조합은 웹페이지의 HTML을 가져오기만 하는 requests와, 가져온 HTML 구조를 탐색하고 실제 관심 있는 부분만 뽑아내도록 도와주는 beautifulsoup입니다. HTML뿐만 아니라 자바스크립트를 사용하는 동적 웹페이지의 경우 selenium이 필요할 것입니다. 수천 개의 웹페이지를 효율적으로 긁어오려면 다른 도구들과 연동 가능하고 다중 세션 처리 및 온갖 부가 기능을 갖춘 scrapy를 시도해 볼 수 있습니다… 이는 사실 “웹 스크래핑 프레임워크”입니다.
코딩을 실제로 보는 것은 언제나 도움이 되므로 지금 바로 해보겠지만, 유저 에이전트(user agents), 스크래핑 요청을 ‘정중하게’ 하는 법, 캐싱과 크롤링을 효율적으로 하는 법과 같은 많은 중요한 세부 사항들은 생략할 것임을 유의하세요.
더 좋은 예시 대신, http://aeturrell.com/의 연구 페이지를 스크래핑해 보겠습니다.
url = "http://aeturrell.com/research"
page = requests.get(url)
page.text[:300]'<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head>\n\n<meta charset="utf-8">\n<meta name="generator" content="quarto-1.8.24">\n\n<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">\n\n<meta name="author" content="Arthur Turrell">\n'
좋습니다, 방금 무슨 일이 일어났나요? requests에 웹페이지의 HTML을 가져오라고 요청한 다음 찾은 텍스트의 처음 300자를 출력했습니다.
이제 beautifulsoup를 사용하여 이를 인간이 읽을 수 있는(또는 더 쉽게 읽을 수 있는) 형태로 파싱해 보겠습니다:
soup = BeautifulSoup(page.text, "html.parser")
print(soup.prettify()[60000:60500])/div>
</div>
<div class="project-details-listing">
<ul class="project-links">
<li class="project-link details">
<a class="listing-name" href="../research/chapters/turrell-2025/index.html">
<i class="fa-solid fa-circle-info">
</i>
Full details »
</a>
</li>
<li class="project-link">
<a href="https://doi.org/10.48550/arXiv.2502.03010">
<i class="fa-sol
이제 페이지의 구조가 더 잘 보이고 ‘title’이나 ’link’와 같은 HTML 태그들도 보입니다. 이제 데이터 추출 부분입니다: 모든 텍스트 단락을 뽑아내고 싶다고 가정해 봅시다. beautifulsoup를 사용하여 HTML 구조를 훑으며 단락 태그(’p’)가 있는 부분만 뽑아낼 수 있습니다.
# 모든 단락 가져오기
all_paras = soup.find_all("p")
# 단락 중 하나만 보여주기
all_paras[1]<p>Blundell, Jack, Emma Duchini, Stefania Simion, and Arthur Turrell. "Pay transparency and gender equality." <i>American Economic Journal: Economic Policy</i> (2024). doi: <a href="https://www.aeaweb.org/articles?id=10.1257/pol.20220766&from=f"><code>10.1257/pol.20220766</code></a></p>
이 단락도 나쁘지는 않지만, .text 메서드를 사용하여 HTML 태그를 완전히 제거하면 더 읽기 좋게 만들 수 있습니다:
all_paras[1].text'Blundell, Jack, Emma Duchini, Stefania Simion, and Arthur Turrell. "Pay transparency and gender equality." American Economic Journal: Economic Policy (2024). doi: 10.1257/pol.20220766'
이제 페이지의 대부분에는 관심이 없고 오직 프로젝트 이름들만 가져오고 싶다고 가정해 봅시다. 이를 위해 우리가 관심 있는 요소의 태그 타입(이 경우 ‘div’)과 클래스 타입(이 경우 “project-name”)을 식별해야 합니다. 다음과 같이 수행합니다(과정 중에 깔끔한 텍스트를 보여줍니다):
projects = soup.find_all("div", class_="project-content listing-pub-info")
projects = [x.text.strip() for x in projects]
projects['Blundell, Jack, Emma Duchini, Stefania Simion, and Arthur Turrell. "Pay transparency and gender equality." American Economic Journal: Economic Policy (2024). doi: 10.1257/pol.20220766',
'Botta, Federico, Robin Lovelace, Laura Gilbert, and Arthur Turrell. "Packaging code and data for reproducible research: A case study of journey time statistics." Environment and Planning B: Urban Analytics and City Science (2024): 23998083241267331. doi: 10.1177/23998083241267331',
'Kalamara, Eleni, Arthur Turrell, Chris Redl, George Kapetanios, and Sujit Kapadia. "Making text count: economic forecasting using newspaper text." Journal of Applied Econometrics 37, no. 5 (2022): 896-919. doi: 10.1002/jae.2907',
'Turrell, A., Speigner, B., Copple, D., Djumalieva, J. and Thurgood, J., 2021. Is the UK’s productivity puzzle mostly driven by occupational mismatch? An analysis using big data on job vacancies. Labour Economics, 71, p.102013. doi: 10.1016/j.labeco.2021.102013',
'Haldane, Andrew G., and Arthur E. Turrell. "Drawing on different disciplines: macroeconomic agent-based models." Journal of Evolutionary Economics 29 (2019): 39-66. doi: 10.1007/s00191-018-0557-5',
'Haldane, Andrew G., and Arthur E. Turrell. "An interdisciplinary model for macroeconomics." Oxford Review of Economic Policy 34, no. 1-2 (2018): 219-251. doi: 10.1093/oxrep/grx051',
'Braun-Munzinger, Karen, Z. Liu, and A. E. Turrell. "An agent-based model of corporate bond trading." Quantitative Finance 18, no. 4 (2018): 591-608. doi: 10.1080/14697688.2017.1380310',
'Turrell, A. E., M. Sherlock, and S. J. Rose. "Efficient evaluation of collisional energy transfer terms for plasma particle simulations." Journal of Plasma Physics 82, no. 1 (2016): 905820107. doi: 10.1017/S0022377816000131',
'Turrell, A. E., M. Sherlock, and S. J. Rose. "Ultrafast collisional ion heating by electrostatic shocks." Nature Communications 6, no. 1 (2015): 8905. doi: 10.1038/ncomms9905',
'Turrell, Arthur E., Mark Sherlock, and Steven J. Rose. "Self-consistent inclusion of classical large-angle Coulomb collisions in plasma Monte Carlo simulations." Journal of Computational Physics 299 (2015): 144-155. doi: 10.1016/j.jcp.2015.06.034',
'Turrell, Arthur E., Mark Sherlock, and Steven J. Rose. "A Monte Carlo algorithm for degenerate plasmas." Journal of Computational Physics 249 (2013): 13-21. doi: 10.1016/j.jcp.2013.03.052',
'Turrell, Arthur. "Cutting through Complexity: How Data Science Can Help Policymakers Understand the World." In The Economy as a Complex Evolving System, Part IV. Sante Fe Institute, 2025. doi: 10.48550/arXiv.2502.03010',
'Duchini, Emma, Stefania Simion, and Arthur Turrell. "A Review of the Effects of Pay Transparency." In Oxford Research Encyclopedia of Economics and Finance, Oxford University Press, 2024. doi: 10.1093/acrefore/9780190625979.013.860',
'Turrell, Arthur, Bradley Speigner, Jyldyz Djumalieva, David Copple, and James Thurgood. "6. Transforming Naturally Occurring Text Data into Economic Statistics." In Big Data for Twenty-First-Century Economic Statistics, pp. 173-208. University of Chicago Press, 2022. doi: 10.7208/chicago/9780226801391-008',
'Turrell, Arthur. "Agent-based models: understanding the economy from the bottom up" In Quarterly Bulletin, Q4. Bank of England, 2016.',
'Cohen, Samuel N., Giulia Mantoan, Lars Nesheim, Áureo de Paula, Arthur Turrell, and Lingyi Yang. Nowcasting using regression on signatures arXiv preprint arXiv:2305.10256v2 (2025).',
'Van Dijcke, David, Marcus Buckmann, Arthur Turrell, and Tomas Key. "Vacancy Posting, Firm Balance Sheets, and Pandemic Policy Interventions." Bank of England Staff Working Paper Series 1033 (2022).',
'Draca, Mirko, Emma Duchini, Roland Rathelot, Arthur Turrell, and Giulia Vattuone. Revolution in Progress? The Rise of Remote Work in the UK. University of Warwick, Department of Economics, 2022.',
'Hill, Edward, Marco Bardoscia, and Arthur Turrell. "Solving heterogeneous general equilibrium economic models with deep reinforcement learning." arXiv arXiv:2103.16977 (2021).',
'Turrell, Arthur, James Thurgood, David Copple, Jyldyz Djumalieva, and Bradley Speigner. "Using online job vacancies to understand the UK labour market from the bottom-up." Bank of England Staff Working Papers 742 (2018).']
만세! 원하는 정보를 얻었습니다. 올바른 태그만 알면 됐습니다. 원하는 정보의 태그를 찾는 좋은 팁은 브라우저(예: 구글 크롬)에서 해당 부분을 보고 우클릭한 다음 ’검사(Inspect)’를 누르는 것입니다. 그러면 클릭한 부분의 HTML 요소가 표시됩니다.
이것으로 웹 스크래핑에 대한 아주 아주 짧은 소개를 마칩니다. 마지막으로 한 가지만 더 보겠습니다: 여러 페이지를 반복하는 방법입니다.
“www.codingforeconomists.com”이라는 루트 웹페이지가 있고 “www.codingforeconomists.com/page=1”, “www.codingforeconomists.com/page=2”와 같은 하위 페이지들이 있다고 상상해 봅시다. 각 페이지를 스크래핑하고 관련 데이터를 반환하는 scraper()라는 함수가 있다면, 첫 50페이지에 대해 다음과 같이 반복하여 HTML 문자열을 만들 수 있습니다.
start, stop = 0, 50
root_url = "www.codingforeconomists.com/page="
info_on_pages = [scraper(root_url + str(i)) for i in range(start, stop)]
여기서 다루는 내용은 여기까지이지만, 우리는 이 방대하고 복잡한 주제의 겉면만 겨우 스크래핑했을 뿐임을 기억하세요. 응용 사례를 읽어보고 싶다면, 의심할 여지 없이 세상을 가장 많이 바꾼, 그리고 여러분의 삶에도 수많은 방식으로 영향을 주었을 웹 스크래핑에 관한 논문을 추천하지 않을 수 없습니다: Page, Brin, Motwani, Winograd의 “The PageRank Citation Ranking: Bringing Order to the Web”. 웹 스크래핑에 대한 더 심층적인 예제는 realpython의 튜토리얼을 확인해 보세요.
웹페이지 전체를 스크래핑하고 싶지 않고 페이지 내의 표(table) 데이터만 가져오고 싶을 때가 종종 있습니다. 다행히 pandas 패키지를 사용하여 개별 표를 스크래핑하는 쉬운 방법이 있습니다.
pandas를 사용하여 ’https://webscraper.io/test-sites/tables’에 있는 표에서 데이터를 읽어오겠습니다. 사용할 함수는 read_html()이며, URL을 전달하면 찾은 모든 표의 데이터 프레임 리스트를 반환합니다. 표 리스트를 필터링하고 싶다면 관심 있는 표에만 나타나는 텍스트를 match= 키워드 인수에 사용하세요.
아래 예제는 이 방식이 어떻게 작동하는지 보여줍니다. 웹사이트를 보면 우리가 관심 있는 표에 ‘First Name’ 열이 있는 것을 알 수 있습니다. 따라서 다음과 같이 실행합니다:
df_list = pd.read_html("https://webscraper.io/test-sites/tables", match="First Name")
# 데이터 프레임 리스트에서 첫 번째 항목 추출
df = df_list[0]
df.head()| # | First Name | Last Name | Username | |
|---|---|---|---|---|
| 0 | 1 | Mark | Otto | @mdo |
| 1 | 2 | Jacob | Thornton | @fat |
| 2 | 3 | Larry | the Bird |
그러면 추가적인 사용을 위해 pandas 데이터 프레임에 깔끔하게 로드된 표를 얻게 됩니다.
만약 ‘403’ 에러가 발생한다면, 웹사이트가 여러분이 웹 스크래핑 중임을 감지하여 pandas를 차단했다는 의미입니다. 이는 일부 사람들이 무책임하게 웹 스크래핑을 하거나, 웹사이트에서 데이터를 얻기 위한 다른 선호되는 방법(예: 전체 다운로드 제공(위키피디아 등) 또는 API를 통한 방식)을 제공했기 때문입니다. (정말로 필요하다면 403 에러를 우회하는 방법도 종종 있습니다.)