Avec Marion Louveaux, nous avons décidé que nous devions construire un robot Twitter pour notre hashtag préféré. Nous avons exploré différentes possibilités mais la vérité est que je n’ai pas pu résister à l’envie de le construire en utilisant R et {rtweet}. Voici les étapes que j’ai utilisées pour installer un robot Twitter sur mon Raspberry Pi.
Créer un robot twitter
Nous allons créer un robot twitter qui retweets les tweets contenant #rspatial
: https://twitter.com/talk_rspatial
Pour ça, nous avons besoin de :
- Un script pour récupérer les tweets avec
#rspatial
. - Un script à retweeter tout en respectant l’utilisation de l’API Twitter
- Un serveur qui exécutera régulièrement les scripts
J’utilise le package {rtweet} pour communiquer avec Twitter et mon Raspberry Pi personnel avec un CRON pour exécuter des scripts R. Les scripts créés sont disponibles sous forme de fonctions dans le package {tweetrbot}.
Notez que la procédure est détaillée dans le paragraphe suivant, pour présenter le code, mais vous pouvez passer directement au paragraphe d’après “Procédure avec package {tweetrbot} et un Raspi” si vous voulez moins de détails.
Procédure détaillée et code
Créer ses tokens Twitter
- Je recommande d’utiliser une adresse mail spécifique pour ce bot, au cas où Twitter aurait quelque chose à vous dire.
- Vous devez créer un compte Twitter spécifique pour ce bot sur Twitter.
- Lire la vignette de {rtweet} pour créer vos tokens : https://rtweet.info/articles/auth.html
Ensuite, vous exécuterez ce type de code pour sauvegarder correctement vos token.
## authenticate via access token
token <- rtweet::create_token(
app = "my_twitter_research_app",
consumer_key = "zzz",
consumer_secret = "zzeee",
access_token = "1234-zzzzz",
access_secret = "zzzzaaaaa")
Les règles d’utilisation de l’API Twitter peuvent se trouver ici https://developer.twitter.com/en/docs/basics/rate-limiting et là https://developer.twitter.com/en/docs/basics/rate-limits.html . Information sélectionnée :
- POST: The 300 per 3 hours is with the POST statuses/update and POST statuses/retweet/:id endpoints is a combined limit. You can only post 300 Tweets or Retweets during a 3 hour period. (e.g. La règle des 300 requêtes toutes les 3 heures se comprend avec les requêtes
POST statuses/update
etPOST statuses/retweet/:id
cumulées. Vous ne pouvez poster que 300 Tweets ou Retweets pendant une période de 3 heures.) - GET: All request windows are 15 minutes in length. Endpoint
sGET earch/tweets
: Resource familysearch
: Requests / window (user auth)180
, Requests / window (app auth)450
. (e.g. Les limites de récupération des tweets par recherche sont de 180 (compte personnel) ou 450 (authentification avec une app) toutes les 15 minutes.)
Aussi, soyez sûrs de respecter les règles d’automatisation de Twitter: https://help.twitter.com/en/rules-and-policies/twitter-automation
Dans ce cas précis:
Retweets automatisés : tant que vous respectez toutes les règles, vous pouvez retweeter ou citer un Tweet de manière automatisée pour proposer du divertissement, des informations ou des nouveautés. Les Retweets automatisés engendrent souvent des expériences utilisateur négatives. Par ailleurs, les Retweets groupés, s’apparentant à du spam ou à caractère agressif constituent une infraction aux Règles de Twitter.
Récupérer les tweets et les stocker localement
La première fonction est utilisée pour récupérer les tweets de Twitter et les stocker sur le serveur. C’est le code de la fonction get_and_store()
dans le package {tweetrbot}.
- Pour chaque itération du CRON, nous téléchargeons les 20 derniers tweets avec
#rspatial
. - Nous créons deux bases de données :
- Une petite pour garder les derniers tweets pour être sûr de ne pas retweeter les tweets déjà tweetés :
to_tweet_rspatial.rds
. - Une grande qui stockera tous les tweets récupérés depuis le début, pour de futures analyses :
complete_tweets_rspatial.rds
.
- Une petite pour garder les derniers tweets pour être sûr de ne pas retweeter les tweets déjà tweetés :
- Nous stockons la sortie console du dernier CRON dans un fichier de log, juste au cas où.
# For logs
sink(file = "rtweet_console.log", append = FALSE)
# Number of tweets to retrieve
n_tweets <- 20
# Retrieve tweets for one hashtag
cat("Retrieve tweets\n") # for log
new_tweets <- rtweet::search_tweets(
"#rspatial", n = n_tweets, include_rts = FALSE
) %>%
mutate(
retweet_order = NA_real_,
bot_retweet = FALSE)
# Add to the existing database
cat("Add tweets to to-tweet database\n") # for log
tweets_file <- "tweets_rspatial.rds"
if (file.exists(tweets_file)) {
old_tweets <- readRDS(tweets_file)
newold_tweets <- new_tweets %>%
bind_rows(old_tweets) %>%
arrange(desc(bot_retweet)) %>% # TRUE first
distinct(status_id, .keep_all = TRUE)
} else {
newold_tweets <- new_tweets
}
saveRDS(newold_tweets, tweets_file)
# Add to the complete database
cat("Add tweets to complete database\n") # for log
complete_tweets_file <- "complete_tweets_rspatial.rds"
if (file.exists(complete_tweets_file)) {
complete_old_tweets <- readRDS(complete_tweets_file)
complete_newold_tweets <- new_tweets %>%
bind_rows(complete_old_tweets) %>%
distinct(status_id, .keep_all = TRUE)
} else {
complete_newold_tweets <- new_tweets
}
saveRDS(complete_newold_tweets, complete_tweets_file)
## # A tibble: 20 x 92
## user_id status_id created_at screen_name text source display_text_wi… reply_to_status… reply_to_user_id
## <chr> <chr> <dttm> <chr> <chr> <chr> <dbl> <chr> <chr>
## 1 948421… 12581094… 2020-05-06 19:00:41 statsandda… "📚Li… Twitt… 277 <NA> <NA>
## 2 948421… 12581065… 2020-05-06 18:49:04 statsandda… "📚#r… Twitt… 275 <NA> <NA>
## 3 507582… 12580813… 2020-05-06 17:08:56 chrisprener "Exc… Twitt… 277 <NA> <NA>
## 4 153192… 12580669… 2020-05-06 16:11:48 kaseyzap "Jus… Twitt… 113 <NA> <NA>
## 5 473551… 12577737… 2020-05-05 20:46:38 SpatialPat… "Gi*… Twitt… 114 <NA> <NA>
## 6 973194… 12577220… 2020-05-05 17:21:30 allisongli… "@le… Twitt… 208 125770284998477… 2930387886
## 7 712203… 12577052… 2020-05-05 16:14:41 TimSalabim3 "#rs… Twitt… 47 <NA> <NA>
## 8 986017… 12576860… 2020-05-05 14:58:08 v_valerioh "@Ci… Twitt… 59 125767343544825… 742379544309567…
## 9 742379… 12576794… 2020-05-05 14:31:52 CivicAngela "@Sh… Tweet… 234 125767863578947… 742379544309567…
## 10 742379… 12576734… 2020-05-05 14:08:08 CivicAngela "Goo… Twitt… 196 <NA> <NA>
## 11 346069… 12576761… 2020-05-05 14:18:47 dikayodata "I’v… Twitt… 132 <NA> <NA>
## 12 400694… 12576424… 2020-05-05 12:04:57 yabellini "Mañ… Twitt… 111 <NA> <NA>
## 13 720884… 12576033… 2020-05-05 09:29:45 hanna123987 "Can… Twitt… 276 <NA> <NA>
## 14 103516… 12574612… 2020-05-05 00:04:52 mdsumner "pol… Twitt… 102 <NA> <NA>
## 15 925963… 12573945… 2020-05-04 19:40:05 AlexaLFH "I'm… Twitt… 255 <NA> <NA>
## 16 148518… 12573795… 2020-05-04 18:40:24 edzerpebes… "Hi … Twitt… 168 <NA> <NA>
## 17 148518… 12573776… 2020-05-04 18:32:50 edzerpebes… "@Md… Twitt… 73 125729207432981… 17159131
## 18 148518… 12573763… 2020-05-04 18:27:40 edzerpebes… "sf … Twitt… 212 <NA> <NA>
## 19 281055… 12573099… 2020-05-04 14:03:47 jakub_nowo… "@ed… Twitt… 274 <NA> 148518970
## 20 124906… 12572954… 2020-05-04 13:06:15 davsjob "🗺️A… Twitt… 215 <NA> <NA>
## # … with 83 more variables: reply_to_screen_name <chr>, is_quote <lgl>, is_retweet <lgl>, favorite_count <int>,
## # retweet_count <int>, quote_count <int>, reply_count <int>, hashtags <list>, symbols <list>, urls_url <list>,
## # urls_t.co <list>, urls_expanded_url <list>, media_url <list>, media_t.co <list>, media_expanded_url <list>,
## # media_type <list>, ext_media_url <list>, ext_media_t.co <list>, ext_media_expanded_url <list>,
## # ext_media_type <chr>, mentions_user_id <list>, mentions_screen_name <list>, lang <chr>, quoted_status_id <chr>,
## # quoted_text <chr>, quoted_created_at <dttm>, quoted_source <chr>, quoted_favorite_count <int>,
## # quoted_retweet_count <int>, quoted_user_id <chr>, quoted_screen_name <chr>, quoted_name <chr>,
## # quoted_followers_count <int>, quoted_friends_count <int>, quoted_statuses_count <int>, quoted_location <chr>,
## # quoted_description <chr>, quoted_verified <lgl>, retweet_status_id <chr>, retweet_text <chr>,
## # retweet_created_at <dttm>, retweet_source <chr>, retweet_favorite_count <int>, retweet_retweet_count <int>,
## # retweet_user_id <chr>, retweet_screen_name <chr>, retweet_name <chr>, retweet_followers_count <int>,
## # retweet_friends_count <int>, retweet_statuses_count <int>, retweet_location <chr>, retweet_description <chr>,
## # retweet_verified <lgl>, place_url <chr>, place_name <chr>, place_full_name <chr>, place_type <chr>, country <chr>,
## # country_code <chr>, geo_coords <list>, coords_coords <list>, bbox_coords <list>, status_url <chr>, name <chr>,
## # location <chr>, description <chr>, url <chr>, protected <lgl>, followers_count <int>, friends_count <int>,
## # listed_count <int>, statuses_count <int>, favourites_count <int>, account_created_at <dttm>, verified <lgl>,
## # profile_url <chr>, profile_expanded_url <chr>, account_lang <lgl>, profile_banner_url <chr>,
## # profile_background_url <chr>, profile_image_url <chr>, retweet_order <dbl>, bot_retweet <lgl>
Tweeter régulièrement et terminer le processus
La seconde fonction tweetera un par un s’il n’y a pas d’autre processus de tweeting. C’est le code de la fonction retweet_and_update()
du package {tweetrbot}.
- Vérifier s’il n’y a pas déjà un processus R qui exécute une boucle de tweets.
- Remplir le PID de processus dans un fichier journal externe. Seulement s’il est vide, on peut exécuter la boucle.
- Créer un script qui retweettera toutes les 10 minutes, si ce n’est déjà fait.
- Définir l’ordre de tweeting : des tweets les plus anciens aux tweets les plus récents.
- Mettre à jour l’information lorsqu’elle est retweetée avec
bot_retweet=TRUE
si ça a fonctionné etbot_retweet=NA
si ça n’a pas fonctionné pour une investigation plus poussée si nécessaire. À l’origine, ce paramètre est défini surbot_retweet=FALSE
lorsqu’il est créé. - Mise à jour de la base de données à la fin de la boucle
- Lire la dernière version de la base de données (au cas où un autre CRON serait arrivé pendant la boucle)
- Mise à jour avec informations après retweet
- Supprimez les tweets si leur taille est supérieure à 3 fois le nombre de tweets récupérés (20 ici, donc 60).
- Sauvegarder la base de données mise à jour
# Get current PID
current_pid <- as.character(Sys.getpid())
# Read log PID to verify no running process
loop_pid_file <- "loop_pid.log"
if (!file.exists(loop_pid_file)) {file.create(loop_pid_file)}
loop_pid <- readLines(loop_pid_file)
# Run loop only if not already running
if (length(loop_pid) != 0) {
cat("Loop already running\n") # for log
return(NULL)
}
cat("Start the loop\n") # for log
# Fill the log file to prevent other process
writeLines(current_pid, loop_pid_file)
# Add a column to database to define retweeting order
tweets_file <- "tweets_rspatial.rds"
to_tweets <- readRDS(tweets_file) %>%
filter(bot_retweet == FALSE) %>%
arrange(desc(created_at)) %>% # older at the end
mutate(retweet_order = rev(1:n())) %>% # older tweeted first
select(retweet_order, bot_retweet, everything())
# Retweet
for (i in sort(to_tweets$retweet_order)) {
cat("Loop: ", i, "/", max(to_tweets$retweet_order), "\n") # for log
# which to retweet
w.id <- which(to_tweets$retweet_order == i)
print(paste(i, "- Retweet: N=",
to_tweets$retweet_order[w.id],
"-",
substr(to_tweets$text[w.id], 1, 180)))
retweet_id <- to_tweets$status_id[w.id]
r <- rtweet::post_tweet(retweet_id = retweet_id)
# Change status
if (r$status_code == 200) {
# status OK
to_tweets$bot_retweet[w.id] <- TRUE
} else {
# status not OK
to_tweets$bot_retweet[w.id] <- NA
}
# # Wait before the following retweet to avoid to be ban
# # Sys.sleep(60*10) # Sleep 10 minutes
# Sys.sleep(10)
# }
# Save failure in other database
failed_tweets <- to_tweets %>%
filter(is.na(bot_retweet))
# _Add failed to the existing database
tweets_failed_file <- "tweets_failed_rspatial.rds"
if (file.exists(tweets_failed_file)) {
old_failed_tweets <- readRDS(tweets_failed_file)
newold_failed_tweets <- failed_tweets %>%
bind_rows(old_failed_tweets) %>%
distinct(status_id, .keep_all = TRUE)
} else {
newold_failed_tweets <- failed_tweets
}
saveRDS(newold_failed_tweets, tweets_failed_file)
# Read current dataset on disk again (in case there was an update)
tweets_file <- "tweets_rspatial.rds"
current_tweets <- readRDS(tweets_file)
# Remove duplicates, keep retweet = TRUE (first in list)
updated_tweets <- to_tweets %>%
bind_rows(current_tweets) %>%
arrange(desc(bot_retweet)) %>% # TRUE first
distinct(status_id, .keep_all = TRUE)
# Remove data from the to-tweets database if number is bigger than 50 and already retweeted
if (nrow(updated_tweets) > (n_tweets * 3)) {
updated_tweets <- updated_tweets %>%
arrange(desc(created_at)) %>%
slice(1:(n_tweets * 3))
}
# Save updated list of tweets
saveRDS(updated_tweets, tweets_file)
# Wait before the following retweet to avoid to be ban
# Sys.sleep(60*10) # Sleep 10 minutes
Sys.sleep(10)
}
# remove pid when loop finished
file.remove(loop_pid_file)
# Stop sink
sink(file = NULL, append = FALSE)
Procedure avec le package {tweetrbot} et un Raspi
Installer R 3.6 sur le Raspberry Pi et les packages nécessaires
R doit être installé sur le serveur.
Sur le dépôt par défaut de Raspberry Pi, il y a la R 3.3, qui est, disons, assez ancienne… Pour obtenir la dernière version, vous devez compiler R à partir des sources. (Code adapté à partir de cette ressource intéressante : Configurez votre propre serveur Shiny / Rstudio sur un Raspberry Pi 3B+). Ça peut prendre beaucoup de temps!
Notez que pour l’installation de R, j’ai spécifié un répertoire personnalisé avec une option comme --prefix=$HOME/R
, car le manque de place m’oblige à stocker R sur un disque externe. Ce n’est pas indispensable.
bash
sudo apt-get install -y gfortran libreadline6-dev libx11-dev libxt-dev \
libpng-dev libjpeg-dev libcairo2-dev xvfb \
libbz2-dev libzstd-dev liblzma-dev \
libcurl4-openssl-dev libssl-dev \
wget
cd /usr/local/src
sudo wget https://cran.rstudio.com/src/base/R-3/R-3.6.1.tar.gz
sudo su
tar zxvf R-3.6.1.tar.gz
cd R-3.6.1
./configure --enable-R-shlib --prefix=$HOME/R #--with-blas --with-lapack #optional
make
make install
cd ..
rm -rf R-3.6.1*
exit
cd
Maintenant, nous allons installer les packages R nécessaires, en particulier {tweetrbot}. Ici j’utilise sudo R
pour ouvrir R dans le terminal, afin de pouvoir configurer {rtweet} avec le super-utilisateur. Ce même utilisateur qui exécutera le CRON. Si comme moi, vous avez installé R dans un répertoire spécifique, changez votre système PATH, ou appelez R en utilisant le chemin complet.
bash
sudo $HOME/R/bin/R
R
install.packages("remotes", repos = "https://cloud.r-project.org/")
remotes::install_github("statnmap/tweetrbot")
[EDIT 2019-09-07] Si vous avez des problèmes pour installer {httpuv} et/ou {later} sur votre Raspberry Pi, vous voudrez sûrement lire cette issue et compiler {later} vous-même : https://github.com/r-lib/later/issues/73
Récupérer le package :
bash
git clone https://github.com/r-lib/later.git
# sudo apt install libboost-atomic-dev #optional if you don't have libboost
sudo vi later/src/Makevars
Modifier Makevars
:
PKG_LIBS = -pthread -latomic
# If that doesn't work, try:
PKG_LIBS = -pthread -lboost_atomic
Installer manuellement :
bash
sudo R CMD INSTALL later
Preparer le script R qui tournera régulièrement
Configurez vos token Twitter à l’aide de rtweet::create_token()
en utilisant la session R appropriée, avant la création du script R. Il n’est pas nécessaire de laisser vos informations d’identification apparaître en clair dans ce script !
Ensuite, créez le script R qui sera exécuté par le CRON.
bash
mkdir ~/talk_rspatial
cd ~/talk_rspatial
vim rtweet_raspi.R
- L’option
complete_tweets_file
permet de sauvegarder la liste complète des tweets retweetés depuis le début. Au cas où vous voudriez faire une analyse de tweets plus tard. debug=FALSE
dans la fonctionretweet_and_update()
tweetera réellement sur Twitter si le compte est correctement configuré. Pour les tests préliminaires, utilisezdebug=TRUE
.
R
library(tweetrbot)
# Where to store tweets and logs
my_dir <- "~/talk_rspatial"
## Retrieve tweets, store on the drive
get_and_store(
query = "#rspatial", n_tweets = 20,
dir = my_dir
)
## Tweet regularly and update the table stored on the drive
retweet_and_update(
dir = my_dir,
n_tweets = 20, n_limit = 3,
sys_sleep = 600, debug = FALSE
)
Configurer le CRON
Un CRON est la version courte de crontab, une chrono table du système permettant de demander à votre système d’exécuter certaines tâches à un moment précis de l’année, du mois, du jour…
Nous éditons la crontab pour exécuter notre script R
bash
sudo crontab -e
# if you want to run for a specific user
crontab -u yourusername -e
Ensuite, un fichier crontab s’ouvrira, dans lequel vous pourrez ajouter une commande avec la formule suivants :
Minute Hour Day-of-Month Month Day-Of-Week Command
Donc, pour exécuter le script R rtweet_raspi.R
toutes les 2 heures pour chaque jour de l’année, nous devons ajouter au fichier crontab la ligne suivante. Si, comme moi, vous avez installé R dans un répertoire spécifique, changez votre système PATH, ou appelez R en utilisant le chemin complet.
0 */2 * * * sudo $HOME/R/bin/Rscript ~/talk_rspatial/rtweet_raspi.R
[EDIT: 2020-05-06] Par ailleurs, si votre raspi plante ou reboot au milieu d’une boucle en cours, les tweets reprendront quand même malgré la présence du fichier loop_pid.log
. J’ai mis à jour les fonctions du package {tweetrbot} pour ça !
Aller plus loin
Maintenant que vous avez un script R pour récupérer et stocker les tweets d’une communauté spécifique, vous pouvez imaginer quelques analyses. De plus, comme vous savez comment configurer un CRON, vous pouvez imaginer quelques tweets programmés avec les analyses graphiques des tweets du mois passé par exemple.
Utilisez votre propre imagination, mais essayez de ne pas trop déranger trop de personnes (réelles) sur Twitter !
Maintenant que le bot est configuré, n’hésitez pas à utiliser
#rspatial
dans vos tweets et à suivre @talk_rspatial. Soyez patient pour le retweet car ils sont récupérés toutes les 2 heures seulement, puis retweetés toutes les 10 minutes. Aussi mon serveur est un Raspi, soyez indulgents !
Autres ressources
- [2017-11-12] - tidyverse tweets bot, a bot that tweets out tidyverse related material: article de blog et dépôt github
- [2017-08-13] - dépôt Github - Bots retweeting language specific R hashtags
- [2015-12-29] - Rrobot, A twitterbot that tweets about #rstats: article de blog et dépôt github
- [2015-01-19] - Programming a Twitter bot – and the rescue from procrastination
- [2015-11-26] - How to program a Twitter bot
- Other Raspi Twitter Bots
Citation :
Merci de citer ce travail avec :
Rochette Sébastien. (2019, août. 30). "Créer un robot twitter sur un Raspberry Pi 3 avec R". Retrieved from https://statnmap.com/fr/2019-08-30-creer-un-robot-twitter-sur-un-raspberry-pi-3-avec-r/.
Citation BibTex :
@misc{Roche2019Créer,
author = {Rochette Sébastien},
title = {Créer un robot twitter sur un Raspberry Pi 3 avec R},
url = {https://statnmap.com/fr/2019-08-30-creer-un-robot-twitter-sur-un-raspberry-pi-3-avec-r/},
year = {2019}
}