[wt.gg] 네이버 웹툰 크롤링

Lee Tae-Sung·2021년 11월 9일
0

Project

목록 보기
6/9

+) 주 2회 이상 연재 웹툰 때문에 데이터들 불일치 확인
지금은 해결 정리는 프로젝트 끝나고 할 계획

wt.gg

1. 포스팅 개요

싸피 5기 2학기 세번째 자율 프로젝트를 진행 중이다.
자율 프로젝트는 내가 좋아하는 동생들인 은지, 성안이와 프로젝트를 시작했고 우리 셋이 팀이 결성되면서 프로젝트를 할 주제도 웹툰으로 결정한 상태였다.

이 주제는 누가 들어도 재밌는 주제이지만
가장 큰 문제는 데이터였다.

작년 비슷한 프로젝트를 하며 네이버 웹툰 댓글 크롤링에 실패했던 경험이 있어서 였다.

워낙 해당 주제가 저작권 관련해 큰 이슈들이 있다보니 개발 과정에서도 조금은 까다롭게 만들어 놓았다.

그래서 최우선적으로 네이버 웹툰 댓글 크롤링을 성안(BE)가 맡아줬.... 는데

취업을 하는 바람에 ㅠㅠ

그 외 핵심적인 크롤링을 내가 맡게 되었다.

고로 그 과정을 포스팅 하겠다.

2. 어려움

일단, FE 직무를 희망하고 크롤링을 안한지 어언 반년이 지났다. 그래서 그 때 그 기억들을 꺼내느라 고생을 좀 했다.

그리고 또 나름 트렌디한 크롤링을 하겠다는 생각에 셀레니움을 배제하고 크롤링을 알아보기 시작했다.

=> 결국 셀레니움 씀 ㅋ

그리고 크롤링은 성공 했는데 생각지 못한 문제들이 있었다.

1) 셀레니움으로 일일히 하다보니 한번 크롤링 할때 너무 오래걸렸다.

=> 웹툰 파워 점수를 먹였는데 그 요소 중에 최대 10회 분량의 댓글 참여자 수의 평균을 구하는 요소가 있었다.

=> 이거 때문에 네이버 웹툰 (약 470개) 을 크롤링 하는데 장장 3시간이 걸렸다.

=> 결국 이 요소는 빼기로 결정

=> 20분으로 줄었다. 만쉐.

2) 에러 발생시 디버깅 어려움

=> 20분으로 줄은건 사실이지만 무려 약 470번의 사이클을 돌리다보니 도대체 어디서 어떤 에러가 발생했는지 찾기가 어려웠다.

=> 결국 print를 모든 과정에 찍어 내 터미널 창은 계속 미친듯이 일을 하게 되었다.

=> ;; 그리고 주 2회 연재되는 웹툰은 별도로 중복 체크를 해줘야하는데 너무 나중에 관련 에러를 발견해서 응급 처치만 하고 일단 DB로 넘겼다.

3) DB 변동에 따라 계속 변경되는 크롤링 코드

=> 급격한 인원 감소로 우리 기존 서비스 목표와는 많이 달라졌다.

=> 그런데 우리는 괜찮겠거니 하며 ERD에 별도 반영을 안해줬는데

=> 이러다보니 내가 크롤링 코드를 짜는데 뭐도 있어야하네 뭐도 있어야 하네 하며 ERD도 바뀌고 코드도 바뀌는 상황이 발생했다.

=> 매우 비효율 적이라고 생각했다.

=> 결국, 내 코드에 따라 테이블을 거의 갈아엎는 수준의 공사가 들어갔다.(물론 아직 큰 일 까지는 아니였지만 비효율 비효율....)

4) Mysql에 한번에 데이터 넣기

=> 생각해보니 코드로 데이터들을 뽑으면 뭐해

=> 이걸 Mysql에 한번에 넣어야 했다. (일일히 470줄의 sql 코드를 넣을 수는 없으니 ....)

=> 생각보다 쉽게 해결책을 찾았지만 순간 겁을 먹었었다.

+)추가된 어려움 (21.11.10)
5) 배열을 MySql로 넣기

(1) MariaDB는 "'"를 인식하지 못한다...

크롤링 하면서 스토리에서 태성이가 말했다'와하하 대박' 이런식으로 되어있는 경우가 있어 syntax 에러 발생..

(2) 장르에서 [] 하나의 리스트가 추가되어 뽑히는 문제 발생
[0]으로 어거지로 한번 더 까줬다.

3. 해결

https://m.blog.naver.com/stu5073/221801367202

GoldShipCastle(금배성)님의 블로그를 참조 했습니다.

=> 나중에 알고보니 성안이도 이 블로그 코드를 참조했더라...

import requests
from bs4 import BeautifulSoup

#네이버 웹툰 페이지 파싱을 통해 총 정식 연재 작품 수 구하기
URL='https://comic.naver.com/webtoon/weekday.nhn'
html=requests.get(URL).text
soup=BeautifulSoup(html,'html.parser')

title=soup.find_all('a',{'class': 'title'})
title_list=[] ; title_num=[]
print(title)
#일주일 2회 이상 연재 작품은 한 번만 데이터 수집
for x in range(len(title)):
    t=title[x].text
    if(t in title_list):
        continue
    else:
        title_list.append(t)

=> 여기서 일주일 2회 힌트를 안줬으면 왜 에러가 뜨는지 또 하루종일 고민 했을 꺼다
=> 시간이 없어 일단 응급처치 하고 프로젝트 진행함

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from time import sleep
import time
import re

URL='https://comic.naver.com/webtoon/weekday.nhn'
driver=webdriver.Chrome('chromedriver.exe')
driver.get(URL)

time.sleep(1)

artist_list=[] ; genre_list=[] ; score_list=[] 
star_parti=[] ; img_list=[] ; link_list=[] ; story_list=[]

for i in range(len(title_list)):

    time.sleep(1)
    
    #전체 웹툰 목록 중 월요일 첫 번째 웹툰으로 페이지 이동
    page=driver.find_elements_by_class_name('title')
    page[i].click()
    
    time.sleep(0.5)
    
    #이동한 페이지 주소 읽고, 파싱하기
    html = driver.page_source
    soup = BeautifulSoup(html,'html.parser')
    #작품 썸네일 이미지
    img=soup.select('#content > div.comicinfo > div.thumb > a > img')[0]['src']
    if img in img_list:
      pass

    img_list.append(img)
    print(img_list)
    
    #작가님 닉네임 수집
    artist = soup.find_all('h2')
    artist = artist[1].find('span',{'class':'wrt_nm'}).text[8:]
    artist_list.append(artist)
    print(artist_list)
    

    #작품 링크 
    link=soup.select('#content > div.comicinfo > div.thumb > a')[0]['href']
    link_list.append('https://comic.naver.com'+link)
    print(link_list)

    #줄거리 링크 
    story=soup.select('#content > div.comicinfo > div.detail > p:nth-child(2)')
    story_list.append(story)
    print(story_list)
    
    
    #작품 장르 수집
    genre=soup.find('span',{'class':'genre'}).text
    genre_list.append([genre])
    print(genre_list)

    #최신 별점 평균 점수 수집 (최대 10화 분량)
    score = soup.find_all('strong')
    # print(score)
    
    # print(score)
    scorelist=[] ; ii=9
    while score[ii].text[0].isnumeric()==True:
        scorelist.append(float(score[ii].text))
        ii +=1
        if len(scorelist) == 3:
          break
    score_list.append(sum(scorelist)/len(scorelist))
    
    print(score_list)

    
    time.sleep(0.5)


    #각 회차를 돌며 댓글 별점 참여자 수집
###########################################################################################
    # length=driver.find_elements_by_class_name('title')
    # for j in range(1, len(length)):
    #     #해당 페이지의 회차 모두 가져오기
    #     titlenum=driver.find_elements_by_class_name('title')
        
    #     time.sleep(0.5)
        
    #     webnum=[y.text for y in titlenum]
    #     enterToon = driver.find_elements_by_partial_link_text(webnum[j])
        
    #     time.sleep(0.5)
        
    #     enterToon[0].click()
    #     pre = []
    #     html = driver.page_source
    #     soup = BeautifulSoup(html,'html.parser')

    #     # 별점 
    #     star_p=soup.select('#topTotalStarPoint > span.pointTotalPerson > em')[0].text
        
    #     print(star_p)
    #     pre.append(int(star_p))
        

        


    #     #페이지 뒤로 가기, 다시 만화 목록으로 
    #     driver.back()
        
    #     time.sleep(1)
        
    #     titlenum.clear()
        
        
        
    #     enterToon.clear()
    # star_parti.append(sum(pre) / len(pre))
    # print(star_parti)
#############################################################################################


    print(title_list[i],"end")
    
    
    driver.back()
    
    time.sleep(0.5)
    
    page.clear()
    
    time.sleep(0.5)
  
webtoon_data = []

=> sleep은 너무 막 이렇게 셀레니움 갈기다 보면 페이지가 뜨기도 전에 눌러서 에러가 발생하는걸 방지

print("작가 리스트 길이 :", len(artist_list))
print("장르 리스트 길이 :", len(genre_list))
print("평점 리스트 길이 :", len(score_list))
# print("별점참여자 리스트 길이 :", len(star_parti))
print("이미지 리스트 길이 :", len(img_list))
print("링크 리스트 길이 :", len(link_list))
print("스토리 리스트 길이 :", len(story_list))

=> 결과값 확인

https://jh-0323.tistory.com/entry/Python-Selenium%EC%9C%BC%EB%A1%9C-%EC%A4%91%EA%B3%A0%EB%82%98%EB%9D%BC-%ED%81%AC%EB%A1%A4%EB%A7%81-%ED%9B%84-MySQL%EC%97%90-%EC%A0%80%EC%9E%A5

이하 jh-0323님의 블로그를 참고했습니다.

CREATE TABLE webtoon (
    id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
    platform_id int DEFAULT NULL,
    title varchar(50) DEFAULT NULL,
    link char(255) DEFAULT NULL,
    img_link char(255) DEFAULT NULL,
    genre char(50) DEFAULT NULL,
    story char(255) DEFAULT NULL,
    artist char(50) DEFAULT NULL,
    score float(10) DEFAULT NULL) ;

=> 기록해두자 기본 테이블 만드는 매크로 sql 문

data = []
# print(len(title_list))
for i in range(len(link_list)):
  # print(title_list[i])
  # print(link_list[i])
  # print(img_list[i])
  # print(genre_list[i][0])
  # print(story_list[i])
  # print(artist_list[i])
  # print(score_list[i])
  data.append([1, title_list[i], link_list[i], img_list[i], genre_list[i][0], '줄거리', artist_list[i], score_list[i]])


print(data)

=> Mysql로 보내기 위해 데이터들을 배열로 만들기
=> 줄거리는 ' 문제로 그냥 일단 떼우기 ....

data = [[1, '이태성', 'www', 'www', '액션', '멋쟁이', '천재'], [1, '김한길', 'www', 'www', '액션', '멋쟁이', '천재']]

import pymysql

connect = pymysql.connect(host='localhost', user='아이디', password='비밀번호', db='디비명', charset='utf8mb4')
cursor = connect.cursor()

for r in data:
    platform_id = int(r[0])
    title = str(r[1])
    link = str(r[2])
    img_link = str(r[3])
    genre = str(r[4])
    story = str(r[5])
    artist = str(r[6])
    
    sql = """insert into test 
    (platform_id, title, link, img_link, genre, story, artist) 
    values (%d, '%s', '%s', '%s', '%s', '%s', '%s')
    """ % (platform_id, title, link, img_link, genre, story, artist)

    cursor.execute(sql)
    connect.commit()
connect.close()

=> test 배열을 만들고 코드에 적용하기 (아직 벡엔드에서 db 테이블 변경하는거 기다리는 중 ....)

import pymysql

connect = pymysql.connect(host='54.166.95.144', user='ssafy', password='ssafyssafy(password)', db='webtoon', charset='utf8mb4')
cursor = connect.cursor()

for r in data:
    platform_id = int(r[0])
    title = str(r[1])
    link = str(r[2])
    img_link = str(r[3])
    genre = str(r[4])
    story = str(r[5])
    artist = str(r[6])
    score = float(r[7])
    
    
    sql = """insert into webtoon 
    (platform_id, title, link, image_link, genre, story, artist, score) 
    values (%s, '%s', '%s', '%s', '%s', '%s', '%s', '%s')
    """ % (platform_id, title, link, img_link, genre, '줄거리', artist, score)

    cursor.execute(sql)
    connect.commit()
connect.close()

(실제 반영된 코드)

=> 여기서 에러가 떠서 코치님 도움을 요청했는데

=> 디비명을 workbench 키면 나오는 네모 상자의 제목으로 생각하고 계속 했다. 그래서 db를 계속 못 찾았음

=> 그 작은 네모는 들어가서 나오는 db 명으로 해야한다

=> 그리고 형식들 int, str 들은 꼼꼼히 맞춰주어야하고

=> %d 는 int, %s 는 str => 가 아니다 ;;;

=> 이렇게 하니 소수점이 중요한 score가 정수로 mysql에 들어갔다. 그래서 찾아보니 이런 기능을 하는 친구가 아니였다!
https://jaeyung1001.tistory.com/48

=> 그냥 %s로 하니까 소수점들도 잘 들어감 ..

=> 앗;; 이렇게 정리하는 도중에도 오타를 발견했다 ;;

profile
긍정적인 에너지를 가진 개발자, 이태성입니다.

1개의 댓글

comment-user-thumbnail
2021년 11월 12일

취업한 성안이 ㅠㅠ

답글 달기