Data Crawling - 파이썬을 이용한 트위터 수집하기

어떤 사건에 대한 사람들의 즉각적인 관심과 반응, 그리고 영향력자들이 어떻게 일반 사용자에게 영향을 미치는지 분석하는 데는 트위터 만한 SNS가 없다.

본 포스트에서는 예제로 19년 4월 4주차에서 어벤져스 또는 스포가 포함된 트윗을 수집해 볼 것이다.

어벤져스 엔드게임은 19년 4월 24일에 개봉해서 엄청난 인기를 끌었는데, 한동안 SNS에서 일명 스포 (스포일러)를 한다고 절대 아무거나 클릭하지 말라고 소동이 있었던 것으로 기억한다.

개봉일 전후로 트윗 수가 차이가 나고, 스포와 같이 작성된 트윗이 많을 것으로 기대된다.

수집 도구 선택

트위터에서는 공식적으로 데이터 수집을 지원하는 API를 제공하고 있는데, 이름은 Tweepy. 최근 자료들을 얻는 데 있어서는 간단하고 빠르다.

그러나, 이 방법에는 치명적인 단점이 존재하는데.. 바로 현재 시간부터 7일 이전까지의 데이터만 수집이 가능하다는 점이다.

그 이전의 데이터를 수집하고 싶다면, Premium-Api를 구매해야 한다.. 트위터 개발자 홈페이지에 접속하면 아래와 같은 가격 플랜을 안내하고 있다.

Pricing for the elevated tiers of the Search Tweets: 30-day endpoint start at $149/month for 500 requests, while pricing for the Search Tweets: Full-archive endpoint starts at $99/month for 100 requests.

상당한 비용이 발생함을 알 수 있다. 그럼 다른 방법은 없을까?

직접 크롤러를 만들수도 있지만 그건 최후의 (?) 수단이고, 누군가 만들어 놓은 도구가 있다면 쓰는 것이 인지상정! 다행히 오래된 트윗을 수집할 수 있는 GetOldTweet3 이라는 패키지가 있다.

이 패키지를 사용해서 수집할 수 있는 변수들을 확인해 보니, 내가 원하는 것은 대부분 수집이 가능했다.

  • 업로드 유저 아이디 (username)
  • 트윗 링크 (permalink)
  • 트윗 내용 (text)
  • 업로드 시간 (date)
  • 리트윗 수 (retweets)
  • 관심글 수 (favorites)

또한, 다양한 기준으로 데이터 수집 범위를 설정할 수 있었다.

  • 특정 유저 아이디로 트윗 검색 (setUsername)
  • 기간 안의 트윗 검색 (setSince / setUntil)
  • 특정 검색어가 포함된 트윗 검색 (setQuerySearch)
  • 기준 위치를 설정하고 근처에서 생성된 트윗 검색 (setNear / setWithin)
  • 출력할 최대 트윗 수 지정 (setMaxTweets)

좀 더 자세한 옵션 및 사용 방법은 GetOldTweet3 github 을 참고하자.

패키지 준비

본격적으로 크롤링에 앞서, 먼저 GetOldTweet3 패키지를 설치한다. 본 포스트에서는 python 3.7, ubuntu 18.04 의 개발 환경을 기본으로 한다. Jupyter Notebook으로 코드를 작성하였다.

1
2
3
4
5
6
# GetOldTweet3 사용 준비
try:
import GetOldTweets3 as got
except:
!pip install GetOldTweets3
import GetOldTweets3 as got

또한, 추가적인 데이터 수집을 위해서 이전 포스트에서도 사용하였던 Beautifulsoup4 가 설치되어 있는지 확인한다.

1
2
3
4
5
6
# BeautifulSoup4 사용 준비
try:
from bs4 import BeautifulSoup
except:
!pip install bs4
from bs4 import BeautifulSoup

수집 기간 정의하기

본격적으로 크롤러를 만들어 보자. 먼저 datetime을 사용하여 원하는 수집 기간을 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 가져올 범위를 정의
# 예제 : 2019-04-21 ~ 2019-04-24

import datetime

days_range = []

start = datetime.datetime.strptime("2019-04-21", "%Y-%m-%d")
end = datetime.datetime.strptime("2019-04-25", "%Y-%m-%d")
date_generated = [start + datetime.timedelta(days=x) for x in range(0, (end-start).days)]

for date in date_generated:
days_range.append(date.strftime("%Y-%m-%d"))

print("=== 설정된 트윗 수집 기간은 {} 에서 {} 까지 입니다 ===".format(days_range[0], days_range[-1]))
print("=== 총 {}일 간의 데이터 수집 ===".format(len(days_range)))

> === 설정된 트윗 수집 기간은 2019-04-21 에서 2019-04-24 까지 입니다 ===
> === 총 4일 간의 데이터 수집 ===

days_range 라는 이름의 리스트에 날짜를 %Y-%m-%d 형태로 저장해 놓았다.

트윗 수집하기

이제 본격적으로 트위터에서 데이터를 크롤링할 차례이다.

GetOldTweet3tweetCriteria로 수집 기준을 정의할 수 있다.

앞에서 설정한 수집 기간에서 어벤져스 또는 스포 가 포함된 트윗을 모두 수집해 보자.

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
# 특정 검색어가 포함된 트윗 검색하기 (quary search)
# 검색어 : 어벤져스, 스포

import time

# 수집 기간 맞추기
start_date = days_range[0]
end_date = (datetime.datetime.strptime(days_range[-1], "%Y-%m-%d")
+ datetime.timedelta(days=1)).strftime("%Y-%m-%d") # setUntil이 끝을 포함하지 않으므로, day + 1

# 트윗 수집 기준 정의
tweetCriteria = got.manager.TweetCriteria().setQuerySearch('어벤져스 OR 스포')\
.setSince(start_date)\
.setUntil(end_date)\
.setMaxTweets(-1)

# 수집 with GetOldTweet3
print("Collecting data start.. from {} to {}".format(days_range[0], days_range[-1]))
start_time = time.time()

tweet = got.manager.TweetManager.getTweets(tweetCriteria)

print("Collecting data end.. {0:0.2f} Minutes".format((time.time() - start_time)/60))
print("=== Total num of tweets is {} ===".format(len(tweet)))

> Collecting data start.. from 2019-04-21 to 2019-04-24
> Collecting data end.. 41.39 Minutes
> === Total num of tweets is 22964 ===

수집하는 데 시간이 조금 걸린다. 참고로 너무 많은 트윗을 한번에 수집하려다 보면, 트위터 측에서 나가라고 쫒아낸다.. (Error 104)

An error occured during an HTTP request: [Errno 104] Connection reset by peer

Connection 관련한 에러가 뜨면, 지정한 날짜 범위에 기준을 만족하는 트윗의 수가 너무 많은 것이니 범위를 좁혀서 다시 시도해 보자.

수집하는 데 얼마나 시간이 걸렸는지 알아보기 위해 time 을 임포트 해서 코드 몇줄을 추가했다. 참고로 나는 이 과정에서 1시간 넘게 소요된 적도 있었으니 참을성있게 기다려보자.

위 코드는 41분 가량 소요되었다. 몇개의 트윗이 수집되었는지 출력되면, 아래 단계로 넘어가자.

변수 저장하기

이제 원하는 정보만을 저장해 보자. GetOldTweet3 에서 제공하는 기본 변수 중 유저 아이디, 트윗 링크, 트윗 내용, 날짜, 리트윗 수, 관심글 수를 수집한다.

또한, 이 패키지에서 제공하지 않는 변수 중 각 유저의 가입일, 전체 트윗 수, 팔로잉 수, 팔로워 수도 같이 수집한다. 이때, 앞서 준비한 BeautifulSoup4 를 사용한다. 자세한 사용 방법은 이전 포스트들을 참고하자.

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
# 원하는 변수 골라서 저장하기

from random import uniform
from tqdm import tqdm_notebook

# initialize
tweet_list = []

for index in tqdm_notebook(tweet):

# 메타데이터 목록
username = index.username
link = index.permalink
content = index.text
tweet_date = index.date.strftime("%Y-%m-%d")
tweet_time = index.date.strftime("%H:%M:%S")
retweets = index.retweets
favorites = index.favorites

# === 유저 정보 수집 시작 ===
try:
personal_link = 'https://twitter.com/' + username
bs_obj = get_bs_obj(personal_link)
uls = bs_obj.find("ul", {"class": "ProfileNav-list"}).find_all("li")
div = bs_obj.find("div", {"class": "ProfileHeaderCard-joinDate"}).find_all("span")[1]["title"]


# 가입일, 전체 트윗 수, 팔로잉 수, 팔로워 수
joined_date = div.split('-')[1].strip()
num_tweets = uls[0].find("span", {"class": "ProfileNav-value"}).text.strip()
num_following = uls[1].find("span", {"class": "ProfileNav-value"}).text.strip()
num_follower = uls[2].find("span", {"class": "ProfileNav-value"}).text.strip()

except AttributeError:
print("=== Attribute error occurs at {} ===".format(link))
print("link : {}".format(personal_link))
pass

# 결과 합치기
info_list = [tweet_date, tweet_time, username, content, link, retweets, favorites,
joined_date, num_tweets, num_following, num_follower]
tweet_list.append(info_list)

# 휴식
time.sleep(uniform(1,2))

(주의: 실행 시 약 24시간이 소요됩니다. 결과를 빨리 확인하려면 유저 정보 수집 부분을 전부 주석처리 해주세요.)

유저의 가입일, 전체 트윗 수, 팔로잉 수, 팔로워 수 와 같은 변수는 GetOldTweet3으로 얻은 username으로 personal_link을 만들어 수집하였다.

중간에 try-except 구문을 사용하였는데, 이는 수집을 시도해 보니 몇몇 사용자의 팔로잉 수 혹은 팔로워 수가 공개되어 있지 않아 AttributeError을 발생시키고 있었다. 이런 에러를 발생시키는 계정은 보통 광고용 찌라시 계정이었는데, 이를 확인하기 위해 에러 발생시 그 link를 출력하도록 코드를 구성하였다.

또한, 공격적인 크롤링 방지를 위해 random.uniform()을 활용하여 아래에 1~2초 사이로 랜덤하게 for문을 쉬게 하는 코드를 추가했다.

트윗 수집 결과는 tweet_list에 저장된다.

파일 저장 후 확인

이제 결과를 csv 파일로 저장하고, 저장된 파일을 불러와서 확인해 보자. Pandas 패키지를 사용할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# 파일 저장하기

import pandas as pd

twitter_df = pd.DataFrame(tweet_list,
columns = ["date", "time", "user_name", "text", "link", "retweet_counts", "favorite_counts",
"user_created", "user_tweets", "user_followings", "user_followers"])

# csv 파일 만들기
twitter_df.to_csv("sample_twitter_data_{}_to_{}.csv".format(days_range[0], days_range[-1]), index=False)
print("=== {} tweets are successfully saved ===".format(len(tweet_list)))

> === 22964 tweets are successfully saved ===

위 코드를 실행시키면, working directory 내에 sample_twitter_data_2019-04-21_to_2019-04-24.csv 파일이 생성되었음을 확인할 수 있다.

생성한 파일을 로드해서 내용을 확인해 보자.

1
2
3
4
# 파일 확인하기

df_tweet = pd.read_csv('sample_twitter_data_{}_to_{}.csv'.format(days_range[0], days_range[-1]))
df_tweet.head(10) # 위에서 10개만 출력

데이터 통계 확인

수집한 데이터라 어떤 특징을 보이고 있는지 간단하게 확인해 보자.

어벤져스 또는 스포가 포함된 트윗을 수집하였는데, 각각의 빈도는 어느 정도일까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 키워드 빈도 분석하기

def get_keywords(dataframe):
keywords = []
text = dataframe["text"].lower()
if "어벤져스" in text:
keywords.append("어벤져스")
if "스포" in text:
keywords.append("스포")
return ",".join(keywords)

df_tweet["keyword"] = df_tweet.apply(get_keywords,axis=1)

# barplot 그리기

import matplotlib.pyplot as plt

counts = df_tweet["keyword"].value_counts()
plt.bar(range(len(counts)), counts)
plt.title("Tweets mentioning keywords")
plt.ylabel("# of tweets")
plt.show()
print(counts)

barplot을 그릴 때에는 파이썬의 visualization package 중 가장 유명한 matplotlib을 사용했다.

스포 가 단일로 포함된 트윗이 14,782개로 가장 많았고, 그 뒤로 어벤져스 단일이 6,902개 , 그리고 어벤져스스포 모두 포함된 트윗이 1,248개 로 파악된다.

이번에는 어벤져스 개봉일이 다가오면서 변화하는 트윗의 빈도를 출력해 보자.

1
2
3
4
5
6
7
8
# 날짜별 빈도 분석하기

counts = df_tweet["date"].value_counts().sort_index()

plt.title("Tweets mentioning keywords in time series")
plt.ylabel("# of tweets")
counts.plot(kind = 'bar')
print(counts)

역시나 예상했던 대로, 영화 개봉일인 4월 24일이 되자 트윗이 14,989개로 폭발적으로 증가했음을 확인할 수 있다.

여기까지 간단하게 데이터의 shape 정도를 확인해 보았다. 이외에도 다양한 방법으로 데이터를 분석할 수 있으니 그건 각자 해보는 걸로..

20.02.25 추가
트위터 크롤링, 패키지 없이 해보자! 라는 주제로 새로 블로그에 글을 업로드 하였으니 필요하신 분은 참고 바랍니다.

20.04.23 추가
트위터 파헤치기 시리즈 첫번째 - 수집하기 라는 트위터 크롤링 관련 포스트를 새로 작성하였습니다. 많은 분들이 댓글로 질문 주셨던 내용들이 여기에 자연스럽게 담겨 있습니다. 일례로, 해당 날짜의 전수 트윗을 모으는 것이 아닌 popular한 100개의 트윗만을 모으는 것도 가능합니다. 자세한 내용은 포스트를 읽어 보시고, 궁금한 점 댓글 달아주세요!