본문 바로가기

Project : 이미지 유사도 기반 추천 서비스

[Project] 이미지 유사도 기반 추천 서비스(2) : 크롤링

0. 지난 포스팅에서는..

더보기

데이터 분석 교육과정을 수료하며, 마지막 한 달은 개인 프로젝트를 진행하게 되었고

'내가 간 맛집과 비슷한 장소를 추천해 주는 서비스'를 기획해 보게 되었습니다.

더 자세히 보고 싶다면? 여기로 >

 

1. 어떤 사진을, 어디서 수집할까?

제 프로젝트의 첫 단계이자, 가장 중요한 부분이기도 한 데이터 수집 단계!

 

제가 모아야 할 데이터는 맛집의 내부 사진이었는데요.

그렇다면 어떤 지역의 맛집 사진들을 수집할 것이냐?

우선.. 학원은 금천구에 있었지만

일단 맛집이 정말 정말 없었고.. 😑

기왕 데이터를 수집할 거면 더 많은 사람들이 선호하는

'핫플'들이 모여있는 지역으로 하고 싶었습니다. (금천 탈락!😏)

그래야 추천 서비스에 더 많은 사람들이 관심을 가져줄 테니까요~

 

일단 핫플하면 떠오르는 지역들이 있었지만

뇌피셜이 아닌지 확인해 보기 위해 관련 자료를 찾아보고,

참고자료 : 힙당동? 힙지로? 요즘 사람들이 가장 많이 찾는 장소는? (어센트 코리아)

 

검색 트렌드도 같이 확인해 봤어요.

네이버 검색어 트렌드

 

확인 결과, 지역은 홍대 / 강남 / 성수로 결정!

맛집 검색 시 지도상으로도 겹치지 않는 지역들이기에

세 군데를 중심으로 데이터를 수집해 보기로 했어요.

 

크롤링할 사이트의 후보군은

네이버 지도 vs 카카오 지도 vs 구글 지도가 있었고,

그중 네이버를 택했는데, 그 이유는 아래 두 가지였습니다~

1) 평소에 네이버 지도 어플을 쓰고 있어서 친숙함

2) 크롤링이 더 쉬워 보이는 UI : 카카오는 가게 사진을 찾으려면 새 창을 한 번 더 띄워야 했음

(실제로 네이버 크롤링이 더 쉬웠느냐? 그건 모릅니다.. 네이버만 해봤기 때문에😥)

 

2. 크롤링 코드 보기

크롤링 코드는 주피터 랩에서 실행했고,

여러 시행착오 끝에 완성한 코드는 깃허브에 올려두었습니다!

[깃허브] 코드 완성본 보러 가기 >

 

크롤링은 수업 때 배우긴 했지만, 직접 해보는 건 처음이라 아주 험난한 과정이었는데요..ㅠ

아무리 구글링을 해봐도 제 프로젝트에 딱! 맞는 샘플을 찾을 수 없어서

이 코드, 저 코드를 섞어 보고 새싹 강사님께도 많은 도움을 받았습니다.. (정말 감사합니다..)

 

시나리오와 간단한 설명을 덧붙일 테니,

코드를 사용하신다면 아래 설명을 참고해 주세요~

 

✅ 크롤링 시나리오

먼저, 진행 방향은 이렇습니다!

1) 네이버 지도에서 '홍대맛집' 검색 결과 페이지를 띄운다.

2) 검색 결과 중 첫 번째 가게의 상세정보를 클릭한다.

3) 우측화면의 하위탭 중 '사진' > '내부'를 순서대로 들어간다.

4) 나오는 사진들을 로컬에 저장한다.

 

✅ 코드 자세히 보기

아래는 시나리오 중 1)과 2)를 위한 코드입니다.

while True:
    # 크롬드라이버 실행
    driver = webdriver.Chrome()

    # 크롬 드라이버에 url 주소 넣고 실행
    # 여기서 '' 안에 새 창으로 띄울 주소를 넣어주세요
    driver.get('https://map.naver.com/p/search/%ED%99%8D%EB%8C%80%EB%A7%9B%EC%A7%91?c=14.00,0,0,0,dh')
    time.sleep(3) # 페이지가 완전히 로딩되도록 3초동안 기다림

    # Switch to the iframe
    iframe = driver.find_element(By.XPATH, '//*[@id="searchIframe"]')
    driver.switch_to.frame(iframe)

    # 가게명을 찾아 변수에 저장 (xpath 이용방식)
    shop_name_tab = driver.find_element(By.XPATH, '//*[@id="_pcmap_list_scroll_container"]/ul/li[1]/div[1]/a/div/div/span[1]')
    time.sleep(3)

    shop_name = shop_name_tab.text
    shop_name_tab.click()
    time.sleep(1)

    shop_folder_path = os.path.join(path_folder, shop_name)

    # shop_name에서 공백 제거
    shop_name_without_spaces = shop_name.replace(" ", "")

    # 중복 여부 확인 및 출력
    # 이 코드는 1차 이후에 반복 시 중복가게를 잡기 위해 넣었습니다.
    if os.path.exists(shop_folder_path) or os.path.exists(os.path.join(path_folder, shop_name_without_spaces)):
        print("중복가게")
        driver.quit()
    else:
        print("중복아님")
        break

제가 맨 처음 크롤링 코드를 작성할 때 가장 어려웠던 점이 'XPATH' 때문이었는데요!

도대체 저게 뭘까..? 싶었는데

웹 페이지에서 내가 접근하고 싶은 영역의 위치를 잡아줄 때 사용하는 부분이었습니다.

 

예) 가게명을 변수에 저장할 때, XPATH 가져오는 방법

1) 원하는 영역을 우클릭해서 개발자 도구를 열어주고

2) 개발자 도구의 'Elements'에서 해당 부분을 우클릭 > 'Copy' > 'Copy XPath' 해주면 아래 내용이 복사됩니다.

//*[@id="_pcmap_list_scroll_container"]/ul/li[1]/div[1]/a/div/div/span[1]

복사한 내용을 'shop_name_tab' 변수에 넣어서 클릭할 수 있도록 해줬어요.

 

다음은 시나리오 중 3)을 위한 코드입니다.

# 가게명 클릭 시 우측에 새로 생성되는 화면으로 이동하기 위한 코드입니다.
driver.switch_to.default_content()

iframe = driver.find_element(By.XPATH, '//*[@id="entryIframe"]')
driver.switch_to.frame(iframe)

# 메타데이터를 찾아 변수에 저장 (xpath 이용방식)
shop_meta_tab = driver.find_element(By.XPATH, '//*[@id="app-root"]/div/div/div/div[2]/div[1]')
shop_meta = shop_meta_tab.text

menu_tabs = driver.find_elements(By.CSS_SELECTOR, 'div.flicking-camera  > a > span')

photo_idx = np.argmax(np.array([t.text for t in menu_tabs]) == '사진')
photo_menu = menu_tabs[photo_idx]
photo_menu.click()
time.sleep(3)

 # '내부' 탭으로 드래그 이동하기
source_element = driver.find_elements(By.CSS_SELECTOR, 'div.place_fixed_subtab > div > div > div > div > span > a')[2]
source_element

# Create an ActionChains object
actions = ActionChains(driver)

# Perform the drag to the left action
# Adjust the x_offset based on your requirement (negative values move to the left)
x_offset = -50  # Adjust as needed
actions.drag_and_drop_by_offset(source_element, x_offset, 0).perform()

원하는 사진을 얻기 위해서는 2개의 하위 탭('사진', '내부')을 클릭해야 하는데,

'사진' 탭과 달리, 위 사진처럼 '내부' 탭은 첫 화면에 노출되지 않고 숨겨져 있어서

마우스 오버 시 볼 수 있는 (>) 화살표를 클릭하거나,

드래그를 해야 볼 수 있었어요.

그래서 드래그하는 코드를 추가해 주었고,

만약 한 번의 드래그로 '내부' 탭이 나오지 않을 경우에는

x_offset = -50  # Adjust as needed
actions.drag_and_drop_by_offset(source_element, x_offset, 0).perform()

위 코드를 실행해서 한 번 더 드래그를 해주었습니다. (생략 가능)

detail_tabs = driver.find_elements(By.CSS_SELECTOR, 'div.flicking-camera > span > a')

indoor_idx = np.argmax(np.array([t.text for t in detail_tabs]) == '내부')
indoor_menu = detail_tabs[indoor_idx]
indoor_menu.click()
time.sleep(3)

print(shop_meta)

드래그로 '내부' 탭이 보이면 해당 부분을 클릭했습니다.

 

다음은 시나리오 중 4)을 위한 코드입니다.

# 이미지 저장

interior_thumbnail = driver.find_elements(By.CSS_SELECTOR, "a.place_thumb > img")

link_thumbnail = []

for img in interior_thumbnail:

    link_thumbnail.append(img.get_attribute('src')) 

# path_folder의 경로는 각자 저장할 폴더의 경로를 적어줄 것(ex.img_download)
path_folder = r"C:\Users\sesac3\project1\img_download3_hongdae"

if not os.path.isdir(path_folder):
    os.mkdir(path_folder)

# 이미지 다운로드 및 메타데이터 수집
meta = []

for i, link in enumerate(link_thumbnail):
    response = requests.get(link)
    if response.status_code == 200:
        extension = os.path.splitext(link)[-1].lower()

        # shop_name에서 공백 및 특수 문자 제거하여 폴더명 생성
        folder_name = ''.join(e for e in shop_name if e.isalnum())

        folder_path = os.path.join(path_folder, folder_name)

        # Create folder if it doesn't exist
        if not os.path.exists(folder_path):
            os.mkdir(folder_path)

        # 이미지 파일명 생성
        image_name = f'{shop_name}_{i}.{extension}'
        file_path = os.path.join(folder_path, image_name)

        meta.append((i, file_path, link))
        
        with open(file_path, 'wb') as file:
            file.write(response.content)

# 메타데이터를 DataFrame으로 변환 및 CSV 파일로 저장

meta.append((i, file_path, link, shop_meta))

meta_df = pd.DataFrame(meta, columns=['Index', 'File Path', 'Image Link', 'Shop Meta'])
meta_df.to_csv(os.path.join(path_folder, f'{folder_name}_meta.csv'), index=False)

driver.quit()  # 작업이 끝나면 창을닫는다.

이 코드까지 실행하면 로컬에 가게명으로 폴더가 생성되고,

해당 폴더 안에 내부 사진과 메타 데이터가 담긴 csv 파일이 저장됩니다.

예시 이미지

 

이렇게 진행하면 가게 하나의 데이터 수집이 완료됩니다!

 

물론, 방법을 더 찾아보면

한 번의 실행으로 여러 가게의 데이터를 수집할 수도 있었겠지만..

크롤링이 처음이라 원했던 데이터가 잘 수집되는지 확인하면서 진행하고 싶었고,

이후의 과정도 남은 기간에 소화해야 하기에

크롤링 코드를 더 연구하기보다는, 약간의 노가다를 하는 방법을 택했습니다. 😅

 

다음에 크롤링을 진행할 때는

더 많은 데이터를 한 번에 수집할 수 있는 시나리오를 진행해보고 싶어요!

 

3. 결과물 & 인사이트

 

이렇게 원했던 사진 데이터를 수집해 보았는데요.

 

가게를 하나씩 확인하며 저장하다 보니,

수집 과정에서도 지역별 특징이 살짝 보여서 재밌었습니다!

(강남 : 회식 장소 제일 많음, 성수 : 강남/홍대에 비해 맛집이 적음 등..)

 

그리고 아쉬웠던 점으로는

폴더명과 파일명을 더 체계적으로 구분하지 못한 부분이 있었어요.

이후 과정에서 폴더명과 파일명을 계속 사용했는데

(데이터를 분류하거나, 스트림릿으로 웹화면에 보여줄 때)

사전에 미리 체계를 잡아두지 않았더니

300개의 폴더명, 8천 개의 파일명을 바꿔서 쓰는 일이 생겼거든요. 😥

 

사전에 좀 더 탄탄한 설계를 했었다면

이미 수집한 데이터를 변경하는 번거로움이 없었을 것 같아요!

다음에도 크롤링을 진행한다면

이런 점을 꼭 보완해서 진행해야겠습니다.

 

긴 글 읽어주셔서 감사하고,

수정할 부분이 있다면 댓글로 알려주시면 감사하겠습니다!

그럼 다음 포스팅에서 만나요!