• Twitter Hashtag Counter

    Twitter Hashtag Counter

    "We hebben binnenkort een nieuwe 24-uur durende twitteractie lopen en daarvoor hebben we een Hashtag counter nodig. Het aantal tweets met een bepaalde hashtag moet getelt worden en dit aantal moet op ieder moment te lezen zijn op onze website. Kan je zoiets maken?". "Tuurlijk!": zei ik. Zoiets zal wel makkelijk haalbaar zijn met de Twitter API... niet?

    first try

    Dat ik gebruik ging moeten maken van de Twitter API, was vooraf al duidelijk. En dus ging ik door de documentatie ervan en kwam al snel uit bij de HTTP request GET search:

    http://search.twitter.com/search.json?q=%23test

    Door deze url aan te roepen, krijg je alle tweets met hashtag #test binnen in json format. Cool, that was easy... right? Als ik dit test, krijg ik de (+/-) 300 meest recente tweets binnen. In de Twitter documentatie staat dat ik dit aantal zou kunnen optrekken tot 1.500 als ik de paramaters page en rrp instel. Deze vaststelling bracht echter enkele vragen met zich mee:

    1. Hoe krijg ik enkel de Tweets vanaf een bepaald uur binnen?
    2. Als de actie een succes is, krijg ik binnen de 24 uur vast meer dan 1.500 Tweets binnen. Wat dan?

    stappenplan

    Door de limiet op het aantal te ontvangen Tweets zou ik die op regelmatige tijdstippen moeten opslaan in een database. De vraag was nu hoe je de meest recente Tweets telkens opnieuw kon opvragen en opslaan in de database. Terug naar de Twitter API doc!

    Ik ging de GET Search parameters af en kwam onder andere "since_id" tegen: Returns results with an ID greater than (that is, more recent than) the specified ID. Bam! Net wat ik nodig had.

    http://search.twitter.com/search.json?q=%23godofwar&since_id=309638870578917377

    Als je dit uitstuurt, krijg je dus alle Tweets vanaf de since_id 309638870578917377. Daarnaast krijg je ook een nieuwe, meer recentere since_id teruggestuurd. Op basis hiervan, bedacht ik deze flow:

    STAP 1: Een search request sturen voor de meest recente tweets
    STAP 2: Alle Tweets opslaan in een database
    STAP 3: De nieuwe since_id opslaan voor de volgende request

    Door deze stappen te herhalen, wordt de database gevuld met Tweets. Om de hoeveel tijd moet deze request nu uitgestuurd worden? Dat beslis je zelf, maar hou rekening dat er max. 1.500 tweets per HTTP request kunnen opgevraagd worden.

    Get tweets

    Ik gebruik onderstaande functie om de Tweets binnen te halen en op te slaan in de databank (m.b.v. Spoon Library).

    // set variables
    global $total, $hashtag;
    $hashtag = '#test';
    $total = 0;
    
    function getTweets($hash_tag, $page) {
    
        // build query
        $query = '
        SELECT value
        FROM settings
        WHERE type = ?';
    	
        // put all articles in a string
        $since_id = getVar($query, array('since_id'));
    	
        // create Twitter search url (exclude retweets)
        $url = "http://search.twitter.com/search.json?q=". urlencode($hash_tag);
        $url .= "&page=" . $page;
        $url .= "&since_id=" . $since_id;
        $url .= "-filter:retweets";
        
        // get contect from json  
        $json = file_get_contents($url,0,null,null);
        
        // decode
        $json_decode = json_decode($json,false);
    
        // get results only
        $resultsOnly = $json_decode->results;
    
        // insert results in database
        for ($i = 0; $i < count($json_decode->results); $i++) {
    
         	// assemble tweet values 
         	$newTweet = array(); 
         	$newTweet['from_user'] = $resultsOnly[$i]->{'from_user'};
         	$newTweet['from_user_name'] = $resultsOnly[$i]->{'from_user_name'};
         	$newTweet['profile_image_url'] = $resultsOnly[$i]->{'profile_image_url'};
         	$newTweet['text'] = $resultsOnly[$i]->{'text'};
            $newTweet['created_at'] = $resultsOnly[$i]->{'created_at'};
            $newTweet['source'] = $resultsOnly[$i]->{'source'};
    
         	// the insert function returns the insert ID 
         	$db->insert('tweets', $newTweet); 
        }
        
        // check for next searchpage
        if($json_decode->next_page){
           $temp = explode("&",$json_decode->next_page);            
           $p = explode("=",$temp[0]);                
           getTweets($hashtag,$p[1]);   
        }
        
        // update since_id value 
        $sinceId = array(); 
        $sinceId['value'] = $json_decode->max_id_str;
        $db->update('settings', $sinceId, 'type=?','since_id');
    }
    
    
    

    Simultane requests

    Om de request op regelmatige tijdstippen uit te sturen, vroeg ik m'n hostingpartner een cronjob op te zetten. Dat was geen probleem. Ze wisten me echter wel op een mogelijk probleem te wijzen.

    Stel: de twitter actie is een enorm succes en het hele land doet eraan mee (jaja, het kan gebeuren...). Dan kan er zich een situatie voordoen dat een cronjob request nog niet afgelopen is tegen dat de volgende request gebeurt. Dit is een gevaarlijke situatie, want op dat moment halen we 2x dezelfde tweets op omdat de since_id nog niet geüpdatet is. Als deze situatie zich bovendien blijft voordoen, kan het boeltje crashen.

    Better safe than sorry. Ik zocht een oplossing en vond die in de vorm van een file lock. Misschien niet de meest goede oplossing, maar de deadline naderde ...

    De opzet is dat je gebruik maakt van fopen() flock(). Voordat we de getTweets functie aanroepen, openen we een (leeg) txt-bestandje op de server. Daardoor krijgt het txt-bestand een 'locked' status. Nadat de functie getTweets succesvol doorlopen is, wordt flock.txt opnieuw unlocked. De status kan nagekeken worden met flock(). Met een eenvoudige IF statement gaan we na of het bestand al geopend is. Zoja, dan mag de functie uitgevoerd worden, anders doe je niets.

    
    $f = fopen("flock.txt", "w");
    if (flock($f, LOCK_EX | LOCK_NB)) {
    getTweets();
    flock($f, LOCK_UN);
    }else{
    //abort
    }
    
    

    Ik heb dit getest door snel twee keer na elkaar de request uit te voeren en dit werkte fantastisch. Terwijl de eerste request liep, werd de tweede afgebroken.