<jupyter_start><jupyter_text>3장 - 시카고 샌드위치 맛집 분석 3-1 웹 데이터를 가져오는 Beautiful soup 익히기앞서 2장까지 우리가 다룬 데이터는 엑셀이든 텍스트든 파일의 형태였다. 이제 3장부터는 데이터를 인터넷에서 직접 얻는 과정을 이야기하려 한다. 1,2 장과는 달리 이번 장은 인터넷에서 웹 페이지의 내용을 가져오는 Beautiful Soup 이라는 모듈의 기초를 익히고, 이번 장의 목표인 시카고 샌드위치 맛집 리스트를 정리하려 한다.<jupyter_code>from bs4 import BeautifulSoup<jupyter_output><empty_output><jupyter_text>BeautifulSoup에서 bs4를 import 한다.<jupyter_code>page = open("/home/jaeyoon89/DataScience/data/03. test_first.html",'r').read()
soup = BeautifulSoup(page, 'html.parser')
print(soup.prettify())<jupyter_output><!DOCTYPE html>
<html>
 <head>
  <title>
   Very Simple HTML Code by PinkWink
  </title>
 </head>
 <body>
  <div>
   <p class="inner-text first-item" id="first">
    Happy PinkWink.
    <a href="http://www.pinkwink.kr" id="pw-link">
     PinkWink
    </a>
   </p>
   <p class="inner-text second-item">
    Happy Data Science.
    <a href="https://www.python.org" id="py-link">
     Python
    </a>
   </p>
  </div>
  <p class="outer-text first-item" id="second">
   <b>
    Data Science is funny.
   </b>
  </p>
  <p class="outer-text">
   <b>
    All I need is Love.
   </b>
  </p>
 </body>
</html><jupyter_text>지금은 파일로 다운받은 html을 읽는 것이기 때문에 open 명령으로 읽기 옵션('r')을 주고 읽으면 된다. 읽은 html 페이지의 내용을 전체 다 보고 싶으면 .prettify() 옵션을 사용하면 들여쓰기가 되어 보기 좋게 나타난다. 지금 위 코드에서 예제로 사용되는 전체 html 코드를 soup라는 변수에 저장했는데, 그 soup라는 변수에서 한 단계 아래에서 포함된 태그들을 알고 싶으면 children 이라는 속성을 사용하면 된다.<jupyter_code>list(soup.children)<jupyter_output><empty_output><jupyter_text>이때 soup는 문서 전체를 저장한 변수이기 때문에 그 안에서 html 태그에 접속하고 싶다면,<jupyter_code>html = list(soup.children)[2]
html<jupyter_output><empty_output><jupyter_text>그러면 위와 같은 결과를 얻게 된다. 다시 html의 children을 조사해보면<jupyter_code>list(html.children)<jupyter_output><empty_output><jupyter_text>이렇게 나타난다. 우리가 본문으로 흔히 보게 되는 부분이 body 태그의 내용이다.<jupyter_code>body = list(html.children)[3]
body<jupyter_output><empty_output><jupyter_text>이렇게 children과 parent를 이용해서 태그를 조사할 수 있고 그냥 한 번에 나타낼 수도 있다.<jupyter_code>soup.body<jupyter_output><empty_output><jupyter_text>이렇게 찾을 수도 있다.<jupyter_code>list(body.children)<jupyter_output><empty_output><jupyter_text>또한 body 태그 안에 children의 리스트를 확인할 수 있다. 리스트 자료형에 대해서는 뒤에서 다시 다루자. 지금은 그냥 배열 정도로 생각하고 넘어가자. 위 코드에서 접근한 대로 단계별로 접근하고 다시 그 구조를 코드 속에 담아두는 것은 체계적으로 생각하고 쉽게 접근할 수 있는 장점이 있지만,  복잡하고 큰 크기의 페이지를 접근하는 것에는 쉽지 않다. 만약 접근해야 할 태그를 알고 있다면 find나 find_all 명령을 많이 사용한다.<jupyter_code>soup.find_all('p')<jupyter_output><empty_output><jupyter_text>위와 같이 모든 p 태그를 찾는 것이다. 물론 하나만 찾을 때는 find 명령을 사용한다.<jupyter_code>soup.find('p')<jupyter_output><empty_output><jupyter_text>이렇게 사용하면 제일 첫 번째 p태그를 찾아준다.<jupyter_code>soup.find_all('p', class_='outer-text')<jupyter_output><empty_output><jupyter_text>이렇게 p 태그의 class가 outer-text 인 것을 찾는 것도 가능하다.<jupyter_code>soup.find_all(class_='outer-text')<jupyter_output><empty_output><jupyter_text>혹은 그냥 class 이름으로만 outer-text를 찾을 수도 있다.<jupyter_code>soup.find_all(id='first')<jupyter_output><empty_output><jupyter_text>또 id가 first인 태그들을 찾을 수도 있다.<jupyter_code>soup.find('p')<jupyter_output><empty_output><jupyter_text>하지만 find 명령은 제일 처음 나타난 태그만 찾아주기 때문에 그 다음 태그만 찾고 싶을 때는 다른 방법을 사용해야 한다.<jupyter_code>soup.head<jupyter_output><empty_output><jupyter_text>soup의 head에 있는 내용이다. 여기서 next_sibling 이라는 명령을 사용할 수 있다.<jupyter_code>soup.head.next_sibling<jupyter_output><empty_output><jupyter_text>soup의 head 다음에 줄바꿈 문자가 있다.<jupyter_code>soup.head.next_sibling.next_sibling<jupyter_output><empty_output><jupyter_text>다시 한번 더 이렇게 head와 같은 위치에 있던 body 태그로 접근할 수 있다.<jupyter_code>body.p<jupyter_output><empty_output><jupyter_text>또한 제일 처음 나타나는 p 태그에 대해<jupyter_code>body.p.next_sibling.next_sibling<jupyter_output><empty_output><jupyter_text>위와 같이 next_sibling을 두 번 걸면 그 다음 p 태그로 이동할 수 있다.<jupyter_code>for each_tag in soup.find_all('p'):
    print(each_tag.get_text())<jupyter_output>Happy PinkWink.
                PinkWink


                Happy Data Science.
                Python



                Data Science is funny.
            



                All I need is Love.<jupyter_text>또 get_text() 명령으로 태그 안에 있는 텍스트만 가지고 올 수 있다.<jupyter_code>body.get_text()<jupyter_output><empty_output><jupyter_text>body 전체에서 get_text()를 하면 태그가 있떤 줄바꿈(\n)이 표시되고 전체 텍스트를 보여준다.<jupyter_code>links = soup.find_all('a')
links<jupyter_output><empty_output><jupyter_text>클릭 가능한 링크를 의미하는 a 태그를 찾았다.<jupyter_code>for each in links:
    href = each['href']
    text = each.string
    print(text + ' -> ' + href)<jupyter_output>PinkWink -> http://www.pinkwink.kr
Python -> https://www.python.org<jupyter_text>거기서 href 속성을 찾으면 링크 주소를 얻을 수 있다.  3-2 크롬 개발자 도구를 이용해서 원하는 데이터 찾기먼저 url로 접근하는 경우 urllib에서 urlopen이라는 함수를 import 해둔다.https://finance.naver.com/marketindex/ 로 접속한 후 미국 usd 1,130.00원 이라는 글자에서 환율을 가져오려 한다.크롬에서 제일 우측 상단의 기능 메뉴를 누르고 '도구 더보기'를 선택한 후 '개발자 도구'를 선택하면 된다.거기에서 왼쪽 상단의 마우스 클릭 버튼을 누르고 내가 알고 싶은 곳을 클릭하면 코드를 찾아준다. 최종적으로 span 태그의 value 라는 class를 얻으면 된다.<jupyter_code>from urllib.request import urlopen
url = "https://finance.naver.com/marketindex/"
page = urlopen(url)

soup = BeautifulSoup(page, "html.parser")

print(soup.prettify())<jupyter_output><script language="javascript" src="/template/head_js.nhn?referer=info.finance.naver.com&amp;menu=marketindex&amp;submenu=market">
</script>
<script src="https://ssl.pstatic.net/imgstock/static.pc/20210401112320/js/info/jindo.min.ns.1.5.3.euckr.js" type="text/javascript">
</script>
<script src="https://ssl.pstatic.net/imgstock/static.pc/20210401112320/js/jindo.1.5.3.element-text-patch.js" type="text/javascript">
</script>
<div id="container" style="padding-bottom:0px;">
 <div class="market_include">
  <div class="market_data">
   <div class="market1">
    <div class="title">
     <h2 class="h_market1">
      <span>
       환전 고시 환율
      </span>
     </h2>
    </div>
    <!-- data -->
    <div class="data">
     <ul class="data_lst" id="exchangeList">
      <li class="on">
       <a class="head usd" href="/marketindex/exchangeDetail.nhn?marketindexCd=FX_USDKRW" onclick="clickcr(this, 'fr1.usdt', '', '', event);">
        <h3 class="h_lst">
         <span class="blind">
          미국 USD
 [...]<jupyter_text>해당 페이지를 읽어온다. prettify()로 print()를 해도 사실 확인하기는 쉽지 않다.<jupyter_code>soup.find_all('span','value')[0].string<jupyter_output><empty_output><jupyter_text>혹시 몰라서 find_all로 찾고 리스트로 결과가 반환되니까 첫 번째를 선택하자.  3-3 실전 : 시카고 샌드위치 맛집 소개 사이트에 접근하기이제 시카고의 베스트 샌드위치 가게를 소개하고 시카고 매거진 홈페이지에 접속하여 샌드위치 가게 정보를 얻어오자.일단 접속 주소는 http://goo.go/wAtvls 이다.<jupyter_code>import requests
from bs4 import BeautifulSoup 
from urllib.request import urlopen

headers = {'User-Agent': 'Mozilla/5.0'}

url = 'http://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-Chicago/'

res = requests.get(url, headers=headers)

soup = BeautifulSoup(res.text)
soup<jupyter_output><empty_output><jupyter_text>위 코드를 입력하면 전체 html 코드를 다 받게 된다.<jupyter_code>print(soup.find_all('div', 'sammy'))<jupyter_output>[<div class="sammy" style="position: relative;">
<div class="sammyRank">1</div>
<div class="sammyListing"><a href="/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/"><b>BLT</b><br/>
Old Oak Tap<br/>
<em>Read more</em> </a></div>
</div>, <div class="sammy" style="position: relative;">
<div class="sammyRank">2</div>
<div class="sammyListing"><a href="/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Au-Cheval-Fried-Bologna/"><b>Fried Bologna</b><br/>
Au Cheval<br/>
<em>Read more</em> </a></div>
</div>, <div class="sammy" style="position: relative;">
<div class="sammyRank">3</div>
<div class="sammyListing"><a href="/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Xoco-Woodland-Mushroom/"><b>Woodland Mushroom</b><br/>
Xoco<br/>
<em>Read more</em> </a></div>
</div>, <div class="sammy" style="position: relative;">
<div class="sammyRank">4</div>
<div class="sammyListing"><a href="/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Als-Deli-R[...]<jupyter_text>find_all 명령을 이용해서 div의 sammy 태그를 찾아 보았다. 더 확실히 하기 위해 len 명령으로 길이를 확인해 보면 50 이라고 나타난다.<jupyter_code>len(soup.find_all('div','sammy'))<jupyter_output><empty_output><jupyter_text>애초 시카고 매거진의 기사 제목에 있지만 맛집 50개 이므로 저 길이가 50이라면 일단 정확하게 찾은 것 같다.<jupyter_code>print(soup.find_all('div','sammy')[0])<jupyter_output><div class="sammy" style="position: relative;">
<div class="sammyRank">1</div>
<div class="sammyListing"><a href="/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/"><b>BLT</b><br/>
Old Oak Tap<br/>
<em>Read more</em> </a></div>
</div><jupyter_text>그중 첫 번째 것만 확인해보면 원하던 정보가 다 있는걸로 보인다.  3-4 접근한 웹 페이지에서 원하는 데이터 추출하고 정리하기그럼 이제 div의 sammy 태그에서 우리가 원하는 정보를 얻는 과정일 보자.<jupyter_code>tmp_one = soup.find_all('div','sammy')[0]
type(tmp_one)<jupyter_output><empty_output><jupyter_text>find_all로 찾은 결과는 bs4.element.Tag 라고 하는 형태로 이런 경우 그 변수에 다시 태그로 찾는 find,find_all 명령을 사용할 수 있다.<jupyter_code>tmp_one.find(class_='sammyRank')<jupyter_output><empty_output><jupyter_text>그래서 find 명령을 한 번 더 사용하고 sammyRank를 찾아보면 나타난다. 여기선 text만 취하면 된다.<jupyter_code>tmp_one.find(class_='sammyRank').get_text()<jupyter_output><empty_output><jupyter_text>get_text() 명령을 사용하면 된다. 그러면 랭킹을 얻을 수 있다.<jupyter_code>tmp_one.find(class_='sammyListing').get_text()<jupyter_output><empty_output><jupyter_text>이제 sammyListing을 얻으면 메뉴 이름과 가게 이름이 비록 같이 나오긴 했지만 얻게 되었다.<jupyter_code>tmp_one.find('a')['href']<jupyter_output><empty_output><jupyter_text>또 a 택에서 href 정보를 가지고 클릭했을 때 연결될 주소도 저장할 수 있다.위로 두 번째 경우는 메뉴 이름과 가게 이름이 같이 있어서 분리해야 한다. 저 구조에서 쉽게 접근해 볼 수 있는 것 중 하나가 정규식이다.<jupyter_code>import re

tmp_string = tmp_one.find(class_='sammyListing').get_text()

re.split(('\n|\r\n'), tmp_string)

print(re.split(('\n|\r\n'), tmp_string)[0])
print(re.split(('\n|\r\n'), tmp_string)[1])<jupyter_output>BLT
Old Oak Tap<jupyter_text>당연히 정규식을 쓰기 위해 import re를 수행한다. 그리고 re에서 사용할 명령은 딱 하나 split이다. 말 그대로 내가 지정한 특정 패턴이 일치하면 분리시킨다. 나는 \n 이거나, \r\n이면 분리 시키고 싶다. 그래서 위처럼 re.split이라고 명령했다. 그러면 결과가 두 개가 되는데 첫 번째는 메뉴이름 두 번째는 가게 이름으로 하면 된다. '/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/' 이 결과가 다른 49개에 적용했을 때 항상 동일하지는 않다. 어떤 경우는 상대 경로로, 또 어떤 경우는 절대경로로 나오기 때문이다. 이럴 때 사용하는 것이 urllib에 있는 urljoin이라는 명령이다.<jupyter_code>from urllib.parse import urljoin
rank = []
main_menu = []
cafe_name = []
url_add = []

list_soup = soup.find_all('div', 'sammy')

for item in list_soup:
    rank.append(item.find(class_='sammyRank').get_text())
    
    tmp_string = item.find(class_='sammyListing').get_text()

    main_menu.append(re.split(('\n|\r\n'), tmp_string)[0])
    cafe_name.append(re.split(('\n|\r\n'), tmp_string)[1])
    
    url_add.append(urljoin(url, item.find('a')['href']))<jupyter_output><empty_output><jupyter_text>위 코드는 먼저 랭크 순위, 메인메뉴, 카페 이름, 각각의 접근 주소를 저장할 빈 리스트를 두었다. 그리고 find_all 로 찾은 50개 정보를 가지고 반복문을 돌리면서 방금 했던 내용인 각각의 정보를 .append 명령으로 빈 리스트에 하나씩 추가하도록 했다. 이 코드를 다 수행하면,<jupyter_code>rank[:5]
main_menu[:5]
cafe_name[:5]
url_add[:5]
len(rank), len(main_menu), len(cafe_name), len(url_add)<jupyter_output><empty_output><jupyter_text>이제 이 데이터를 4개의 리스트에 저장할 수는 없으니 pandas를 이용해보자.<jupyter_code>import pandas as pd

data = {'Rank':rank,'Menu':main_menu,'Cafe':cafe_name,'URL':url_add}
df = pd.DataFrame(data)
df.head()<jupyter_output><empty_output><jupyter_text>위처럼 각 컬럼의 이름을 정의하고 해당 자료를 알려주면 된다.<jupyter_code>df = pd.DataFrame(data, columns=['Rank', 'Cafe', 'Menu', 'URL'])
df.head()<jupyter_output><empty_output><jupyter_text>이렇게 결과가 나타난다. 이렇게 일차적으로 한 페이지에서 각 원하는 부분의 데이터를 읽어와서 다시 원하는 형태로 정리를 완료했다. 여기서 한단계 더 나아가자.  3-4 다수의 웹 페이지에 자동으로 접근해서 원하는 정보 가져오기<jupyter_code>from bs4 import BeautifulSoup
from urllib.request import urlopen

import pandas as pd
df = pd.read_csv('/home/jaeyoon89//DataScience/data/03. best_sandwiches_list_chicago.csv', index_col=0)
df.head()<jupyter_output><empty_output><jupyter_text>이전에 작업했던 것을 읽어오자. 위 표에서 URL 컬럼에 있는 내용을 50개 읽어서 각 페이지에서 가게 주소, 대표 샌드위치 가격, 가게 전화번호를 얻는 것이다.<jupyter_code>df['URL'][0]<jupyter_output><empty_output><jupyter_text>첫번째 URL이 있다. 시카고 매거진의 또 다른 페이지이다. 이 주소를 BeautifulSoup로 읽는다.<jupyter_code>import requests
from bs4 import BeautifulSoup 
from urllib.request import urlopen

headers = {'User-Agent': 'Mozilla/5.0'}

html = (df['URL'][0])

res = requests.get(url, headers=headers)

soup_tmp = BeautifulSoup(res.text)
soup_tmp
print(soup_tmp.find('p', 'addy'))<jupyter_output><p class="addy">
<em>$10. 2109 W. Chicago Ave., 773-772-0406, <a href="http://www.theoldoaktap.com/">theoldoaktap.com</a></em></p><jupyter_text>위처럼 태그를 이용해서 찾는다. 원하는 정보가 다 나와있다. 주소와 가격과 전화번호까지 있다. 그럼 이 상태에서 text로 가져와서 빈 칸으로 나누면 된다.<jupyter_code>price_tmp = soup_tmp.find('p','addy').get_text()
price_tmp<jupyter_output><empty_output><jupyter_text>일단 get.text()로 가지고 왔다. 원하는 내용은 다 있다.<jupyter_code>price_tmp.split()<jupyter_output><empty_output><jupyter_text>그리고 split()을 적용하면 위 결과처럼 나타난다. 여기서 빨리 눈치를 채야하는 것은 가격은 제일 첫 번째이다. 그리고 가운데가 주소의 체계에 따라 칸 수가 바뀔수는 있지만 제일 뒤는 웹 주소고, 제일 뒤에서 두번째는 전화번호이다. 파이썬 리스트에서 제일 뒤는 -1로 호출할 수 있다.<jupyter_code>price_tmp.split()[0]<jupyter_output><empty_output><jupyter_text>맨 뒤에 점이 붙어서 아래와 같이 사용하자.<jupyter_code>price_tmp.split()[0][:-1]<jupyter_output><empty_output><jupyter_text>앞에 리스트를 기준으로 두 번째부터 맨 마지막에서 세 번째까지 선택하고 싶은데 그렇게 해도 list 형인것은 변함없다. 바로 하나의 문장을 만들고 싶은 것입니다. 이럴 때 사용하는 명령이 join 명령이다.<jupyter_code>' '.join(price_tmp.split()[1:-2])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13994544627973934, 'fraction_numerical': 0.012426125170480376, 'mean_word_length': 5.03796889295517, 'pattern_counts': {'":': 0, '<': 298, '<?xml version=': 0, '>': 300, 'https://': 6, 'lorem ipsum': 0, 'www.': 6, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '47', 'dir': 'jupyter-structured-clean-dedup', 'id': '290556', 'license': 'MIT', 'n_tokens_mistral': 7636, 'n_tokens_neox': 8723, 'n_words': 1280, 'path': 'ch3_best_sandwiches_list_chicago.ipynb', 'repo_name': 'jaeyoon89/python_datascience_styudy', 'size': '13198'}