Posts Beautiful Soup을 이용한 시카고 샌드위치 맛집 정보 추출
Post
Cancel

Beautiful Soup을 이용한 시카고 샌드위치 맛집 정보 추출

1. 시카고 매거진


1.1 시카고 매거진

https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-Chicago/

  • 미국 시카고 매거진의 베스트 50개의 샌드위치 맛집 리스트
  • 메뉴와 가게 이름이 정리


1.2 가게 상세 페이지

  • 각각소개한 50개의 페이지에 들어가면 가게 주소와 대표메뉴의 가격
  • 즉, 총 51개의 페이지에서, 가게이름, 대표메뉴, 대표메뉴의 가격, 가게주소를 수집


2. 기본 페이지


2.1 기본 페이지 파싱

1
2
3
4
5
6
7
8
9
10
11
12
from bs4 import BeautifulSoup
from urllib.request import urlopen
import pandas as pd

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

html = urlopen(url)
soup = BeautifulSoup(html, 'html.parser')

soup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE doctype html>

<html lang="en">
<head>
<!-- Urbis magnitudo. Fabulas magnitudo. -->
<meta charset="utf-8"/>
<style>a.edit_from_site {display: none !important;}</style>
<title>
  The 50 Best Sandwiches in Chicago |
  Chicago magazine
      |  November 2012
    </title>
...
</script>
<!--[if lt IE 9]>
<script  type="text/javascript" language="JavaScript" src="/core/media/themes/Respond/js/respond.js?ver=1473876729"></script>
<![endif]-->
<script language="JavaScript" src="/core/media/js/base.js?ver=1473876728" type="text/javascript"></script>
<script language="JavaScript" src="/core/media/themes/Respond/js/bootstrap.min.js?ver=1473876729" type="text/javascript"></script>
<script language="JavaScript" src="//maps.googleapis.com/maps/api/js?v=3.exp&amp;sensor=false" type="text/javascript"></script>
<script language="JavaScript" src="/theme_overrides/Respond/js/interstitial.js?ver=1524154906" type="text/javascript"></script>
<script language="JavaScript" src="/theme_overrides/Respond/js/newsletter-subscribe.js?ver=1524850607" type="text/javascript"></script>
<script language="JavaScript" src="/theme_overrides/Respond/js/RivistaGoogleDFP.js?ver=1447178886" type="text/javascript"></script>
<!-- godengo-monitor --></body>
</html>
  • 기본 시카고 매거진의 url과 2012년 11월 시카고 매거진의 50개 샌드위치 url을 합쳐서 완성된 url을 만듬
  • 이후 Beautifulsoup을 사용하여 html로 파싱함


2.2 50개 가게 정보 가져오기

1
len(soup.find_all('div', 'sammy'))
1
50
  • div의 sammy 클래스를 이용해 총 50개의 가게 정보를 가져옴


2.3 가게 정보

1
print(soup.find_all('div', 'sammy')[0])
1
2
3
4
5
6
<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> </br></br></a></div>
</div>
  • 각 가게 정보에 랭킹, 가게이름, 메뉴, 상세페이지로연결되는 url 이 포함 되어있음


2.4 랭킹 가져오기

1
2
tmp_one = soup.find_all('div', 'sammy')[0]
tmp_one.find(class_ = 'sammyRank').get_text()
1
'1'
  • find 메소드와 sammyRank 클래스를 이용하여 랭킹을 확보


2.5 가게이름과 메뉴 가져오기

1
tmp_one.find(class_ = 'sammyListing').get_text()
1
'BLT\r\nOld Oak Tap\nRead more '
  • 가게이름(Old Oak Tap)과 메뉴이름(BLT)은 sammyListing 클래스에 한번에 있음


1
2
3
4
5
6
import re

tmp_string = tmp_one.find(class_ = 'sammyListing').get_text()
print(re.split(('\n|\r\n'), tmp_string)[0])
print(re.split(('\n|\r\n'), tmp_string)[1])
print(re.split(('\n|\r\n'), tmp_string))
1
2
3
BLT
Old Oak Tap
['BLT', 'Old Oak Tap', 'Read more ']
  • 가게이름과 메뉴이름은 re 모듈의 split으로 간단히 구분 가능


2.6 상세페이지

1
tmp_one.find('a')['href']
1
'/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/'
  • 가게의 상세페이지로 가는 url은 상대 경로로, href로 찾으면 됨


1
2
print(url_base)
print(tmp_one.find('a')['href'])
1
2
https://www.chicagomag.com
/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/
  • 처음에 설정한 url_base를 사용하여 가게 상세 url과 합치면 될듯 하다.


1
2
from urllib.parse import urljoin
urljoin(url_base, tmp_one.find('a')['href'])
1
'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Old-Oak-Tap-BLT/'
  • urljoin을 사용하여 url_base와 가게 상제 페이지 주소를 합쳐줌(절대 주소)


1
2
3
tmp_one = soup.find_all('div', 'sammy')[10].find('a')['href']
print(tmp_one)
urljoin(url_base, tmp_one)
1
2
3
4
http://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Lula-Cafe-Ham-and-Raclette-Panino/


'http://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Lula-Cafe-Ham-and-Raclette-Panino/'
  • 11번쨰의 주소도 잘 합쳐짐을 확인함


2.7 50개 메뉴에 적용하기

1
2
3
4
5
6
7
8
9
10
11
rank = []
main_menu = []
cafe_name = []
url_add = []

for item in soup.find_all('div', 'sammy'):
    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_base, item.find('a')['href']))
1
2
3
4
print(len(rank), len(main_menu), len(cafe_name), len(url_add))
print(main_menu[:5])
print(cafe_name[:5])
url_add[:5]
1
2
3
4
5
6
7
8
50 50 50 50
['BLT', 'Fried Bologna', 'Woodland Mushroom', 'Roast Beef', 'PB&L']
['Old Oak Tap', 'Au Cheval', 'Xoco', 'Al’s Deli', 'Publican Quality Meats']
['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-Bologna/',
 'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Xoco-Woodland-Mushroom/',
 'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Als-Deli-Roast-Beef/',
 'https://www.chicagomag.com/Chicago-Magazine/November-2012/Best-Sandwiches-in-Chicago-Publican-Quality-Meats-PB-L/']
  • 전체 50개로 잘 가져와 진것 같음


2.8 데이터프레임 생성

1
2
3
4
5
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.tail()
RankCafeMenuURL
4546ChickpeaKuftahttp://www.chicagomag.com/Chicago-Magazine/Nov...
4647The Goddess and GrocerDebbie’s Egg Saladhttp://www.chicagomag.com/Chicago-Magazine/Nov...
4748ZenwichBeef Curryhttp://www.chicagomag.com/Chicago-Magazine/Nov...
4849Toni PatisserieLe Végétarienhttp://www.chicagomag.com/Chicago-Magazine/Nov...
4950Phoebe’s BakeryThe Gatsbyhttp://www.chicagomag.com/Chicago-Magazine/Nov...
  • 50개 자료에 대해 이름, 메뉴 , 상세페이지 URL까지 모두 정리 완료


1
df.to_csv('./data/best_sandwiches_list.csv', sep =',', encoding = 'utf-8')
  • csv 파일로 저장완료


3. 상세페이지


3.1 Data Load

1
2
df = pd.read_csv('https://raw.githubusercontent.com/hmkim312/datas/main/chicagosandwiches/best_sandwiches_list.csv', index_col=0)
df.head()
RankCafeMenuURL
01Old Oak TapBLThttps://www.chicagomag.com/Chicago-Magazine/No...
12Au ChevalFried Bolognahttps://www.chicagomag.com/Chicago-Magazine/No...
23XocoWoodland Mushroomhttps://www.chicagomag.com/Chicago-Magazine/No...
34Al’s DeliRoast Beefhttps://www.chicagomag.com/Chicago-Magazine/No...
45Publican Quality MeatsPB&Lhttps://www.chicagomag.com/Chicago-Magazine/No...
  • 데이터 불러옴


3.2 상세페이지 파싱

1
2
3
html = urlopen(df['URL'][0])
soup_tmp = BeautifulSoup(html, 'html.parser')
soup_tmp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE doctype html>

<html lang="en">
<head>
<!-- Urbis magnitudo. Fabulas magnitudo. -->
<meta charset="utf-8"/>
<style>a.edit_from_site {display: none !important;}</style>
<title>
  1. Old Oak Tap BLT |
  Chicago magazine
      |  November 2012
    </title>
...
</script>
<!--[if lt IE 9]>
<script  type="text/javascript" language="JavaScript" src="/core/media/themes/Respond/js/respond.js?ver=1473876729"></script>
<![endif]-->
<script language="JavaScript" src="/core/media/js/base.js?ver=1473876728" type="text/javascript"></script>
<script language="JavaScript" src="/core/media/themes/Respond/js/bootstrap.min.js?ver=1473876729" type="text/javascript"></script>
<script language="JavaScript" src="//maps.googleapis.com/maps/api/js?v=3.exp&amp;sensor=false" type="text/javascript"></script>
<script language="JavaScript" src="/theme_overrides/Respond/js/interstitial.js?ver=1524154906" type="text/javascript"></script>
<script language="JavaScript" src="/theme_overrides/Respond/js/newsletter-subscribe.js?ver=1524850607" type="text/javascript"></script>
<script language="JavaScript" src="/theme_overrides/Respond/js/RivistaGoogleDFP.js?ver=1447178886" type="text/javascript"></script>
<!-- godengo-monitor --></body>
</html>
  • 하나의 페이지 파싱


  • p태그에 addy 클래스에 정보가 있는듯 하다


3.3 가격 및 주소 정보 가져오기

1
2
price_tmp = soup_tmp.find('p', 'addy').get_text()
price_tmp
1
'\n$10. 2109 W. Chicago Ave., 773-772-0406, theoldoaktap.com'
  • 가격, 주소, 전화번호, 홈페이지 정보가 한번에 있음


1
2
price_tmp = re.split('.,', price_tmp)[0]
price_tmp
1
'\n$10. 2109 W. Chicago Ave'
  • 미국은 주소 뒤에 .,가 붙으니, 그걸 기준으로 split하여 가격과 주소만 가져옴


1
re.search('\$\d+\.(\d+)?', price_tmp).group()
1
'$10.'
  • 숫자로 시작하다가 꼭 .을 만나고 그 뒤에 숫자가 있을수도 있고 없을수도 있는 정규표현식으로 가격만 가져옴


1
2
end = re.search('\$\d+\.(\d+)?', price_tmp).end()
price_tmp[end+1:]
1
'2109 W. Chicago Ave'
  • 가격이 끝나는 지점의 위치를 이용해서 그 뒤는 주소로 저장


3.4 3개에 시범삼아 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
price = []
address = []

for idx, row in df[:3].iterrows():
    html = urlopen(row['URL'])
    soup_tmp = BeautifulSoup(html, 'lxml')
    
    gettings = soup_tmp.find('p', 'addy').get_text()
    
    price_tmp = re.split('.,', gettings)[0]
    tmp = re.search('\$\d+\.(\d+)?', price_tmp).group()
    price.append(tmp)
    
    end = re.search('\$\d+\.(\d+)?', price_tmp).end()
    address.append(price_tmp[end+1:])
    
    print(row['Rank'])
1
2
3
1
2
3
1
price, address
1
2
(['$10.', '$9.', '$9.50'],
 ['2109 W. Chicago Ave', '800 W. Randolph St', ' 445 N. Clark St'])
  • 잘되는듯 하다.


3.5 50개 모두 돌리기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
price = []
address = []

for idx, row in df.iterrows():
    html = urlopen(row['URL'])
    soup_tmp = BeautifulSoup(html, 'lxml')
    
    gettings = soup_tmp.find('p', 'addy').get_text()
    
    price_tmp = re.split('.,', gettings)[0]
    tmp = re.search('\$\d+\.(\d+)?', price_tmp).group()
    price.append(tmp)
    
    end = re.search('\$\d+\.(\d+)?', price_tmp).end()
    address.append(price_tmp[end+1:])
    
    time.sleep(0.5)
    print(row['Rank'], ' / ', '50')
1
2
3
4
5
6
7
8
1  /  50
2  /  50
3  /  50
...
47  /  50
48  /  50
49  /  50
50  /  50
1
len(price), len(address), len(df)
1
(50, 50, 50)
  • 잘 된듯 하다


3.6 데이터 프레임에 추가

1
2
3
4
5
6
df['Price'] = price
df['Address'] = address

df = df.loc[:, ['Rank', 'Cafe', 'Menu', 'Price', 'Address']]
df.set_index('Rank', inplace = True)
df.head()
CafeMenuPriceAddress
Rank
1Old Oak TapBLT$10.2109 W. Chicago Ave
2Au ChevalFried Bologna$9.800 W. Randolph St
3XocoWoodland Mushroom$9.50445 N. Clark St
4Al’s DeliRoast Beef$9.40914 Noyes St
5Publican Quality MeatsPB&L$10.825 W. Fulton Mkt
  • 완료되었음.


3.7 csv 저장

1
df.to_csv('./data/best_sandwiches_list2.csv', sep =',', encoding = 'utf-8')
This post is licensed under CC BY 4.0 by the author.