Translate

2017년 6월 15일 목요일

Python BeautifulSoup 네이버 블로그 크롤링 + txt 파일 만들기



Laptop
운영체제Windows 10 Home 64bit
개발프로그램PyCharm Community Edition 2017.1.3
Python 3.4.4


크롤링(Crawling)은 웹페이지의 데이터를 가져오는 것을 말한다.

Beautifulsoup라는 라이브러리가 이 역할을 수행하는데, pip을 통한 별도의 설치가 필요하다.

설치과정은 다른 블로그에 소개된 곳이 많으니 링크로 대체한다.

pip 설치: http://codingdojang.com/scode/371
Beautifulsoup 설치: http://shaeod.tistory.com/900

또한 requests 라는 연결 확인을 위한 라이브러리도 필요하다. 
같은 방식으로 커맨드창에 pip install requests 라고 입력하면 된다.

Beautifulsoup에 대한 기본적인 튜토리얼은 공식사이트에 잘 나와 있다.
id검색, class 검색 등 다양한 예제가 있으니 처음 접한다면 읽어보는 게 좋다.
http://beautifulsoup-korean.readthedocs.io/ko/latest/


프로그램 방식은 먼저 블로그 ID와 시작과 끝 페이지 수를 입력받고, 그 범위 안의 포스트(글)들을 긁어와 출력하면서 txt 파일에 저장한다.

naver_blog_crawling.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# --*-- coding: utf-8 --*--
import codecs

import re

import collections
import requests

from bs4 import BeautifulSoup


# 에러 리스트 생성 함수
def insert_error(blog_id, error, error_doc):
    for i in error_doc:
        error_log = str(error_doc["page"]) + "page / " + str(error_doc["post_number"]) \
                    + "th post / " + error + " / http://blog.naver.com/PostList.nhn?blogId=" + blog_id + "&currentPage=" + str(error_doc["page"])
    error_list.append(error_log)

total_num = 0;

error_list = []

print("블로그 ID->")
blog_id = input()

print("\n탐색 시작 페이지 수->")
start_p = int(input())

print("\n탐색 종료 페이지 수->")
end_p = int(input())

print("\nCreating File Naver_Blog_Crawling_Result.txt...\n")

# 파일 열기
file = codecs.open("Naver_Blog_Crawling_Result.txt", 'w', encoding="utf-8")

# 페이지 단위
for page in range(start_p, end_p + 1):
    print("=" * 50)
    file.write("=" * 50 + "\n")

    doc = collections.OrderedDict()

    url = "http://blog.naver.com/PostList.nhn?blogId=" + blog_id + "&currentPage=" + str(page)
    r = requests.get(url)
    if (not r.ok):
        print("Page" + page + "연결 실패, Skip")
        continue

    # html 파싱
    soup = BeautifulSoup(r.text.encode("utf-8"), "html.parser")

    # 페이지 당 포스트 수 (printPost_# 형식의 id를 가진 태그 수)
    post_count = len(soup.find_all("table", {"id": re.compile("printPost.")}))

    doc["page"] = page

    # 포스트 단위
    for pidx in range(1, post_count + 1):
        print('-' * 50)
        file.write('-' * 50 + "\n")

        doc["post_number"] = pidx
        post = soup.find("table", {"id": "printPost" + str(pidx)})

        # 제목 찾기---------------------------

        title = post.find("h3", {"class": "se_textarea"})

        # 스마트에디터3 타이틀 제거 임시 적용 (클래스가 다름)
        if (title == None):
            title = post.find("span", {"class": "pcol1 itemSubjectBoldfont"})

        if (title != None):
            doc["title"] = title.text.strip()
        else:
            doc["title"] = "TITLE ERROR"

        # 날짜 찾기---------------------------

        date = post.find("span", {"class": re.compile("se_publishDate.")})

        # 스마트에디터3 타이틀 제거 임시 적용 (클래스가 다름)
        if (date == None):
            date = post.find("p", {"class": "date fil5 pcol2 _postAddDate"})

        if (date != None):
            doc["date"] = date.text.strip()
        else:
            doc["date"] = "DATE ERROR"

        # 내용 찾기---------------------------

        content = post.find("div", {"class": "se_component_wrap sect_dsc __se_component_area"})

        # 스마트에디터3 타이틀 제거 임시 적용 (클래스가 다름)
        if (content == None):
            content = post.find("div", {"id": "postViewArea"})

        if (title != None):
            # Enter 5줄은 하나로
            doc["content"] = "\n" + content.text.strip().replace("\n" * 5, "\n")
        else:
            doc["content"] = "CONTENT ERROR"

        # doc 출력 (UnicodeError - 커맨드 창에서 실행 시 발생)
        for i in doc:

            str_doc = str(i) + ": " + str(doc[i])
            try:
                print(str_doc)
            except UnicodeError:
                print(str_doc.encode("utf-8"))

            # 파일 쓰기
            file.write(str_doc + "\n")

            # 에러 처리
            if ("ERROR" in str(doc[i])):
                insert_error(blog_id, doc[i], doc)

        # 전체 수 증가
        total_num += 1


# 결과 출력 (전체 글 수, 에러 수)
print("=" * 50)
file.write("=" * 50 + "\n")

print("Total : " + str(total_num))

error_num = len(error_list)
print("Error : " + str(error_num))

# 에러가 있을 경우 출력
if (error_num != 0):
    print("Error Post : ")
    for i in error_list:
        print(i)

# 파일 닫기
file.close()


Line 42:
doc 을 초반에 dict 형식으로 선언했다가 출력할 때 순서가 바뀐다는 것을 알고 OrderedDict로 변경했다. 그 후 값 입력 등 사용 방법은 dict 형식과 동일하다.

Line 71:
개발 중 에러가 발생하는 글들을 보면 블로그 페이지 소스보기에서 <!--스마트에디터3 타이틀 제거 임시 적용--> 이라는 주석이 붙은 글들이 있길래 (아마도 구버전으로 추정) 살펴봤더니 제목이나 내용의 태그 속성이 아예 다르다. if문으로 따로 구분해 두었다.
(가끔 이 두 형태가 섞여있는 블로그도 종종 있었다.)

페이지 상에서도 모양이 다르다.



현재 일반적인 모양

스마트에디터3 제거



Line 112:
UnicodeError는 커맨드 창에서 print 출력 시 종종 발생하는데, Pycharm의 경우 결과창 인코딩이 utf-8이기 때문에 문제없이 출력된다.
그래서 커맨드 출력의 경우 유니코드상태로 그대로 출력되는 경우가 많을텐데, txt 파일에 쓸 때는 별 문제 없으니 크게 신경쓰지 않아도 된다.



결과는 적당히 네이버 공식 블로그(http://blogpeople.blog.me/) 를 파싱한 결과로 캡쳐했다.


PyCharm 결과 화면






Cmd 결과 화면





생성된 txt 파일 (Naver_Blog_Crawling_Result.txt)
메모장으로 열면 \n가 깨진다. 인코딩을 선택할 수 있는 Microsoft Word 등의 프로그램으로 정상적인 결과를 확인할 수 있다.


댓글 1개: