Networks/Project

SK networks AI Camp - Toy Project 크롤링 코드 리뷰

코딩하는 Español되기 2024. 8. 10. 20:30

현재 코드를 짜서 화면에 구현하는 것까지 완료하였습니다. 코드를 리뷰하면서 

지금까지 진행한 걸 다시 적어보면서 코드를 다시 한번 공부해 보는 시간을 가지겠습니다.

(* 크롤링하다가 오류가 발생하여 1달을 주기로 끊어서 진행함)

(* 달이 바뀔 때마다 변경할 사항
  : 구현 부분의 첫 번째 줄 숫자, 5번째 push_2024_game에서의 두 번째 인자, json 저장 시 파일 이름)

(* 2023.01 ~ 2024.07까지 5,9,10,11,12월은 경기 없음)

(* 2023년을 진행할 경우 go_to_2024의 7번째 부분에 XPath를 아래 코드로만 변경)

 규칙으로 보아 현재 월이 마지막 div가 1이고 연도가 줄어들수록 1이 증가하는 것으로 보임

# 2023년 선택 XPATH
"/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[1]/section/div[2]"

# 2024년 선택 XPATH
"/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[1]/section/div[1]"

크롤링 코드 흐름

1. 사이트 접속 및 찾고자 하는 연, 월 선택 : go_to_2024 함수 → click_month 함수 → select_month 함수

2. 월별 게임 최대 수 검색 : find_month_child 함수

3. 게임 클릭 : push_2024_game 함수

4. 일당 게임 자식요소 찾고 세트 함수 접속 (push_2024_game 함수 안)

    :   find_day_child 함수  → 게임 클릭 → push_2024_set 함수 

5.  세트 버튼의 자식요소 찾고 for문 돌리기 (push_2024_set 함수 안)

    : → find_set_child 함수 → 필요한 데이터 찾기 → 필요한 값을 추가 → 다시 원래 화면으로 돌아가기

      * 이유 : 뒤로 가기를 해도 처음 화면으로 돌아가기 때문

      → 세트 수만큼 반복 후 push_2024_set 함수 끝

6. push_2024_set이 끝난 후 잠시 대기 후 다시 원하는 월 선택(push_2024_game 함수 안)  

    - go_to_2024 함수를 재실행 (처음 페이지로 돌아가기 때문에 하나의 게임을 끝내고 나면 다시 눌러줘야 함)

    - 즉 3번만 제외하고 1,2,4,5번을 반복

    - 월별 게임 최대수(바깥쪽 for문)와 일당 게임 최대 수(안쪽 for문)의 for문 종료 후 함수 종료

7. 자동으로 브라우저 닫기 및 데이터 확인

8. json 파일로 저장하여 크롤링을 다시 하는 시간을 줄이기

 

느낀 점

1. 크롤링을 할 경우 시간이 걸리더라도 전체 시간 단축을 위해 예외처리가 필수적

2. 시간이 좀 걸리더라도 페이지 로드가 안 되는 경우가 있어 time.sleep이나 driver.implicitly_wait 걸어주기

3. 자식 요소를 찾는 값을 저장하는 child_count가 0이면 에러가 발생하여 1로 수정함. 왜 해결되었는지는 의문

4. Copy full XPath를 통해 요소를 찾는 것이 제일 좋은 것으로 보임

크롤링 코드

    ○ Import

import pandas as pd
import time 
from selenium import webdriver 
from selenium.webdriver.common.by import By 
from selenium.webdriver import Keys, ActionChains
import pyperclip
from sqlalchemy import create_engine
import re
from collections import defaultdict
import json
import requests
from lxml import html
from selenium.common.exceptions import NoSuchElementException

 

    ○ 사이트 접속 및 변수 선언

driver = webdriver.Chrome()
url = "https://esports.op.gg/schedules/lck"
driver.get(url)
driver.maximize_window()
driver.implicitly_wait(5)
time.sleep(3)
lck_2024_data_2 = list()
child_count = 1
# 연도/시간/경기 스코어 데이터
score_data = '/html/body/div[1]/div[2]/div/main/div[1]/div[1]'
# 긁어올 상세 게임데이터
need_data = '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div[2]/div/div[2]/div[1]/div[2]'
game_time = ''
detail = ''

[score_data & need_data Pull XPATH]

 

    ○ 연 블록 & 월 블록 클릭 및 월 선택 함수

     * 각 월을 선택할 때 마지막 div만 숫자가 변하는 것을 확인 So, format을 통해 동적으로 넣어줌

        ● go_to_2024( ) : 연도 블록을 찾아서 클릭 →  2024년의 XPATH를 찾아서 클릭 → 달 블록 함수 → 달 선택 함수

                                      결국 2024년 선택과 월 선택을 다 담당하기에 이동하고자 하는 연/월을 클릭해 주는 함수라 보면 됨

        ● click_month( ) : PullXPATH를 통해 월 블록 클릭

        ●select_month( ) : PullXPATH를 통해 월을 동적 선택 

def go_to_2024(p_i):
    # 연도 클릭
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[1]/div").click()
    driver.implicitly_wait(5)
    time.sleep(1.5)
    # 2024년으로 이동
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[1]/section/div[1]").click()
    driver.implicitly_wait(5)
    time.sleep(1.5)
    click_month()
    driver.implicitly_wait(5)
    time.sleep(1.5)
    select_month(p_i)
    driver.implicitly_wait(5)

def click_month():
    # 월 블록 클릭
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[2]/div").click()
    driver.implicitly_wait(5)
    time.sleep(1)

def select_month(i):
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[2]/section/div[{}]".format(i)).click()
    driver.implicitly_wait(5)
    time.sleep(1)

 

 

     ○ for 문을 돌릴 횟수를 정해주기 위한 자식 요소 찾기 함수

         * 트러블 : find_day_child() 함수에서 no such element 오류가 계속 발생하여 창 새로고침 후 다시 찾는 걸로 해결

         ● find_month_child( ) : ul의 자식 요소 개수인 li의 개수를 찾아서 달에 존재하는 게임 한 일자 수를 찾기

                                                 push_2024_game() 함수의 바깥쪽 for문을 돌릴 횟수를 설정

         ● find_day_child( ) : find_month_child()의 ul 안에 li의 자식 요소를 찾아 하루에 존재하는 경기 수 찾기

                                            push_2024_game() 함수의  안 쪽 for 문 횟수 설정

         ● find_set_child( ) : 세트 수의 자식 요소를 찾아 세트 수만큼 for문 돌리기

                                            push_2024_set() 함수의 for문 횟수 설정

보라색 네모가 find_month_child()로 찾는 게임 수로 4월 달 기준 6개

빨간색 네모는 find_day_child()로 찾는 일별 게임 수로 4월 3일 기준 1, 4월 4일 1, 4월 6일 1.... 4월 14일 1이 반환

def find_month_child():
    current_url = driver.current_url

    # 페이지 소스 가져오기
    page_source = driver.page_source

    # lxml을 사용하여 파싱
    tree = html.fromstring(page_source)

    # 특정 태그의 XPath를 통해 자식 요소 찾기
    xpath = '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul'

    elements = tree.xpath(xpath)

    # if elements:
        # 첫 번째 요소의 자식 요소 개수 찾기
    child_elements = elements[0].getchildren()
    child_count = len(child_elements)
    return child_count

def find_day_child(i):
    current_url = driver.current_url

    # 페이지 소스 가져오기
    page_source = driver.page_source

    # lxml을 사용하여 파싱
    tree = html.fromstring(page_source)

    # 특정 태그의 XPath를 통해 자식 요소 찾기
    xpath = '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul/li[{}]/ul'.format(i)  # 예: '//div[@id="content"]'
    elements = tree.xpath(xpath)
    try:
        # if elements:
            # 첫 번째 요소의 자식 요소 개수 찾기
        child_elements = elements[0].getchildren()
        child_count = len(child_elements)
    except NoSuchElementException:
        driver.refresh()
        driver.implicitly_wait(5)
        time.sleep()
        find_day_child(i)
    finally:
        return child_count

def find_set_child():
    current_url = driver.current_url

    # 페이지 소스 가져오기
    page_source = driver.page_source

    # lxml을 사용하여 파싱
    tree = html.fromstring(page_source)
            ##/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul
    # 특정 태그의 XPath를 통해 자식 요소 찾기
    xpath = '/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul' 
    elements = tree.xpath(xpath)

    # if elements:
        # 첫 번째 요소의 자식 요소 개수 찾기
    child_elements = elements[0].getchildren()
    child_count = len(child_elements)
    return child_count

 

    ○ 게임 누르기 함수 및 Set 번호 누르기 함수

        ● push_2024_game(find_month_child()에서 반환받은 값, 월)

          - 달마다 돌리기 때문에 달의 자식 요소 찾기 블록을 한 번 실행하여 인자로 전달

          - 받은 인자를 바깥쪽 for문에 1부터 p_month까지 실행

          - 해당 블록 안에서 일마다 게임 횟수를 찾는 find_day_child() 실행

          - find_day_child() 함수를 통해 얻은 값으로 else 문 안에 for 문을 실행

          - 트러블 : 자식 요소가 1개인 경우 no such element 에러가 발생하여 if문을 통해 1개인 경우 통제하여 해결

                          하루에 1개의 게임이 존재하는 경우에는 li 뒤에 숫자가 붙지 않고 li만 있었음(9번째 코드)

          - 20번째 코드 :'/html/body/div [1]/div [2]/div/main/div [2]/div/div [2]/ul/li[{}]/ul/li[{}]'. format(i, j)

                                 아래의 사진과 같은 형태로 li의 숫자가 변하여 format으로 할당

           게임에 접속한 후 push_2024_set을 통해 set번호를 누르면서 필요한 데이터를 긁어오기

        ● push_2024_set() 세트 번호 누르기 함수

           - find_set_child 함수를 통해 Set블록의 자식 요소를 찾아서 반환받은 값 + 1까지 for 문 반복

             (= 세트 수만큼 돌리기)

           - 에러가 발생하면 refresh 후 다시 찾기 button도 1세트부터 반복

           - 데이터를 긁어오는 부분에서 가장 많이 에러가 발생하였다. 기본적으로 이전에 선언한 score_data와 need_data
             를 찾고 요소를 찾지 못하면(페이지에서 로드되지 않거나 그냥 못 찾는 경우 빈번) 

             execpt로 들어가 refresh 한 후 다른 요소를 text로 받아옴

          - 마지막으로 리스트에 값을 더해준 후 원래 페이지로 돌아감

# 게임 누르기
def push_2024_game(p_month, p_i):
    for i in range(1, p_month +1):
        # (일별)ul안의 각 li 안의 ul의 자식요소 개수 파악(각 일별 게임 갯수)
        # 일별 게임 최대 수
        during_day = find_day_child(i)
        time.sleep(2)
        if during_day == 1:
            driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul/li[{}]/ul/li'.format(i)).click()
            driver.implicitly_wait(5)
            time.sleep(1.5)
            push_2024_set()
            driver.implicitly_wait(5)
            time.sleep(0.5)
            go_to_2024(p_i) # 2023년 1월 
        else:    # (일별)ul안의 각 li 안의 ul의 자식요소 개수 파악(각 일별 게임 갯수)
            for j in range(1, during_day + 1): 
                driver.implicitly_wait(5)
                time.sleep(2)                        
                driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul/li[{}]/ul/li[{}]'.format(i, j)).click()
                driver.implicitly_wait(5)
                time.sleep(1.5)
                push_2024_set()
                driver.implicitly_wait(5)
                time.sleep(0.5)
                go_to_2024(p_i) # 2023년 1월 

        
# 게임 내에서 set 번호 누르기
def push_2024_set():
    set_num = find_set_child()
    driver.implicitly_wait(5)
    time.sleep(1.5)

    for k in range(1, set_num + 1):         
            try:
                driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul/button[{}]'.format(k)).click()
                driver.implicitly_wait(10)
                time.sleep(7)
            except NoSuchElementException:
                driver.refresh()
                driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul/button[{}]'.format(k)).click()
                driver.implicitly_wait(10)
                time.sleep(5)
            finally:
                # 값 추가
                try:
                    game_time = driver.find_element(By.XPATH, score_data).text
                    detail = driver.find_element(By.XPATH, need_data).text
                    driver.implicitly_wait(5)
                    time.sleep(1.5)
                except NoSuchElementException:
                    driver.refresh()
                    time.sleep(1.5)
                    game_time = driver.find_element(By.XPATH, score_data).text
                    detail = driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div[1]/div/div[2]/div[1]/div[2]/div/ul').text
                    driver.implicitly_wait(5)
                    time.sleep(1.5)
                finally:
                    lck_2024_data_2.append(game_time)
                    lck_2024_data_2.append(detail)
                    lck_2024_data_2.append('='*50)
    # 끝나면 다시 원래 화면으로 돌아가기
    driver.get(url)
    driver.implicitly_wait(5)
    time.sleep(2)

 

    ○ 해당 함수들을 사용하여 크롤링 구현

        - 처음 접속 시 웹에서 현재 날짜로 되어 있기에 go_to_2024 (i) 함수를 통해 i월로 이동

        - 한 달마다 끊어서 크롤링하여 먼저 월의 자식 요소 개수 찾기(월 블록)

        - push_2024_game(월의 자식 요소 개수, 찾고자 하는 월)

        - 받아온 데이터 값 형태(구분을 위해 '='*50을 append 해줌)

go_to_2024(2)
# 달별 게임 최대 수
during_month = find_month_child()
# 게임 클릭
push_2024_game(during_month, 2)

driver.implicitly_wait(3)
time.sleep(2)
# 저장될때까지 기다렸다가 창 자동 종료
driver.close()

 

    ● Json 파일로 저장

        - LCK_연도_data_월. json로 파일을 저장

# json 모듈 로드
import json

# json 파일로 저장
with open('LCK_2024_data_2.json', 'w') as f : 
	json.dump(lck_2024_data_2, f)

 

○ 전체 코드

더보기
import pandas as pd
import time 
from selenium import webdriver 
from selenium.webdriver.common.by import By 
from selenium.webdriver import Keys, ActionChains
import pyperclip
from sqlalchemy import create_engine
import re
from collections import defaultdict
import json
import requests
from lxml import html
from selenium.common.exceptions import NoSuchElementException

# 드라이브 열기
driver = webdriver.Chrome()
url = "https://esports.op.gg/schedules/lck"
driver.get(url)
driver.maximize_window()
driver.implicitly_wait(5)
time.sleep(3)

# 변수 선언
lck_2024_data_2 = list()
child_count = 1
# 연도/시간/경기 스코어 데이터
score_data = '/html/body/div[1]/div[2]/div/main/div[1]/div[1]'
# 긁어올 상세 게임데이터
need_data = '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div[2]/div/div[2]/div[1]/div[2]'
game_time = ''
detail = ''

# 함수
# 월 블록 클릭 함수
def click_month():
    # 월 블록 클릭
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[2]/div").click()
    driver.implicitly_wait(5)
    time.sleep(1)
# 월 선택 함수
def select_month(i):
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[2]/section/div[{}]".format(i)).click()
    driver.implicitly_wait(5)
    time.sleep(1)
    
    
# 자식 요소 찾기 함수
# 월의 자식 요소(월당 게임 수)
def find_month_child():
    current_url = driver.current_url

    # 페이지 소스 가져오기
    page_source = driver.page_source

    # lxml을 사용하여 파싱
    tree = html.fromstring(page_source)

    # 특정 태그의 XPath를 통해 자식 요소 찾기
    xpath = '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul'

    elements = tree.xpath(xpath)

    # if elements:
        # 첫 번째 요소의 자식 요소 개수 찾기
    child_elements = elements[0].getchildren()
    child_count = len(child_elements)
    return child_count
    
# day의 자식요소 찾기 함수(일당 게임 수)
def find_day_child(i):
    current_url = driver.current_url

    # 페이지 소스 가져오기
    page_source = driver.page_source

    # lxml을 사용하여 파싱
    tree = html.fromstring(page_source)

    # 특정 태그의 XPath를 통해 자식 요소 찾기
    xpath = '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul/li[{}]/ul'.format(i)  # 예: '//div[@id="content"]'
    elements = tree.xpath(xpath)
    try:
        # if elements:
            # 첫 번째 요소의 자식 요소 개수 찾기
        child_elements = elements[0].getchildren()
        child_count = len(child_elements)
    except NoSuchElementException:
        driver.refresh()
        driver.implicitly_wait(5)
        time.sleep()
        find_day_child(i)
    finally:
        return child_count

# 세트의 자식 요소 찾기 함수(세트 번호 찾기)
def find_set_child():
    current_url = driver.current_url

    # 페이지 소스 가져오기
    page_source = driver.page_source

    # lxml을 사용하여 파싱
    tree = html.fromstring(page_source)
            ##/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul
    # 특정 태그의 XPath를 통해 자식 요소 찾기
    xpath = '/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul' 
    elements = tree.xpath(xpath)

    # if elements:
        # 첫 번째 요소의 자식 요소 개수 찾기
    child_elements = elements[0].getchildren()
    child_count = len(child_elements)
    return child_count
    
    
# 연도 이동 함수(결국 달 블록클릭 및 달 선택 함수가 포함되어 있어 이동하는 함수)
def go_to_2024(p_i):
    # 연도 클릭
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[1]/div").click()
    driver.implicitly_wait(5)
    time.sleep(1.5)
    # 2024년으로 이동
    driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div/div/div[1]/div[1]/section/div[1]").click()
    driver.implicitly_wait(5)
    time.sleep(1.5)
    click_month()
    driver.implicitly_wait(5)
    time.sleep(1.5)
    select_month(p_i)
    driver.implicitly_wait(5)
    

# 게임 누르기 및 세트만큼 반복하여 데이터 뽑는 함수

# 게임 누르기
def push_2024_game(p_month, p_i):
    for i in range(1, p_month +1):
        # (일별)ul안의 각 li 안의 ul의 자식요소 개수 파악(각 일별 게임 갯수)
        # 일별 게임 최대 수
        during_day = find_day_child(i)
        time.sleep(2)
        if during_day == 1:
            driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul/li[{}]/ul/li'.format(i)).click()
            driver.implicitly_wait(5)
            time.sleep(1.5)
            push_2024_set()
            driver.implicitly_wait(5)
            time.sleep(0.5)
            go_to_2024(p_i) # 2023년 1월 
        else:    # (일별)ul안의 각 li 안의 ul의 자식요소 개수 파악(각 일별 게임 갯수)
            for j in range(1, during_day + 1): 
                driver.implicitly_wait(5)
                time.sleep(2)                        
                driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/ul/li[{}]/ul/li[{}]'.format(i, j)).click()
                driver.implicitly_wait(5)
                time.sleep(1.5)
                push_2024_set()
                driver.implicitly_wait(5)
                time.sleep(0.5)
                go_to_2024(p_i) # 2023년 1월 

        
# 게임 내에서 set 번호 누르기   
def push_2024_set():
    set_num = find_set_child()
    driver.implicitly_wait(5)
    time.sleep(1.5)

    for k in range(1, set_num + 1):         
            try:
                driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul/button[{}]'.format(k)).click()
                driver.implicitly_wait(10)
                time.sleep(7)
            except NoSuchElementException:
                driver.refresh()
                driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[1]/div[2]/ul/button[{}]'.format(k)).click()
                driver.implicitly_wait(10)
                time.sleep(5)
            finally:
                # 값 추가
                try:
                    game_time = driver.find_element(By.XPATH, score_data).text
                    detail = driver.find_element(By.XPATH, need_data).text
                    driver.implicitly_wait(5)
                    time.sleep(1.5)
                except NoSuchElementException:
                    driver.refresh()
                    time.sleep(1.5)
                    game_time = driver.find_element(By.XPATH, score_data).text
                    detail = driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/main/div[2]/div/div[2]/div[1]/div/div[2]/div[1]/div[2]/div/ul').text
                    driver.implicitly_wait(5)
                    time.sleep(1.5)
                finally:
                    lck_2024_data_2.append(game_time)
                    lck_2024_data_2.append(detail)
                    lck_2024_data_2.append('='*50)
    # 끝나면 다시 원래 화면으로 돌아가기
    driver.get(url)
    driver.implicitly_wait(5)
    time.sleep(2)
    


# 구동 및 저장
go_to_2024(2)
# 달별 게임 최대 수
during_month = find_month_child()
# 게임 클릭
push_2024_game(during_month, 2)

driver.implicitly_wait(3)
# 저장될때까지 기다렸다가 창 자동 종료
time.sleep(2)
driver.close()

# json 모듈 로드
import json

# json 파일로 저장
with open('LCK_2024_data_2.json', 'w') as f : 
	json.dump(lck_2024_data_2, f)