README.md 28.7 KB
Newer Older
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
1
Este proyecto NO se puede clonar.
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
2
3
4
5
6

# Acceso a interfaces REST. Descarga masiva de Tweets

### Aplicaciones y librerías a utilizar

7
* Eclipse **Para esta práctica podemos usar el eclipse ya instalado en la máquina**
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
8
9
10
11
12
13
14
15
16
17
18
19
* [Unirest](http://unirest.io/java.html): librería de peticiones HTTP (más adelante veremos como descargarla)
* [Commons](https://commons.apache.org/): librerías de funciones auxiliares (más adelante veremos como descargarla)
* [Google-Gson](https://github.com/google/gson): librería de manipulación de JSON (más adelante veremos como descargarla)

Será necesaria una cuenta de twitter

## Crear una aplicación en Twitter

Si no tenías twitter, ha llegado el momento de tenerlo, hazte una cuenta. Si ya tienes una, haz login en twitter.

Tuvieras o no, consulta #uc3m_cd que es la cuenta de la asignatura aunque se usa poco ;)

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
20
Vamos a descargar tweets. Para ello, debes dar de alta una aplicación. Ve a la [gestión de apps de twitter](https://developer.twitter.com/) y crea una aplicación si no lo has hecho ya.
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
21

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
22
El el dashboard verás tus proyectos y tus aplicaciones. Pulsando en la aplicación que has creado, podrás ver los tokens de seguridad (simbolo de la llave) debes copiar y pegar todos ellos (consumer keys, bearer token, authentication tokens) en algún lugar para usarlos después.
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
23

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
24
<img src="img/keys_tokens.png" width="500px"/>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
25
26
27
28
29
30
31
32
33
34

## Descarga de tweets con la ayuda de desarrollador de Twitter

Ahora vamos a usar un API de verdad. Está documentado correctamente, de hecho, el de twitter, está muy bien comentado y tiene utilidades que veremos. Ve a la página:

https://dev.twitter.com/rest/public

Observa como todo lo que se puede hacer con twitter tiene su correspondiente entrada en el API.
Vamos a echar un vistazo a la función para obtener tweets que es `GET statuses/user_timeline`

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
35
Para ello, ve a la página https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline  (nosotros usaremos el API v1.1.)
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
36
37
38

Twitter cuida mucho su interfaz, y ayuda a conservarlo y entenderlo. Pero dado su éxito, ha prohibido el acceso anónimo. Por lo que es necesario usar OAuth (usando los tokens que hemos generado) para acceder a cada función del API rest. Para usar OAuth, es necesario calcular una firma y añadirla a la cabecera Authorization de HTTP.

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
39
Con la información que proporciona, puedes ver cómo, mediante peticiones REST, puedes acceder a los contenidos de la siguiente manera:
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
40
41
42
43
44
```
GET 1.1/statuses/user_timeline.json?count=2&screen_name=twitterapi HTTP/1.1
Host: api.twitter.com
Authorization: ...
```
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
45
Esto nos daría un JSON de este tipo (en breve lo haremos nosotros):
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
46
47
48
49
50

```json
[{"retweeted_status":{"contributors":null,"text":"We have disabled SSLv3 protocol support in response to the vulnerability published today. You may need to update your browser to use Twitter","geo":null,"retweeted":false,"in_reply_to_screen_name":null,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[],"hashtags":[],"user_mentions":[]},"in_reply_to_status_id_str":null,"id":522190947782643712,"source":"<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client<\/a>","in_reply_to_user_id_str":null,"favorited":false,"in_reply_to_status_id":null,"retweet_count":695,"created_at":"Wed Oct 15 01:03:18 +0000 2014","in_reply_to_user_id":null,"favorite_count":226,"id_str":"522190947782643712","place":null,"user":{"location":"Twitter HQ","default_profile":false,"profile_background_tile":true,"statuses_count":40,"lang":"en","profile_link_color":"009999","id":1137751093,"following":false,"protected":false,"favourites_count":0,"profile_text_color":"333333","description":"The Product Security Team at Twitter.","verified":true,"contributors_enabled":false,"profile_sidebar_border_color":"EEEEEE","name":"Twitter Security","profile_background_color":"131516","created_at":"Thu Jan 31 19:37:42 +0000 2013","is_translation_enabled":false,"default_profile_image":false,"followers_count":13852,"profile_image_url_https":"https://pbs.twimg.com/profile_images/3321149170/6bf0ef1ae272b203acfdee4d7e61df49_normal.png","geo_enabled":false,"profile_background_image_url":"http://abs.twimg.com/images/themes/theme14/bg.gif","profile_background_image_url_https":"https://abs.twimg.com/images/themes/theme14/bg.gif","follow_request_sent":false,"entities":{"description":{"urls":[]},"url":{"urls":[{"expanded_url":"https://twitter.com/about/security","indices":[0,23],"display_url":"twitter.com/about/security","url":"https://t.co/E9FCUUoXKF"}]}},"url":"https://t.co/E9FCUUoXKF","utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","notifications":false,"profile_use_background_image":true,"friends_count":0,"profile_sidebar_fill_color":"EFEFEF","screen_name":"twittersecurity","id_str":"1137751093","profile_image_url":"http://pbs.twimg.com/profile_images/3321149170/6bf0ef1ae272b203acfdee4d7e61df49_normal.png","listed_count":188,"is_translator":false},"coordinates":null},"contributors":null,"text":"RT @twittersecurity: We have disabled SSLv3 protocol support in response to the vulnerability published today. You may need to update your \u2026","geo":null,"retweeted":false,"in_reply_to_screen_name":null,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[],"hashtags":[],"user_mentions":[{"id":1137751093,"name":"Twitter Security","indices":[3,19],"screen_name":"twittersecurity","id_str":"1137751093"}]},"in_reply_to_status_id_str":null,"id":522339641815748608,"source":"<a href=\"http://itunes.apple.com/us/app/twitter/id409789998?mt=12\" rel=\"nofollow\">Twitter for Mac<\/a>","in_reply_to_user_id_str":null,"favorited":false,"in_reply_to_status_id":null,"retweet_count":695,"created_at":"Wed Oct 15 10:54:09 +0000 2014","in_reply_to_user_id":null,"favorite_count":0,"id_str":"522339641815748608","place":null,"user":{"location":"San Francisco, CA","default_profile":false,"profile_background_tile":true,"statuses_count":3520,"lang":"en","profile_link_color":"0084B4","profile_banner_url":"https://pbs.twimg.com/profile_banners/6253282/1347394302","id":6253282,"following":false,"protected":false,"favourites_count":26,"profile_text_color":"333333","description":"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.","verified":true,"contributors_enabled":false,"profile_sidebar_border_color":"C0DEED","name":"Twitter API","profile_background_color":"C0DEED","created_at":"Wed May 23 06:01:13 +0000 2007","is_translation_enabled":false,"default_profile_image":false,"followers_count":2418318,"profile_image_url_https":"https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","geo_enabled":true,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","follow_request_sent":false,"entities":{"description":{"urls":[]},"url":{"urls":[{"expanded_url":"http://dev.twitter.com","indices":[0,22],"display_url":"dev.twitter.com","url":"http://t.co/78pYTvWfJd"}]}},"url":"http://t.co/78pYTvWfJd","utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","notifications":false,"profile_use_background_image":true,"friends_count":48,"profile_sidebar_fill_color":"DDEEF6","screen_name":"twitterapi","id_str":"6253282","profile_image_url":"http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","listed_count":12846,"is_translator":false},"coordinates":null},{"contributors":null,"text":"The OAuth / xAuth issue affecting REST endpoints has been resolved. Please visit our OAuth forums for support https://t.co/XiKCrlfCNX","geo":null,"retweeted":false,"in_reply_to_screen_name":"twitterapi","possibly_sensitive":false,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[{"expanded_url":"https://twittercommunity.com/category/oauth","indices":[110,133],"display_url":"twittercommunity.com/category/oauth","url":"https://t.co/XiKCrlfCNX"}],"hashtags":[],"user_mentions":[]},"in_reply_to_status_id_str":"515255192787230720","id":515288036242763776,"source":"<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client<\/a>","in_reply_to_user_id_str":"6253282","favorited":false,"in_reply_to_status_id":515255192787230720,"retweet_count":79,"created_at":"Thu Sep 25 23:53:36 +0000 2014","in_reply_to_user_id":6253282,"favorite_count":55,"id_str":"515288036242763776","place":null,"user":{"location":"San Francisco, CA","default_profile":false,"profile_background_tile":true,"statuses_count":3520,"lang":"en","profile_link_color":"0084B4","profile_banner_url":"https://pbs.twimg.com/profile_banners/6253282/1347394302","id":6253282,"following":false,"protected":false,"favourites_count":26,"profile_text_color":"333333","description":"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.","verified":true,"contributors_enabled":false,"profile_sidebar_border_color":"C0DEED","name":"Twitter API","profile_background_color":"C0DEED","created_at":"Wed May 23 06:01:13 +0000 2007","is_translation_enabled":false,"default_profile_image":false,"followers_count":2418318,"profile_image_url_https":"https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","geo_enabled":true,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","follow_request_sent":false,"entities":{"description":{"urls":[]},"url":{"urls":[{"expanded_url":"http://dev.twitter.com","indices":[0,22],"display_url":"dev.twitter.com","url":"http://t.co/78pYTvWfJd"}]}},"url":"http://t.co/78pYTvWfJd","utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","notifications":false,"profile_use_background_image":true,"friends_count":48,"profile_sidebar_fill_color":"DDEEF6","screen_name":"twitterapi","id_str":"6253282","profile_image_url":"http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","listed_count":12846,"is_translator":false},"coordinates":null}]
```

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
51
52
## Descarga de tweets programática

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
53
### Oauth bearer token
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
54

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
55
56
57
¿Qué es ese "bearer token" que he copiado en un notepar? Oauth permite delegar el uso de tu cuenta de twitter (o de Facebook o de google...) a una aplicación, de forma que pueda hacer uso de ella **en tu nombre**. Para hacer eso, es necesario incluir una firma generada mediante OAuth en todas las peticiones HTTP que haga tu aplicación al API. 

Lee detenidamente las siguientes instrucciones para usar OAUTH y permitir que el usuario delegue a la aplicación el uso (read/write) de tu cuenta de twitter aunque nosotros usaremos la última de todas (application only), que es para lo que se usa el "bearer token":
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
58
59
60
61
62
63
64

* Información de la aplicación: https://apps.twitter.com/
* Overview: https://dev.twitter.com/oauth/overview
* Autenticación por PIN: https://dev.twitter.com/oauth/pin-based
* Autenticación 3-Legged Oauth: https://dev.twitter.com/oauth/3-legged
* Application only https://dev.twitter.com/oauth/application-only

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
65
Bearer Token es una buena forma de recabar datos de twitter dado que sólo queremos acceso read only a la cuenta para leer tweets. El esquema es el siguiente:
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
66

67
<img src="img/auth-5.png.twimg.1920.png" width="500px"/>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
68
69
70

### Crear proyecto y descargar dependencias

71
72
73
**Para esta práctica no hace falta un Eclipse especial, podemos usar el eclipse ya instalado en la máquina**
Crea un proyecto en Java, por ejemplo `CdistREST`. Transformalo en `Maven` (botón derecho sobre el `proyecto > Configure > Convert to Maven Project`).
Añade las dependencias entre los tags `<project>` `</project>`:
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
74
```xml
75
	</build>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
76
	<dependencies>
77
		<!-- Pull in as a traditional dependency -->
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
78
		<dependency>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
79
			<groupId>com.konghq</groupId>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
80
			<artifactId>unirest-java</artifactId>
81
			<version>3.12.0</version>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
82
		</dependency>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
83

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
84
85
86
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
87
			<version>2.8.6</version>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
88
89
		</dependency>
	</dependencies>
90
</project>
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
91
92
```

93
### Obtención del Bearer Token (no necesario)
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
94

95
**Antes, era necesario obtener el bearer token, ahora te lo dan generado (solo tienes que copiarlo)**. Esta es la explicación (**no tenéis que hacerlo**) de cómo se obtendría el bearer token a partir de los secretos de la aplicación:
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
96
97
98
99
100
101

1. codificar en forma de URL ([URLEncode](https://en.wikipedia.org/wiki/Percent-encoding)) tanto el consumer key como el consumer secret. 
2. unir ambos en una string separándolos por ":"
3. codificar el resultado con base64
4. hacer una petición a "https://api.twitter.com/oauth2/token" añadiendo una cabecera con el formato Authorization: Basic {Credenciales codificadas}

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
102
Para conseguirlo, era necesario usar código (**desde el 2019 no es necesario, se puede obtener el bearer token directamente de twitter**):
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
103
104
105

```java
package cdistRest;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
106
import java.io.IOException;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
107
import java.net.URLEncoder;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
108
import java.util.List;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
109
110

import org.apache.commons.codec.binary.Base64;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
111
112
113
114
115
116
117
/* es necesario incluir esta dependencia en el fichermo pom
	<dependency>
    	<groupId>commons-codec</groupId>
		<artifactId>commons-codec</artifactId>
		<version>1.10</version>
	</dependency>
*/
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
118
119

import com.google.gson.Gson;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
120
121
122
123
124

import kong.unirest.GetRequest;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import kong.unirest.UnirestException;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173

public class TwitterCrawler {

	private static final String oauth_consumer_key = "XXXXXXXXx";
	private static final String oauth_consumer_secret = "XXXXXXXXXXXXXXXXXXXXXXXXXX";
	
	public class BearerToken
	{
		 private String access_token;
         private String token_type;
         
	}

	
	public static String deserializeJson(String input_json)
    {
          Gson gson = new Gson();
          BearerToken new_bearerToken = gson.fromJson(input_json, BearerToken.class);
          return new_bearerToken.access_token;
    }

	public static void main(String args[]) throws UnsupportedEncodingException {
		try {
			/* get bearer token according to https://dev.twitter.com/oauth/application-only */
			String URLEncoderConsumerKey = URLEncoder.encode(oauth_consumer_key, "UTF-8");
			String URLEncoderConsumerSecret = URLEncoder.encode(oauth_consumer_secret, "UTF-8");
			String AuthorizationHeader = URLEncoderConsumerKey+":"+URLEncoderConsumerSecret;
			String AuthorizationHeaderB64 = Base64.encodeBase64String(AuthorizationHeader.getBytes("UTF8"));
			System.out.println(AuthorizationHeaderB64);
			RequestBodyEntity postReq = Unirest.post("https://api.twitter.com/oauth2/token")
					.header("User-Agent","TwitterApp")
					.header("host", "api.twitter.com")
					.header("Authorization", "Basic " + AuthorizationHeaderB64)
					.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
					.body("grant_type=client_credentials");

			HttpResponse<String> res = postReq.asString();
			System.out.println("Bearer Token : " + TwitterCrawler.deserializeJson(res.getBody()));
			
			/* we now have a brearer token so can make requests to Twitter */
			
		} catch (UnirestException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
```

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
174
175
176
177
178
179
180
181
182
183
Si usas Java 11, tu fichero `module-info.java` deberá contener (recuerda que el nombre del módulo es el del package, por lo que si lo has cambiado, éste fichero variará):
```java
module CDistTwitter {
	requires com.google.gson;
	requires unirest.java;
	
	exports cdistRest;
}
```

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
184
Sustituyendo `oauth_consumer_key` y `oauth_consumer_secret` por los valores de tu aplicación se obtenía el bearer token.
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
185

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
186
187
### Gestión de tweets con Json

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
188
189
Los tweets devueltos por Twitter (en general cualquier información que devuelva el API) vienen codificados con Json. En la práctica 1 de rest, usamos Gson para almacenar datos en json dentro de clases java.

190
191
192
193
194
195
196
197
198
199
Recuerda que, si usas Java 11, tu fichero `module-info.java` deberá contener (recuerda que el nombre del módulo es el del package, por lo que si lo has cambiado, éste fichero variará):
```java
module CDistTwitter {
	requires com.google.gson;
	requires unirest.java;
	
	exports cdistRest;
}
```

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
200
201
202
203
En esta ocasión, creamos dos clases para guardar, por un lado el Tweet y por otro lado el usuario que lo creó, aunque el primero apunta al segundo. Las clases son las siguientes (usad el mismo paquete para todos, en este caso, por ejemplo **cdistRest**), `Tweet` para almacenar el tweet propiamente dicho, y `User` para guardar los datos del usuario que envía el tweet:


**Clase `Tweet.java`:**
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
204
205
206

```java
package cdistRest;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
207

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
208
209
210
211
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
212

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
213
214
215
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
216

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
217
public class Tweet {
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

    public static Tweet deserializeJson(String input_json) {
        Gson gson = new Gson();
        Tweet new_Tweet = gson.fromJson(input_json, Tweet.class);
        return new_Tweet;
  }

  public static List<Tweet> deserializeJsonArray(String input_json) {
        Gson gson = new Gson();
        Type collectionType = new TypeToken<Collection<Tweet>>() {}.getType();
        Collection<Tweet> new_tweet_collection = gson.fromJson(input_json,
                      collectionType);
        ArrayList<Tweet> new_Tweet_list = new ArrayList<Tweet>(
                      new_tweet_collection);

        return new_Tweet_list;
  }

  @SerializedName("geo")
  public String geo;

  @SerializedName("in_reply_to_status_id")
  public String in_reply_to_status_id;

  @SerializedName("truncated")
  public String truncated;

  @SerializedName("created_at")
  public String created_at;

  @SerializedName("retweet_count")
  public String retweet_count;

  @SerializedName("in_reply_to_user_id")
  public String in_reply_to_user_id;

  @SerializedName("id_str")
  public String id_str;

  @SerializedName("place")
  public transient String place;

  @SerializedName("favorited")
  public boolean favorited;

  @SerializedName("source")
  public String source;

  @SerializedName("in_reply_to_screen_name")
  public String in_reply_to_screen_name;

  @SerializedName("in_reply_to_status_id_str")
  public String in_reply_to_status_id_str;

  @SerializedName("id")
  public long id;

  @SerializedName("contributors")
  public String contributors;

  @SerializedName("coordinates")
  public String coordinates;

  @SerializedName("retweeted")
  public boolean retweeted;

  @SerializedName("text")
  public String text;

  @SerializedName("profile_image_url")
  public String profile_image_url;

  public User user;

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
292
}
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
293

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
294
295
```

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
296
297
**Clase `User.java`:**

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
```java
package cdistRest;

import com.google.gson.annotations.SerializedName;

public class User {
 
       @SerializedName("friends_count")
       public int friends_count;
 
       @SerializedName("profile_background_color")
       public String profile_background_color;
 
       @SerializedName("profile_background_image_url")
       public String profile_background_image_url;
 
       @SerializedName("created_at")
       public String created_at;
 
       @SerializedName("description")
       public String description;
 
       @SerializedName("favourites_count")
       public int favourites_count;
 
       @SerializedName("lang")
       public String lang;
 
       @SerializedName("notifications")
       public boolean notifications;
 
       @SerializedName("id_str")
       public String id_str;
 
       @SerializedName("default_profile_image")
       public boolean default_profile_image;
 
       @SerializedName("profile_text_color")
       public String profile_text_color;
 
       @SerializedName("default_profile")
       public boolean default_profile;
 
       @SerializedName("show_all_inline_media")
       public boolean show_all_inline_media;
 
       @SerializedName("contributors_enabled")
       public boolean contributors_enabled;
 
       @SerializedName("geo_enabled")
       public boolean geo_enabled;
 
       @SerializedName("screen_name")
       public String screen_name;
 
       @SerializedName("profile_sidebar_fill_color")
       public String profile_sidebar_fill_color;
 
       @SerializedName("profile_image_url")
       public String profile_image_url;
 
       @SerializedName("profile_background_tile")
       public boolean profile_background_tile;
 
       @SerializedName("follow_request_sent")
       public boolean follow_request_sent;
 
       @SerializedName("url")
       public String url;
 
       @SerializedName("statuses_count")
       public int statuses_count;
 
       @SerializedName("following")
       public boolean following;
 
       @SerializedName("time_zone")
       public String time_zone;
 
       @SerializedName("profile_link_color")
       public String profile_link_color;
 
       @SerializedName("protected")
       public boolean protectedd;
 
       @SerializedName("verified")
       public boolean verified;
 
       @SerializedName("profile_sidebar_border_color")
       public String profile_sidebar_border_color;
 
       @SerializedName("followers_count")
       public int followers_count;
 
       @SerializedName("location")
       public String location;
 
       @SerializedName("name")
       public String name;
 
       @SerializedName("is_translator")
       public boolean is_translator;
 
       @SerializedName("id")
       public long id;
 
       @SerializedName("listed_count")
       public int listed_count;
 
       @SerializedName("profile_use_background_image")
       public boolean profile_use_background_image;
 
       @SerializedName("utc_offset")
       public int utc_offset;
 
}
```

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
416
417
### Anade una consulta al API de Twetter

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
418
419
420
Vamos a probar con la función del API [get user timeline](https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline) que permite obtener los tweets más recientes de un usuario.

Inspecciona la página del API. Revisa los parámetros de la llamada a esa función del API Rest, verás que puedes usar `user_id` que es el ID de twetter (poco conocido, un número) o bien el `screen_name' que es el nombre de la cuenta de twitter que se muestra por internet, por ejemplo `@realmadrid`.
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
421

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
422
423
Además se pueden especificar el número de Tweets a obtener.

424
425
426
427
El bearer token se obtiene tras crear una aplicación y generar las claves:

<img src="img/twitter.png" sizes="(max-width: 500px)" style="display: block; margin-left: auto;"/> 

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
428
Para obtener 2 tweets de la cuenta `@realmadrid` usa el siguiente código:
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
429
430
431
432
433
434
435

```java
package cdistRest;
import java.io.IOException;
import java.util.List;


DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
436
437
438
439
import kong.unirest.GetRequest;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import kong.unirest.UnirestException;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
440
441


DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
442
public class TwitterCrawler {
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
443

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
444
445
	private static final String bearer_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
		
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
446
447
	public static void main(String args[]) throws IOException {
		try {
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
448
					
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
449
450
451
452
453
454
455
456
457
458
459
460

						
			/* https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=realmadrid&count=2 */
			HttpResponse<String>  json_str_Response = null;
            GetRequest getReq = null;
            getReq = Unirest.get("https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name={screen_name}&count={count}")
            		.routeParam("screen_name","realmadrid")
            		.routeParam("count","2")
            		.header("Authorization", "Bearer " + bearer_token);
            System.out.println("Request to: " + getReq.getUrl());
            System.out.println("Authorization header Bearer " + bearer_token);
            json_str_Response = getReq.asString();
461
            
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
462
463
464
           
            List<Tweet> tweet_list = Tweet.deserializeJsonArray(json_str_Response.getBody());
           
465
466
467
468
469
470
            for(int i=0; i<tweet_list.size(); i++)
            {
            	Tweet t = tweet_list.get(i);
            	System.out.println("Tweet(" + i+ ")" + ": " + t.text);
            }
            
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
471
            Unirest.shutDown();
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
472
473
474
475
476
477
478
479
			
			
			
		} catch (UnirestException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
480

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
481
482
}
```
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
483

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
484
485
486
487
488
489
490
491
492
Observa, depurando paso a paso y usando break points (en ocasiones entrar dentro de una función con eclipse no funciona - dado que no encuentra el código - lo mejor es usar breakpoints en los métodos en los que queremos parar) lo siguiente:

* Creación de la cabecera de autenticación a partir del objeto BearerToken
* Como se procesa un array de tweets en Json usando el método deserializeJsonArray de la clase Tweet
* Observa cómo GSON automáticamente procesa el objeto User que está dentro de la cadena de texto json de cada tweet
* Observa cómo se procesan los campos, para ver claramente como es el JSON de partida usa un parser online como el de http://json.parser.online.fr/

### Peticiones con cursores

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
493
La función del API descrita en https://dev.twitter.com/rest/reference/get/statuses/user_timeline tiene un límite de 300 peticiones cada 15 minutos. Pero por lo general tiene un límite total de 1500 tweets por cuenta consultada (los 1500 últimos). Esto varía bastante, por lo que es recomendable leer https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits.
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
494

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
495
Pese a la limitación, en cada petición se pueden pedir como máximo 200 (parámetro `count` de la llamada al API)
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
496

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
497
El problema es que los API REST no tienen estado, entonces, **¿cómo bajar los más de 200 tweets?**
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
498
499
500
501

Para ello, es necesario usar **cursores**, elementos que le informen a Twitter a partir de qué Tweet deseas obtener los siguientes 200.

Para ello utilizamos el parámetro `max_id` de la petición, usando la información contenida aquí https://dev.twitter.com/rest/public/timelines
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
502

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
503
Usemos una cuenta popular, cualquiera con más de 200 Tweets. Para ello usa cualquiera de las más populares del mundo (yo voy a usar `@realmadrid`).
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
504
505
506
507

De todos los tweets recibidos, hay que buscar cuál de ellos tiene menor id y usarlo como parámetro (restándole 1) para la siguiente petición.


DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
508
509
```java
package cdistRest;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
510

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
511
import java.io.IOException;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
512
import java.util.ArrayList;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
513
514
import java.util.List;

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
515
516
517
import kong.unirest.GetRequest;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
518
519
520

public class TwitterCrawler {

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
521
522
523
524
525
526
527
	private static final String bearer_token = "xxxxxx";
	private static final int MAX_TWEET_COUNT_PER_REQUEST = 200;

	private long max_id = Long.MAX_VALUE;
	private long tweetcount = 0;
	private String screen_name = "";
	private long pending = 0;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
528
	
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
529
	public TwitterCrawler(int count, String screen_name )
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
530
	{
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
531
532
		this.tweetcount = count;
		this.screen_name = screen_name;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
533
534
	}

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
	public List<Tweet> gettweets() {
		/*
		 * si los tweets caben en una sóla petición, o bien se piden más de
		 * MAX_TWEET_COUNT_PER_REQUEST en principio la primera petición hay que hacerla
		 * para obtener el max_id (máximo id de la secuencia de tweets)
		 */
		List<Tweet> tweet_list_total = new ArrayList<Tweet>();
		pending = tweetcount;
		long max_id = Long.MAX_VALUE;
		
		do {

			long request_twetcount = 0;
			long tweets_obtained = 0;
			if (pending > MAX_TWEET_COUNT_PER_REQUEST) {
				request_twetcount = MAX_TWEET_COUNT_PER_REQUEST;
			} else {
				request_twetcount = pending;
			}

			HttpResponse<String> json_str_Response = null;
			GetRequest getReq = null;
			getReq = Unirest.get(
					"https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name={screen_name}&count={count}&max_id={max_id}")
					.routeParam("screen_name", screen_name).routeParam("count", "" + request_twetcount)
					.routeParam("max_id", ""+(max_id-1))
					.header("Authorization", "Bearer " + bearer_token);
			System.out.println("Request " + request_twetcount + " tweets to: " + getReq.getUrl());
			json_str_Response = getReq.asString();
			List<Tweet> tweet_list_request = Tweet.deserializeJsonArray(json_str_Response.getBody());
			tweets_obtained = tweet_list_request.size();
			System.out.println("received " + tweets_obtained + " tweets");

			/* actualizamos tweetcount con los recibidos */
			pending -= tweets_obtained;
			
			/* actualizamos la lista total de tweets */
			tweet_list_total.addAll(tweet_list_request);
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
573

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
574
			
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
575

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
576
577
578
579
580
			/*
			 * PARADA
			 * si hemos hemos recibido en total de tweets, paramos 
			 */
			if(pending <= 0) break;
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
581
			
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
582
583
584
585
586
587
588
589
590
			/*
			 * PARADA
			 * si hemos hemos recibido menos de request_twetcount es que no hay más. 
			 * Hay que finalizar
			 */
			
			if(tweets_obtained < request_twetcount) break;
			
            for (Tweet tw : tweet_list_request) {
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
591
592
593
594
                if (tw.id < max_id)
                    max_id = tw.id;
            }
			
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
595
						
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
596
			
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
597
598
599
600
601
602
603
		} while (true);
		return tweet_list_total;
	}

	public static void main(String args[]) throws IOException {
		
		TwitterCrawler tc = new TwitterCrawler(400, "realmadrid");
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
604
        List<Tweet> my_tweets = tc.gettweets();
605
606
607
608
609
610
                   
        for(int i=0; i<my_tweets.size(); i++)
        {
           	Tweet t = my_tweets.get(i);
           	System.out.println("Tweet(" + i+ ")" + ": " + t.text);
        }
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
611
612
613
614
			
	}
}
```
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
615

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
616

DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
617
Prueba el código para más y menos de 200 tweets. 
DANIEL DIAZ SANCHEZ's avatar
DANIEL DIAZ SANCHEZ committed
618