EDA-웹 데이터 분석

BlackLabel·2023년 10월 18일
0

EDA, 크롤링 기초 

목록 보기
7/13

Beautiful Soup 기초와 웹 데이터

Beautiful Soup

  • HTML : 웹 페이지를 구성하는 마크업 언어
    html 태그 : 웹 페이지 표현
    head 태그 : 눈에 보이지 않지만 문서에 필요한 헤더 정보 보관
    body 태그 : 눈에 보이는 정보 보관
    Beautiful Soup 테스트를 위해 html 작성

  • Beautiful Soup : 태그로 이루어진 문서를 해석하는 기능을 가진 모듈
    html.parser : Beautiful Soup의 html을 읽는 엔진 중 하나(lxml도 많이 사용)
    html.parser, lxml, lxml-xml, xml, html5lib 등
    prettify() : html을 태그 기준으로 들여쓰기를 통해 정리해주는 기능
    모듈 설치 : pip install beautifulsoup4, conda install -c beautifulsoup4

공식 문서 : https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.html

from bs4 import BeautifulSoup

page = open('../data/03. test_firdt.html','r').read()
soup = BeautifulSoup(page, 'html.parser')
print(soup.prettify())

  • 파일로 저장된 html 파일을 읽을 때
  • open : 파일명과 함께 (r)/ 쓰기(w) 속성을 지정

태그 키워드 활용

  • 원하는 태그를 키워드로 선택할 수 있다. 처음 만나는 태그만을 선택한다.
    head 키워드 : head 태그를 선택
    body 키워드 : body 태그를 선택
    p 키워드 : p 태그를 선택
soup.body
  • body태그만 출력
soup.find('p')
soup.find_all('p')
  • find는 상위 하나만 출력
  • find_all은 해당하는 모든 태그를 리스트 형태로 출력
soup.find_all(class='outer-text')

soup.find('p', class_='inner-text') # p태그중에서 클래스가 inner-text인 태그 출력
  • 클래스가 'outer-text'인 모든 태그를 리스트 형태로 출력
soup.find(id='first')
  • HTML내에서 속성 id는 한번만 나타난다
  • 그래서 find_all()은 결과를 리스트 형태로 받고 싶은 경우에만 사용한다
for each_tag in soup.find_all('p'):
	print('-------------')
    print(each_tag.get_text())

텍스트 키워드 : 태그에 포함된 텍스트 반환

  • text 키워드 : 태그에 포함된 텍스트를 반환
  • string 키워드 : 태그에 포함된 텍스트를 반환(단, 단일 태그인 경우에만 동작)
  • get_text() : 태그에 포함된 텍스트를 반환
    텍스트가 여러 개 있다면 태그를 기준으로 개행되어 반환
    '구분자' : 태그 사이의 구분자 설정
  • strip 옵션 : 데이터의 양끝 공백 제거 설정(True / False)
  • stripped_strings 키워드 : for을 사용하여 리스트, 튜플 등의 형태로 반환할 수 있다.
links = soup.find_all('a')

for each in links:
	href = each['href']
    text = each.string
    print(text + '->' + href)
    

외부로 연결되는 링크의 주소 알아내기

  • find("a")로 링크 태그를 찾는다.
  • attrs 키워드 : 해당 태그의 속성 dcit형으로 반환
  • 태그[속성], 태그.get(속성) : 해당 태그의 속성 값 반환

크롬 개발자 도구 이용하기

크롬 개발자 도구를 활용해 찾고자 하는 데이터의 태그, 클래스, 아이디 등 정보 확인

  • 소스 코드를 통해 확인할 수 있다

네이버 금융

네이버 금융 : 환율 확인

from urllib.request import urlopen
from bs4 import BeautifulSoup

url = 'https://finance.naver.com/marketindex/'
page = urlopen(url) # page대신 response나 res 변수명도 많이 사용
soup = BeautifulSoup(page, 'html.parser')
print(page.status)
print(soup.prettify())
# .status 키워드 : http 상태 코드로 200일 경우 정상적으로 정보를 받았다는 의미

# 환율 정보
soup.find_all("span", class_="value"), len(soup.find_all("span", class_="value"))

# 미국 환율 정보
# string/text/get_text()/getText() : 텍스트
soup.find_all("span", class_="value")[0].string

네이버 금융 : 환전 고시 환율

  • requests 모듈 사용
    get(), post() 모드가 있다.
    웹 페이지를 해당 명령으로 열게되면 http 상태 코드를 확인할 수 있다.
    text 키워드 : 웹 페이지의 내용 반환
import requests
# from urllib.requests.Request 위에와 사실상 동일한 기능
from bs4 import BeautifulSoup

# 환전 고시 환율
# 환율, 등락, 링크
url = 'https://finance.naver.com/marketindex/'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
print(soup.prettify())

  • find, select_one : 단일 선택
  • find_all, select : 다중 선택
  • requests.get(), requests.post() 두 가지가 있다

urllib.request vs requests

  1. requests
  • 데이터를 보낼 때 딕셔너리 형태로 보낸다
  • 없는 페이지를 요청해도 에러를 띄우지 않는다
  1. urllib.request
  • 데이터를 보낼 때 인코딩하여 *바이너리 형태로 보낸다
  • 없는 페이지를 요청하면 에러를 띄운다

대체로 requests를 많이 쓴다
reqeusts.get()방법이 HTTP method와 연관되어 있으니 직관적으로 이해하기 쉽다

  • 바이너리 : 이진 형태로 저장된 이진파일을 의미한다

find_all vs select

  • find, find_all 과 같은 기능
  • select == find_all : 다중 선택
  • select_one == find : 단일 선택
  1. find 함수
  • 원하는 태그를 찾는 것이 목적이다
  • tag 캑체에 담아 반환, tag 객체는 태그의 요소를 자신의 속성으로 갖는다
  1. select
  • CSS selector로 tag객체를 찾아 반환한다
  • CSS 이외에도 다양한 선택자를 갖기 때문에 여러 요소를 조합하여 태그를 특정하기 쉽다
  • 더 다양한 조건으로, 더 직관적인 방법으로 태그를 쫓을 수 있다

select가 수행시간도 더 빠르며 메모리 소모량도 더 적다

exchangeList = soup.select('#exchangeList > li')
len(exchangeList), exchangeList

데이터 추출

  • soup.find_all("li", class_ = 'on') : 태그와 클래스는 중복이 가능하므로
    원하지 않는 데이터도 추출될 수 있다.
  • class : .클래스 (>) 태그 - 클래스 소속 중 해당 태그 선택
  • id: #아이디 (>) 태그 - 아이디 소속 중 해당 태그 선택
  • tag : 태그 - 태그 선택
  • 꺽쇠(>) : 해당 클래스, 아이디의 바로 하위를 의미(>가 없다면 깊이 우선 탐색을 하게 된다.)
  • 아이디 : exchangeList 하위의 li 태그 추출

데이터 추가 추출

title = exchangeList[0].select_one('.h_lst').text
print(title)
exchange = exchangeList[0].select_one('.value').text
print(exchange)
change = exchangeList[0].select_one('.change').text
print(change)
# head_info point_(up/dn)
# 공백이 들어간 클래스는 공백을 기준으로 클래스가 2개임을 의미한다.
# 따라서 head_info, point_(up/dn) 2가지를 가지게 된다.
# 여기서 point_up 또는 point_dn은 값에 따라 다르게 되므로 head_info로만 값을 찾는다.
updown = exchangeList[0].select_one('div.head_info > .blind').text
print(updown)
baseUrl = 'https://finance.naver.com'
link = baseUrl + exchangeList[0].select_one('a').get('href')
print(link)

baseUrl = 'https://finance.naver.com'
baseUrl + exchangeList[0].select_one('a').get('href')

# 4개의 데이터 수집
exchange_datas = []

for item in exchangeList:
    data = {
        'title': item.select_one('.h_lst').text,
        'change': item.select_one('.value').text,
        'change': item.select_one('.change').text,
        'updown': item.select_one('div.head_info.point_dn > .blind').text,
        'link': baseUrl + item.select_one('a').get('href')
    }
    exchange_datas.append(data)
exchange_datas

import pandas as pd
df = pd.DataFrame(exchange_datas)
df

위키백과 문서 정보 가져오기

import urllib
from urllib.request import Request,urlopen

html = 'https://ko.wikipedia.org/wiki/{search_words}'
req = Request(html.format(search_words=urllib.parse.quote('여명의_눈동자')))

response = urlopen(req)

soup = BeautifulSoup(response, 'html.parser')
soup
  • format 함수를 통해 html이란 str에 변수설정 가능
  • quote 함수 : '여명의_눈동자'를 utf-8로 변환
n = 0
for each in soup.select('ul'):
    print('=>' + str(n) + '=============')
    print(each)
    n += 1
  • 주요인물에 대한 정보가 어디쯤에 있는지 확인
  • 강의에선 15번째이지만 바뀌어서 현재는 32번째
soup.select('ul')[32]

soup.select('ul')[32].text.replace('\xa0','').split('\n')

  • replace 함수를 이용해 필요없는 부분 제거

list 데이터형

  • extend : 제일 뒤에 다수의 자료를 추가
  • remove : 같은 이름의 자료를 지움
  • 슬라이싱 : [n:m] n번째 부터 m-1까지
  • insert : 원하는 위치에 자료를 삽입
  • isinstance : 자료형이 list인지 확인 할 수 있다
  • list안에 list를 가질 수 있다

시카고 맛집 분석

시카고 맛집 메인페이지 분석

User-Agnet 생성 모듈

User-Agnet 생성 모듈
from fake_useragent import UserAgent
url_base = 'https://www.chicagomag.com/'
url_sub = '/Chicago-Magazine/November-2012/Best-Sandwiches-Chicago/'
url = url_base + url_sub

# html = urlopen(url) : Error 403 서버에서 접근 차단(자동 봇 차단)
# 크롬 개발자 도구 -> Network 상세 내용 확인
# User-Agent 생성
# ua = UserAgent()
# ua.ie
# req = Request(url, headers={'User-Agent' : ua.ie})
from bs4 import BeautifulSoup
from urllib.request import Request, urlopen

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

req = Request(url, headers={'User-Agent':'Chrome'})
html = urlopen(req).read()
soup = BeautifulSoup(html, 'html.parser')
soup
  • headers : 자동화된 봇이 들어오는 것이 아닌지 확인하기 위해서 웹 브라우저 종류를 묻는 것
  • fake_useragent를 통해 임시로 생성된 user-agent를 사용할 수 있다
  • url을 두 부분으로 나눈 이유는 '상대경로' 때문에
print(soup.select('div.sammy'))
  • 시카고 맛집 리스트
soup.select('div.sammy')[0]

  • 맛집 중 첫 번째
  • 랭킹, 가게 이름, 메뉴
tmp_one = soup.select('div.sammy')[0]
type(tmp_one)
  • type이 bs4.element.Tag라는 것은 find 명령을 사용할 수 있다는 뜻
tmp_one.find(class_='sammyRank').get_text()
  • 랭킹
tmp_one.select_one('.sammyListing').text
  • 메뉴와 가게 이름
tmp_one.select_one('a')['href']
  • 연결되는 홈페이지 주소가 '상대경로'이다
import re

tmp_string = tmp_one.select_one('.sammyListing').text
re.split(('\n|\r\n'), tmp_string)
  • 가게 이름과 메뉴는 re모듈의 split으로 쉽게 구분할 수 있다
  • '|'는 or과 같은 기능
from urllib.parse import urljoin

url_base = 'http://www.chicagomag.com'

# 각 정보를 담을 빈 리스트
# 모든 정보를 얻은 후 DataFrame으로 합치면 된다.
rank = []
main_menu = []
cafe_name = []
url_add = []
# div의 sammy 태그 추출
list_soup = soup.find_all("div", class_="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 주소
    # urljoin(a, b) : b가 상대 주소라면 a와 결합하고 b가 절대 주소라면 결합하지 않는다. 
    url_add.append(urljoin(url_base, item.find('a').get('href')))
  • 필요한 내용을 담을 빈 리스트를 준비
  • 리스트로 하나씩 컬럼을 만들고 DataFrame으로 합칠 예정
  • for문으로 랭크와 가게이름, 메뉴 데이터를 가져온다
  • urljoin : 내가 2번째로 가져온 주소가 만약 절대주소라면 붙이지 않고 상대주소면 url_base를 붙여라
import pandas as pd

data = {'Rank':rank, 'Menu': main_menu, 'Cafe': cafe_name, 'URL': url_add}
df = pd.DataFrame(data, columns=['Rank','Cafe','Menu','URL'])
df.head()
  • DataFrame 형태로 변환
  • columns로 칼럼 순서 변경

시카고 맛집 데이터 하위 페이지 분석

  • 가격과 주소를 가져와야 한다
df['URL'][0]

req = Request(df['URL'][0], headers={'user-agent':'Chrome'})
html = urlopen(req).read()
soup_tmp = BeautifulSoup(html,'html.parser')

soup_tmp.select_one('p.addy')

  • 가격과 주소가 같이 있다

Regular Expression(정규식)

price_tmp = soup_tmp.select_one('p.addy').text

import re

price_tmp = re.split('.,', price_tmp)[0]
price_tmp

tmp = re.search('\$\d+\.(\d+)?', price_tmp).group()
tmp
# >>> '$10.'

price_tmp[len(tmp) + 2:]
# >>> '2109 w. chicago Ave'
  • $(달러 기호가 반드시 와야하고)
  • \d(숫자가 여러개 있을 수 있다)
  • .("."이 반드시 와야하고)
  • ()? : 있을 수도 있고 없을 수도 있고
  • 숫자로 시작하다가 꼭 '.'을 만나고 그 뒤 숫자가 있을 수도 있고 아닐 수도 있다
from tqdm import tqdm

price = []
address = []

for idx,row in tqdm(df.iterrows()):
    req = Request(row['URL'], headers={'user-agent':'Chrome'})
    html = urlopen(req).read()
    
    soup_tmp = BeautifulSoup(html,'html.parser')
    
    gettings = soup_tmp.select_one('p.addy').text
    
    price_tmp = re.split('.,', gettings)[0]
    tmp = re.search('\$\d+\.(\d+)?', price_tmp).group()
    
    price.append(tmp)
    address.append(price_tmp[len(tmp)+2:])


df['Price'] = price
df['Address'] = address

df = df.loc[:,['Rank','Cafe','Menu','Price','Address']]
df.set_index('Rank', inplace=True)
df.head()
  • DataFrame에 가격, 주소 데이터 추가

시카고 맛집 데이터 지도 시각화

import googlemaps
import numpy as np

gmaps_key = 'API키'
gmaps = googlemaps.Client(key=gmaps_key)


lat = []
lng = []

for idx, row in tqdm(df.iterrows()):
    if not row['Address'] == 'Multiple location':
        target_name = row['Address'] + ',' + 'chicago'
        gmaps_output = gmaps.geocode(target_name)
        location_output = gmaps_output[0].get('geometry')
        lat.append(location_output['location']['lat'])
        lng.append(location_output['location']['lng'])
    else:
        lat.append(np.nan)
        lng.append(np.nan)
  • multiple location : 가게가 여러 지점이 있는 경우
df['lat'] = lat
df['lng'] = lng
df.head()
  • df에 위도, 경도 데이터 추가
import folium

mapping = folium.Map(location=[41.8781136,-87.6297982],zoom_start=11)
mapping

for idx, row in df.iterrows():
    if not row['Address'] == 'Multiple location':
        folium.Marker([row['lat'], row['lng']], popup=row['Cafe']).add_to(mapping)
mapping

  • 가게 위치에 마커 추가
profile
+database

0개의 댓글