본문 바로가기
Database & Bigdata/공공 빅데이터 청년 인턴십

[ DAY 8 ] 파이썬 크롤러 만들기

by jionee 2020. 9. 2.
SMALL

1. 기본 웹 크롤러

[ 기본 웹 크롤러 ]

- Requests로 웹 페이지를 추출하고, lxml로 웹 페이지 스크래핑 및 sqlite3 DB에 데이터를 저장

- 크롤링 대상 = 한빛 미디어 사이트의 "새로나온 책"목록

- 전형적인 목록/상세 패턴을 가진 웹사이트를 기반으로 도서 정보 추출 크롤러 제작

(목록 페이지 : 제목, 저자 정보 / 상세 페이지 : 제목, 가격, 목차 정보 추출 )

 

[ 목록 페이지에서 퍼머 링크 목록 추출 ]

크롤링 대상 페이지 : https://www.hanbit.co.kr/store/books/new_book_list.html

 

한빛출판네트워크

더 넓은 세상, 더 나은 미래를 위한 아시아 출판 네트워크 :: 한빛미디어, 한빛아카데미, 한빛비즈, 한빛라이프, 한빛에듀

www.hanbit.co.kr

목록 페이지

- 목록 페이지에서 상세 페이지로의 링크 목록을 추출

import requests
import lxml.html

response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
root = lxml.html.fromstring(response.content)

for a in root.cssselect('.view_box a'):
    url = a.get('href')
    print(url)

제목(a)에서 하이퍼링크(href) 추출 

현재는 목록페이지에서의 상대경로

 

 

- "javascript" 로 시작하는 목록 제거 & 퍼머 링크 목록 추출

import requests
import lxml.html

response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
root = lxml.html.fromstring(response.content)

# 모든 링크를 절대 URL로 변환
root.make_links_absolute(response.url)

# 목록에서 javascript 제거
for a in root.cssselect('.view_box .book_tit a'):
    url = a.get('href')
    print(url)

make_links_absolute 로 response.url을 절대 URL 로 변환함

 

.book_tit에서 href를 다시 추출 -> javascript 제거

 

절대 경로

 

- scrape_llist_page() 함수의 반환값은 llist처럼 반복 가능한 제너레이터로 구현

def main():
    '''
    크롤러의 메인 처리
    '''
    
    # 여러 페이지에서 크롤링을 위해 Session 사용
    session = request.Session()
    
    #scrpae_list_page() 함수를 호출해 제너레이터를 추출
    response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
    urls = scrape_list_page(response)
    
    for url in urls :
        print (url)
        print ('-'*70)

main 함수에서 scrape_list_page 함수를 호출해 url 추출

 

def scrape_list_page(response):
    root = lxml.html.fromstring(response.content)
    root.make_links_absolute(response.url)
    for a in root.cssselector('.view_box .box_tit a'):
        url = a.get('href')
        
        #yield 구문으로 제너레이터 요소 반환
        yield url

scrape_list_page 함수 정의 (href 추출 함수)

 

 

[ 상세 페이지 스크래핑 ]

- 개발자 도구로 CSSSelector 확인

타이틀 : .store_product_info_box h3

가격 : .pbr strong

목차 : #tabs_3.hanbit_edit_view 내부의 p 태그들

 

- response를 매개변수로 scrape_detail_page()를 호출해서 책의 상세 정보를 추출

- scrape_detail_page() 함수에서는 CSS Selector를 사용해 스크래핑

- 제목과 가격은 root.cssselector() 함수로 추출한 리스트의 첫번째 요소에서 문자열을 추출

- 목차는 List Comprehension을 사용해 목차를 리스트로 추출

 

- 목차에 포함되어 있는 공백을 제거할 수 있는 normalize_space() 함수 정의

List Comprehension 구문에 조건을 추가해 빈 문자열을 제거

import requests
import lxml.html
import re

def scrape_detail_page(response):
    """
    상세 페이지의 Response에서 책 정보를 dict로 추출
    """
    root = lxml.html.fromstring(response.content)
    ebook = {
        'url': response.url,
        'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
        'price': root.cssselect('.pbr strong')[0].text_content(),
        'content': [normalize_spaces(p.text_content())
            for p in root.cssselect('#tabs_3.hanbit_edit_view p')
            if normalize_spaces(p.text_content()) != '']
    }
    return ebook

url, title, price, content 추출

content는 normalize_spaces 함수로 공백 제거

def normalize_spaces(s):
    """
    연결된 공백을 하나의 공백으로 변경
    """
    return re.sub(r'\s+', ' ', s).strip()

공백 제거 함수 정의

 

 

 

- 전체 페이지 크롤링을 위해 main() 함수의 for 구문에서 break를 제거

대상 서버에 부하를 주지 않은 상태에서 크롤링을 위해 time 모듈 import 및 time.sleep(1)을 넣어 1초 동안 대기

import time
import requests
import lxml.html
import re

def main():
    # 여러 페이지에서 크롤링을 위해 Session 사용
    session = requests.Session()  
    # scrape_list_page() 함수를 호출해서 제너레이터를 추출
    response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
    urls = scrape_list_page(response)
    for url in urls:
        time.sleep(1) # 1초간 대기
        response = session.get(url)  # Session을 사용해 상세 페이지를 추출
        ebook = scrape_detail_page(response)  # 상세 페이지에서 상세 정보를 추출
        print(ebook)  # 상세 정보 출력
        break  

 

 

2. 고급 웹 크롤러

[고급 웹 크롤러]

- daum IT 뉴스 크롤러

https://news.daum.net/breakingnews/digital

 

1. 뉴스 목록 첫페이지 -> 2. 뉴스 상세페이지 -> 3. 다음 페이지 이동 -> 1. 뉴스 목록 첫페이지

반복

 

 

- 다음 뉴스 목록 첫 페이지

import requests
import lxml.html
import pandas as pd
import sqlite3
from pandas.io import sql
import os

REG_DATE = '20200902'
response = requests.get('https://news.daum.net/breakingnews/digital?regDate={}'.format(REG_DATE))                   
root = lxml.html.fromstring(response.content)
for li in root.xpath('//*[@id="mArticle"]/div[3]/ul/li'):
    a = li.xpath('div/strong/a')[0]
    url = a.get('href')
    print(url, a.text)

for문을 이용해 한 기사씩 추출 (for li in root.xpath)

a 에 기사 제목 추출 (li 상대 경로)

url에 하이퍼링크 추출 (a 태그에서 href 추출)

-> url, 제목 출력

 

 

 

* XPath 찾기

 

1. 개발자 도구 실행 후 Select 도구로 추출할 기사 선택

2. 해당 태그에서 오른쪽 마우스 - copy - copy XPath

 

 

- 다음 뉴스 목록 상세 페이지

import requests
import lxml.html
import pandas as pd
import sqlite3
from pandas.io import sql
import os

import re
import string

def get_detail(url):
    body = []
    punc = '[!"#$%&\'()*+,-./:;<=>?[\]^_`{|}~“”·]'
    response = requests.get(url)
    root = lxml.html.fromstring(response.content)
    for p in root.xpath('//*[@id="harmonyContainer"]/section/p'):
        if p.text: # 체크
            body.append(re.sub(punc, '', p.text)) # 특수문자 제거
    full_body = ' '.join(body)
    
    return full_body

get_detail('https://news.v.daum.net/v/20200902130715304')

상세 내용을 가져오는 get_detaill 함수 생성

특수문자 정규식 만듬

harmonyContainer div의 /section/p 전체 검사

p를 체크해 특수문자를 제고하고 body에 추가

full body 변수에 p간 공백을 주며 조인 (반복)

full body 변수 출력

 

본문이 harmonyContainer div의 p 태그에  위치함

 

 

 

- 다음 뉴스 첫 페이지 목록과 상세 페이지

page = 1
max_page = 0
REG_DATE = '20200819'
response = requests.get('http://news.daum.net/breakingnews/digital?page={}&regDate={}'\
                        .format(page, REG_DATE))
root = lxml.html.fromstring(response.content)
for li in root.xpath('//*[@id="mArticle"]/div[3]/ul/li'):
    a = li.xpath('div/strong/a')[0]
    url = a.get('href')
    article = get_detail(url)
    print(f'URL : {url}')
    print(f'TITLE : {a.text}')
    print(f'ARTICLE : {article}')
    print('-' * 100)

response 의 url 각 {}에 page 번호, 날짜(REG_DATE) 를 대입

for문으로 기사를 한개씩 읽어옴 (mArticle div의 li 태그)

a 변수에 제목 

url 변수에 a 태그 내 href 추출

article에 get_detail 함수를 사용해 본문 내용 저장

url, title, article을 출력한 후 대시(-)줄로 구분함

 

 

 

- 다음 뉴스 다음 페이지 이동과 마지막 페이지

import requests
import lxml.html
import pandas as pd
import sqlite3
from pandas.io import sql
import os
import time

page = 58
max_page = 0
REG_DATE = '20200819'

while(True):
    df_list = []
    response = requests.get('http://news.daum.net/breakingnews/digital?page={}&regDate={}'\
                            .format(page, REG_DATE))
    root = lxml.html.fromstring(response.content)
    for li in root.xpath('//*[@id="mArticle"]/div[3]/ul/li'):
        a = li.xpath('div/strong/a')[0]
        url = a.get('href')
        article = get_detail(url)
        df = pd.DataFrame({'URL' : [url],'TITLE':[a.text],'ARTICLE' : [article]})
        df_list.append(df)   
        
    if df_list:   
        df_10 = pd.concat(df_list)
        db_save(df_10)

    # 페이지 번호 중에서 max 페이지 가져오기    
    for a in root.xpath('//*[@id="mArticle"]/div[3]/div/span/a'):
        try:
            num = int(a.text)
            if max_page < num:
                max_page = num       
        except:
            pass

    # 마지막 페이지 여부 확인     
    span = root.xpath('//*[@id="mArticle"]/div[3]/div/span/a[@class="btn_page btn_next"]')

    if (len(span) <= 0) & (page > max_page):
        break
    else:
        page = page + 1
        
    time.sleep(1)      

for문으로 기사의 제목, 링크, 본문을 읽어와 데이터 프레임에 넣음

데이터 프레임 리스트에 여러 기사 정보를 넣음

 

 

- 크롤링 정보 저장 및 조회

def db_save(NEWS_LIST):
    with sqlite3.connect(os.path.join('.','sqliteDB')) as con:
        try:
            NEWS_LIST.to_sql(name = 'NEWS_LIST', con = con, index = False, if_exists='append') 
            #if_exists : {'fail', 'replace', 'append'} default : fail
        except Exception as e:
            print(str(e))
        print(len(NEWS_LIST), '건 저장완료..')

NEWS_LIST 테이블에 저장

 

def db_delete():
    with sqlite3.connect(os.path.join('.','sqliteDB')) as con: 
        try:
            cur = con.cursor()
            sql = 'DELETE FROM NEWS_LIST'
            cur.execute(sql)
        except Exception as e:
            print(str(e)) 

 

NEWS_LIST 테이블에서 삭제

 

def db_select():
    with sqlite3.connect(os.path.join('.','sqliteDB')) as con: 
        try:
            query = 'SELECT * FROM NEWS_LIST'
            NEWS_LIST = pd.read_sql(query, con = con)
        except Exception as e:
            print(str(e)) 
        return NEWS_LIST   

NEWS_LIST 테이블에서 조회

 

 

3. 셀레늄을 이용한 크롤러

셀레늄이란 ?

다양한 프로그래밍 언어로 웹드라이버를 통해 다양한 브러우저 상에서 웹 자동화 테스트 혹은 웹 자동화 프로그램을 구현하기 위한 라이브러리

* 지원 브라우저 - Chrome, FireFox, Safari, Opera, Internet Explor

* 지원 언어 - Python, R, JavaScript, Ruby, PHP, C#, Objective-C 등

 

Selenium 설치

pip install selenium (윈도우)
pip3 install selenium (맥, 리눅스)

 

Selenium Standalone Server 다운로드(크롬 브라우저로 접속, ipynb파일 위치에 다운로드)

https://www.selenium.dev/downloads/

 

Webdriver 설치 (크롬 브라우저 버전 확인 필요)

https://chromedriver.chromium.org/downloads

 

 

 

- Webdriver 로드 및 크롬 브라우저 가동

from selenium.webdriver import Chrome
import time
import sqlite3
from pandas.io import sql
import os
import pandas as pd

from selenium import webdriver

path = '/Users/jione/Downloads/chromedriver' #  크롬 드라이버 경로 변수


options = webdriver.ChromeOptions()
options.add_argument("--start-maximized");

browser = webdriver.Chrome(path, options=options) # 크롬 드라이버 

 

- 가동된 브라우저를 통한 URL 접속

browser.get('https://www.data.go.kr/')
browser.implicitly_wait(5)

공공 데이터 포털 사이트 열림

 

- 가동된 브라우저를 통한 URL 접속

browser.find_element_by_xpath('//*[@id="header"]/div/div/div/div[2]/div/a[1]').click() 
# 로그인 버튼 XPath
browser.implicitly_wait(5)

 

로그인 페이지로 이동됨

 

 

- ID/Password 입력 및

browser.find_element_by_xpath('//*[@id="mberId"]').send_keys('사용자 ID')
browser.find_element_by_xpath('//*[@id="pswrd"]').send_keys('사용자 Password')
browser.find_element_by_xpath('//*[@id="loginVo"]/div[2]/div[2]/div[2]/div/div[1]/button').click()
browser.implicitly_wait(5)

자동으로 아이디,비밀번호 입력 및 로그인

 

 

- 정보공유 링크 클릭

browser.find_element_by_xpath('//*[@id="M000400_pc"]/a').click()
browser.find_element_by_xpath('//*[@id="M000402_pc"]/a').click()

 

- 자료실 데이터 추출 및 저장

def db_save(ARTICLE_LIST):
    with sqlite3.connect(os.path.join('.','sqliteDB')) as con: # sqlite DB 파일이 존재하지 않는 경우 파일생성
        try:
            ARTICLE_LIST.to_sql(name = 'ARTICLE_LIST', con = con, index = False, if_exists='append') 
            #if_exists : {'fail', 'replace', 'append'} default : fail
        except Exception as e:
            print(str(e))
        print(len(ARTICLE_LIST), '건 저장완료..')
trs = browser.find_elements_by_xpath('//*[@id="searchVO"]/div[5]/table/tbody/tr')
df_list = []
for tr in trs:
    df = pd.DataFrame({
            'NO': [tr.find_element_by_xpath('td[1]').text],
            'TITLE': [tr.find_element_by_xpath('td[2]').text],
            'IQRY': [tr.find_element_by_xpath('td[3]').text],
            'REGDT': [tr.find_element_by_xpath('td[4]').text],
            'CHGDT': [tr.find_element_by_xpath('td[5]').text],
        })
    df_list.append(df)
    
ARTICLE_LIST = pd.concat(df_list)
db_save(ARTICLE_LIST)

10 건 저장완료..

 

No에 No.

TITlE에 제목

IQRY에 조회수

REGDT에 등록일

CHGDT에 수정일

개발자 도구에서 XPath 찾기

 

 

- 자료실 글목록 상세보기 클릭

browser.find_element_by_xpath('//*[@id="searchVO"]/div[5]/table/tbody/tr[1]/td[2]/a').click()
browser.implicitly_wait(3)

 

- 상세보기 첨부파일 다운로드 및 브라우저 종료

browser.find_element_by_xpath('//*[@id="recsroomDetail"]/div[2]/div[4]/div/a').click()
time.sleep(10)
browser.quit()

 

댓글