[업무 지식]/Crawling

[Beautiful Soup] chicago sandwiches

에디터 윤슬 2024. 11. 21. 13:22

 

 

파이썬으로 데이터 주무르기 - 예스24

독특한 예제를 통해 배우는 데이터 분석 입문이 책은 누구나 한 권 이상 가지고 있을 파이썬 기초 문법책과 같은 내용이 아닌, 데이터 분석이라는 특별한 분야에서 초보를 위해 처음부터 끝까지

m.yes24.com

 

1. 웹 데이터 가져오는 Beautiful Soup 익히기

import pandas as pd
import numpy as np
from bs4 import BeautifulSoup
  • html 파일 읽기
page = open('~~~.html', 'r').read()
soup = BeautifulSoup(page, 'html.parser')
print(soup.prettify())

- open(): 지정된 경로에 있는 파일을 열어 내용을 읽습니다.
    - 첫 번째 인자: '파일 경로'는 로컬에 있는 HTML 파일의 경로입니다.
    - 두 번째 인자: 'r'은 파일을 읽기 모드(read mode)로 연다는 의미입니다.
- .read(): 파일의 내용을 한 번에 모두 읽어와 문자열 형태로 반환합니다.
    - 결과적으로, page 변수에는 해당 HTML 파일의 내용이 문자열로 저장됩니다.
- BeautifulSoup(page, 'html.parser'): BeautifulSoup 객체를 생성하여 HTML 코드를 파싱합니다.
    - 첫 번째 인자: page는 앞서 읽어온 HTML 파일의 내용입니다.
    - 두 번째 인자: 'html.parser'는 기본 제공되는 Python의 내장 HTML 파서를 사용하여 HTML을 분석합니다.
- 결과적으로, soup 변수에는 파싱된 HTML 구조가 저장됩니다.
- soup.prettify(): BeautifulSoup 객체에서 제공하는 메서드로, HTML 코드를 보기 좋게 들여쓰기(indent)하여 출력합니다.
    - 원래는 한 줄로 되어 있거나 들여쓰기가 없는 HTML 코드도 이 메서드를 사용하면 읽기 쉽게 정리됩니다.
  • 태그 접속 코드
html = list(soup.children)[2]
html

# 본문으로 보는 body 태그를 보기 위해 3번 인덱스 조사
body = list(html.children)[3]
body

# 위 과정 없이 바로 body 검색 가능
soup.body

# 접근한 태그를 알고 있다면 find_all 명령 사용
soup.find_all('p')

# p 태그의 class가 outer-text인 것을 찾는 것도 가능
soup.find_all(class_='outer-text')

# get_text() 명령으로 태그 안에 있는 텍스트만 추출
for each_tag in soup.find_all('p'):
    print(each_tag.get_text())
    
# 클릭 가능한 링크 a 태그
links = soup.find_all('a')
links

# href 속성을 찾으면 링크 주소 추출 가능

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

 

  • requests로 웹 페이지 가져오기
import requests
from bs4 import BeautifulSoup

# requests로 페이지 가져오기 (SSL 처리 자동)
url = 'https://finance.naver.com/marketindex/'
response = requests.get(url)

# BeautifulSoup으로 HTML 파싱
soup = BeautifulSoup(response.text, 'html.parser')

# 보기 좋게 출력
print(soup.prettify())

2. 시카고 샌드위치 맛집 소개 사이트 접근

 

The 50 Best Sandwiches in Chicago

Our list of Chicago’s 50 best sandwiches, ranked in order of deliciousness

www.chicagomag.com

 

  • 브라우저처럼 설정하여 페이지 접근
import requests
from bs4 import BeautifulSoup

# User-Agent를 브라우저처럼 설정
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}

# URL 설정
url_base = 'https://www.chicagomag.com'
url = 'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-Chicago/'

# 페이지 요청 (User-Agent 포함)
html = requests.get(url, headers=headers)

# BeautifulSoup으로 HTML 파싱
soup = BeautifulSoup(html.text, 'html.parser')

# 보기 좋게 출력
print(soup.prettify())

 

  • 가게 이름 등의 정보가 있는 태그 검색(크롬브라우저에서 해당 위치 우선 탐색)
soup.find_all('div', 'sammy')

  • 맛집 리스트 갯수 및 정보 확인
len(soup.find_all('div', 'sammy'))

# 정보 확인. 
soup.find_all('div', 'sammy')[0]

필요 정보:
class="sammyRank">1</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/>

 

3. 접근한 웹 페이지에서 데이터 추출하고 정리

  • 텍스트 추출
# 원하는 데이터 위치 선언
# bs4.element.Tag인 경우 그 변수에 다시 태그로 찾는 명령 사용 가능
tmp_one = soup.find_all('div', 'sammy')[0]
type(tmp_one)

# 텍스트만 추출 get.text()
print('랭킹 순위:', tmp_one.find(class_= 'sammyRank').get_text())
print('메뉴 및 가게 이름:', tmp_one.find(class_= 'sammyListing').get_text())

  • 가게 이름과 메뉴 분리 확인
# 가게 이름과 메뉴 분리
# 정규식(Regular Express)

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])

BLT
Old Oak Tap

- re.split(('\n|\r\n'), tmp_string):
    - 정규식을 사용하여 문자열을 줄바꿈 문자(\n 또는 \r\n) 기준으로 분리합니다.
    - \n은 줄바꿈(newline)을 의미하고, \r\n은 캐리지 리턴과 줄바꿈이 함께 사용되는 경우를 의미합니다. 두 가지 경우 모두 줄바꿈으로 처리됩니다.
    - 예시로, tmp_string이 "가게 이름\n메뉴 1\n메뉴 2"라면, 이 코드는 이를 ['가게 이름', '메뉴 1', '메뉴 2']로 분리합니다.

 

  • 데이터 저장 리스트 생성
from urllib.parse import urljoin  # urljoin을 가져옴

# 데이터 저장 리스트 초기화
rank = []
main_menu = []
cafe_name = []
url_add = []

# 'sammy' 클래스를 가진 div 요소들을 모두 찾음
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을 절대 URL로 변환하여 추가
    url_add.append(urljoin(url_base, item.find('a')['href']))

# 결과 확인
print(rank[:5])
print(cafe_name[:5])
print(main_menu[:5])
print(url_add[:5])

['1', '2', '3', '4', '5']
['Old Oak Tap', 'Au Cheval', 'Xoco', 'Al’s Deli', 'Publican Quality Meats']
['BLT', 'Fried Bologna', 'Woodland Mushroom', 'Roast Beef', 'PB&L']
['https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/', 'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Au-Cheval-Fried-Bolog...

 

  • 데이터프레임화
data = {
    'Rank': rank,
    'Cafe': cafe_name,
    'Menu': main_menu,
    'URL': url_add
}
df = pd.DataFrame(data)
df.head()

 

4. 다수의 웹 페이지에서 자동으로 원하는 정보 가져오기

  • 첫 번째 url로 테스트 진행
df['URL'][0]
'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/'

# User-Agent를 브라우저처럼 설정
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}

# URL 가져오기
url = df['URL'][0]

# 페이지 요청 (User-Agent 포함)
html1 = requests.get(url, headers=headers)

# BeautifulSoup으로 HTML 파싱
soup1 = BeautifulSoup(html1.text, 'html.parser')

print(soup1.prettify())

 

  • 원하는 정보: 주소, 가격, 전화번호 텍스트로 추출
price_tmp = soup1.find('p', 'addy').get_text()
price_tmp

'\n$10. 2109 W. Chicago Ave., 773-772-0406, theoldoaktap.com'

 

  • split 사용하여 구분
print('가격:', price_tmp.split()[0][:-1])
print('주소:', ' '.join(price_tmp.split()[1:-2]))
print('웹 주소:', price_tmp.split()[-1][:-1])
print('전화번호:', price_tmp.split()[-2][:-1])

가격: $10
주소: 2109 W. Chicago Ave.,
웹 주소: theoldoaktap.co
전화번호: 773-772-0406

 

  • 웹 페이지 3개 테스트
# 페이지 3개 테스트

price = []
address = []
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}
telephone = []
webpage = []

for n in df.index[:3]:
    html = requests.get(df['URL'][n], headers=headers)
    soup_tmp = BeautifulSoup(html.text, 'html.parser')
    
    gettings = soup_tmp.find('p', 'addy').get_text()
    
    price.append(gettings.split()[0][:-1])
    address.append(' '.join(gettings.split()[1:-2]))
    telephone.append(gettings.split()[-2][:-1])
    webpage.append(gettings.split()[-1][:-1])
    
    
print(price)
print(address)
print(telephone)
print(webpage)

['$10', '$9', '$9.50']
['2109 W. Chicago Ave.,', '800 W. Randolph St.,', '445 N. Clark St.,']
['773-772-0406', '312-929-4580', '312-334-3688']
['theoldoaktap.co', 'aucheval.tumblr.co', 'rickbayless.co']

 

  • 페이지 50개 진행
# 상태바 tqdm import

from tqdm import tqdm

price = []
address = []
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}
telephone = []
webpage = []

for n in tqdm(df.index):
    html = requests.get(df['URL'][n], headers=headers)
    soup_tmp = BeautifulSoup(html.text, 'html.parser')
    
    gettings = soup_tmp.find('p', 'addy').get_text()
    
    price.append(gettings.split()[0][:-1])
    address.append(' '.join(gettings.split()[1:-2]))
    telephone.append(gettings.split()[-2][:-1])
    webpage.append(gettings.split()[-1][:-1])
    
    
print(price)
print(address)
print(telephone)
print(webpage)

['$10', '$9', '$9.50', '$9.40', '$10', '$7.25', '$16', '$10', '$9', '$17', '$11', '$5.49', '$14', '$10', '$13', '$4.50', '$11.95', '$11.50', '$6.25', '$15', '$5', '$6', '$8', '$5.99', '$7.52', '$11.95', '$7.50', '$12.95', '$7', '$21', '$9.79', '$9.75', '$13', '$7.95', '$9', '$9', '$8', '$8', '$7', '$6', '$7.25', '$11', '$6', '$9', '$5.49', '$8', '$6.50', '$7.50', '$8.75', '$6.85']
['2109 W. Chicago Ave.,', '800 W. Randolph St.,', '445 N. Clark St.,', '914 Noyes St., Evanston,', '825 W. Fulton Mkt.,', '100 E. Walton', '1639 S. Wabash Ave.,', '2211 W. North Ave.,', '3619 W. North Ave.,', '3267 S. Halsted St.,', '2537 N. Kedzie Blvd.,', 'Multiple', '3124 N. Broadway,', '3455 N. Southport Ave.,', '2657 N. Kedzie Ave.,', '1120 W. Grand Ave.,', '1141 S. Jefferson St.,', '333 E. Benton Pl.,', '1411 N. Wells St.,', '1747 N. Damen Ave.,', '3209 W. Irving Park', 'Multiple', '5347 N. Clark St.,', '2954 W. Irving Park Rd.,', 'Multiple', '191 Skokie Valley Rd., Highland Park,', 'Multiple', '1818 W. Wilson Ave.,', '2517 W. Division St.,', '218 W. Kinzie', 'Multiple', '1547 N. Wells St.,', '415 N. Milwaukee Ave.,', '1840 N. Damen Ave.,', '1220 W. Webster Ave.,', '5357 N. Ashland Ave.,', '1834 W. Montrose Ave.,', '615 N. State St.,', 'Multiple', '241 N. York Rd., Elmhurst,', '1323 E. 57th St.,', '655 Forest Ave., Lake Forest,', 'Hotel Lincoln, 1816 N. Clark St.,', '100 S. Marion St., Oak Park,', '26 E. Congress Pkwy.,', '2018 W. Chicago Ave.,', '25 E. Delaware Pl.,', '416 N. York St., Elmhurst,', '65 E. Washington St.,', '3351 N. Broadway,']
['773-772-0406', '312-929-4580', '312-334-3688', '847-475-9400', '312-445-8977', 'St.', '312-360-9500', '773-276-2100', '773-772-8435', '312-929-2486', '773-489-9554', 'locations', '773-661-9166', '773-883-2525', '773-276-7110', '312-666-0730', '312-939-2855', '773-234-3449', '312-944-0459', '773-489-1747', 'Rd.', 'locations', '773-275-5725', '773-539-5321', 'locations', '847-831-0600', 'locations', '773-293-2489', '773-862-8313', 'St.', 'locations', '312-624-9430', '312-829-6300', '773-681-9914', '773-883-1313', '773-275-4297', '773-334-5664', '312-265-0434', 'locations', '630-516-3354', '773-538-7372', '847-234-8800', '312-254-4665', '708-725-7200', '312-922-2233', '773-384-9930', '312-896-2600', '630-359-5234', '312-726-2020', '773-868-4000']
['theoldoaktap.co', 'aucheval.tumblr.co', 'rickbayless.co', 'alsdeli.ne', 'publicanqualitymeats.co', '312-649-671', 'acadiachicago.co', 'birchwoodkitchen.co', 'cemitaspuebla.co', 'nanaorganic.co', 'lulacafe.co', 'ricobenespizza.co', 'frognsnail.co', 'crosbyskitchenchicago.co', 'longmanandeagle.co', 'bariitaliansubs.co', 'mannysdeli.co', 'eggysdiner.co', 'oldjerusalemchicago.co', 'hotchocolatechicago.co', '773-539-803', 'dawalikitchen.co', 'bigjoneschicago.co', 'lapanechicago.co', 'pastoralartisan.co', 'maxs-deli.co', 'luckysandwich.co', 'cityprovisions.co', 'papascachesabroso.co', '312-624-815', 'hannahsbretzel.co', 'lafournette.co', 'paramountroom.co', 'meltsandwichshoppechicago.co', 'floriole.co', 'firstslice.or', 'troquetchicago.co', 'grahamwich.co', 'saigonsisters.co', 'rosaliasdeli.co', 'zhmarketcafe.co', 'themarkethouse.co', 'jdvhotels.com/hotels/chicago/lincol', 'marionstreetcheesemarket.co', 'cafecitochicago.co', 'chickpeaonthego.co', 'goddessandgrocer.co', 'eatmyzenwich.co', 'tonipatisserie.co', 'phoebesbakery.co']

 

  • 데이터프레임 추가
df['Price'] = price
df['Address'] = address
df['Telephone'] = telephone
df['Webpage'] = webpage

df.head()

 

  • 인덱스 설정 및 컬럼 순서 변경
# Rank 인덱스화
df.set_index('Rank', inplace=True)

# 컬럼 순서 변경
df = df.loc[:, ['Cafe', 'Menu', 'Price', 'Address', 'Telephone', 'Webpage', 'URL']]
df.head()

 

5. 맛집 위치 지도에 표기

import folium
import googlemaps

# googlemaps를 이용해 API 키 저장
gmaps_key = '*********'
gmaps = googlemaps.Client(key = gmaps_key)

 

  • 50개의 맛집 위도, 경도 정보 불러오기
from tqdm import tqdm

lat = []
lng = []

for n in tqdm(df.index):
    if df['Address'][n] != 'Mutiple':
        target_name = df['Address'][n] + ', ' + '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)

 

  • 위도, 경도 데이터프레임 추가
df['lat'] = lat
df['lng'] = lng

 

  • 50개 맛집 위도, 경도 지도에 표기
# 50맛집 위도, 경도 지도에 표기

mapping = folium.Map(location = [df['lat'].mean(), df['lng'].mean()], zoom_start=11)

for n in df.index:
    if df['Address'][n] != 'Multiple':
        folium.Marker([df['lat'][n], df['lng'][n]], popup = df['Cafe'][n]).add_to(mapping)
        
mapping