일곱째 날: Twitter streaming API 사용하기

저자

@gypark - gypark.pe.kr의 주인장. 홈페이지에 Perl에 대해 정리해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.

시작하며

트위터에서 제공하는 스트리밍 API를 사용해 타임라인을 감시하다가 어떤 조건에 맞는 트윗에 대해 원하는 작업을 수행하는 방법을 소개합니다.

배경

2010 - 넷째 날: 선물 세 가지 :-D에서, CPAN의 Net::Twitter::Lite 모듈을 사용하여 펄로 트위터에 트윗을 올리는 것을 소개한 적이 있습니다. 이 모듈을 쓰면 트윗을 올리는 것 뿐 아니라 내 타임라인을 가져올 수도 있습니다. (물론 그 외에도 트위터 API에서 지원하는 모든 작업을 할 수 있습니다.) 그렇다면, 타임라인을 주기적으로 가져와 그 타임라인 안의 트윗을 분석해 어떤 유용한 일을 할 수 있을 것입니다. 예를 들어 특정한 문자열이 들어 있는 트윗들만 뽑아내어 저장한다거나 말이죠.

그런데 이 때 몇 가지 불편한 점이 있습니다. 트윗이 올라오자마자 처리하지 못하고, 다음 번 주기에 스크립트가 실행된 후에야 처리하게 됩니다. 그렇다고 반복 실행 주기를 매우 짧게 준다면, 트위터 서버에서 제한하는 API 호출 제한에 걸려서 일정 시간 동안 API 사용을 할 수 없게 됩니다. 타임라인을 한 번 가져오고 그 다음 번 가져오는 사이에 올라온 트윗 중에 일부를 놓치거나, 반대로 한 트윗을 두 번 이상 중복 처리하는 일을 막기 위해서는 매번 어느 트윗부터 어느 트윗까지 읽었는지를 기록해주어야 합니다.

다행히도, 트위터는 스트리밍 API를 따로 제공하기 시작했습니다. 이 API를 사용하는 프로그램은 트위터 서버에 계속 연결된 상태로 있으면서, 어떤 이벤트(새로운 트윗이 올라온다거나, 새 팔로워가 추가되거나, 다이렉트 메시지를 받는 것 등)가 발생할 때마다 그 내용을 통보받을 수 있습니다.

CPAN의 AnyEvent::Twitter::Stream 모듈을 이용해 펄에서 스트리밍 API를 활용하는 간단한 예제를 소개합니다.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan \
  AnyEvent::Twitter::Stream \
  Net::Twitter::Lite \
  Try::Tiny;

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan \
  AnyEvent::Twitter::Stream \
  Net::Twitter::Lite \
  Try::Tiny;

트위터 OAuth

트위터 API를 사용하려면 자신만의 앱을 트위터 서버에 등록한 후, OAuth 인증을 해야 합니다. 트위터 앱 관리 서비스에 접속하여 Create New App 버튼을 눌러 진행하면 되며, 등록 과정에 나오는 항목에 대해서는 2010 - 넷째 날: 선물 세 가지 :-D를 참고하세요.

앱을 등록할 때 앱의 권한을 조절할 수 있는데, 관심글 등록 기능을 쓰려면 write 권한이 있어야 하고, DM 기능을 쓰려면 "Read, Write and Access direct messages"를 선택해야 합니다.

권한 설정 그림 1. 앱 권한 설정 (원본)

전체 코드

다음 코드는 실제로 동작하는 스크립트입니다. 코드의 각 부분에 대한 설명은 주석문으로 상세히 적어두었으니 참고하세요. :)

#!/usr/bin/env perl

use v5.20;
use strict;
use warnings;

use AnyEvent::Twitter::Stream;
use Net::Twitter::Lite::WithAPIv1_1;
use Try::Tiny;

binmode STDOUT, ':encoding(UTF-8)';

my $done = AE::cv;

#
# http://dev.twitter.com 에서 자신의 앱을 등록한 후 받아오는 키와 토큰
# 각 항목은 자신이 받은 값으로 채웁니다.
# 
my $oauth = {
    consumer_key        => '****',
    consumer_secret     => '****',
    access_token        => '****',
    access_token_secret => '****',
};

#
# 내 트위터 ID - 디엠을 보낼 때 수신자 아이디에 넣기 위해 필요
# 
my $my_id = '****';

#
# 트위터 REST API 클라이언트
# favorite, DM 등을 쓰기 위해 필요
# 
my $api = Net::Twitter::Lite::WithAPIv1_1->new(
    %{$oauth},
    legacy_lists_api => 1,
    ssl              => 1
);

#
# 트위터 스트리밍 API 이벤트 처리기
# 
my $streamer = AnyEvent::Twitter::Stream->new(
    # oAuth 정보
    consumer_key    => $oauth->{consumer_key},
    consumer_secret => $oauth->{consumer_secret},
    token           => $oauth->{access_token},
    token_secret    => $oauth->{access_token_secret},

    #
    # 스트림 타입
    # 세 가지 중 하나를 선택할 수 있는데,
    # 우리는 자신의 타임라인을 보고 싶은 것이므로 userstream 선택
    # https://dev.twitter.com/streaming/overview
    # 
    method   => 'userstream',

    #
    # 1로 하면 종종 문제가 생김
    # 
    use_compression => 0,

    #
    # 여러 가지 이벤트에 대해 그 이벤트를 처리할 핸들러를 등록한다:
    #
    # 이벤트에 대한 설명은 다음 주소 참조:
    # https://dev.twitter.com/streaming/overview/messages-types
    #

    #
    # 새 트윗이 올라왔을 때 - 코드가 길어서 별도 함수로 분리
    # 
    on_tweet => \&on_tweet,

    #
    # 에러가 발생하면 에러 내용을 출력한 후 종료
    # 
    on_error => sub {
        my $error = shift;
        warn "ERROR: $error";
        $done->send;
    },

    #
    # EOF는 정상적인 상태라면 발생하지 않을 것으로 생각됨
    # 
    on_eof   => sub {
        $done->send;
    },

    #
    # 굳이 따로 처리하지 않아도 무방해 보이는 이벤트
    # 
    #on_connect   => sub {},
    #on_keepalive => sub {},

    #
    # 다음 이벤트들은 따로 핸들러를 지정하지 않으면 on_tweet 핸들러가 불린다.
    # 따라서 명시적으로 빈 서브루틴을 지정하여 아무 일도 하지 않게 함
    # 
    on_direct_message => sub {},
    on_event          => sub {},
    on_friends        => sub {},
    on_delete         => sub {},
);

#
# 스크립트는 이 지점에서 대기하면서 이벤트 발생을 기다리게 되고,
# 이벤트가 발생하면 핸들러가 불리는 과정을 반복한다.
# $done->send 가 호출되면 그 때 대기를 끝내고 종료
# 
$done->recv;

exit 0;

#
# 새 트윗이 올라왔을 때 불릴 핸들러
# 인자로 트윗 객체가 익명 해시의 형태로 전달된다.
# 그 해시의 키와 값은 다음 주소 참조
# https://dev.twitter.com/overview/api/tweets
# 
sub on_tweet {
    my $tweet = shift;

    # 트윗 기본 정보 (트윗 아이디, 작성자 아이디 등) 추출
    my $tweet_id       = $tweet->{id};
    my $tweet_userid   = $tweet->{user}{screen_name};
    my $tweet_username = $tweet->{user}{name};
    my $tweet_text     = $tweet->{text};
    # URL이 포함된 트윗은 URL 추출
    my @tweet_urls;
    if ( $tweet->{entities}{urls} ) {
        @tweet_urls = map { $_->{expanded_url} } @{$tweet->{entities}{urls}};
    }

    unless ($tweet_id and $tweet_userid) {
        return;
    }

    #
    # 디버그용 출력
    # 
    print <<"EOF";

* id     : $tweet_id
* author : $tweet_username (\@$tweet_userid)
* text   : $tweet_text
EOF
    foreach my $url ( @tweet_urls ) {
        print "* url    : $url\n";
    }

    #
    # 'perl' '펄'이 포함된 트윗에 대하여 특별 처리
    # 
    if ( $tweet_text =~ /perl|펄/i ) {
        #
        # Net::Twitter::Lite의 여러 메쏘드들은 실행 도중 에러가 나면
        # 에러의 내용이 담긴 Net::Twitter::Lite::Error 객체를 던지며 die한다.
        # eval { ... } 로 감싸고 $@ 변수의 내용을 검사해도 되고,
        # 여기서는 Try::Tiny 를 사용하여 에러 핸들링을 함
        # 
        try {
            #
            # favorite 체크를 하거나
            # 
            $api->create_favorite( { id => $tweet_id } );

            #
            # DM으로 자기 자신에게 그 내용을 보내거나
            # 
            $api->new_direct_message( { screen_name => $my_id, text => $tweet_text } );
        }
        catch {
            warn "API error: $_";
        };
    }
}

실행

이 스크립트를 실행하면 조용히 있다가, 로그인한 계정이 팔로우하는 사람들이 트윗을 올릴 때마다 반응합니다. 위 코드에서는 디버그를 위해서 표준 출력으로도 간단한 정보를 출력하도록 했기 때문에 실행 결과는 다음과 같습니다. 테스트한 계정의 팔로잉의 개인적인 트윗은 가려두었고 테스트를 위해 @JEEN_LEE님께서 도와주셨습니다.

디버그용 출력 그림 2. 디버그용 출력 (원본)

트윗들 중에 "perl" 또는 "펄"이라는 문자열이 있으면, 그 트윗들은 자동으로 관심글 목록에 추가되어 있음을 확인할 수 있습니다.

관심글 목록 그림 3. 관심글 목록 (원본)

더불어 쪽지함에도 내용이 들어가 있음을 확인할 수 있습니다.

쪽지함 그림 4. 쪽지함 (원본)

보다시피 "단순 문자열 검색"만으로는 의도하지 않은 내용도 같이 수집될 수 있다는 것을 알 수 있습니다. 너무 당연한가요? :-)

정리하며

여기서는 단순히 관심글 목록에 담는 것으로 끝났지만, 좀 더 응용하면 내용과 링크를 추출하여 데이터베이스에 넣는다거나, 통계 처리를 한다거나, REST API를 같이 사용하여 마치 봇처럼 자동으로 특정 조건에 반응하게 할 수도 있을 것입니다. 재미있는 응용법이 많이 나오길 기대해봅니다. :)

blog comments powered by Disqus