본문 바로가기
DEV/Web 개발

Web 개발 :: 프로젝트 코드 및 정리_TIL64

by 올커 2022. 12. 2.

■ JITHub 개발일지 64일차

□  TIL(Today I Learned) ::

웹 크롤링을 통한 네이버 맛집 데이터 크롤링

  1. API

  - Application Programming Interface, 프로그램과 프로그램 사이를 연결해주는 매개체

  - API를 이용해서 데이터를 불러오는 경우에는 데이터가 동적으로 변화하여 실시간으로 값을 불러오는 경우가 많다.

  - 크롭 개발자 도구의 Network 탭에서 웹사이트가 데이터를 요청하는 API를 확인할 수 있는데 이 API의 URL에 GET요청을 보냄을 통해 Json데이터를 얻을 수 있다.

 request.get(<url>)

 

  2. 웹 크롤링

  - 웹사이트에 Request를 통해 Html 데이터를 가져오고, Response를 통해 받은 Html데이터를 parsing, 즉 요소를 분리해내는 방식으로 필요한 데이터만 가져오는 것을 말한다.

  - 간단한 구조는 아래와 같았다.

# 예시) 네이버 뉴스에서 링크 가져오기
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://news.naver.com/")
bsObject = BeautifulSoup(html, "html.parser")

for link in bsObject.find_all('a'):
	print(link.text.strip(), link.get('hred'))

  - 개념을 이해하기에는 위의 코드가 편하였으나, 실제로 구글링을 해보면 다양한 크롤링 방식들이 많았다.

  - 또 어려웠던 점은 이전의 코드를 보아도, 네이버 지도 등은 접근할 수 있는 선택자(SELECTOR)를 변경하는 경우가 많아 시간이 지나면 나중에 코드를 다시 짜야 하는 경우가 많았다.

 

  - html을 가져와 분석하기 위해서는 BeautifulSoup라는 파이썬 라이브러리를 사용한다.

import requests
from bs4 import Beautifulsoup

header = {'User-agent' : 'Mozila/2.0'}		# ConnectionError 발생시 사용, ↓ 아래 headers=header추가
response = requests.get("http://news.naver.com", headers=header)
html = response.text
soup = Beautifulsoup(html, 'html.parser')

# 하나만 가져올 경우 → select_one
title = soup.select_one(".lnk_hdline_article")
print(title.text.strip())		# .strip()은 양쪽 공백을 삭제하기 위해 사용


# 여러개 가져올 경우 → select
titles = soup.select(".lnk_hdline_article")
for title in titles:
	title.text.strip()		# 태그 안의 텍스트 요소를 가져온다.
    url = link.attrs['href']		# href의 속성값을 가져온다.

   위와 같이 BeautifulSoup를 사용하면 선택된 태그에 대한 text등 다양한 정보들을 출력할 수 있다.

   위의 코드에서는 .text를 통해 텍스트 요소를 그대로 가져오거나,

   태그의 속성 안에있는 주소를 가져올 때에는 .attrs['href']를 통해 가져와야 한다.

  - 여기서 select, select_one의 괄호 안에 쓰는 것은 선택자이다.

    선택자는 class의 경우 앞에 '.'을 쓰고, id의 경우 앞에 '#'를 붙여준다.

  - 위의 코드에서 titles에 입력되는 결과값들은 파이썬 '리스트' 타입으로 담겨진다.

 

  그런데! BeautifulSoup만으로는 스크롤을 더 해서 보이는 결과들을 가져올 수 없다.

  그렇기 때문에 추가로 사용하는 것이 Selenium이다.

  이를 사용하려면 예전에는 크롬드라이버를 먼저 다운로드 받아서 사용해야 했지만, 현재는 자동설치하여 사용이 가능하다. 설치해서 사용하는 방법을 보면 OS에 맞는 드라이버를 다운로드 받은 후 작성하고 있는 파이썬 파일과 같은 경로에 압축을 푼다. 그리고 아래와 같이 코드를 작성한다.

from bs4 import BeautifulSoup
from selenium import webdriver

base_url = "https://search.naver.com/search.naver?where=view&sm=tab_jum&query="
keyword = input("검색어를 입력하세요 : ")
search_url = base_url + keyword

# selenium을 사용하면서 아래 2줄의 코드가 기존의 request를 대체한다.
driver = webdriver.Chrome()
driver.get(search_url)

html = driver.page_source

soup = Beautifulsoup(html, 'html.parser')
items = soup.select(".api_txt_lines.total_tit")

for e, item in enumerate(items, 1):
	print(f"{e}:{item.text}")

 - 자바스크립트를 이용하여 스크롤을 하려면 아래와 같이 코드를 수정해야 한다.

from bs4 import BeautifulSoup
from selenium import webdriver
import time		#

base_url = "https://search.naver.com/search.naver?where=view&sm=tab_jum&query="
keyword = input("검색어를 입력하세요 : ")
search_url = base_url + keyword

driver = webdriver.Chrome()
driver.get(search_url)

# 스크롤하기 (3회 반복)
time.sleep(3)	# 대기
for i in range(3):
	driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")		# 최대 높이만큼 스크롤해서 내려간다.
	time.sleep(1)	# 대기


html = driver.page_source

soup = Beautifulsoup(html, 'html.parser')
items = soup.select(".api_txt_lines.total_tit")

for e, item in enumerate(items, 1):
	print(f"{e}:{item.text}")
    
driver.quit()

  - driver.execute_script의 괄호 안에 자바스크립트 문법을 넣어 스크롤을 하도록 했다.

    1회만 하면 스크롤을 한번만 내리지만, 페이지에 따라 스크롤을 내리면 추가로 페이지가 나오는 곳이 있으므로, 그럴 때에는 위의 코드처럼 for문을 만들어서 해결한다.

  - 사실 selenium과 beautiful을 같이 사용하는 이유는 beautifulsoup이 훨씬 빠르기 때문이다. 그렇기 때문에 가져와야 할 데이터가 많을 경우 selenium을 사용하지 않을 수 있다면 beautifulsoup만 사용하는 것이 좋다.


이제 위의 내용을 인지하고 구글링을 통해 가져온 아래 3개의 네이버 지도를 가져오는 코드를 해석해보았다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC #selenium에서 사용할 모듈 import

import time
import requests
from bs4 import BeautifulSoup
import re
import csv


driver = webdriver.Chrome("./chromedriver") #selenium 사용에 필요한 chromedriver.exe 파일 경로 지정

driver.get("https://map.naver.com/v5/")

try:
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CLASS_NAME, "input_search"))
        ) #입력창이 뜰 때까지 대기
finally:
    pass

search_box = driver.find_element(By.CLASS_NAME, "input_search")
search_box.send_keys("서울 칵테일바")
search_box.send_keys(Keys.ENTER) #검색창에 "서울 칵테일바" 입력

time.sleep(7) #화면 표시 기다리기
frame = driver.find_element(By.CSS_SELECTOR, "iframe#searchIframe")

driver.switch_to.frame(frame)

time.sleep(3)
# 여기까지 iframe 전환




scroll_div = driver.find_element(By.XPATH, "/html/body/div[3]/div/div[2]/div[1]")
#검색 결과로 나타나는 scroll-bar 포함한 div 잡고
driver.execute_script("arguments[0].scrollBy(0,2000)", scroll_div)
time.sleep(2)
driver.execute_script("arguments[0].scrollBy(0,2000);", scroll_div)
time.sleep(2)
driver.execute_script("arguments[0].scrollBy(0,2000);", scroll_div)
time.sleep(2)
driver.execute_script("arguments[0].scrollBy(0,2000);", scroll_div)
time.sleep(2)
driver.execute_script("arguments[0].scrollBy(0,2000);", scroll_div)
time.sleep(2)
#여기까지 scroll
#맨 아래까지 내려서 해당 페이지의 내용이 다 표시되게 함

# csv 파일 생성
file = open('stores.csv', mode='w', newline='')
writer = csv.writer(file)
writer.writerow(["place", "rate", "address", "info", "image"])
final_result = []
time.sleep(1)
# # 반복 시작

i = 2
while i<=5: #몇 페이지까지 크롤링할 것인지 지정
    stores_box = driver.find_element(By.XPATH, "/html/body/div[3]/div/div[2]/div[1]/ul")
    stores = driver.find_elements(By.CSS_SELECTOR, "li._3t81n._1l5Ut") 
    #해당 페이지에서 표시된 모든 가게 정보

    for store in stores: #한 페이지 내에서의 반복문. 순차적으로 가게 정보에 접근
        name = store.find_element(By.CSS_SELECTOR, "span._3Yilt").text #가게 이름
        try:    
            rating = re.search('/span>(\d).', store.find_element(By.CSS_SELECTOR, "span._3Yzhl._1ahw0").get_attribute('innerHTML')).groups()[0]
        except:
            rating = ''
        time.sleep(3)
        # 평점 숫자 부분만 rating에 담음. 평점이 없는 경우가 있어 예외 처리
        try:
            img_src = re.search('url[(]"([\S]+)"', store.find_element(By.CSS_SELECTOR, "div.cb7hz.undefined").get_attribute('style')).groups()[0]
        except:
            img_src = ''
            #역시 대표 이미지가 없는 경우가 있어 예외 처리
        click_name = store.find_element(By.CSS_SELECTOR, "span._3Yilt")
        click_name.click() 
        # 가게 주소, 홈페이지 링크를 확인하려면 가게 이름을 클릭해 세부 정보를 띄워야 함.


        driver.switch_to.default_content()
        time.sleep(7)        
        ## switch_to.default_content()로 전환해야 frame_in iframe을 제대로 잡을 수 있다. 
        
        frame_in = driver.find_element(By.XPATH, '/html/body/app/layout/div[3]/div[2]/shrinkable-layout/div/app-base/search-layout/div[2]/entry-layout/entry-place-bridge/div/nm-external-frame-bridge/nm-iframe/iframe')

        driver.switch_to.frame(frame_in) 
        # 가게 이름을 클릭하면 나오는 세부 정보 iframe으로 이동
        time.sleep(3)
        try:
            address = re.search('서울\s(\w+)\s', driver.find_element(By.CSS_SELECTOR, "span._2yqUQ").text).groups()[0]
        except:
            address = ''
            #주소 정보 확인
        try:
            link_url = driver.find_element(By.CSS_SELECTOR, "a._1RUzg").text
        except:
            link_url = ''
            # 홈페이지 url 확인
        store_info = {
            'placetitle':name,
            'rate':rating,
            'address':address,
            'info':link_url,
            'image':img_src
        }
        #크롤링한 정보들을 store_info에 담고
        print(name, rating, address, img_src, link_url)
        print("*" * 50)
        final_result.append(store_info)
        # 출력해서 확인 후 final_result에 저장

        driver.switch_to.default_content()
        driver.switch_to.frame(frame)
        time.sleep(8)
        # 한 페이지 크롤링 끝
        
        # '2'페이지로 이동하는 버튼 클릭 후 i 1증가 
    next_button = driver.find_element(By.LINK_TEXT, str(i))
    next_button.click()
    i = i+1
    time.sleep(8)
    
    #while문이 종료되면 크롤링 종료

    for result in final_result: #크롤링한 가게 정보에 순차적으로 접근 & csv 파일 작성
        row = []
        row.append(result['placetitle'])
        row.append(result['rate'])
        row.append(result['address'])
        row.append(result['info'])
        row.append(result['image'])
        writer.writerow(row)
        
    print(final_result)
    #최종 결과 확인

  - 실제로 지도를 모두 불러오고 스크롤도 잘 진행하고, 페이지 넘김까지 진행은 잘 하고 있지만, 현재 선택자가 제대로 되어있지않아 결과에 null을 반환하고 있다. 

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC #selenium에서 사용할 모듈 import
from selenium.webdriver.common.by import By

driver = webdriver.Chrome("./chromedriver")

driver.get("https://map.naver.com/v5/") #네이버 신 지도 
try:
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CLASS_NAME, "input_search"))
        ) #입력창이 뜰 때까지 대기
finally:
    pass

search_box = driver.find_element(By.CLASS_NAME, "input_search")
search_box.send_keys("강남 카페")
search_box.send_keys(Keys.ENTER)

time.sleep(7) #화면 표시 기다리기
frame = driver.find_element(By.CSS_SELECTOR, "iframe#searchIframe")

driver.switch_to.frame(frame)

time.sleep(3)

# 크롤링
for p in range(20):
    # 5초 delay
    time.sleep(2)
    
    js_script = "document.querySelector(\"body > app > layout > div > div.container > div.router-output > "\
                "container > shrinkable-layout > div > app-base > search-layout > div.main.-top_space.ng-star-inserted > combined-search-list > salt-search-list > nm-external-frame-bridge > nm-iframe > iframe\").innerHTML"
    raw = driver.execute_script("return " + js_script)

    html = BeautifulSoup(raw, "html.parser")

    contents = html.select("div > div.ps-content > div > div > div .item_search")
    for s in contents:
        search_box_html = s.select_one(".search_box")

        name = search_box_html.select_one(".title_box .search_title .search_title_text").text
        print("식당명: " + name)
        try:
            phone = search_box_html.select_one(".search_text_box .phone").text
        except:
            phone = "NULL"
        print("전화번호: " + phone)
        address = search_box_html.select_one(".ng-star-inserted .address").text
        print("주소: " + address)

        print("--"*30)
    # 다음 페이지로 이동
    try:
        next_btn = driver.find_element(By.CSS_SELECTOR, "button.btn_next")
        next_btn.click()
    except:
        print("데이터 수집 완료")
        break

# 크롭 웹페이지를 닫음
driver.close()

 

 

import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep
from bs4 import BeautifulSoup
import re
import json

import chromedriver_autoinstaller
from webdriver_manager.chrome import ChromeDriverManager
import webbrowser

driver = webdriver.Chrome(ChromeDriverManager().install())


# --크롬창을 숨기고 실행-- driver에 options를 추가해주면된다
# options = webdriver.ChromeOptions()
# options.add_argument('headless')

url = 'https://map.naver.com/v5/search'
driver = webdriver.Chrome('./chromedriver')  # 드라이버 경로
# driver = webdriver.Chrome('./chromedriver',chrome_options=options) # 크롬창 숨기기
driver.get(url)
key_word = '강남구'  # 검색어
# time.sleep(1)


# css 찾을때 까지 10초대기
def time_wait(num, code):
    try:
        wait = WebDriverWait(driver, num).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, code)))
    except:
        print(code, '태그를 찾지 못하였습니다.')
        driver.quit()
    return wait

# css를 찾을때 까지 10초 대기    
time_wait(10, 'div.input_box > input.input_search')

#################################
# req = driver.page_source
# soup = BeautifulSoup(req, 'html.parser')


# 검색창 찾기
# search = driver.find_element(By.CSS_SELECTOR, 'div.input_box > input.input_search')

#################################
# search = soup.select('div.input_box > input.input_search')
# search = soup.find_element('div.input_box > input.input_search')
# search = soup.select_one('#container > shrinkable-layout > div > app-base > search-input-box > div > div.search_box > div')

search = driver.find_element(By.CSS_SELECTOR, 'div.input_box > input.input_search')

search.send_keys("강남구 치킨")  # 검색어 입력
search.send_keys(Keys.ENTER)  # 엔터버튼 누르기


sleep(1)


# frame 변경 메소드
def switch_frame(frame):
    driver.switch_to.default_content()  # frame 초기화
    driver.switch_to.frame(frame)  # frame 변경


# 페이지 다운
def page_down(num):
    body = driver.find_element(By.CSS_SELECTOR, 'body')
    body.click()
    for i in range(num):
        body.send_keys(Keys.PAGE_DOWN)


# frame 변경
switch_frame('searchIframe')
page_down(40)
sleep(5)


# 매장 리스트
store_list = driver.find_element(By.CSS_SELECTOR, '._1EKsQ')
# 페이지 리스트
next_btn = driver.find_element(By.CSS_SELECTOR, '._2ky45 > a')

# dictionary 생성
store_dict = {'매장정보': []}
# 시작시간
start = time.time()
print('[크롤링 시작...]')

# 크롤링 (페이지 리스트 만큼)
for btn in range(len(next_btn))[1:]:  # next_btn[0] = 이전 페이지 버튼 무시 -> [1]부터 시작
    store_list
    for data in range(len(store_list)):  # 매장 리스트 만큼
        page = driver.find_element(By.CSS_SELECTOR, '.OXiLu')
        page[data].click()
        sleep(2)
        try:
            # 상세 페이지로 이동
            switch_frame('entryIframe')
            time_wait(5, '._3XamX')
            # 스크롤을 맨밑으로 1초간격으로 내린다.
            for down in range(3):
                sleep(1)
                driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

            # -----매장명 가져오기-----
            store_name = driver.find_element(By.CSS_SELECTOR, '._3XamX').text

            # -----평점-----
            try:
                store_rating_list = driver.find_element(By.CSS_SELECTOR, '._1A8_M').text
                store_rating = re.sub('별점', '', store_rating_list).replace('\n', '')  # 별점이라는 단어 제거
            except:
                pass
            print(store_rating)

            # -----주소(위치)-----
            try:
                store_addr_list = driver.find_element(By.CSS_SELECTOR, '._1aj6-')
                for i in store_addr_list:
                    store_addr = i.find_element(By.CSS_SELECTOR, '._1h3B_').text
            except:
                pass
            print(store_addr)
            # -----전화번호 가져오기-----
            try:
                store_tel = driver.find_element(By.CSS_SELECTOR, '._3ZA0S').text
            except:
                pass

            print(store_tel)
            # -----영업시간-----
            try:
                store_time_list = driver.find_element(By.CSS_SELECTOR, '._2vK84')  # 아니 태그가 그세 바뀌네ㅡ,.ㅡ
                for i in store_time_list:
                    store_time = i.find_element(By.CSS_SELECTOR, '._3uEtO > time').text
            except:
                pass
            print(store_time)

            # -----메뉴-----
            try:
                menu_list = driver.find_element(By.CSS_SELECTOR, '._1XxR5')
                menu_name = []  # 메뉴이름
                menu_price = []  # 메뉴가격

                for i in menu_list:
                    name = i.find_element(By.CSS_SELECTOR, '._1q3GD').text
                    menu_name.append(name)

                    price = i.find_element(By.CSS_SELECTOR, '._3IAAc').text  # 텍스트를 넣을 변수 생성
                    price = re.sub('원\d{2},\d{3}', '', price)  # 할인 전 가격 제거  # 재정의
                    menu_price.append(price)  # 추가

            except:
                pass

            # -----메뉴2 (다른 태그의 메뉴)-----
            try:
                menu_list_two = driver.find_element(By.CSS_SELECTOR, '._20R25')
                menu_name_two = []
                menu_price_two = []

                for i in menu_list_two:
                    name_two = i.find_element(By.CSS_SELECTOR, '._2E0Gk').text
                    name_two = re.sub('\n사진|\n대표', '', name_two)

                    price_two = i.find_element(By.CSS_SELECTOR, '._3GJcI').text

                    menu_name_two.append(name_two)
                    menu_price_two.append(price_two)

            except:
                pass

            # 메뉴 리스트 합체
            menus = menu_name + menu_name_two
            prices = menu_price + menu_price_two

            print(menus)
            print(prices)
            # -----키워드 리뷰 가져오기-----
            try:
                keyword_list = driver.find_element(By.CSS_SELECTOR, '._28hFN')  # 키워드가 담긴 리스트 클릭
                keyword_list.click()

            except:  # 키워드리뷰 없으면 다음 음식점으로
                print('키워드리뷰 없음 >>> 다음으로',)
                switch_frame('searchIframe')
                continue

            try:
                keyword_review_list = driver.find_element(By.CSS_SELECTOR, '._3FaRE')  # 리뷰 리스트
                kwd_title = []
                kwd_count = []
                sleep(2)

                for i in keyword_review_list:
                    keyword_title = i.find_element(By.CSS_SELECTOR, '._1lntw').text  # 키워드리뷰
                    keyword_count = i.find_element(By.CSS_SELECTOR, '.Nqp-s').text   # 리뷰를 선택한 수

                    # db에 넣을 때 편의를 위해 요청하였음
                    title_re = re.sub('"', '', keyword_title) \
                        .replace('양이 많아요', '1').replace('음식이 맛있어요', '2').replace('재료가 신선해요', '3') \
                        .replace('가성비가 좋아요', '4').replace('특별한 메뉴가 있어요', '5').replace('화장실이 깨끗해요', '6') \
                        .replace('주차하기 편해요', '7').replace('친절해요', '8').replace('특별한 날 가기 좋아요', '9').replace(
                        '매장이 청결해요',
                        '10') \
                        .replace('인테리어가 멋져요', '11').replace('단체모임 하기 좋아요', '12').replace('뷰가 좋아요', '13').replace(
                        '매장이 넓어요',
                        '14') \
                        .replace('혼밥하기 좋아요', '15')

                    title_num = list(map(str, range(1, 16)))  # 1~15만 리스트에추가 (이외에 다른 키워드들은 추가하지않음)
                    count_keyword = re.sub('이 키워드를 선택한 인원\n', '', keyword_count)
                    if title_re in title_num:
                        kwd_title.append(title_re)
                        kwd_count.append(count_keyword)
                    else:
                        pass
            except:
                pass
            kwd_count = list(map(int, kwd_count))  # int 형변환

            print(kwd_title)
            print(kwd_count)

            # -----썸네일 사진 주소-----
            try:
                thumb_list = driver.find_element(By.CSS_SELECTOR, '.cb7hz') \
                    .value_of_css_property('background-image')  # css 속성명을 찾는다
                store_thumb = re.sub('url|"|\)|\(', '', thumb_list)  # url , (" ") 제거
            except:
                pass
            print(store_thumb)

            # ---- dict에 데이터 집어넣기----
            dict_temp = {
                'name': store_name,
                'tel': store_tel,
                'star': store_rating,
                'addr': store_addr,
                'time': store_time,
                'menu': menus,
                'price': prices,
                'kwd': kwd_title,
                'kwd_count': kwd_count,
                'thumb': store_thumb
            }

            store_dict['매장정보'].append(dict_temp)

            print(f'{store_name} ...완료')
            switch_frame('searchIframe')
            sleep(1)

        except:
            print('ERROR!' * 3)

    # 다음 페이지 버튼
    if page[-1]:  # 마지막 매장일 경우 다음버튼 클릭
        next_btn[-1].click()
        sleep(2)
    else:
        print('페이지 인식 못함')
        break

print('[데이터 수집 완료]\n소요 시간 :', time.time() - start)
driver.quit()  # 작업이 끝나면 창을닫는다.

# json 파일로 저장
with open('data/store_data.json', 'w', encoding='utf-8') as f:
    json.dump(store_dict, f, indent=4, ensure_ascii=False)

  - 다음으로 나온 2개의 코드도 둘 다 동일하게 선택자 문제가 발생하였는데, 하나씩 풀어가며 코드를 변경해보아야겠다.

반응형

댓글