웹 스크래핑(Web Scraping)

r5_jun·2023년 5월 8일
0

서론

글쓴이도 대부분의 사람들처럼 처음 웹 스크래핑을 배울때 네이버 뉴스로 연습을 했으며 연습문제들을 다 풀고나서 웹 스크래핑은 별것 아니라고 생각했었던 적이 있었다.
하지만 실무에서 데이터를 수집하기 위해 웹 스크래핑을 하다 보면 모든 웹 사이트가 네이버 뉴스, 네이버 쇼핑, 아마존의 웹사이트 처럼 스크래핑하기 편한 것은 아니라는 걸 깨닫게 된다 (대기업은 다르다...)

오늘은 웹 스크래핑하기 재미있어(귀찮아)보이는 웹 사이트를 발견해 글쓴이의 블로그 첫 게시글의 소재로 써보고자 한다.

사전작업

웹사이트 소개

스크래핑할 웹 사이트 : 데이터바우처
오늘 스크래핑해볼 웹 사이트는 데이터바우처 사이트로 데이터바우처라는 정부지원사업에 대한 정보가 공지되는 사이트이다.
루트 페이지를 보면 평범한 공공기관 웹사이트의 모습이다. (악의 기운이 스멀스멀 올라온다)
우리는 통합검색에 있는 모든 상품에 대한 상품명, 기업명(영문), 담당자연락처, 이메일, 태그를 스크래핑 해보도록 하자

Robots.txt 확인하기

글쓴이가 웹 스크래핑하기 전 첫번째로 확인하는 것은 대상 웹 사이트의 웹 스크래핑 허용 여부이다
웹 스크래핑같은 자동화 코드는 해당 사이트의 서버에 과부하를 줄수도 있고 데이터는 해당기업의 중요한 자산이기 때문에 스크래핑이 허용이 되는지? 허용된다면 어디까지 허용이 되는지 살펴봐야한다.
허용 여부는 Robots.txt를 보고 확인할 수 있다.
Robots.txt는 해당 사이트의 루트(root)페이지 url에 /robots.txt를 치면 다운로드 받을 수 있다.
예시 : https://www.naver.com/robots.txt (네이버의 robots.txt)

하지만 우리가 오늘 스크래핑하고자 하는 사이트는 아무리 시도를 해봐도 robots.txt를 찾을 수 없다...(어느정도 예상은 했었다)
https://kdata.or.kr/datavoucher/index.do/robots.txt
https://kdata.or.kr/datavoucher/robots.txt
https://kdata.or.kr/robots.txt
그냥 공개 데이터니까 써도 된다고 생각하고 서버에 무리 주지 않도록 time.sleep을 많이 주고 해야겠다...
Robots.txt를 해석하는 방법은 구글에 이미 잘 설명한 글이 많기 때문에 검색해보시길 바란다.

새 탭으로 열리는지 확인

Robots.txt를 확인한 다음 개인적으로 중요하게 생각하는 것이 새 탭으로 열리는지 여부이다.
보통 ctrl을 누른채로 클릭을 하면 새 탭으로 열기가 된다.
웹 스크래핑을 할 때 글쓴이는 이 기능을 이용해 부모 페이지에서 자식 페이지를 열고 스크래핑 후 자식 페이지를 닫고 부모 페이지에서 또 다시 다른 자식 페이지를 열어 스크래핑을 반복하는 식으로 코드를 작성한다.
하지만 새 탭으로 열기가 되지 않는다면 코드를 다른식으로 작성해야하기 때문에 (글쓴이 기준에서) 코딩이 조금 귀찮아진다.
그리고 역시 이 사이트는 새 탭으로 열리지 않는다... 젠장
일반적인 경우 ctrl+클릭을 하면 위와 같이 새 탭으로 열린다하지만 데이터바우처 사이트는 ctrl+클릭을 하더라도 기존 탭에서 클릭한 페이지를 열어준다.
이렇게되면 위에서 설명한 새 탭으로 열기 기능을 활용한 방법은 사용하지 못하고 클릭하고 뒤로가기를 반복해야한다.

개인적으로 느끼는 뒤로가기의 단점

  • 부모->자식(뒤로가기 클릭)->부모 페이지로 돌아왔을 때 부모 페이지가 이전에 접속했을 때와 달라질 수 있음
  • 뒤로가기 버튼을 누르면 탭을 닫는 것보다 속도가 느리다 (많은 데이터를 수집해야할 때 문제가 됨)

자세한 내용은 뒤에 코드를 설명에서 언급하겠다

페이지 번호가 URL에 있는지 확인

또 개인적으로 확인하는 것은 페이지 번호가 URL에 반영이 되어 있는지 여부이다.
네이버 뉴스의 정치 뉴스페이지를 예시로 들어보겠다.

페이지1 URL: https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=100
페이지2 URL: https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=100#&date=%2000:00:00&page=2
페이지3 URL: https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=100#&date=%2000:00:00&page=3
페이지4 URL: https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=100#&date=%2000:00:00&page=4

페이지 마다의 URL을 확인하다보면 2페이지부터 URL 제일뒤에 page=2, page=3, page=4처럼 페이지 번호가 반영되어 있다
페이지1 URL 뒤에 #&date=%2000:00:00&page=1를 붙여줘도 똑같은 페이지나 나온다
이런식으로 URL에 페이지 번호가 반영이 되어있다면 URL에 숫자를 차례대로 변경하여 다음 페이지로 넘어갈 수 있어 편리하다.

하.지.만.
역시나 역시나 역시나 우리가 스크래핑하려는 사이트는 그런 기능따위는 없다.

페이지1: https://kdata.or.kr/datavoucher/is/selectPortalSearch.do
페이지2: https://kdata.or.kr/datavoucher/is/selectPortalSearch.do
페이지3: https://kdata.or.kr/datavoucher/is/selectPortalSearch.do
...
페이지611: https://kdata.or.kr/datavoucher/is/selectPortalSearch.do

모든 번호의 페이지 URL이 같다...
이렇게 되면 Selenium과 반복문을 이용해 페이지 버튼을 하나하나 클릭해 넘겨야해서 개인적으로 조금 귀찮아진다.

스크래핑을 간편하게 해줄 기능이 하나도 없다는 절망적인 사실을 확인했다.
눈물 닦고 코드 작성으로 넘어가보자

코드

먼저 필요한 라이브러리를 import하자

라이브러리 임포트(import)

# selenium의 Web Driver 관련 라이브러리
from selenium import webdriver
# chromedriver 설치경로를 매개변수로 받아서 chromedriver를 시작하고 중지하는 역할
from selenium.webdriver.chrome.service import Service as ChromeService
# selenium에서 키보드를 사용할 수 있게 해주는 역할
from selenium.webdriver.common.keys import Keys
# By를 이용해 웹의 element를 찾을 수 있게 해주는 역할
from selenium.webdriver.common.by import By 

# 안티 스크래핑 방지를 위해 중간에 잠시 멈추는 용도
import time 

# 데이터 프레임(Data Frame) 형식으로 저장 위한 라이브러리
import pandas as pd

# csv파일을 다루기 위한 라이브러리
import csv

페이지 번호가 URL에 반영되어 있지 않아 Beautiful Soup은 쓰지 않고 Selenium을 이용해 모두 해결하겠다.

빈(empty) CSV파일 만들기

이번 스크래핑의 목적은 기업들의 정보를 수집하는 것이다.
데이터바우처 상품 리스트 페이지에 들어가 아무 상품을 클릭하면 아래와 같은 상품 페이지를 볼 수 있다
스크래핑할 속성을 확인하고 미리 CSV파일 첫줄에 속성명을 적어주자

# pandas(pd)를 이용해 DataFrame 만들어 주기
df = pd.DataFrame(columns=['idx',
							'상품명',
							'기업명 (영문)', 
							'담당자연락처', 
							'이메일', 
							'태그'])

# 위에 만든 DataFrame을 이용해 원하는 경로에 "데이터바우처_상품_1.csv"이라는 csv 파일을 만들어줌
# 인덱스index는 따로 만들어줄거니까 False해줌
# 한글을 사용하니 encoding은 "utf-8-sig"를 주겠음
df.to_csv('C:/Users/r5_jun/Desktop/Workspace/데이터바우처_크롤링/데이터바우처_상품_1.csv', index=False, encoding='utf-8-sig')

idx인덱스, 상품명, 기업명 (영문), 담당자연락처, 이메일, 태그라는 데이터 속성명을 CSV파일에 넣어주었다
CSV파일을 열어보면 아래와 같다

Chrome Web driver로 크롬 브라우저 열기

크롬 웹 드라이버(chromedriver.exe)를 이용해 빈 크롬 브라우저를 열어준다
chromedriver에 대한 내용은 설명하면 너무 장황해질거 같아서 줄였다. 인터넷이 이미 잘 설명해놓은 글이 많으니 모른다면 검색해보길 바란다
(다음에 시간날때 chromedriver에 대한 글도 써보겠다)

#chromedriver.exe의 경로를 받아와서 driver에 선언
driver = webdriver.Chrome('chromedriver_win32/chromedriver')
#위 코드를 실행하면 빈 크롬 페이지가 열림

# 3초 기다리기
time.sleep(3)

위 코드를 실행해 빈 크롬 브라우저가 열렸다

스크래핑할 웹 페이지 열기

# url변수에 스크래핑할 웹 페이지의 url를 복사에 붙여줌
url = "https://kdata.or.kr/datavoucher/is/selectPortalSearch.do"

# url에 정의된 주소 열기
driver.get(url)
# 페이지가 다 열리도록 3초 기다려주기
time.sleep(3)

이렇게 하면 빈 크롬 페이지가 url변수에 설정해둔 페이지를 열어준다!!이.렇.게.

상품 페이지 스크래핑 함수

방금전 빈 CSV파일 만들던때 처럼 아무 상품이나 클릭해 X.path를 이용해 text를 가져오는 기능을 하는 함수 코드를 만들자

def prod_scraper(idx, product_cnt):
    
    # 상품 수 만큼 반복(product_cnt는 다음 상품 리스트 페이지 스크래핑 함수에서 받아올 것임)
    for j in range(1,product_cnt+1):
        
        # 상품 페이지(자식 페이지)에 접속
        driver.find_element(By.XPATH, '//*[@id="listType"]/div['+str(j)+']/div[2]/a').send_keys(Keys.ENTER)
        # 페이지에 접속할 수 있도록 3초 기다리기
        time.sleep(3)

        # 상품명(product_name) 가져오기
        product_name=driver.find_element(By.XPATH, '//*[@id="prdcInfoDiv"]/div[1]/ul/li[3]/dl/dt').text
        # 가져올 text가 없을때 "가공서비스"라고 값을 넣어주기
        if product_name!="":
            print(product_name)
        else:
            product_name="가공서비스"
            print(product_name)

        # 기업명(company_name) 가져오기
        company_name=driver.find_element(By.XPATH, '//*[@id="frm"]/div[2]/div/div/div/div[2]/div[1]/ul[1]/li[1]/dl/dd').text
        print(company_name)

        # 담당자연락처(phone_num) 가져오기
        phone_num=driver.find_element(By.XPATH, '//*[@id="frm"]/div[2]/div/div/div/div[2]/div[1]/ul[2]/li[1]/dl/dd').text
        print(phone_num)

        # 이메일(email) 가져오기
        email=driver.find_element(By.XPATH, '//*[@id="frm"]/div[2]/div/div/div/div[2]/div[1]/ul[2]/li[2]/dl/dd').text
        print(email)

        # 태그(tag) 가져오기
        tag=driver.find_element(By.XPATH, '//*[@id="prdcInfoDiv"]/div[1]/ul/li[1]/span[1]').text
        # 가져올 text가 없을때 "가공"이라고 값을 넣어주기
        if tag!="":
            print(tag)
        else:
            tag="가공"
            print(tag)

        # 위에서 만들어둔 빈 csv에 한 행씩 저장하기
        with open("C:/Users/r5_jun/Desktop/Workspace/데이터바우처_크롤링/데이터바우처_상품_1.csv", 'a+', newline="", encoding="utf-8-sig") as write_obj:
            csv_writer = csv.writer(write_obj)
            csv_writer.writerow([idx, product_name, company_name, phone_num, email, tag])

        # 인덱스 숫자 1 더하기
        idx+=1    

        # 뒤로가기 버튼 누르기 
        driver.back()
        # 상품 리스트 페이지(부모 페이지)로 완전히 넘어갈 수 있도록 3초 기다리기
        time.sleep(3)

        # 해당 상품 스크래핑 완료 표시 
        print("----------------------------"+str(j)+'번 상품 완료--------------------------------------')
    
    # idx를 리턴
    return idx

다음 페이지 이동하는 함수

def move_to_next_page(idx,page_cnt):
    
    # 페이지 수만큼 반복
    for i in range(1, page_cnt+1):

        # 현재 스크래핑하는 상품 리스트 페이지(부모 페이지) 번호를 page에 정의 
        page = driver.find_element(By.XPATH, '//*[@id="paging"]/strong').text
        print(page+" 페이지")

        # 스크롤 다운코드
        for c in range(0,3):
            driver.find_element(By.TAG_NAME, 'body').send_keys(Keys.PAGE_DOWN)
            time.sleep(1)

        # 해당 페이지의 상품 갯수(product_cnt) 세기
        product_list=driver.find_elements(By.XPATH, '//*[@id="listType"]/div/div[2]/a')
        product_cnt=len(product_list)
        print("상품갯수 "+str(product_cnt))

        # 상품 페이지 스크래핑는 함수 쓰기
        idx=prod_scraper(idx, product_cnt)
    
        ##################################################################
        # 페이지의 정보 가져오기
        # 페이지를 왔다갔다하면 a태그가 초기화되어 한번 더 a태그를 가져왔음
        a_tag_click=driver.find_elements(By.XPATH, '//*[@id="paging"]/a')
		###################################################################

        # i(현재 페이지 순서)+1 = 다음페이지 순서 
        # 다음페이지로 이동 버튼 클릭
        a_tag_click[i+1].click()
        print("다음 페이지 이동 버튼 누름")
        time.sleep(5)
        
    return idx


a_tag_click[11]일때 ▶(다음 10페이지 이동 버튼)을 눌러 11~20페이지를 가진 부모 페이지로 넘어가게 된다

끝내는 시점을 정하는 함수

# 마지막 페이지 번호를 input해주자 
# 현재는 마지막 페이지가 611번이라 611를 input
total_page = input()
divid10_page = int(total_page)//10 + 1

input입력창이 뜨면 마지막 페이지 번호를 입력해준다

# 인덱스 정의
idx = 1
# 총 페이지가 611이기 떄문에 페이지 10개를 62번 돌리도록 for문 작성
for a in range(1,divid10_page+1):
    
    # 모든 a태그를 리스트에 정의 
    a_tag_list=driver.find_elements(By.XPATH, '//*[@id="paging"]/a')
    # 페이지 갯수=리스트 갯수
    a_tag_cnt=len(a_tag_list)
    print("a태그수"+str(a_tag_cnt))
    page_cnt=a_tag_cnt-3
    print("페이지수"+str(page_cnt))
    
    # 10 페이지씩 돌리기 람수 쓰기
    idx=move_to_next_page(idx, page_cnt)

끝내기 시점정하기 함수에서 a태그를 가져오지만 다음 페이지 이동하는 함수에서 한번 더 a태그를 한번 더 가져오는 이유는 페이지를 이동할때마다 a태그 element가 변하게되기 때문이다.
아래에 보듯이 페이지 이동할때마다 페이지 버튼의 element가 변한다 (사실 이런 건 처음봐서 많이 헤매었다)

처음 페이지1 버튼의 element: ce46d32f-3d13-40e0-8cb1-8f455bd9c81c
다음 페이지1 버튼의 element: ce46d32f-3d13-40e0-8cb1-8f455bd9c81c
그 다음 페이지1 버튼의 element: a5f73abc-2e09-4de7-bea9-765219ea5ac3
그 다음 페이지1 버튼의 element: b52a4385-805e-4cb7-95bb-ed1f1ca29814
그 다음 페이지1 버튼의 element: 15860e6e-ea9f-46df-b102-08b2ad092ec2
...

따라서 이전에 받아온 a태그(페이지 이동 버튼 element)를 사용하면 StaleElementReferenceException에러가 뜨기 때문에 사용할 수 없었고 다시 a태그를 받아와 페이지 이동을 할 수 있었다.

결과

아래와 같이 모두 스크래핑 하였다

느낀점

사실 이번 웹 스크래핑 프로젝트를 하면서 옛 생각이 많이 났다. 학부 시절 첫 프로그래밍 과제도 Java 웹 스크래핑을 이용했었고 직장에서는 정보를 수집하기 위해 웹 스크래핑을 할 일이 많았다. 많은 양의 데이터를 빠짐없이 수집할 수 있는 코드를 작성하기 위해 몇 날 며칠을 야근했고 개발자 분들도 많이 귀찮게히곤 했었다.
하지만 이제는 누군가 나에게 웹 스크래핑에 대해 물어보기도 하고 수집요청을 한다면 이렇게 반나절 만에 코드를 짤 수 있게 된 것이 개인적으로는 자랑스럽다.
최근 제자리에 멈춰있는 느낌이라 불안감이 많았는데 이 프로젝트 덕분에 나름 위안을 많이 받았다.

다른 분야도 이런 식으로 발전하기를 바라며 이만 줄인다.

마지막으로 이 글에 문제되는 점이 있으면 수정 또는 삭제할 것이며
코드나 글 내용 중 이상한 점이 있다면 여러분의 생각이 무적권 맞으니 댓글을 남겨주시면 감사하겠습니다.

profile
나도 내가 뭔지 모르겠습니다

0개의 댓글