안녕하세요, JM입니다!
지난번에 이어서, 스크래퍼를 마무리해 보려고 합니다!
지난번의 포스팅은 아래 링크를 참고해주세요!
다시 한 번 우리의 전략을 정리해 볼게요.
1. 원하는 웹 사이트에 요청을 보내고 응답을 받는다.
2. 원하는 부분을 추출한다.
3. 추출한 데이터를 저장한다.
그리고 다시 2를 쪼개보겠습니다.
2-1. 총 페이지 수를 추출하기
2-2. 한 페이지에 있는 게시글을 추출하기
2-3. 한 게시글 안에 있는 정보 중 필요한 정보를 추출하기
이 전 게시글에서 2-1까지는 진행했다고 할 수 있겠네요!
그럼 2-2부터 진행해 보도록 하겠습니다.
다음과 같이 코드를 작성해 보겠습니다.
def get_post_list(last_page):
posts = []
for page in range(last_page):
print(f"Getting page {page+1} posts ...")
result = rq.get(f"{TISTORY_URL}/?page={page+1}")
soup = BeautifulSoup(result.text, "html.parser")
post_items = soup.find_all("div", {"class" : "post-item"})
for post_item in post_items:
post = get_post_info(post_item)
posts.append(post)
return posts
def get_post_info(post_item):
title = post_item.find("span", {"class" : "title"}).string.strip()
date = post_item.find("span", {"class" : "date"}).string.strip()
link = post_item.find("a")["href"]
return {"title" : title,
"date" : date,
"link" : f"{TISTORY_URL}{link}"}
이 전에 정의한 get_last_page에서 반환한 값을 get_post_list함수의 iuput으로 넣어주면 됩니다.
get_post_list 함수에서는, last_page의 수만큼 URL을 수정하며 rq.get을 호출합니다.
이를 통해 class명이 post-item이라는 div들을 list형태로 post_items라는 변수로 저장합니다.
*find와 find_all의 차이점 : find의 경우는 제일 처음 발견되는 한 개를 반환하는 반면에, find_all의 경우 해당하는 모든 요소를 리스트 형태로 출력하게 됩니다.
여기서 post_items은 해당 페이지에서 스크랩해온 class=post-item인 모든 div들을 가지고 있겠죠?
그럼 post_items라는 리스트 형태의 변수의 길이(length)는 해당 페이지의 게시글의 개수가 되겠군요!
이 개수만큼 for문을 사용하여 새로 정의한 get_post_info라는 함수를 call 하여, 각각의 post에 대한 정보를 리턴 받고, 이를 posts라고 정의한 리스트형 변수에 append(추가) 해 줍니다.
post_item.find("a")["href"] 이 부분을 주목해볼게요.
실제로 HTML 코드를 보면 아래와 같습니다.
<a href="/entry/2103%E6%9C%88-%ED%88%AC%EC%9E%90%EB%B3%B4%EA%B3%A0%EC%84%9C">
이 경우 find("a")["href"] 이와 같은 문법을 사용하면, a라는 태그를 달고 있는 것 중에서, href의 값을 추출해줍니다.
이는 BeautifulSoup의 기능 중 하나로 생각하시고 사용하시면 쓸 일이 많을 것 같습니다!
get_post_info 함수에서는, post_item이라는 div를 입력받아서, 이 포스팅의 제목, 게시날짜, 링크를 묶어서 dictionary형태로 반환해주는 함수입니다.
*div, span, a 등은 제 생각에 자바스크립트, HTML 이런 웹 개발에 대해 공부하면 정확한 뜻을 알 수 있을 것 같습니다.
*꼭 저와 같은 형태를 사용하지 않고도, 원하는 정보를 스크랩할 수 있습니다.(div대신 다른 것을 써도 된다는 말이죠, 가능하다면)
*. string은 해당 객체의 문자열만을 원한다는 의미이며(BeautifulSoup객체의 매써드로 보입니다)
*. strip()은 string객체의 매써드...로 보이는데, 아무 옵션 없이 call 하면, 양쪽 공백을 없애주는 옵션을 제공합니다.
최종적으로 posts라는 list형 변수에는, 각 포스팅의 '제목', '게시 날짜', '링크'가 dictionary형태로 포함되어 나열되어 있을 것입니다.
위 함수들을 실행하고, print 해본 모습입니다.
14페이지까지 잘 반복된 것 같고, 출력된 posts를 보니 제가 원하는 대로 '제목', '게시 날짜', '링크'가 딕셔너리들의 list로 저장된 것을 확인할 수 있습니다.
그럼 우리는 Posts라는 list를 csv파일로 저장하기만 하면 되겠네요!
csv파일로 저장하기 위해 우리는 csv라는 모듈을 사용할 예정입니다.
그리고 아래와 같은 코드를 작성합니다.
def save_post_to_csv(posts):
file = open("posts.csv", mode = "w")
writer = csv.writer(file)
writer.writerow(["제목", "게시 날짜", "링크"])
for post in posts:
writer.writerow(list(post.values()))
print("Save Finished!")
save_post_to_csv라는 함수는, posts라는 리스트를 입력받아서, 이 입력을 '제목', '게시 날짜', '링크' 순으로. csv 파일에 넣어주는 역할을 합니다.
post.csv라는 파일명으로 저장하기 위해 file이라는 변수에 mode = "w"라는 옵션으로 open이라는 함수를 call 해 줍니다.
writer라는 것을 정의할 건데요, 이 친구는 우리가 실제로 csv파일에 써나가기 위해 필요한 친구입니다.
writer객체의 writerow매써드를 활용하여 행을 채워 넣습니다.
이렇게 리스트 형태로 넣지 않고, "제목", "게시 날짜", "링크"와 같이 넣어도 뭐.. 만들어지긴 만들어집니다만.
ㄱ ㅔ ㅅ ㅣ ㄴ ㅏ ㄹ ㅉ ㅏ <- 이런 형태로 각 셀에 입력되더라고요? 그래서 리스트로 채워 넣었습니다.
자 이제, 실제 포스팅에 대한 정보들을 csv파일에 넣어볼게요.
posts 리스트에 있는 포스트 한 개당 한 번씩 writerow매써드를 사용하면 되겠죠?
그리고, post의 하나하나는 dictionary 타입으로 이루어져 있죠. 우리는 여기서 key가 아닌 value만을 사용하고 싶습니다.
딕셔너리의 강력한 기능 중 하나는, 위 코드와 같이 values()라는 매써드를 지원한다는 겁니다!
또한 이들을 리스트 형태로 형 변환을 해서 넣어주었습니다. 이 차이점은 직접 비교해보세요!
*csv란 comma separated value의 약자입니다.
*open의 옵션은 "w" : 쓰기, "r" 읽기... 더 있겠지만 이 정도만 알아도 될 것 같습니다.
자 그럼 결과물을 볼게요
만들어진 csv파일입니다.
엑셀, 구글 스프레드 등 모두에서 열리는 파일이죠.
여기서 신기한 게 있습니다. CSV 파일이란, Comma, 즉 쉼표로 셀을 분리하는데요.
만약 내용에 쉼표(,)가 들어가 있으면 어떻게 될까요? 이를테면 "[파이썬] 간단한,,, 웹 스크래퍼(Web Scrapper) 만들기! -1" 이런 이름으로요.
csv의 정상적인 포맷대로라면 저 쉼표 하나하나를 모두 구분하겠죠? 하지만 우리는 csv라는 모듈을 import트 했잖아요?
이 모듈의 writer는 바로 이 작업을 처리해줍니다.
이 정도로 웹 스크랩 + csv파일로의 저장까지 진행해보았습니다.
이런 과정을 통해서 주식공부, 업무 등도 많이 자동화할 수 있을 거라고 생각합니다.
앞으로는 이러한 과정들을 제가 공부하는 대로 포스팅을 조금씩 진행해 볼까 합니다.
긴 글 읽어주셔서 감사합니다!
다음번에 찾아뵐게요!
'공부 > [Python]' 카테고리의 다른 글
[파이썬] 간단한 웹 스크래퍼(Web Scrapper)만들기! - 1 (0) | 2021.04.07 |
---|
댓글