[빅 리더 AI] 크롤러 3일차

KrTeaparty·2022년 7월 7일
0

빅 리더 AI

목록 보기
4/7

대한민국구석구석 크롤링

# Chap 15.riss.kr 에서 특정 키워드로 논문 / 학술 자료 검색하기
#Step 1. 필요한 모듈을 로딩합니다
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
from bs4 import BeautifulSoup
import time
#Step 2. 사용자에게 검색 관련 정보들을 입력 받습니다.
print("=" *100)
print(" 이 크롤러는 연습문제용 웹크롤러입니다.")
print("=" *100)

#Step 3. 수집된 데이터를 저장할 파일 이름 입력 받기
f_dir = './result/'
query_txt = '제주도여행'

#Step 4. 크롬 드라이버 설정 및 웹 페이지 열기
s = Service("../chrome_driver/chromedriver.exe")
driver = webdriver.Chrome(service=s)
url = 'https://korean.visitkorea.or.kr'
driver.get(url)
driver.maximize_window()
time.sleep(2)

#Step 5. 자동으로 검색어 입력 후 조회하기
element = driver.find_element(By.ID,'inp_search')
driver.find_element(By.ID,'inp_search').click( )
element.send_keys(query_txt)
element.send_keys("\n")
time.sleep(1)

import math
cnt = 20
page_cnt = math.ceil(cnt / 10)

no2 = []
title2 = []
location2 = []
contents2 = []
hashtag2 = []
no = 1

for a in range(1, page_cnt+1):
    print("\n")
    print("%s 페이지 내용 수집 시작합니다 =======================" %a)
    html = driver.page_source
    soup = BeautifulSoup(html, 'html.parser')
    content_list = soup.find('ul','list_thumType type1').find_all('li')
    cnt_no = 1
    
    for i in content_list:        
        try:
            title = i.find('div','area_txt').find('div','tit').get_text().strip()
        except:
            continue
        else:
            print('\n')
            print('\n%s 번째 정보를 추출하고 있습니다==================' %no)
            no2.append(no)
            print('1.번호: ', str(no))
            
            title2.append(title)
            print('2.제목: ', title)
            
            location = i.find('div','service').find('p').get_text().strip()
            location2.append(location)
            print('3.지자체명: ', location)
        
            try:
                hashtag = i.find('p','tag_type').get_text().replace('#',' #')
            except:
                hashtag = " "

            hashtag2.append(hashtag)
            print('4.해시태그: ', hashtag)
            
            if a==1 and cnt_no == 5:
                cnt_no += 1
            
            
            driver.find_element(By.XPATH, '//*[@id="listBody"]/ul/li[%s]/div[2]/div[1]/a' %cnt_no).click()
            cnt_no += 1
            time.sleep(1)
            
            html2 = driver.page_source
            soup2 = BeautifulSoup(html2, 'html.parser')
        
            content1 = soup2.find('div','inr_wrap')
            content2 = soup2.find('div','box_txtPhoto')
            
            if content1:
                content = content1.find('div','inr').find('p').get_text()
                contents2.append(content)
                print('5.본문내용:\n',content)
            elif content2:
                content_1 = soup2.find('div','box_txtPhoto')
                try:
                    content_1.style.decompose()
                    content_1.script.decompose()
                except:
                    content_2 = content_1.find_all('div','txt_p')
                    print('5.본문내용: ')
                    for i in content_2:
                        content = i.get_text().strip()
                        print(content)
                        contents2.append(content)
                else:
                    content_2 = content_1.find_all('div','txt_p')
                    print('5.본문내용: ')
                    for i in content_2:
                        content = i.get_text().strip()
                        print(content)
                        contents2.append(content)

            driver.back()
            time.sleep(2)
        
            no += 1
            
            if no > cnt:
                break
                
            time.sleep(0.5)
    a += 1
    try:
        driver.find_element(By.LINK_TEXT, '%s' %a).click()
    except:
        driver.find_element(By.LINK_TEXT, '다음').click()
       
    time.sleep(10)

print('작업완료')   
    
import os
n = time.localtime()
s = '%04d-%02d-%02d-%02d-%02d-%02d' %(n.tm_year, n.tm_mon, n.tm_mday, n.tm_hour, n.tm_min, n.tm_sec)
os.makedirs(f_dir+'대한민국구석구석'+'-'+s)
fc_name = f_dir+'대한민국구석구석'+'-'+s+'\\'+'대한민국구석구석'+'-'+s+'-'+'제주도여행'+'.csv'
fx_name = f_dir+'대한민국구석구석'+'-'+s+'\\'+'대한민국구석구석'+'-'+s+'-'+'제주도여행'+'.xls'

# 데이터 프레임 생성 후 xls , csv 형식으로 저장하기
import pandas as pd
df = pd.DataFrame()
df['번호']=pd.Series(no2)
df['제목']=pd.Series(title2)
df['지자체명']=pd.Series(date2)
df['본문내용']=pd.Series(contents2)
df['해시태그']=pd.Series(hashtag2)

# xls 형태로 저장하기
df.to_excel(fx_name,index=False, encoding="utf-8" , engine='openpyxl')

# csv 형태로 저장하기
df.to_csv(fc_name,index=False, encoding="utf-8-sig")
print('요청하신 데이터 수집 작업이 정상적으로 완료되었습니다')

동작 과정
1. 웹사이트 접속
2. "제주도여행" 검색
3. 검색 결과들에서 제목, 지자체명, 해시태그를 추출
4. 게시물을 클릭하여 들어가 본문 내용 추출 후 뒤로가기
5. 이를 반복하면서 필요 시 페이지 전환

이 크롤러를 만들 때 첫 번째 페이지의 정보는 잘 수집했으나 2페이지로 넘어가자 프리징이 걸리는 상황이 발생했으며 driver.page_source를 통해 얻어오지 못하는 것으로 보인다.
이를 해결하기 위해 페이지 전환 후 대기 시간을 길게 줘보기도 하는 등의 조치를 취했으나 해결할 수 없었다.

코드의 세부적인 내용은 전날에 했던 것과 크게 추가해서 설명할만한 부분은 없다.

네이버 영화 리뷰 및 평점 수집

# 필요한 모듈과 라이브러리를 로딩합니다.
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
import time
import math
import numpy  
import pandas as pd  
import random
import os
import re

# 사용자에게 검색어 키워드를 입력 받고 저장할 폴더와 파일명을 설정합니다.
print("=" *80)
print("네이버 영화 리뷰 정보  수집하기")
print("=" *80)
print("\n")

query_txt = '네이버영화리뷰'
query_url =  'https://movie.naver.com'
title = input('1.크롤링할 영화 제목을 입력하세요: ')
cnt = int(input('2.크롤링 할 건수는 몇건입니까?(10건단위로 입력): '))
page_cnt = math.ceil(cnt / 10)

f_dir = './result/'
    
# 저장될 파일위치와 이름을 지정합니다
now = time.localtime()
s = '%04d-%02d-%02d-%02d-%02d-%02d' % (now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec)

os.makedirs(f_dir+s+'-'+query_txt)

ff_name=f_dir+s+'-'+query_txt+'\\'+s+'-'+query_txt+'.txt'
fc_name=f_dir+s+'-'+query_txt+'\\'+s+'-'+query_txt+'.csv'
fx_name=f_dir+s+'-'+query_txt+'\\'+s+'-'+query_txt+'.xls'

# 크롬 드라이버를 사용해서 웹 브라우저를 실행합니다.
s_time = time.time( )

s = Service("../chrome_driver/chromedriver.exe")
driver = webdriver.Chrome(service=s)

driver.get(query_url)
driver.maximize_window()
time.sleep(5)

# 현재 총 리뷰 건수를 확인하여 사용자의 요청건수와 비교 후 동기화합니다
element = driver.find_element(By.ID, 'ipt_tx_srch')
driver.find_element(By.ID, 'search_placeholder').click()
element.send_keys(title)
time.sleep(1)
driver.find_element(By.XPATH, '//*[@id="jAutoMV"]/ul/li[1]/a/div').click()
time.sleep(1)

driver.find_element(By.LINK_TEXT, '평점').click()
time.sleep(0.5)

# iframe 변경
driver.switch_to.frame('pointAfterListIframe')

html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')

result= soup.find('div', class_='score_total').find('strong','total').find('em')
result2 = result.get_text()

print("=" *80)
result3 = result2.replace(",","")
result4 = re.search("\d+",result3)
search_cnt = int(result4.group())

if cnt > search_cnt :
    cnt = search_cnt

print("전체 검색 결과 건수 :",search_cnt,"건")
print("실제 최종 출력 건수",cnt)
print("실체 출력될 최종 페이지수" , page_cnt)

star2=[]            # 별점
writer2=[]          # 리뷰 작성자 ID
review2=[]          # 리뷰 내용
write_date2=[]      # 리뷰 작성 일자
gogam_0=[]          # 공감 횟수
gogam_1=[]          # 비공감 횟수
count = 0
f = open(ff_name, 'a',encoding='UTF-8')

for a in range(1,page_cnt+1) :
    print("%s페이지 이동 완료========================================================" %a)
    review_list = soup.find('div','score_result').find('ul').find_all('li')
    for i in review_list:
        count += 1
        print("\n")
        print("총 %s건 중 %s번째 댓글 수집 중입니다 ==================" %(cnt,count))
        
        # 별점
        try:
            star = i.find('div','star_score').find('em').get_text().strip()
        except:
            star = '별점 정보 없음'
            print('**********별점 정보 예외************')
        star2.append(star)
        print('1.별점: ',star)
        f.write("\n")
        f.write("총 %s 건 중 %s 번째 리뷰 데이터를 수집합니다==============" %(cnt,count) + "\n")
        f.write('1.별점: '+star+'\n')
        
        # 리뷰 내용
        try:
            review = i.find('div','score_reple').find('span',id='_filtered_ment_0').find('a')['data-src']
        except:
            review = i.find('div','score_reple').find('p').get_text().strip()
        review2.append(review)
        print('2.리뷰내용: ', review)
        f.write('2.리뷰내용: '+review+'\n')
        
        # 작성자
        try:
            writer = i.find('div','score_reple').find_all('em')[0].get_text().strip()
        except:
            writer = ''
            print('작성자 정보 예외')
        writer2.append(writer)
        print('3.작성자: ', writer)
        f.write('3.작성자: '+writer+'\n')
        
        # 작성일자
        try:
            write_date = i.find('div','score_reple').find_all('em')[1].get_text().strip()
        except:
            write_date = ''
            print('*************작성일자 정보 예외***********')
        write_date2.append(write_date)
        print('4.작성일자: ', write_date)
        f.write('4.작성일자: '+write_date+'\n')
        
        # 공감
        try:
            gogam_00 = i.find('div','btn_area').find_all('strong')[0].get_text().strip()
            gogam_00 = gogam_00.replace(",","")
            gogam_00 = re.search("\d+",gogam_00)
            gogam_00 = int(gogam_00.group())
        except:
            gogam_00 = ''
            print("*************공감 정보 예외************")
        gogam_0.append(gogam_00)
        print('5.공감: ', gogam_00)
        f.write('5.공감: '+str(gogam_00)+'\n')
        
        # 비공감
        try:
            gogam_11 = i.find('div','btn_area').find_all('strong')[1].get_text().strip()
            gogam_11 = gogam_11.replace(",","")
            gogam_11 = re.search("\d+",gogam_11)
            gogam_11 = int(gogam_11.group())
        except:
            gogam_11 = ''
            print("*************비공감 정보 예외*************")
        gogam_1.append(gogam_11)
        print('5.비공감: ', gogam_11)
        f.write('5.비공감: '+str(gogam_11)+'\n')
        
        if count == cnt:
            break
    a += 1
    try:
        driver.find_element(By.LINK_TEXT, '%s' %a).click()
    except:
        driver.find_element(By.LINK_TEXT, '다음').click()
    
    time.sleep(random.randrange(1,3))  # 3-8 초 사이에 랜덤으로 시간 선택
        
# xls 형태와 csv 형태로 저장하기
movie_review = pd.DataFrame()
movie_review['별점(평점)']=pd.Series(star2)
movie_review['리뷰내용']=pd.Series(review2)
movie_review['작성자']=pd.Series(writer2)
movie_review['작성일자']=pd.Series(write_date2)
movie_review['공감횟수']=pd.Series(gogam_0)
movie_review['비공감횟수']=pd.Series(gogam_1)

f.close()
# csv 형태로 저장하기
movie_review.to_csv(fc_name,encoding="utf-8-sig",index=True)

# 엑셀 형태로 저장하기
movie_review.to_excel(fx_name ,index=True , engine='openpyxl')

# 요약 정보 출력하기
e_time = time.time( )
t_time = e_time - s_time

print("\n") 
print("=" *120)
print("1.요청된 총 %s 건의 리뷰 중에서 실제 크롤링 된 리뷰수는 %s 건입니다" %(cnt,count))
print("2.총 소요시간은 %s 초 입니다 " %round(t_time,1))
print("3.파일 저장 완료: txt 파일명 : %s " %ff_name)
print("4.파일 저장 완료: csv 파일명 : %s " %fc_name)
print("5.파일 저장 완료: xls 파일명 : %s " %fx_name)
print("=" *120)

driver.close( )

동작 과정
1. 사용자에게서 영화 제목, 크롤링할 수를 입력 받는다.
2. 저장될 파일위치에 폴더를 생성
3. 웹사이트 접속
4. 영화 검색
5. 평점 탭 클릭
6. iframe 전환
7. 별점, 리뷰내용, 작성자, 작성일자, 공감 수, 비공감 수를 수집
8. 이를 반복하고, 필요 시 페이지 전환
9. 파일로 저장

이번에 추가된 중요한 것은 iframe 개념이다. 단순히 말하자면 HTML 내에 다른 HTML 문서가 있는 것을 생각하면 된다.
이 iframe으로 나뉘어 있는 상태에서 driver.page_source로 가져오면 기본값으로 정해져 있는 데이터만 가져오고, 다른 iframe내에 있는 HTML 정보는 가져오지 않는다.
가져오기 위해서는 iframe과 iframe의 id를 찾아내야 한다. 찾아낸 후에는 driver.switch_to.frame('iframe의 id')를 통해 해당 iframe 내부로 들어갈 수 있고, 이 이후에 HTML 정보를 가져올 수 있다.

# 리뷰 내용
try:
    review = i.find('div','score_reple').find('span',id='_filtered_ment_0').find('a')['data-src']
except:
    review = i.find('div','score_reple').find('p').get_text().strip()
review2.append(review)
print('2.리뷰내용: ', review)
f.write('2.리뷰내용: '+review+'\n')

try/except문을 이렇게 사용한 이유는 리뷰 내용이 없을리 없다는 전제로 했기 때문이고, 긴 리뷰의 경우 get_text()를 사용하면 뒷 부분이 잘려서 수집되었기 때문에 태그 안의 속성이 전문을 가지고 있는 것에 착안해 위와 같이 작성했다.

result= soup.find('div', class_='score_total').find('strong','total').find('em')
result2 = result.get_text()

print("=" *80)
result3 = result2.replace(",","")
result4 = re.search("\d+",result3)
search_cnt = int(result4.group())

위는 총 리뷰 건수를 추출하기 위해 정규식을 사용한 부분이다. 정규식의 경우 이전에 github의 TIL로 정리한 것이 있기 때문에 자세히 다루지는 않겠다.

유튜브 영상의 댓글 수집

중요한 부분만 보겠다.

from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
import time
import sys
import re
import math
import numpy 
import pandas as pd   
import xlwt      # excel로 저장할 때 사용, pandas와 같이 동작
import random
import os
import openpyxl  # pandas가 버전이 올라가면서 openpyxl를 권장
import pyautogui # 사용자가 할 일을 자동으로 하도록 도와줌
from tqdm.notebook import tqdm    # 프로그레스바를 출력

사용한 모듈들이다. 이 중에서 추가된 것들만 본다.

  • xlwt: excel로 저장할 때 사용하며, pandas와 같이 동작한다.
  • openpyxl: pandas가 버전이 올라가면서 openpyxl을 권장하게 되었다.
  • pyautogui: 사용자가 할 일(스크롤, 키입력 등)을 자동으로 하도록 도와준다.
  • tqdm: 반복문에 사용하여 프로그레스바를 출력해 진행도를 알 수 있게 해준다.

유튜브의 경우 검색 결과가 첫 화면에 20개가 나와서 크롤링을 20건 이상해야 한다면 스크롤을 통해 추가적으로 불러와야 한다.

def scroll_down(driver):
    #driver.execute_script("window.scrollTo(500,document.body.scrollHeight);") # 현재 화면의 끝까지 내려라
    driver.execute_script("window.scrollBy(0,3000);") # 한페이지 20개씩 출력값
    time.sleep(5)

이 코드에서 주석처리 된 부분은 현재 화면의 끝까지 스크롤을 내리라는 의미이다.

for addr in full_url :
    driver.get(addr)
    time.sleep(5)
    pyautogui.scroll(1000)
    time.sleep(5)
    pyautogui.scroll(-500)
    time.sleep(5)

이 부분은 위와 달리 pyautogui 모듈을 이용한 스크롤이다.

soup.find('div','query').decompose()

추가로 decompose()는 하위 태그를 제거하며 필요없는 부분을 없앨 때 사용할 수 있다.

이미지 다운로드

웹 사이트 분류

  • 이미지를 그냥 다운로드 받을 수 있는 사이트
  • 인증을 거친 뒤에 이미지를 받을 수 있는 사이트

이미지를 인증을 거친 뒤에 이미지를 받을 수 있는 사이트의 경우 이미지를 그냥 받을 수 있는 사이트의 코드에서 자격증명 부분을 추가해주면 된다.

import urllib.request # 이미지 주소로 이미지를 다운받을 때 사용
import urllib.parse   # 한글을 변환할 때 사용

위의 두 줄이 추가되었다. urllib.request는 주석 그대로 HTML에 있는 이미지 주소를 통해 이미지를 다운받을 때 사용한다. urllib.parse는 이미지의 원본 주소에 한글이 포함될 경우 에러가 발생하기 때문에 한글을 변환시킬 때 사용한다.

img_src = soup.find('ul','vod_list').find_all('img')

for i in img_src :
    img_src1=i['src']
    img_src2.append(img_src1)
    print(img_src1)
    count += 1
    if count > cnt :
        break

이미지는 img 태그로 되어 있으며 속성으로 'src'를 가지고, 이는 이미지의 원본 주소에 해당한다.
그 외에도 'alt'가 있는데, 이는 이미지를 대표하는 이름을 주로 갖는다.

for i in range(0,len(img_src2)) :
    file_no += 1 
    # 한글을 urllib.request.urlretrieve()가 못 알아들어서 변환한다
    urllib.request.urlretrieve(urllib.parse.quote(img_src2[i].encode('utf8'), '/:'),str(file_no)+'.jpg')

    time.sleep(0.5)

이 부분이 한글이 포함된 url의 한글을 utf8로 변환하는 부분이다.

		# 이미지를 가지고 있는 서버로 가서 인증을 하는 것 (자격증명)
        class AppURLopener(urllib.request.FancyURLopener):
            version = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) \
                       Chrome/47.0.2526.69 Safari/537.36"

        urllib.urlopener = AppURLopener()
        # 한글이 있다면 이 부분을 변경
        urllib.urlopener.retrieve(img_src2[e],str(file_no)+'.jpg')

이 부분은 자격증명 부분으로 이미지를 가지고 있는 서버로 가서 인증을 하는 역할을 한다.(class AppURLopener)
위에서는 한글이 없다는 가정하에 retrieve() 내부에 한글을 변환하는 부분이 없지만 한글이 있다면 변환부분을 추가해줘야 한다.

profile
데이터를 접하는 중

0개의 댓글