24 웹 스크래핑
24.1 소개
이 장에서는 rvest를 사용한 웹 스크래핑의 기초를 소개합니다. 웹 스크래핑은 웹페이지에서 데이터를 추출하는 데 매우 유용한 도구입니다. 일부 웹사이트는 API(데이터를 JSON으로 반환하는 구조화된 HTTP 요청 세트)를 제공하며, 이는 Chapter 23 의 기술을 사용하여 처리합니다. 가능하면 API를 사용해야 합니다1. 일반적으로 더 신뢰할 수 있는 데이터를 제공하기 때문입니다. 그러나 불행히도 웹 API 프로그래밍은 이 책의 범위를 벗어납니다. 대신, 사이트가 API를 제공하는지 여부와 관계없이 작동하는 기술인 스크래핑을 가르칩니다.
이 장에서는 HTML의 기초를 다루기 전에 먼저 스크래핑의 윤리와 법적 측면에 대해 논의할 것입니다. 그런 다음 페이지의 특정 요소를 찾기 위한 CSS 선택자의 기초와 rvest 함수를 사용하여 HTML에서 텍스트와 속성의 데이터를 가져와 R로 옮기는 방법을 배울 것입니다. 그 후 두 가지 사례 연구와 동적 웹사이트에 대한 짧은 논의로 마무리하기 전에, 스크래핑하려는 페이지에 어떤 CSS 선택자가 필요한지 파악하는 몇 가지 기술을 논의할 것입니다.
24.1.1 선수 지식
이 장에서는 rvest에서 제공하는 도구에 초점을 맞출 것입니다. rvest는 tidyverse의 멤버이지만 핵심 멤버는 아니므로 명시적으로 로드해야 합니다. 스크래핑한 데이터를 다루는 데 일반적으로 유용하므로 전체 tidyverse도 로드할 것입니다.
24.2 스크래핑 윤리 및 법적 측면
웹 스크래핑을 수행하는 데 필요한 코드를 논의하기 전에, 그렇게 하는 것이 합법적이고 윤리적인지 여부에 대해 이야기해야 합니다. 전반적으로 이 두 가지와 관련하여 상황은 복잡합니다.
법적 측면은 거주 지역에 따라 크게 달라집니다. 그러나 일반적인 원칙으로서 데이터가 공개적이고, 비개인적이며, 사실적이라면 괜찮을 가능성이 높습니다2. 이 세 가지 요소는 아래에서 논의할 것처럼 사이트의 이용 약관, 개인 식별 정보 및 저작권과 연결되어 있기 때문에 중요합니다.
데이터가 공개적이지 않거나, 비개인적이지 않거나, 사실적이지 않거나, 특히 돈을 벌기 위해 데이터를 스크래핑하는 경우 변호사와 상담해야 합니다. 어떤 경우이든 스크래핑하는 페이지를 호스팅하는 서버의 리소스를 존중해야 합니다. 가장 중요한 것은 많은 페이지를 스크래핑하는 경우 각 요청 사이에 잠시 기다려야 한다는 것입니다. 그렇게 하는 한 가지 쉬운 방법은 Dmytro Perepolkin이 만든 polite 패키지를 사용하는 것입니다. 요청 사이에 자동으로 일시 중지하고 결과를 캐시하므로 동일한 페이지를 두 번 요청하지 않습니다.
24.2.1 서비스 약관
자세히 살펴보면 많은 웹사이트의 페이지 어딘가에 “이용 약관” 또는 “서비스 약관” 링크가 포함되어 있음을 알 수 있으며, 해당 페이지를 자세히 읽어보면 사이트가 웹 스크래핑을 명시적으로 금지하고 있음을 발견하는 경우가 많습니다. 이러한 페이지는 회사가 매우 광범위한 주장을 하는 법적 토지 강탈이 되는 경향이 있습니다. 가능한 한 이러한 서비스 약관을 존중하는 것이 예의이지만, 모든 주장을 곧이곧대로 받아들이지는 마세요.
미국 법원은 웹사이트 하단에 서비스 약관을 배치하는 것만으로는 귀하가 그에 구속되기에 충분하지 않다고 일반적으로 판결했습니다(예: HiQ Labs v. LinkedIn). 일반적으로 서비스 약관에 구속되려면 계정 생성이나 확인란 선택과 같은 명시적인 조치를 취해야 합니다. 이것이 데이터가 공개적인지 여부가 중요한 이유입니다. 액세스하는 데 계정이 필요하지 않다면 서비스 약관에 구속될 가능성이 낮습니다. 그러나 법원이 명시적으로 동의하지 않더라도 서비스 약관을 집행할 수 있다고 판결한 유럽에서는 상황이 다소 다릅니다.
24.2.2 개인 식별 정보
데이터가 공개적이더라도 이름, 이메일 주소, 전화번호, 생년월일 등과 같은 개인 식별 정보를 스크래핑할 때는 매우 주의해야 합니다. 유럽은 이러한 데이터의 수집 또는 저장에 대해 특히 엄격한 법률(GDPR)을 가지고 있으며, 거주 지역에 관계없이 윤리적 수렁에 빠질 가능성이 높습니다. 예를 들어, 2016년에 한 연구 그룹은 데이팅 사이트인 OkCupid에서 약 70,000명의 공개 프로필 정보(예: 사용자 이름, 나이, 성별, 위치 등)를 스크래핑하여 익명화 시도 없이 이 데이터를 공개적으로 출시했습니다. 연구원들은 데이터가 이미 공개되어 있었기 때문에 이에 문제가 없다고 느꼈지만, 이 작업은 데이터셋에 정보가 공개된 사용자의 식별 가능성에 대한 윤리적 우려로 인해 널리 비난받았습니다. 작업에 개인 식별 정보 스크래핑이 포함된 경우 OkCupid 연구3와 개인 식별 정보의 획득 및 출시와 관련된 의심스러운 연구 윤리를 가진 유사한 연구에 대해 읽어보는 것을 강력히 추천합니다.
24.2.3 저작권
마지막으로 저작권법에 대해서도 걱정해야 합니다. 저작권법은 복잡하지만 무엇이 보호되는지 정확히 설명하는 미국 법률을 살펴볼 가치가 있습니다: “[…] 모든 유형의 표현 매체에 고정된 저자의 독창적인 저작물, []”. 그런 다음 문학 저작물, 음악 저작물, 영화 등과 같이 적용되는 특정 범주를 설명합니다. 저작권 보호에서 눈에 띄게 빠진 것은 데이터입니다. 즉, 스크래핑을 사실(facts)로 제한하는 한 저작권 보호는 적용되지 않습니다. (하지만 유럽에는 데이터베이스를 보호하는 별도의 “sui generis” 권리가 있음에 유의하세요.)
간단한 예로 미국에서는 재료 목록과 지침은 저작권으로 보호될 수 없으므로 저작권은 레시피를 보호하는 데 사용될 수 없습니다. 하지만 그 레시피 목록에 실질적인 새로운 문학적 내용이 수반된다면 그것은 저작권으로 보호될 수 있습니다. 이것이 인터넷에서 레시피를 찾을 때 항상 앞에 그렇게 많은 내용이 있는 이유입니다.
독창적인 콘텐츠(텍스트나 이미지 등)를 스크래핑해야 하는 경우에도 여전히 공정 이용 원칙(doctrine of fair use)에 따라 보호받을 수 있습니다. 공정 이용은 고정된 규칙은 아니지만 여러 요소를 따져봅니다. 연구 또는 비상업적 목적으로 데이터를 수집하고 스크래핑하는 내용을 필요한 것만으로 제한하는 경우 적용될 가능성이 더 높습니다.
24.3 HTML 기초
웹페이지를 스크래핑하려면 먼저 웹페이지를 설명하는 언어인 HTML에 대해 조금 이해해야 합니다. HTML은 HyperText Markup Language의 약자로 다음과 같이 생겼습니다:
<html>
<head>
<title>페이지 제목</title>
</head>
<body>
<h1 id='first'>제목</h1>
<p>일부 텍스트 및 <b>일부 굵은 텍스트.</b></p>
<img src='myimg.png' width='100' height='100'>
</body>HTML은 시작 태그(예: <tag>), 선택적 속성(id='first'), 종료 태그4(예: </tag>) 및 내용(시작 태그와 종료 태그 사이의 모든 것)으로 구성된 요소(elements) 에 의해 형성된 계층 구조를 가집니다.
<와 >는 시작 및 종료 태그에 사용되므로 직접 작성할 수 없습니다. 대신 HTML 이스케이프인 >(보다 큼)와 <(보다 작음)를 사용해야 합니다. 그리고 이러한 이스케이프는 &를 사용하므로 리터럴 앰퍼샌드를 원한다면 &로 이스케이프해야 합니다. 다양한 가능한 HTML 이스케이프가 있지만 rvest가 자동으로 처리해 주므로 너무 걱정할 필요는 없습니다.
웹 스크래핑이 가능한 이유는 스크래핑하려는 데이터가 포함된 대부분의 페이지가 일반적으로 일관된 구조를 가지고 있기 때문입니다.
24.3.1 요소(Elements)
100개 이상의 HTML 요소가 있습니다. 가장 중요한 것들은 다음과 같습니다:
모든 HTML 페이지는
<html>요소 내에 있어야 하며 두 개의 자식을 가져야 합니다: 페이지 제목과 같은 문서 메타데이터를 포함하는<head>와 브라우저에서 보는 내용을 포함하는<body>입니다.<h1>(제목 1),<section>(섹션),<p>(단락) 및<ol>(순서가 있는 목록)과 같은 블록 태그는 페이지의 전반적인 구조를 형성합니다.<b>(굵게),<i>(기울임꼴) 및<a>(링크)와 같은 인라인 태그는 블록 태그 내부의 텍스트 형식을 지정합니다.
처음 보는 태그를 만나면 약간의 구글링으로 그 역할을 찾을 수 있습니다. 시작하기 좋은 또 다른 장소는 웹 프로그래밍의 거의 모든 측면을 설명하는 MDN 웹 문서입니다.
대부분의 요소는 시작 태그와 종료 태그 사이에 내용을 가질 수 있습니다. 이 내용은 텍스트이거나 더 많은 요소일 수 있습니다. 예를 들어 다음 HTML은 한 단어가 굵게 표시된 텍스트 단락을 포함합니다.
<p>
안녕! 내 <b>이름</b>은 해들리야.
</p>
자식(children) 은 포함된 요소이므로 위의 <p> 요소는 하나의 자식인 <b> 요소를 가집니다. <b> 요소는 자식이 없지만 내용(텍스트 “이름”)을 가집니다.
24.3.2 속성(Attributes)
태그는 name1='value1' name2='value2'와 같이 생긴 이름이 지정된 속성을 가질 수 있습니다. 가장 중요한 두 가지 속성은 id와 class로, CSS(Cascading Style Sheets)와 함께 사용되어 페이지의 시각적 모양을 제어합니다. 이들은 페이지에서 데이터를 스크래핑할 때 종종 유용합니다. 속성은 링크의 목적지(<a> 요소의 href 속성)와 이미지의 소스(<img> 요소의 src 속성)를 기록하는 데에도 사용됩니다.
24.4 데이터 추출
스크래핑을 시작하려면 스크래핑하려는 페이지의 URL이 필요하며 이는 일반적으로 웹 브라우저에서 복사할 수 있습니다. 그런 다음 read_html()을 사용하여 해당 페이지의 HTML을 R로 읽어들여야 합니다. 이는 xml_document5 객체를 반환하며, 이를 rvest 함수를 사용하여 조작하게 됩니다:
html <- read_html("http://rvest.tidyverse.org/")
html
#> {html_document}
#> <html lang="en">
#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UT ...
#> [2] <body>\n <a href="#container" class="visually-hidden-focusable">Ski ...rvest에는 HTML을 인라인으로 작성할 수 있게 해주는 함수도 포함되어 있습니다. 이 장에서 다양한 rvest 함수가 간단한 예제와 함께 어떻게 작동하는지 가르치기 위해 이 함수를 자주 사용할 것입니다.
html <- minimal_html("
<p>이것은 단락입니다</p>
<ul>
<li>이것은 글머리 기호 목록입니다</li>
</ul>
")
html
#> {html_document}
#> <html>
#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UT ...
#> [2] <body>\n<p>이것은 단락입니다</p>\n <ul>\n<li>이것은 글머리 기호 목록입니다</li>\n </ul>\n ...이제 R에 HTML이 있으므로 관심 있는 데이터를 추출할 차례입니다. 먼저 관심 있는 요소를 식별할 수 있게 해주는 CSS 선택자와 거기서 데이터를 추출하는 데 사용할 수 있는 rvest 함수에 대해 배우게 될 것입니다. 그런 다음 몇 가지 특수 도구가 있는 HTML 표에 대해 짧게 다룰 것입니다.
24.4.1 요소 찾기
CSS는 Cascading Style Sheets의 약자로 HTML 문서의 시각적 스타일을 정의하기 위한 도구입니다. CSS에는 CSS 선택자(selectors) 라고 불리는 페이지의 요소를 선택하기 위한 미니어처 언어가 포함되어 있습니다. CSS 선택자는 HTML 요소를 찾기 위한 패턴을 정의하며, 추출하려는 요소를 설명하는 간결한 방법을 제공하므로 스크래핑에 유용합니다.
Section 24.5 에서 CSS 선택자에 대해 더 자세히 다루겠지만, 다행히 다음 세 가지만으로도 많은 것을 할 수 있습니다:
p는 모든<p>요소를 선택합니다..title은class가 “title”인 모든 요소를 선택합니다.#title은id속성이 “title”과 같은 요소를 선택합니다. id 속성은 문서 내에서 고유해야 하므로 이는 항상 단일 요소만 선택합니다.
간단한 예제로 이 선택자들을 시도해 봅시다:
html <- minimal_html("
<h1>이것은 제목입니다</h1>
<p id='first'>이것은 단락입니다</p>
<p class='important'>이것은 중요한 단락입니다</p>
")html_elements()를 사용하여 선택자와 일치하는 모든 요소를 찾으세요:
html |> html_elements("p")
#> {xml_nodeset (2)}
#> [1] <p id="first">이것은 단락입니다</p>
#> [2] <p class="important">이것은 중요한 단락입니다</p>
html |> html_elements(".important")
#> {xml_nodeset (1)}
#> [1] <p class="important">이것은 중요한 단락입니다</p>
html |> html_elements("#first")
#> {xml_nodeset (1)}
#> [1] <p id="first">이것은 단락입니다</p>또 다른 중요한 함수는 html_element()로, 항상 입력과 동일한 수의 출력을 반환합니다. 전체 문서에 적용하면 첫 번째 일치 항목을 제공합니다:
html |> html_element("p")
#> {html_node}
#> <p id="first">일치하는 요소가 없는 선택자를 사용할 때 html_element()와 html_elements() 사이에는 중요한 차이점이 있습니다. html_elements()는 길이가 0인 벡터를 반환하는 반면, html_element()는 결측값을 반환합니다. 이것은 곧 중요해질 것입니다.
html |> html_elements("b")
#> {xml_nodeset (0)}
html |> html_element("b")
#> {xml_missing}
#> <NA>24.4.2 중첩 선택
대부분의 경우 html_elements()와 html_element()를 함께 사용하게 되는데, 일반적으로 관측값이 될 요소를 식별하기 위해 html_elements()를 사용한 다음 변수가 될 요소를 찾기 위해 html_element()를 사용합니다. 간단한 예제를 사용하여 이를 실제로 살펴보겠습니다. 여기에는 스타워즈(StarWars)의 네 캐릭터에 대한 정보가 포함된 순서가 없는 목록(<ul>)이 있습니다:
html <- minimal_html("
<ul>
<li><b>C-3PO</b>는 <span class='weight'>167 kg</span>인 <i>드로이드</i>입니다</li>
<li><b>R4-P17</b>은 <i>드로이드</i>입니다</li>
<li><b>R2-D2</b>는 <span class='weight'>96 kg</span>인 <i>드로이드</i>입니다</li>
<li><b>Yoda</b>는 <span class='weight'>66 kg</span>입니다</li>
</ul>
")html_elements()를 사용하여 각 요소가 서로 다른 캐릭터에 해당하는 벡터를 만들 수 있습니다:
characters <- html |> html_elements("li")
characters
#> {xml_nodeset (4)}
#> [1] <li>\n<b>C-3PO</b>는 <span class="weight">167 kg</span>인 <i>드로이드</i>입니다 ...
#> [2] <li>\n<b>R4-P17</b>은 <i>드로이드</i>입니다</li>
#> [3] <li>\n<b>R2-D2</b>는 <span class="weight">96 kg</span>인 <i>드로이드</i>입니다< ...
#> [4] <li>\n<b>Yoda</b>는 <span class="weight">66 kg</span>입니다</li>각 캐릭터의 이름을 추출하기 위해 html_element()를 사용합니다. html_elements()의 출력에 적용될 때 요소당 하나의 응답을 반환하는 것이 보장되기 때문입니다:
characters |> html_element("b")
#> {xml_nodeset (4)}
#> [1] <b>C-3PO</b>
#> [2] <b>R4-P17</b>
#> [3] <b>R2-D2</b>
#> [4] <b>Yoda</b>html_element()와 html_elements()의 구별은 이름에 대해서는 중요하지 않지만 몸무게에 대해서는 중요합니다. 몸무게 <span>이 없더라도 각 캐릭터에 대해 하나의 몸무게를 얻고 싶습니다. 그것이 html_element()가 하는 일입니다:
characters |> html_element(".weight")
#> {xml_nodeset (4)}
#> [1] <span class="weight">167 kg</span>
#> [2] NA
#> [3] <span class="weight">96 kg</span>
#> [4] <span class="weight">66 kg</span>html_elements()는 characters의 자식인 모든 몸무게 <span>을 찾습니다. 이들 중 세 개만 있으므로 이름과 몸무게 사이의 연결을 잃게 됩니다:
characters |> html_elements(".weight")
#> {xml_nodeset (3)}
#> [1] <span class="weight">167 kg</span>
#> [2] <span class="weight">96 kg</span>
#> [3] <span class="weight">66 kg</span>이제 관심 있는 요소를 선택했으므로 텍스트 내용이나 일부 속성에서 데이터를 추출해야 합니다.
24.4.3 텍스트와 속성
html_text2()6는 HTML 요소의 일반 텍스트 내용을 추출합니다:
characters |>
html_element("b") |>
html_text2()
#> [1] "C-3PO" "R4-P17" "R2-D2" "Yoda"
characters |>
html_element(".weight") |>
html_text2()
#> [1] "167 kg" NA "96 kg" "66 kg"모든 이스케이프가 자동으로 처리된다는 점에 유의하세요. HTML 이스케이프는 소스 HTML에서만 볼 수 있으며 rvest에서 반환된 데이터에서는 볼 수 없습니다.
html_attr()는 속성에서 데이터를 추출합니다:
html <- minimal_html("
<p><a href='https://en.wikipedia.org/wiki/Cat'>고양이</a></p>
<p><a href='https://en.wikipedia.org/wiki/Dog'>개</a></p>
")
html |>
html_elements("p") |>
html_element("a") |>
html_attr("href")
#> [1] "https://en.wikipedia.org/wiki/Cat" "https://en.wikipedia.org/wiki/Dog"html_attr()는 항상 문자열을 반환하므로 숫자나 날짜를 추출하는 경우 사후 처리를 좀 해야 합니다.
24.4.4 표(Tables)
운이 좋다면 데이터가 이미 HTML 표에 저장되어 있을 것이고, 단순히 그 표에서 읽어오기만 하면 될 것입니다. 브라우저에서 표를 인식하는 것은 대개 간단합니다: 행과 열의 직사각형 구조를 가질 것이며, Excel과 같은 도구에 복사해서 붙여넣을 수 있습니다.
HTML 표는 4개의 주요 요소로 구성됩니다: <table>, <tr>(표 행), <th>(표 머리글) 및 <td>(표 데이터). 여기에 두 개의 열과 세 개의 행이 있는 간단한 HTML 표가 있습니다:
html <- minimal_html("
<table class='mytable'>
<tr><th>x</th> <th>y</th></tr>
<tr><td>1.5</td> <td>2.7</td></tr>
<tr><td>4.9</td> <td>1.3</td></tr>
<tr><td>7.2</td> <td>8.1</td></tr>
</table>
")rvest는 이런 종류의 데이터를 읽는 방법을 아는 함수인 html_table()을 제공합니다. 페이지에서 찾은 각 표에 대해 하나의 티블을 포함하는 리스트를 반환합니다. 추출하려는 표를 식별하기 위해 html_element()를 사용하세요:
html |>
html_element(".mytable") |>
html_table()
#> # A tibble: 3 × 2
#> x y
#> <dbl> <dbl>
#> 1 1.5 2.7
#> 2 4.9 1.3
#> 3 7.2 8.1x와 y가 자동으로 숫자로 변환된 것을 주목하세요. 이 자동 변환이 항상 작동하는 것은 아니므로 더 복잡한 시나리오에서는 convert = FALSE로 이를 끄고 직접 변환을 수행하고 싶을 수 있습니다.
24.5 올바른 선택자 찾기
데이터에 필요한 선택자를 파악하는 것이 일반적으로 문제의 가장 어려운 부분입니다. 구체적(즉, 관심 없는 것은 선택하지 않음)이면서 민감한(즉, 관심 있는 모든 것을 선택함) 선택자를 찾기 위해 종종 실험을 좀 해야 합니다. 많은 시행착오는 프로세스의 정상적인 부분입니다! 이 프로세스를 돕기 위해 사용할 수 있는 두 가지 주요 도구가 있습니다: SelectorGadget과 브라우저의 개발자 도구입니다.
SelectorGadget은 제공한 긍정적 예제와 부정적 예제를 기반으로 CSS 선택자를 자동으로 생성하는 자바스크립트 북마클릿입니다. 항상 작동하는 것은 아니지만 작동할 때는 마법 같습니다! https://rvest.tidyverse.org/articles/selectorgadget.html을 읽거나 Mine의 비디오 https://www.youtube.com/watch?v=PetWV5g1Xsc를 시청하여 SelectorGadget을 설치하고 사용하는 방법을 배울 수 있습니다.
모든 현대 브라우저에는 개발자를 위한 툴킷이 함께 제공되지만, 일반 브라우저가 아니더라도 크롬(Chrome)을 권장합니다: 웹 개발자 도구가 가장 뛰어나고 즉시 사용할 수 있기 때문입니다. 페이지의 요소를 마우스 오른쪽 버튼으로 클릭하고 검사(Inspect)를 클릭합니다. 그러면 방금 클릭한 요소를 중심으로 전체 HTML 페이지의 확장 가능한 뷰가 열립니다. 이를 사용하여 페이지를 탐색하고 어떤 선택자가 작동할지 감을 잡을 수 있습니다. class와 id 속성은 종종 페이지의 시각적 구조를 형성하는 데 사용되므로 찾고 있는 데이터를 추출하기 위한 좋은 도구가 되므로 특히 주의를 기울이세요.
요소(Elements) 뷰 내에서 요소를 마우스 오른쪽 버튼으로 클릭하고 Copy as Selector를 선택하여 관심 있는 요소를 고유하게 식별할 선택자를 생성할 수도 있습니다.
SelectorGadget이나 크롬 개발자 도구가 이해할 수 없는 CSS 선택자를 생성했다면 CSS 선택자를 평범한 영어로 번역해주는 Selectors Explained를 시도해 보세요. 이 작업을 자주 하게 된다면 일반적으로 CSS 선택자에 대해 더 많이 배우고 싶을 것입니다. 재미있는 CSS dinner 튜토리얼부터 시작하여 MDN 웹 문서를 참조하는 것을 추천합니다.
24.6 종합하기
웹사이트 몇 군데를 스크래핑하기 위해 이 모든 것을 종합해 봅시다. 이 예제들이 실행할 때 더 이상 작동하지 않을 위험이 있습니다. 이것이 웹 스크래핑의 근본적인 과제입니다. 사이트 구조가 변경되면 스크래핑 코드를 변경해야 합니다.
24.6.1 스타워즈(StarWars)
rvest는 vignette("starwars")에 매우 간단한 예제를 포함하고 있습니다. 이것은 최소한의 HTML을 가진 간단한 페이지이므로 시작하기에 좋은 곳입니다. 지금 해당 페이지로 이동하여 “요소 검사”를 사용하여 스타워즈 영화 제목인 제목 중 하나를 검사해 보시길 권장합니다. 키보드나 마우스를 사용하여 HTML 계층 구조를 탐색하고 각 영화에서 사용되는 공유 구조에 대한 감을 잡을 수 있는지 확인해 보세요.
각 영화가 다음과 같은 공유 구조를 가지고 있음을 볼 수 있을 것입니다:
<section>
<h2 data-id="1">The Phantom Menace</h2>
<p>Released: 1999-05-19</p>
<p>Director: <span class="director">George Lucas</span></p>
<div class="crawl">
<p>...</p>
<p>...</p>
<p>...</p>
</div>
</section>우리의 목표는 이 데이터를 title, year, director, intro 변수를 가진 7행 데이터 프레임으로 변환하는 것입니다. 먼저 HTML을 읽고 모든 <section> 요소를 추출하는 것으로 시작하겠습니다:
url <- "https://rvest.tidyverse.org/articles/starwars.html"
html <- read_html(url)
section <- html |> html_elements("section")
section
#> {xml_nodeset (7)}
#> [1] <section><h2 data-id="1">\nThe Phantom Menace\n</h2>\n<p>\nReleased: 1 ...
#> [2] <section><h2 data-id="2">\nAttack of the Clones\n</h2>\n<p>\nReleased: ...
#> [3] <section><h2 data-id="3">\nRevenge of the Sith\n</h2>\n<p>\nReleased: ...
#> [4] <section><h2 data-id="4">\nA New Hope\n</h2>\n<p>\nReleased: 1977-05-2 ...
#> [5] <section><h2 data-id="5">\nThe Empire Strikes Back\n</h2>\n<p>\nReleas ...
#> [6] <section><h2 data-id="6">\nReturn of the Jedi\n</h2>\n<p>\nReleased: 1 ...
#> [7] <section><h2 data-id="7">\nThe Force Awakens\n</h2>\n<p>\nReleased: 20 ...이 코드는 해당 페이지에서 찾은 7편의 영화와 일치하는 7개의 요소를 검색하므로 section을 선택자로 사용하는 것이 좋음을 시사합니다. 데이터는 항상 텍스트에서 발견되므로 개별 요소를 추출하는 것은 간단합니다. 올바른 선택자를 찾기만 하면 됩니다:
section |> html_element("h2") |> html_text2()
#> [1] "The Phantom Menace" "Attack of the Clones"
#> [3] "Revenge of the Sith" "A New Hope"
#> [5] "The Empire Strikes Back" "Return of the Jedi"
#> [7] "The Force Awakens"
section |> html_element(".director") |> html_text2()
#> [1] "George Lucas" "George Lucas" "George Lucas"
#> [4] "George Lucas" "Irvin Kershner" "Richard Marquand"
#> [7] "J. J. Abrams"각 구성 요소에 대해 이 작업을 완료하면 모든 결과를 티블로 묶을 수 있습니다:
tibble(
title = section |>
html_element("h2") |>
html_text2(),
released = section |>
html_element("p") |>
html_text2() |>
str_remove("Released: ") |>
parse_date(),
director = section |>
html_element(".director") |>
html_text2(),
intro = section |>
html_element(".crawl") |>
html_text2()
)
#> # A tibble: 7 × 4
#> title released director intro
#> <chr> <date> <chr> <chr>
#> 1 The Phantom Menace 1999-05-19 George Lucas "Turmoil has engulfed …
#> 2 Attack of the Clones 2002-05-16 George Lucas "There is unrest in th…
#> 3 Revenge of the Sith 2005-05-19 George Lucas "War! The Republic is …
#> 4 A New Hope 1977-05-25 George Lucas "It is a period of civ…
#> 5 The Empire Strikes Back 1980-05-17 Irvin Kershner "It is a dark time for…
#> 6 Return of the Jedi 1983-05-25 Richard Marquand "Luke Skywalker has re…
#> # ℹ 1 more row나중에 분석에서 쉽게 사용할 수 있는 변수를 얻기 위해 released에 대해 처리를 조금 더 했습니다.
24.6.2 IMDB 인기 영화
다음 과제로 인터넷 영화 데이터베이스(IMDb)에서 상위 250개 영화를 추출하는 조금 더 까다로운 작업을 처리해 보겠습니다. 우리가 이 장을 썼을 때 페이지는 Figure 24.1 처럼 보였습니다.
이 데이터는 명확한 표 구조를 가지고 있으므로 html_table()로 시작할 가치가 있습니다:
url <- "https://web.archive.org/web/20220201012049/https://www.imdb.com/chart/top/"
html <- read_html(url)
table <- html |>
html_element("table") |>
html_table()
table
#> # A tibble: 250 × 5
#> `` `Rank & Title` `IMDb Rating` `Your Rating` ``
#> <lgl> <chr> <dbl> <chr> <lgl>
#> 1 NA "1.\n The Shawshank Redempt… 9.2 "12345678910\n… NA
#> 2 NA "2.\n The Godfather\n … 9.1 "12345678910\n… NA
#> 3 NA "3.\n The Godfather: Part I… 9 "12345678910\n… NA
#> 4 NA "4.\n The Dark Knight\n … 9 "12345678910\n… NA
#> 5 NA "5.\n 12 Angry Men\n … 8.9 "12345678910\n… NA
#> 6 NA "6.\n Schindler's List\n … 8.9 "12345678910\n… NA
#> # ℹ 244 more rows여기에는 몇 개의 빈 열이 포함되어 있지만 전반적으로 표의 정보를 잘 캡처합니다. 그러나 사용하기 쉽게 만들려면 처리를 좀 더 해야 합니다. 먼저 열의 이름을 작업하기 쉽게 바꾸고 순위(rank)와 제목(title)에 있는 불필요한 공백을 제거하겠습니다. 한 단계에서 이 두 열만 선택하고 이름을 바꾸기 위해 select()(rename() 대신)를 사용하여 이 작업을 수행할 것입니다. 그런 다음 줄바꿈과 추가 공백을 제거한 다음, separate_wider_regex()(Section 15.3.4 에서 배움)를 적용하여 제목, 연도 및 순위를 자체 변수로 뽑아내겠습니다.
ratings <- table |>
select(
rank_title_year = `Rank & Title`,
rating = `IMDb Rating`
) |>
mutate(
rank_title_year = str_replace_all(rank_title_year, "\n +", " ")
) |>
separate_wider_regex(
rank_title_year,
patterns = c(
rank = "\\d+", "\\. ",
title = ".+", " +\\(",
year = "\\d+", "\\)"
)
)
ratings
#> # A tibble: 250 × 4
#> rank title year rating
#> <chr> <chr> <chr> <dbl>
#> 1 1 The Shawshank Redemption 1994 9.2
#> 2 2 The Godfather 1972 9.1
#> 3 3 The Godfather: Part II 1974 9
#> 4 4 The Dark Knight 2008 9
#> 5 5 12 Angry Men 1957 8.9
#> 6 6 Schindler's List 1993 8.9
#> # ℹ 244 more rows대부분의 데이터가 표 셀에서 나오는 경우에도 여전히 원시 HTML을 살펴볼 가치가 있습니다. 그렇게 하면 속성 중 하나를 사용하여 약간의 추가 데이터를 더할 수 있다는 것을 발견하게 될 것입니다. 이것이 페이지 소스를 탐색하는 데 시간을 조금 투자할 가치가 있는 이유 중 하나입니다. 추가 데이터를 찾거나 약간 더 쉬운 파싱 경로를 찾을 수도 있습니다.
html |>
html_elements("td strong") |>
head() |>
html_attr("title")
#> [1] "9.2 based on 2,536,415 user ratings"
#> [2] "9.1 based on 1,745,675 user ratings"
#> [3] "9.0 based on 1,211,032 user ratings"
#> [4] "9.0 based on 2,486,931 user ratings"
#> [5] "8.9 based on 749,563 user ratings"
#> [6] "8.9 based on 1,295,705 user ratings"이를 표 형식의 데이터와 결합하고 다시 separate_wider_regex()를 적용하여 관심 있는 데이터 조각을 추출할 수 있습니다:
ratings |>
mutate(
rating_n = html |> html_elements("td strong") |> html_attr("title")
) |>
separate_wider_regex(
rating_n,
patterns = c(
"[0-9.]+ based on ",
number = "[0-9,]+",
" user ratings"
)
) |>
mutate(
number = parse_number(number)
)
#> # A tibble: 250 × 5
#> rank title year rating number
#> <chr> <chr> <chr> <dbl> <dbl>
#> 1 1 The Shawshank Redemption 1994 9.2 2536415
#> 2 2 The Godfather 1972 9.1 1745675
#> 3 3 The Godfather: Part II 1974 9 1211032
#> 4 4 The Dark Knight 2008 9 2486931
#> 5 5 12 Angry Men 1957 8.9 749563
#> 6 6 Schindler's List 1993 8.9 1295705
#> # ℹ 244 more rows24.7 동적 사이트
지금까지는 html_elements()가 브라우저에서 보는 내용을 반환하는 웹사이트에 집중하여 반환된 내용을 파싱하는 방법과 해당 정보를 깔끔한 데이터 프레임으로 구성하는 방법을 논의했습니다. 그러나 때때로 html_elements()와 친구들이 브라우저에서 보는 것과 전혀 다른 내용을 반환하는 사이트를 만나게 될 것입니다. 많은 경우 자바스크립트로 페이지 내용을 동적으로 생성하는 웹사이트를 스크래핑하려고 하기 때문입니다. rvest는 원시 HTML을 다운로드하고 자바스크립트를 실행하지 않기 때문에 현재는 이런 방식이 작동하지 않습니다.
이런 유형의 사이트를 스크래핑하는 것은 여전히 가능하지만 rvest는 모든 자바스크립트 실행을 포함하여 웹 브라우저를 완전히 시뮬레이션하는 더 비용이 많이 드는 프로세스를 사용해야 합니다. 이 기능은 이 글을 쓰는 시점에는 사용할 수 없지만 우리가 적극적으로 작업 중인 기능이며 여러분이 이 글을 읽을 때쯤이면 사용할 수 있을 것입니다. 배경에서 실제로 크롬(Chrome) 브라우저를 실행하는 chromote 패키지를 사용하며, 사람이 텍스트를 입력하고 버튼을 클릭하는 것과 같이 사이트와 상호 작용할 수 있는 추가 도구를 제공합니다. 자세한 내용은 rvest 웹사이트를 확인하세요.
24.8 요약
이 장에서는 웹페이지에서 데이터를 스크래핑하는 이유와 이유가 아닌 것, 그리고 방법에 대해 배웠습니다. 먼저 HTML의 기초와 특정 요소를 참조하기 위해 CSS 선택자를 사용하는 방법을 배웠고, 그런 다음 rvest 패키지를 사용하여 HTML에서 R로 데이터를 가져오는 방법을 배웠습니다. 그런 다음 두 가지 사례 연구로 웹 스크래핑을 시연했습니다: rvest 패키지 웹사이트에서 스타워즈 영화에 대한 데이터를 스크래핑하는 간단한 시나리오와 IMDB에서 상위 250개 영화를 스크래핑하는 더 복잡한 시나리오입니다.
웹에서 데이터를 스크래핑하는 기술적 세부 사항은 특히 사이트를 다룰 때 복잡할 수 있지만, 법적 및 윤리적 고려 사항은 훨씬 더 복잡할 수 있습니다. 데이터를 스크래핑하기 전에 이 두 가지 모두에 대해 스스로 교육하는 것이 중요합니다.
이로써 데이터가 있는 곳(스프레드시트, 데이터베이스, JSON 파일 및 웹사이트)에서 R의 깔끔한 형태로 데이터를 가져오는 기술을 배운 책의 가져오기(import) 파트가 끝납니다. 이제 새로운 주제인 프로그래밍 언어로서의 R을 최대한 활용하는 것으로 시선을 돌릴 때입니다.
그리고 많은 인기 있는 API에는 이미 이를 래핑하는 CRAN 패키지가 있으므로 먼저 조사를 조금 해보세요!↩︎
분명히 우리는 변호사가 아니며 이것은 법률 자문이 아닙니다. 하지만 이 주제에 대해 많은 것을 읽은 후 우리가 줄 수 있는 최선의 요약입니다.↩︎
OkCupid 연구에 대한 기사 중 하나가 Wired에 게시되었습니다: https://www.wired.com/2016/05/okcupid-study-reveals-perils-big-data-science.↩︎
여러 태그(
<p>및<li>포함)는 종료 태그가 필요하지 않지만 HTML 구조를 보기가 조금 더 쉽기 때문에 포함하는 것이 가장 좋다고 생각합니다.↩︎rvest는
html_text()도 제공하지만 중첩된 HTML을 텍스트로 변환하는 작업을 더 잘 수행하므로 거의 항상html_text2()를 사용해야 합니다.↩︎