본문 바로가기
파이썬(Python)/파이썬 관련

Selenium으로 웹사이트(디시인사이드) 크롤링하기

by Kaya_Alpha 2023. 4. 15.

이번 포스트에서는 Selenium으로 국내 온라인 커뮤니티중 하나인 DC Inside(디시인사이드)를 크롤링하는 방법에 대해 정리하고자 합니다.

 

일반적으로 웹에서 데이터를 가져올 때 해당 사이트에서 제공하는 공식 API가 있으면 가장 좋지만, 그렇지 않은 경우에 request와 BeautifulSoup을 이용해서 크롤링을 진행할 수 있습니다. 하지만 request를 사용하는 경우 sleep()을 사용했음에도 불구하고 접속 차단을 하는 사이트가 존재합니다. 또한 크롤링 타깃 사이트가 동적페이지인 경우, request를 이용하여 동적 페이지를 크롤링 못하는 건 아니지만(예시 : 네이버 데이터랩 크롤링) 개인적으로 다른 사이트에 적용 해 보았을 때 잘 안 되는 경우가 있었습니다. 이러한 경우에는 request를 사용하였을 때의 속도는 포기하더라도(?) 데이터를 가져오고 싶다면 Selenium을 고려해 볼 수 있습니다.

 

Selenium은 웹 브라우저를 컨트롤 할 수 있는 라이브러리입니다. 전체적인 프로세스는 selenium을 이용하여 웹 사이트를 컨트롤 한 뒤, BeautifulSoup을 이용하여 웹 사이트의 HTML source를 가져온 다음, 우리가 원하는 정보를 빼오는 방식으로 진행합니다.

 

크롤링의 목적DC인사이드의 '프로그래밍' 갤러리에서 특정 기간동안 발생한 게시글 목록의 제목과 해당 게시글의 내용 및 댓글정보를 모두 가져오는것으로 하겠습니다. (단, 그림 및 디시콘은 제외합니다.)

 

우선 selenium 및 기타 사용할 라이브러리들을 import 해줍니다.

#selenium 라이브러리
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

#추가 조작 라이브러리
import time
from bs4 import BeautifulSoup
from time import sleep

#기타 라이브러리
import pandas as pd


ser = Service(r"chromedriver.exe 경로 넣는 부분")
op = webdriver.ChromeOptions()
driver = webdriver.Chrome(service = ser,options = op)

다음으로 특정 날짜에 작성된 게시글을 찾아야 하므로 미리 날짜의 범위를 지정해 줍니다.

selenium으로 컨트롤을 할 때, 페이지를 앞에서부터 뒤로 넘어갈 예정이므로 날짜 범위중 현재와 가장 가깝게 작성된 게시글부터 발견이 되므로 start_date로 네이밍을 하였습니다. 반대로 end_date는 수집 기간의 마지막(오래된) 날짜를 의미합니다.

start_date = time.strptime("2023.1.1","%Y.%m.%d")
end_date = time.strptime("2022.1.1","%Y.%m.%d")

다음으로 수집한 정보를 저장해 둘 리스트를 설정합니다. 저 같은 경우는 서로 다른 DataFrame으로 설정하기 위해 아래와 같이 지정하였습니다.

#수집한 정보를 저장하는 리스트
c_gall_no_list = []
title_list = [] #제목
contents_list = [] #게시글 내용
contents_date_list = []

#수집한 정보를 저장하는 리스트
gall_no_list = [] #글 번호
reply_id = [] #답글 아이디
reply_content = [] #답글 내용
reply_date = [] #답글 등록 일자

이제는 selenium을 이용하여 직접 크롤링하는 부분입니다.

프로그래밍 갤러리가 아닌 다른 갤러리의 경우, "base_url = BASE + '/board/lists/?id=programming&page='+str(start_page)" 부분에서 가운데 주소 부분만 수정하면 바로 적용이 가능합니다.

 

#기본 URL
BASE = "http://gall.dcinside.com"

start_page = 1000
Flag = True

while Flag: #게시글의 페이지마다 loop를 수행(for i in range(100000000) 을 써도 무방)
    
    #게시글 목록 페이지
    base_url = BASE + '/board/lists/?id=programming&page='+str(start_page)
    
    try:
        driver.get(base_url)
        sleep(3)
    except : #예외 발생 시 다시 load
        continue
    #게시글 목록의 HTML source를 읽어옴
    page_source = driver.page_source
    soup = BeautifulSoup(page_source, "html.parser")
    
    #모든 게시글의 정보를 찾음
    article_list = soup.find('tbody').find_all('tr')
    
    #수집하는 기간에 맞는 게시글이 목록에 있는지 체크
    date = "20" + article_list[-1].find("td",{"class" : "gall_date"}).text
    contents_date = time.strptime(date,"%Y.%m.%d")
 
    if start_date < contents_date: #게시글의 마지막 날짜가 수집기간 날짜에 포함되는가?
        start_page += 1
        continue
        
    elif contents_date < end_date: #수집기간보다 오래된 날짜면 수집을 완료함
        print("수집을 종료합니다.")
        Flag = False
        continue
        
    for article in article_list:
        
        #게시글의 제목을 가져오는 부분
        title = article.find('a').text
        
        #게시글의 종류(ex-일반/설문/투표/공지/등등...)
        head = article.find('td',{"class": "gall_subject"}).text
        
        if head not in ['설문','AD','공지']: #사용자들이 쓴 글이 목적이므로 광고/설문/공지 제외
                
            #게시글 번호 찾아오기
            gall_id = article.find("td",{"class" : "gall_num"}).text
            
            if gall_id in c_gall_no_list:
                continue
            
            #각 게시글의 주소를 찾기 -> 내용 + 댓글 수집 목적
            tag = article.find('a',href = True)
            content_url = BASE + tag['href']
            
            #게시글 load
            try:
                driver.get(content_url)
                sleep(3)
                contents_soup = BeautifulSoup(driver.page_source,"html.parser")
                #게시글에 아무런 내용이 없는 경우 -> 에러뜸 (빈 문자열로 처리하고 댓글만 가져와도 됩니다.)
                contents = contents_soup.find('div', {"class": "write_div"}).text
            except : 
                continue
                
            #게시글의 작성 날짜
            c_date = "20" + article.find("td",{"class" : "gall_date"}).text
            
            #게시글 제목과 내용을 수집
            c_gall_no_list.append(gall_id)
            title_list.append(title)
            contents_list.append(contents)
            contents_date_list.append(c_date)
            
            #댓글의 갯수를 파악
            reply_no = contents_soup.find_all("li",{"class" : "ub-content"})
            if len(reply_no) > 0 :
                for r in reply_no:
                    try:
                        user_name = r.find("em").text #답글 아이디 추출
                        user_reply_date = r.find("span",{"class" : "date_time"}).text #답글 등록 날짜 추출
                        user_reply = r.find("p",{"class" : "usertxt ub-word"}).text #답글 내용 추출
                        
                        #댓글의 내용을 저장
                        gall_no_list.append(gall_id)
                        reply_id.append(user_name)
                        reply_date.append(user_reply_date)
                        reply_content.append(user_reply)    
                    except: #댓글에 디시콘만 올려놓은 경우
                        continue
            else:
                pass
    #다음 게시글 목록 페이지로 넘어가기
    start_page += 1

수집이 완료되면 원하는 형태로 DataFrame을 만든 다음, csv파일로 저장할 수 있습니다.

#수집한 데이터를 저장
contents_df = pd.DataFrame({"id" : c_gall_no_list,
                               "title" : title_list,
                               "contents" : contents_list,
                            "date" : contents_date_list
                           })

reply_df = pd.DataFrame({"id" : gall_no_list,
                        "reply_id" : reply_id,
                        "reply_content" : reply_content,
                        "reply_date" : reply_date})

contents_df.to_csv("contents.csv",encoding ='utf8',index = False)
reply_df.to_csv("reply.csv",encoding = 'utf8',index = False)

 

위 코드가 중간에 작동이 잘 안 되는 경우가 가끔(?) 있습니다. 갑자기 selenium이 멈춘다던지...

selenium이 request에 비해 속도가 느리다 보니 크롤링 속도를 빠르게 하기 위해 창 없이 크롤링하는 방법기타 등등의 방법이 있습니다만, 저는 문제가 생길 시 바로 대처하기 위해서 그냥 창을 띄워두었습니다.(보기도 좋습니다)

 

위 코드를 작동시키면 다음과 같이 저장이 됩니다. 예시는 프로그래밍 갤러리가 아닌 '지하성과 용사 마이너 갤러리'로 해보았습니다.

보시는바와 같이 게시글 ID, 게시글 제목, 게시글 내용, 그리고 작성일자가 올바르게 수집되었음을 확인할 수 있었습니다.

 

지하성과 용사 마이너 갤러리 크롤링_게시글 정보

 

지하성과 용사 마이너 갤러리_댓글 정보

 

또한 댓글 정보도 마찬가지로 게시글 ID, 댓글 작성자 ID, 댓글 내용, 그리고 댓글 작성일자가 올바르게 수집되었음을 확인할 수 있습니다.

각 게시글에 달린 댓글은 게시글 ID 속성으로 연결되어 있기 때문에 각 게시글에 달린 댓글의 내용을 확인 할 수 있습니다.

 

 

여담으로 더욱 빠르게 게시글과 댓글을 수집하기 위하여 Async를 이용하여 크롤링을 해보았지만, 자꾸 차단먹어서 포기했습니다..

혹시 방법을 아시는분이 계시다면 알려주시면 감사하겠습니다..

 

감사합니다.