원광성

노벨피아 부수기 #1 (데이터 스크래핑) 본문

소프트웨어

노벨피아 부수기 #1 (데이터 스크래핑)

SASmammoth 2024. 3. 20. 13:55

0. 서론

노벨피아의 마스코트 '노벨짱' - 출처 나무위키

 

웹소설 플랫폼 노벨피아를 즐겨보는 주인장.

어느 날 어떤 장르/제목등의 소설이 잘 나가는지에 관해 탐구해 보기로 결심하게 되는데...

 

 

1. 이론

노벨피아에서 어떤 장르가 잘 나가는지, 얼마나 많은 작가들이 연중을 때리고 탈주를 쳤는지 등을 알아보려 합니다.

 

노벨피아에는 현재 약 6,000여개의 소설이 있습니다.

플러스 비성인작품 기준

 

여려 분들이 약 6,000여 개의 소설을 모두 손으로 통계낼 의지가 없다면 데이터 스크래핑이라는것을 해야 합니다.

 

노벨피아와 같은 보통의 회사는 자기네들의 고유의 데이터를 가져가서 재사용하는 걸 싫어합니다.

스크래핑 봇은 공개된 웹사이트를 돌아다니며 원하는 데이터를 가져올 수 있습니다.

 

데이터 스크래핑은 다음과 같은 단계를 거칩니다.

  1. 1. 먼저, 스크래퍼 봇이라고 하는, 정보를 빼내오는 데 사용되는 코드가 특정 웹 사이트에 HTTP GET 요청을 보냅니다.
  2. 2. 웹사이트가 응답하면 스크래퍼는 HTML 문서를 분석해서 특정 패턴의 데이터를 찾습니다.
  3. 3. 추출된 데이터는 스크래퍼 봇의 작성자가 설계한 특정 형식으로 변환됩니다.

출처 - CLOUDFLARE  https://www.cloudflare.com/ko-kr/learning/bots/what-is-data-scraping/

 

여기에서 우리는 2단계의 스크래퍼가 찾을 '특정 패턴의 데이터'를 지정하고, 3단계의 '작성자가 설계한 특정 형식'을 제작해야 합니다.

 

우리는 이를 파이썬과 브라우저 자동화 라이브러리인 셀레니움을 사용할 것입니다.

파이썬과 셀레니움 로고

 

 

2. 코딩

2.1  출력 데이터 형식 세팅

가장 먼저 스크롤링한 데이터를 어떤 형식으로 출력할지를 정해야 합니다.

 

주인장은 영원한 여고생짱(17세)이기 때문에 19금 소설을 볼 수가 없습니다.

따라서 스크롤링할 소설은 플러스/비성인 작품 6,349(2024-03-19 기준) 개입니다.

노벨피아의 플러스 작품을 조회순으로 정렬한 창
개별 소설로 들어온 창

개별 소설은 다양한 속성이 있습니다.

 

우리는 그중에서 중요해 보이는 23개의 아래와 같은 속성을 스크래핑할 것입니다.

(제목, 소설id, 독점, 완결, 연재중단, 연재지연, 15세, 작가, 소개, 조회수, 회차, 선소, 추천수, 태그1~10)

 

따라서 데이터의 형식은 (6,349 rows x 24 colums)의 테이블 형태가 될 것입니다.

스포금지

 

2.2 특정 데이터 패턴 지정

웹 페이지에서 특정 데이터를 추출하려면 그 데이터가 있는 요소 알아야 합니다.

 

보통 요소는 그 요소의 XPath나 CLASS_NAME 등을 검색하여 찾아냅니다.

네이버페이 증권 웹페이지에서 코스피 지수를 나타내는 요소 - 오른쪽은 개발자도구(F12)열어 요소를 보고있다

XPath와 CLASS_NAME은 HTML을 분석하여 얻을 수 있는데 이를 쉽게 하기 위해선 개발자도구(F12)를 이용한다.

 

노벨피아에서 요소를 찾기 위해 F12를 누르는 순간...

노벨피아의 보안

저작권 보호를 위해 개발자모드를 종료하라는 문구가 나옵니다.

하지만 노벨피아가 오락가락해서 나올 수도 안 나올 수도 있습니다

 

이를 해결하기 위해 제가 생각해 낸 방법이 있습니다.

 

※ 주의※

아래의 방법은 오직 공정이용의 목적만으로 사용하였고

이 방법을 악용하여 저작물의 무단복제나 배포를 할시 처벌받을 수 있습니다.

 

우클릭 - 다른이름으로 저장

먼저 웹 사이트에서 우클릭 - 다른 이름으로 저장합니다.

 

Notepad++로 수정

그 후 노트패드와 같은 텍스트편집기를 이용하여 열어줍니다.

 

노벨피아의 HTML파일

HTML파일에서 끌려가는 링크가 있는 부분의 스크립트를 지워버린 뒤 저장하고 열어주면...

 

개발자도구를 사용할 수 있습니다.

이제 파이참과 인터넷을 이용해 열심히 코딩을 해 봅시다.

 

2.3  코드

코드는 맨 마지막에 있습니다.

 

 

3. 스크래핑

코드가 정상적으로 동작하면 아래 화면과 같이 자동으로 데이터를 스크래핑할 것이다.

 

스크롤링중...

 

 

4. 결과물

자기 전에 코드를 돌려놓고 아침에 일어나면 아래와 같은 csv파일이 생성되어 있을 것입니다.

 

total_novel.csv
2.79MB

 

 

이 파일을 잘 열어보면 아래와 같습니다.

csv파일을 엑셀로 연 모습

 

우리는 조회순으로 정렬된 페이지를 스크래핑하여서 자동으로 정렬되어 있습니다.

이 데이터를 통해 다양한 방법으로 통계를 작성을 하는 건 다음시간에 계속하겠습니다.

아래의 링크를 이용해 주세요

 

 

노벨피아 부수기 #2 (데이터 분석)

0. 서론 지난 시간 우리는 열심히 작업한 끝에 노벨피아의 데이터를 얻을 수 있었습니다. 노벨피아 부수기 #1 (데이터 스크래핑) 0. 서론 웹소설 플랫폼 노벨피아를 즐겨보는 주인장. 어느 날 어떤

distantstar.tistory.com

 

 

5. 마치며

이번 시간에는 노벨피아의 데이터 스크래핑을 해보았습니다.

이 작업을 하면서 노벨피아의 보안이 상당히 취약하단걸 알 수 있었습니다.

 

비록 노벨피아가 보안이 약한 데다가 서버도 오락가락하고 운영을 못하지만, 노벨피아만 한 곳 없습니다.

여러분들 노벨피아를 많이 사랑해 주세요.

 

 

6. 코드

import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import Select

root = "https://novelpia.com"

driver = webdriver.Chrome('chromedriver.exe')

driver.maximize_window()

#노벨피아의 전체 소설 데이터를 pd의 dataFrame으로 저장함 속성은 아래와 같고 각 컬럼은 하나의 소설이다.
total_novel_df = pd.DataFrame(columns=['제목', '소설id','독점', '완결', '연재중단', '연재지연', '15세', '작가', '소개', '조회수', '회차', '선호', '추천수', '태그_1',
                                       '태그_2', '태그_3', '태그_4', '태그_5' ,'태그_6' ,'태그_7', '태그_8', '태그_9', '태그_10'])

#노벨피아 전체 소설 데이터프레임의 열수이다
total_novel_df_index = 0


#노벨피아 플러스 소설의 조회순으로 정렬한 전체 페이지를 반복문을 통해 돌아본다.
for novel_list_page in range(212):
    #20240318 현재 6349개의 작품이 212페이지로 존재함으로 212번 반복한다.

    #하이퍼링크의 숫자를 변경하여 드라이버를 이동시킨다.
    driver.get('https://novelpia.com/plus/all/view/'+ str(novel_list_page+1) +'/?main_genre=')


    WebDriverWait(driver, 10)  # 페이지 로드되는 속도가 있으므로 필요함. 로드되기전 클릭해버리면 오발생할 수 있음.

    #클래스 이름이 col-md-12.novelbox.mobile_hidden인 요소를 individual_novel_eliment_list 리스트에 저장한다
    #이는 전페 페이지속 개별 소설을 의미한다.
    individual_novel_eliment_list = driver.find_elements(By.CLASS_NAME, """col-md-12.novelbox.mobile_hidden""")



    #반복문을 이용하여 전체 페이지속 개별 소설의 요소를 가져온다.
    for individual_novel_eliment in individual_novel_eliment_list:

        WebDriverWait(driver, 4)


        # 개별 소설의 id을 individual_novel_id에 저장하는 부분
        #   print(str([s for s in individual_novel_eliment.find_element(By.CLASS_NAME, "name_st").get_attribute("outerHTML").split('\'') if "novel" in s]))
        individual_novel_id = str([s for s in individual_novel_eliment.find_element(By.CLASS_NAME, "name_st").get_attribute("outerHTML").split('\'') if "novel" in s][0].split('/')[2])

        try:
            #개별 소설의 제목을 individual_novel_title에 저장하는 부분
            individual_novel_title = individual_novel_eliment.find_element(By.CLASS_NAME, "name_st").get_attribute('innerHTML')
        except: individual_novel_title = None

        try:
            # 작가를 찾아 individual_novel_writer에 저장하는 부분
            individual_novel_writer = (individual_novel_eliment.find_element(By.CLASS_NAME, "info_font")
                                       .find_element(By.XPATH, "b").get_attribute('innerHTML'))
        except:
            individual_novel_writer = None


        #소설의 독점, 완결, 연재중단, 연재지연 여부를 판별하는 부분.
        individual_novel_monopoly = len(individual_novel_eliment.find_elements(By.CLASS_NAME, "b_plus.s_inv")) >= 1
        individual_novel_complete = len(individual_novel_eliment.find_elements(By.CLASS_NAME, "b_comp.s_inv")) >= 1
        individual_novel_PG15 = len(individual_novel_eliment.find_elements(By.CLASS_NAME, "b_15.s_inv")) >= 1
        individual_novel_discontinuation = False
        individual_novel_delay = False
        for i in individual_novel_eliment.find_elements(By.CLASS_NAME, "s_inv"):
            if i.get_attribute('innerHTML') == "연재중단" :
                individual_novel_discontinuation = True
                break
        for i in individual_novel_eliment.find_elements(By.CLASS_NAME, "s_inv"):
            if i.get_attribute('innerHTML') == "연재지연" :
                individual_novel_delay = True
                break


        # 소설의 태그 리스트(파이썬 리스트)를 10개 None으로 생성함
        individual_novel_delay_tag_list = [None, None, None, None, None, None, None, None, None, None]

        try:
            #소설의 태그 리스트(셀레니움 요소형태)를 기록하는 부분
            individual_novel_eliment_tag_list = individual_novel_eliment.find_elements(By.CLASS_NAME, "hash_tag_off")
            for i, tag_ in enumerate(individual_novel_eliment_tag_list):
                individual_novel_delay_tag_list[i] = tag_.get_attribute('innerHTML')
                if i >= 9 : break
                #반복문을 이용하여 리스트에 저장
        except:
            individual_novel_delay_tag_list = [None, None, None, None, None, None, None, None, None, None]


        #개별 소설창으로 들어가는 부분 새탭을 이용하여 들어간다
        driver.execute_script("window.open('https://novelpia.com/novel/" + individual_novel_id + "');")
        driver.switch_to.window(driver.window_handles[1])  # 새로 연 탭으로 이동
        WebDriverWait(driver, 4)

        #조회수, 추천수, 소개글, 회차 파싱작업
        try:
            individual_novel_views = int(driver.find_element(By.CLASS_NAME, "counter-line-a").find_elements
                                         (By.CSS_SELECTOR, "span")[1].get_attribute('innerHTML').translate({ord(','): None}))
        except:
            individual_novel_views = None

        try:
            individual_novel_recommend = int(driver.find_element(By.CLASS_NAME, "counter-line-a").find_elements
                                             (By.CSS_SELECTOR, "span")[3].get_attribute('innerHTML').translate({ord(','): None}))
        except:
            individual_novel_recommend = None

        try:
            individual_novel_synopsis = driver.find_element(By.CLASS_NAME, "synopsis").get_attribute("innerHTML").replace("<br>", "")
        except:
            individual_novel_synopsis = None

        try:
            individual_novel_episodeNum = int(driver.find_elements(By.CLASS_NAME, "writer-name")[-1].get_attribute("innerHTML")
                                              .replace("회차", '').replace(",", ""))
        except:
            individual_novel_episodeNum = None

        try:
            individual_novel_preferenceNum = int(driver.find_elements(By.CLASS_NAME, "writer-name")[1].get_attribute("innerHTML").replace(",", ''))
        except:
            individual_novel_preferenceNum = None

        driver.close()  # 링크 이동 후 탭 닫기
        driver.switch_to.window(driver.window_handles[0])
        WebDriverWait(driver, 4)


        total_novel_df.loc[total_novel_df_index] = [individual_novel_title, individual_novel_id, individual_novel_monopoly,
                                                    individual_novel_complete, individual_novel_discontinuation, individual_novel_delay,
                                                    individual_novel_PG15,
                                                    individual_novel_writer, individual_novel_synopsis, individual_novel_views,
                                                    individual_novel_episodeNum, individual_novel_preferenceNum,
                                                    individual_novel_recommend] + individual_novel_delay_tag_list

        print(total_novel_df_index)
        total_novel_df_index += 1

total_novel_df.to_csv("./total_novel.csv")
driver.quit()