Twitter: conversaciones gráficas con Python (IV)

Dentro de Twitter: estructuras de datos

Esta es la cuarta entrada de este blog, dedicado a usar el API de Twitter y las librerías de Python para generar una imagen de la conversación que tenga un formato gráfico:

nolla

Tengo que recuperar los tuits con el API de Twitter y usar Python para filtrar la información, crear una lista ordenada y generar un gráfico con los nodos.

En esta entrada examinaremos la estructura de los datos que devuelve el API de Twitter, y cómo los manipulamos con Python. Supongo cierta familiaridad con Python; en caso contrario, hay todo tipo de tutoriales en la red y seguro que alguno se adapta a tu nivel. En caso de duda, escribe un comentario o un tuit e intentaré ayudarte.

La línea de tiempo (según el API de Twitter)

Si llegaste a ejecutar el último comando que te proponía en el post anterior, tuviste que recibir un gran flujo de datos parecido al que ves a continuación(vuelvo a mostrar todos los comandos. Recuerda que las claves que uses han de ser las tuyas):

>>> import twitter
>>> token = "93826987-b17gKPaq4a5sNtbu3PfXPHdBcCm4ff533PSlMKGMV"
>>> token_key = "wZZ5VUOPzOy1QABHiA4gUQEfrRoMxn4qlewNTJ1yxhZem"
>>> con_secret = "gsIT85KKCwn96YZdEm5i3uGtq"
>>> con_secret_key = "SvWhbZNUZIx9Kz3sMuMaditk3phMyhw2tLHrrxXLX7SaYm9Ntm"
>>> auth = twitter.OAuth(token, token_key, con_secret, con_secret_key)
>>> t = twitter.Twitter(auth=auth)
>>> t.statuses.home_timeline()
 [{'retweet_count': 1, 'coordinates': None, 'retweeted': False, 'id_str': '501334102357073921', 'favorite_count': 1, 'favorited': False, 'id': 501334102357073921, 'user': {'profile_banner_url': 'https://pbs.twimg.com/profile_banners/100731315/1401451804', 'listed_count': 5080, 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1563598007/rt_sepa_mas_normal.png', 'verified': True, 'time_zone': 'Moscow', 'profile_link_color': '5CBA1D', 'url': 'http://t.co/D4csDmdhn8', 'created_at': 'Thu Dec 31 09:33:16 +0000 2009', 'statuses_count': 175603, 'contributors_enabled': False, 'profile_sidebar_fill_color': '042107', 'profile_background_color': '005E15', 'profile_text_color': 'FFFFFF', 'id': 100731315, 'is_translation_enabled': False,  ... (abreviado)

Esta información está en un formato llamado JSON. JSON permite guardar datos estructurados de una forma muy sencilla. Se pueden usar dos tipos de estructuras:

  • Las listas. Son colecciones de valores separados por comas y entre paréntesis cuadrados [ ]. Esta lista podría ser la información de inventario de un producto:
['2014-08-18', 250.99, 'Reloj de oro', 9, 'En stock']
  • Los diccionarios. Los dicionarios son colecciones de parejas de valores, del estilo código-valor. La sintaxis usa corchetes { } para definir el diccionario y los dos puntos para separar la clave del valor. La lista anterior podría reescribirse como diccionario, si precedemos cada elemento con el nombre del concepto que representa:
{'fecha': '2014-08-18', 
 'precio': 250.99, 
 'descripcion: 'Reloj de oro', 
 'cantidad': 9, 
 'estado': 'En stock'}

El problema con el JSON que hemos recibido es que no es muy legible. A continuación he reorganizado el texto para que se vea más claro.

[ {'retweet_count': 1, 
   'coordinates': None, 
   'retweeted': False, 
   'id_str': '501334102357073921', 
   'favorite_count': 1, 
   'favorited': False, 
   'id': 501334102357073921, 
   'user': {
       'profile_banner_url': 'https://pbs.twimg.com/profile_banners/100731315/1401451804',
       'listed_count': 5080, 
       'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1563598007/rt_sepa_mas_normal.png' ...

Puesto que empieza con un paréntesis cuadrado, lo primero que debería quedar claro es que esta secuencia contiene una lista con uno o más elementos.

El segundo signo es un corchete, por tanto el primer elemento de la lista es un diccionario. Las claves de este diccionario son: ‘retweet_count’, ‘coordinates’, ‘retweeted’… Algunas de estas claves tienen un significado bastante explícito para el usuario de Twitter, mientras que hay otros más oscuros. Una de las cosas que tendremos que hacer es examinar estas estructuras para comprender cómo modelaron las cosas los ingenieros de Twitter.

La facilidad de JSON para anidar objetos dentro de otros objetos es lo que lo hace tan práctico para estructurar datos complejos. Además, se necesita muy poco formato extra para cumplir con la sintaxis, es un formato muy económico. Esto es una ventaja cuando se trata de mover datos por la red.

Listas y diccionarios en Python

Una buena noticia: Python es idéntico a JSON en lo que se refiere a su sintaxis de listas y diccionarios. De hecho, listas y diccionarios son tipos de datos nativos, igual que los números o las cadenas. Crear variables de tipo lista o diccionario es fácil:

>>> mi_lista = ['Diego Buendia', 'dbuendiab', '2009-12-01']
>>> print(mi_lista)
['Diego Buendia', 'dbuendiab', '2009-12-01']
>>> mi_diccionario = {'name': 'Diego Buendia', 'screen_name': 'dbuendiab', 'created_at': '2009-12-01'}
>>> print(mi_diccionario)
{'name': 'Diego Buendia', 'screen_name': 'dbuendiab', 'created_at': '2009-12-01'}

Explorando listas y diccionarios

El acceso a los elementos de listas y diccionarios se hace mediante una sintaxis de paréntesis cuadrados: variable[<indice-o-clave>].

Las listas indexan sus elementos por números consecutivos, empezando por el cero, y los diccionarios lo hacen mediante las claves que lo componen.

>>> mi_lista[0]
 'Diego Buendia'

Los diccionarios, en cambio, lo hacen por claves:

>>> mi_diccionario['created_at']
 '2009-12-01'

Una forma muy de Python de averigüar el tipo de dato en una variable es asumir que es una lista. Si no da error, es que lo era:

>>> mi_diccionario[0]
 Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 KeyError: 0

Un error KeyError significa que esa clave no existe en el diccionario. Desafortunadamente, puede que estemos tratando realmente con un diccionario:

>>> d = {1: 'Francia', 2: 'Italia', 5: 'Alemania'}
>>> d[2]
 'Italia'
>>> d[0]
 Traceback (most recent call last):
 File "", line 1, in 
 KeyError: 0

¿Qué diferencia hay entre este diccionario y una lista? Bueno, la diferencia está en que los índices de una lista empiezan forzosamente por cero, mientras que el diccionario podría tenerlos arbitrarios (como el de Alemania, que es 5).

¿Hay una forma de saber si una variable contiene un diccionario o una lista sin examinarla? Pues sí. La función isinstance(<objeto>, <tipo>):

 >>> isinstance(d, list)
 False
 >>> isinstance(d, dict)
 True

También nos interesa saber cómo recuperar todas las claves, o valores, de un diccionario:

 >>> d.keys()
 [1, 2, 5]
 >>> d.values()
 ['Francia', 'Italia', 'Alemania']

Observarás que estas funciones tienen una sintaxis diferente a len() o dir(). En éstas, el argumento va dentro de los paréntesis, y en cambio keys() y values() están asociadas al objeto d mediante la notación ‘.’ (punto). Todo en Python deriva de una clase Object, y las clases son estructuras que contienen atributos (digamos variables asociadas) y métodos (funciones asociadas). La notación ‘.’ explicita esta dependencia.

Con estos recursos, es posible navegar y conocer poco a poco las estructuras de nuestro programa. Lo peor que puede pasar, y que de hecho pasa, es que al intentar mostrar un elemento inadvertidamente resulte ser un elemento de tamaño descomunal y se produzca un pantallazo continuo de datos. La solución es teclear pronto un <Ctrl>-C para interrumpir la salida.

La estructura del objeto Tweet de Twitter

Para terminar, vamos a recuperar el código con el que iniciábamos el presente post, pero ahora, para poder examinarlo con calma, guardaremos el timeline recuperado en una variable:

>>> import twitter
>>> token = "93826987-b17gKPaq4a5sNtbu3PfXPHdBcCm4ff533PSlMKGMV"
>>> token_key = "wZZ5VUOPzOy1QABHiA4gUQEfrRoMxn4qlewNTJ1yxhZem"
>>> con_secret = "gsIT85KKCwn96YZdEm5i3uGtq"
>>> con_secret_key = "SvWhbZNUZIx9Kz3sMuMaditk3phMyhw2tLHrrxXLX7SaYm9Ntm"
>>> auth = twitter.OAuth(token, token_key, con_secret, con_secret_key)
>>> t = twitter.Twitter(auth=auth)
>>> tl = t.statuses.home_timeline()

Ya vimos que la variable tl tiene aspecto de lista. Puedes comprobar que tiene 20 elementos (es lo que devuelve el API de Twitter por defecto, si no se le dice nada al respecto).

 >>> len(tl)
 20

Extrae el primer elemento a una variable, para examinarla con más calma. Recordarás que los elementos de esta lista son diccionarios, que corresponden a tuits. Puedes mirar las claves del diccionario con keys(): lo que ves son los atributos (públicos) de un tuit, según el API de Twitter. Uso el bucle for de Python (for elem in lista:) para presentar los atributos con más pulcritud.

 >>> tuit = tl[0]
 >>> for atributo in tuit.keys():
 ...     print(atributo)
 ...
 retweeted
 in_reply_to_status_id
 favorite_count
 entities
 possibly_sensitive
 contributors
 in_reply_to_user_id
 geo
 favorited
 truncated
 in_reply_to_screen_name
 retweet_count
 id
 text
 id_str
 in_reply_to_status_id_str
 lang
 source
 retweeted_status
 place
 in_reply_to_user_id_str
 extended_entities
 user
 created_at
 coordinates

He marcado en color aquellos campos que en mi opinión son más relevantes, para comentarlos a continuación. Si quieres conocer con más detalle esta y otras estructuras, puedes consultar la documentación de objetos del API de Twitter.

Campos esenciales
  • id: campo numérico que identifica el tuit de una forma única. Este (y otros campos numéricos) tiene una versión en formato cadena (id_str) para evitar problemas en aquellos lenguajes que tienen problemas con los números muy grandes. No es el caso de Python, así que en adelante ignoraré las versiones _str.
  • text: el contenido del tuit. O sea, lo que lee la gente, propiamente dicho.
  • created_at: la fecha en que se publicó el tuit.
  •  user: user es un diccionario que corresponde al objeto User de Twitter. Contiene toda la información del usuario
Campos reply

Existen siempre (menos el último), pero sólo se rellenan cuando el tuit es contestación a otro previo. En caso contrario, contienen el valor None. Si es tuit de contestación, estos campos se rellenan con información del tuit al que ha contestado.

  • in_reply_to_status_id: es el id del tuit original al que contestamos.
  • in_reply_to_user_id: el id del usuario que creó el tuit original.
  • in_reply_to_screen_name: el nombre Twitter de dicho usuario.
  • retweeted_status: la copia del tuit original. Este campo no existe si el tuit no es contestación de otro.
Campos estadísticos

Corresponden al número de retweets y favorites que tenga el tuit.

  • favorite_count: número de veces que fue marcado como favorito.
  • retweet_count: número de veces que fue retuiteado.

La estructura del  objeto User de Twitter

Selecciona el elemento user del tuit y guárdalo en una variable. Busca las keys(), como antes:

>>> user = tuit['user']
>>> for atributo in user:
...     print(atributo)
...
contributors_enabled
profile_background_tile
url
profile_link_color
is_translation_enabled
screen_name
entities
time_zone
utc_offset
geo_enabled
profile_background_image_url_https
listed_count
name
protected
verified
location
profile_background_image_url
profile_sidebar_border_color
profile_text_color
default_profile_image
id
profile_use_background_image
follow_request_sent
following
profile_image_url_https
lang
profile_sidebar_fill_color
is_translator
id_str
description
statuses_count
favourites_count
friends_count
notifications
followers_count
profile_background_color
profile_image_url
profile_banner_url
created_at
default_profile
Campos esenciales
  • id: código del usuario. Su DNI en Twitter.
  • screen_name: el nombre que aparece en las menciones.
  • name: el nombre que aparece en grande junto a la foto.
  • created_at: fecha en la que el usuario se dio de alta en Twitter. Este campo no se ve en la web, a pesar de que es muy interesante (en mi opinión).
  • description: texto que el usuario publica de sí mismo y que aparece en su perfil.
Campos estadísticos
  • statuses_count: número de tuits publicados por este usuario.
  • favourites_count: número de tuits marcados como favoritos por este usuario.
  • friends_count: número de personas a las que este usuario sigue.
  • followers_count: número de personas que siguen a este usuario.
  • listed_count: número de listas en las que está presente este usuario.

Conclusión

Con esto termino el post de hoy. Resumo su contenido:

  • Hemos examinado lo que nos devuelve el API de Twitter (en un caso concreto).
  • Hemos hablado del formato JSON.
  • Hemos visto las listas y diccionarios de Python, y cómo explorarlos.
  • Hemos analizado las estructuras Tweet y User.

Ahora ya tienes unas nociones de las estructuras de datos que tiene Twitter, y de cómo se manejan desde Python. El trabajo del próximo día será estudiar el API de Twitter y su uso a través de la librería Python correspondiente. Hasta mañana.

Ah! Me gustaría pedirte tu opinión sobre este blog. En caso de que tengas alguna sugerencia o crítica me gustaría mucho conocerla.

Twitter: conversaciones gráficas con Python (III)

Obtener las claves de Twitter

Esta es la tercera entrada de este blog, dedicado a usar el API de Twitter y las librerías de Python para generar una imagen de la conversación que tenga un formato gráfico:

nolla

Tengo que recuperar los tuits con el API de Twitter y usar Python para filtrar la información, crear una lista ordenada y generar un gráfico con los nodos.

El API de Twitter exige desde 2013 el uso de autenticación de usuario. Por ello, lo primero que necesitarás es, aparte de tener cuenta en Twitter, registrar tu futura aplicación y generar las claves de autenticación necesarias. Esto es lo que te explico en este post.

Regístrate como usuario

Lo primero es tener una cuenta en Twitter. Voy a dar por supuesto que este paso ya lo tienes superado. Si no, solo necesitas un nombre, un correo y una contraseña.

registro en twitter
Registro en Twitter

Da de alta una aplicación

Twitter te exige que des de alta tu aplicación para poder acceder a sus datos. Te pedirá un nombre y una descripción. También una URL (te la pide, pero no tiene por qué ser real porque no la usaremos aquí). Puedes opcionalmente decir qué permisos tendrá la aplicación en Twitter, algo importante porque para Twitter la aplicación actúa en tu nombre, pero que tampoco es esencial aquí.

Para dar de alta una nueva aplicación, entra en apps.twitter.com:

twitter apps
Página principal de Twitter Apps

Pulsa el botón Create New App.

alta app twitter
Dando de alta una aplicación en Twitter

Los datos obligatorios que nos piden son nombre , descripción y website.

Nombre es un identificador único. No podrás usar Mi aplicación porque ese nombre ya está en uso, así que tendrás que cavilar un poco más. Tienes 32 caracteres de espacio.

Descripción es un espacio más amplio y de formato libre para describir qué hace tu aplicación.

Website es el sitio donde los usuarios acceden para descargar tu aplicación. Tiene que tener formato de URL válida: http://www.holaholahola.es, por ejemplo. El campo es obligatorio, pero Twitter no valida esa URL, así que cualquier cosa vale en nuestro caso. Aquí no vamos a publicar, sólo queremos tener una autorización para acceder al API de Twitter.

A continuación viene un texto explicando las normas (que tendrías que leer), un checkbox de aceptación de las normas y el botón Create your Twitter application, que debes pulsar para continuar.

Obtener las claves de autenticación

Tendrás que tener ahora la página de administración de aplicaciones de Twitter.

administración apps
Panel de administración de aplicaciones Twitter

Verás que aquí se usan el nombre, la descripción y la URL introducidas antes.

Hay tres pestañas más:

  • Settings, que permite cambiar los datos que ya hemos introducido.
  • API Keys, que sirve para crear las claves OAuth que necesitarás (ésta es la importante)
  • Permissions, donde defines si la aplicación leerá sólo o también podrá mandar tuits y mensajes directos.

Si entras en API Keys, en la parte superior verás dos códigos, que son la mitad de lo que necesitamos para autenticarnos. El primero, API Key, es el código que identifica la aplicación, y el segundo, API Secret, una suerte de contraseña que nadie debe de conocer (yo te la muestro porque borraré la aplicación en cuanto termine el post ;)).

9a

En la parte inferior de la página (igual tienes que desplazarte) verás lo siguiente:

pre token

Puesto que aún no tenemos el resto de la información para OAuth, pulsa el botón para generarla. Volverá a cargarse la página, pero no verás nada nuevo porque el proceso tarda unos instantes. Twitter te avisa:

api token wait

Al cabo de un rato, cuando refresques el navegador, verás que en la parte de abajo de la página ya han aparecido unos códigos nuevos:

api keys 2

Access token y Access token secret son las otras dos claves necesarias para autenticarse. Los detalles del funcionamiento de OAuth están fuera del ámbito de este post, de modo que vamos a centrarnos en copiar estas cuatro claves para usarlas en Python.

Probar de nuevo en Python

Abre un bloc de notas y copia las cuatro claves en cuatro lineas. Ten en cuenta que en la librería se invocan en un orden distinto a como las vemos en Twitter: primero las dos claves de token (líneas 3 y 4) y despues las del consumer (líneas 1 y 2):

consumer_key = gsIT85KKCwn96YZdEm5i3uGtq
consumer_secret = SvWhbZNUZIx9Kz3sMuMaditk3phMyhw2tLHrrxXLX7SaYm9Ntm
access_token = 93826987-b17gKPaq4a5sNtbu3PfXPHdBcCm4ff533PSlMKGMV
access_token_secret = wZZ5VUOPzOy1QABHiA4gUQEfrRoMxn4qlewNTJ1yxhZem

Guarda el fichero con un nombre cualquiera, por ejemplo api_keys.txt. Así no tendrás que volver a apps.twitter.com a recordarlas. Y guarda el fichero en lugar seguro (esto se dice siempre, pero me voy a ahorrar las admoniciones paranoicas habituales).

Es el momento de ver si la librería de acceso a Twitter que nos falló en el post anterior funciona ahora que tenemos las claves para acceder a Twitter. Abre Python y ve escribiendo los siguientes comandos:

>>> token = '93826987-b17gKPaq4a5sNtbu3PfXPHdBcCm4ff533PSlMKGMV'
>>> token_key = 'wZZ5VUOPzOy1QABHiA4gUQEfrRoMxn4qlewNTJ1yxhZem'
>>> con_secret = 'gsIT85KKCwn96YZdEm5i3uGtq'
>>> con_secret_key = 'SvWhbZNUZIx9Kz3sMuMaditk3phMyhw2tLHrrxXLX7SaYm9Ntm'

Recuerda usar tus propias claves. Y escribirlas entre comillas, ya sean simples o dobles, para que Python interprete que son cadenas de texto.

Ahora esas cuatro variables contienen los valores para la autenticación que nos dio Twitter, y ya podemos ir con la librería de acceso a Twitter.

>>> token = "gsIT85KKCwn96YZdEm5i3uGtq"
>>> token = "93826987-b17gKPaq4a5sNtbu3PfXPHdBcCm4ff533PSlMKGMV"
>>> token_key = "wZZ5VUOPzOy1QABHiA4gUQEfrRoMxn4qlewNTJ1yxhZem"
>>> con_secret = "gsIT85KKCwn96YZdEm5i3uGtq"
>>> con_secret_key = "SvWhbZNUZIx9Kz3sMuMaditk3phMyhw2tLHrrxXLX7SaYm9Ntm"
>>>
>>> import twitter
>>> auth = twitter.OAuth(token, token_key, con_secret, con_secret_key)
>>> t = twitter.Twitter(auth=auth)
>>>

Verás que ningún comando devuelve nada, lo que es buena señal. Aunque no lo parezca, estamos autenticados en Twitter y podemos empezar a pedir información. Pero eso ya es para el próximo post.

Continuará...

¡No, hombre, no! ¿Cómo voy a dejarte así justo ahora? Vamos a pedir tu línea de tiempo, a ver qué sale.

>>> timeline = t.statuses.home_timeline()
>>> for tuit in timeline:
...      print(tuit['text'])

Ya tenemos (si todo ha ido bien) una conexión con Twitter autenticada y operativa.  ¡Y sólo en tres posts!

Para terminar, te comento: Twitter nos envía mucha más información que el texto. Prueba a ejecutar lo anterior sustituyendo print(tuit[‘text’]) por print(tuit) a secas. Bárbaro, ¿verdad? En el próximo post explico cómo desbrozar ese (aparente) caos de información. Hasta la próxima.

Twitter: conversaciones gráficas con Python (II)

Preparativos

Esta es la segunda entrada de este blog, dedicado a usar el API de Twitter y las librerías de Python para generar una imagen de la conversación que tenga un formato gráfico:

nolla

Tengo que recuperar los tuits con el API de Twitter y usar Python para filtrar la información, crear una lista ordenada y generar un gráfico con los nodos.

En esta entrada te explico los requisitos del proyecto y la forma de satisfacerlos.

Python

No tiene mucho misterio. En Windows se instala con un ejecutable. Existe la opción de usar la versión 2.7 o la 3.4, en sus versiones de 32 y 64 bits. Para Linux y OSX hay sus propios instaladores.

Instalaciones de Python
Instalaciones de Python

Yo uso la 3.4 que corresponde a mi máquina, que es de 64 bits.

Al principio usé la 2.7, porque leí que había muchas librerías que no estaban portadas aún a la versión 3 (que apareció en 2008), y quise evitarme problemas. Luego me di cuenta de que esas mismas informaciones eran quizás demasiado antiguas, y que ha pasado suficiente tiempo ya. Ahora seguro que hay más librerías portadas a Python 3.

Así que he usado mucho la versión 2 de Python, y encuentro que tiene un defecto muy enojoso para un programador que usa acentos y eñes: los errores debidos a problemas de codificación de caracteres son frecuentes e impredecibles. Si importas un lote de tuits sin caracteres raros, todo va bien. Y un día te tropiezas con un carácter unicode raro y de repente del programa falla.

Esto es muy frustrante, y le quita encanto a jugar con Python, el cual por otro lado es un lenguaje muy acogedor. Por esa razón decidí salir de la versión 2.7 y volver a la 3.4, esperando que las librerías que quiero usar ya estuvieran portadas.

Y ese ha sido el caso, salvo un pequeño problema con pydot2 que ya explicaré.

Bueno. Eso. Que instaléis Python 3.4 si queréis seguir estos artículos, o la 2.7 si sois espíritus inquietos que no se arredran ante las dificultades.

No olvidar poner la ruta de Python en el PATH del sistema, para que puedas encontrar fácilmente el ejecutable desde tu directorio de trabajo.

En estas condiciones, invocando python desde la línea de comandos tendrás esto:

C:\Users\Diego>python
Python 3.4.1 (v3.4.1:c0e311e010fc, May 18 2014, 10:45:13) [MSC v.1600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

El prompt cambia a >>>, Python está listo para recibir comandos, lo que demuestra que la instalación fue correcta.

pip

El instalador de paquetes es básico para extender Python según nuestras necesidades. En la página de instalación se nos pide que descarguemos el fichero get-pip.py, que no es más que un script de Python que contiene las instrucciones para descargar e instalar pip.

El comando de instalación, en la línea de comandos:

c:\Users\Diego\> python get-pip.py
instalación pip
Página de instalación de pip

Para ver si está instalado, ejecuto el comando pip. Debe aparecer la pantalla de ayuda:

pip instalado
Instalación correcta de pip

Ahí también verás la colección de comandos que pip admite. La opción que más usaremos es install, pero también resulta útil list, para ver qué paquetes tenemos instalados.

twitter

Con pip instalado, instalar twitter es trivial:

c:\Users\Diego\> pip install twitter
Downloading/unpacking twitter
Installing collected packages: twitter
Successfully installed twitter
Cleaning up...

Para ver que el paquete se instaló bien, vuelve a abrir Python.

Usa el comando import twitter. Esto le pide a Python que busque el paquete twitter y lo haga accesible para trabajar con él. Si todo está bien, no pasará nada. Python devuelve el prompt, esperando un nuevo comando. Si hay algún mensaje de error, mal asunto.

C:\Users\Diego> python
Python 3.4.1 (v3.4.1:c0e311e010fc, May 18 2014, 10:45:13) [MSC v.1600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import twitter
>>>

Comprobación

Un comando interesante de Python es help. Te devolverá información del objeto que le pases como parámetro. Puedes probar con twitter, te saldrán varias páginas.

>>> help(twitter)
Help on package twitter:

NAME
 twitter - The minimalist yet fully featured Twitter API and Python toolset.

DESCRIPTION
 The Twitter and TwitterStream classes are the key to building your own
 Twitter-enabled applications.

 The Twitter class
 -----------------

The minimalist yet fully featured Twitter API class.

Get RESTful data by accessing members of this class. The result
 is decoded python objects (lists and dicts).

The Twitter API is documented at:

http://dev.twitter.com/doc

 Examples::

t = Twitter(
 auth=OAuth(token, token_key, con_secret, con_secret_key)))

# Get your "home" timeline
 t.statuses.home_timeline()

De hecho, ahí está el código que usaremos para importar nuestra línea de tiempo (TL). Peeero… si lo ejecutas tal como está tendrás un error. Lo vas a corregir y de paso te explico su por qué.

Error del espacio de nombres

Voy a suponer que has vuelto a entrar en Python (por cierto, para salir se escribe exit() en el prompt). Importa twitter y escribe el primer comando que vimos en la ayuda. Tendrás lo siguiente:

>>> import twitter
>>> t = Twitter(
... auth=OAuth(token, token_key, con_secret, con_secret_key))
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
NameError: name 'Twitter' is not defined
>>>

El comando dir() te mostrará los objetos y atributos que tiene cada objeto de Python. Prueba a curiosear dentro de la librería twitter.

>>> dir(twitter)
['NoAuth', 'OAuth', 'OAuth2', 'Twitter', 'TwitterError', 'TwitterHTTPError', 'TwitterResponse', 'TwitterStream', 'UserPassAuth', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', 
'__spec__', 'api', 'auth', 'dedent', 'oauth', 'oauth2', 'oauth_dance', 'oauth_doc', 'read_token_file', 'stream', 'twitter_globals', 'write_token_file']

Uy, aquí hay muchas cosas que ver.

Para empezar, lo que devuelve dir() es una lista. Una lista no es más que una colección de objetos agrupados juntos. Verás objetos que empiezan con mayúscula (Twitter), objetos con subrayados antes y después del nombre (__all__) y objetos en minúsculas.

Los del doble subrayado son objetos internos de Python. Algunos son más esotéricos que otros. Prueba, por ejemplo los comandos:

>>> twitter.__name__
 'twitter'
>>> twitter.__path__
 ['C:\\Python34\\lib\\site-packages\\twitter']

Los objetos con mayúsculas son clases. Las clases son el concepto en el que se basa la Programación Orientada a Objetos (OOP, en inglés), tema en el que no entraré si no es bajo coacción. No al menos mientras no nos haga falta. El caso es que la librería twitter contiene clases que usarás; y será mucho más fácil usarlas que adquirir maestría en el tema de la OOP.

Una cosa amable de Python es que no te obliga a usar esa mayúscula para nombrar una clase; son los programadores de Python los que convienen que mejor usar esa convención para facilitarse la vida mutuamente.

El resto de objetos, en minúsculas, son funciones o atributos que contiene el paquete. La funcionalidad de una librería se disemina a través de esas funciones y atributos (y las clases que dijimos).

Volviendo al error, observa que la clase ‘Twitter’ sí que pertenece al paquete. ¿Por qué no la encontró Python?

Porque Python esperaba ‘Twitter’, y lo que hay es ‘twitter.Twitter’

Cuando ejecutas

>>> import twitter

le estás diciendo a Python que hay un espacio de nombres llamado ‘twitter’, al que pertenecen los objetos que hemos visto vía dir():

['NoAuth', 'OAuth', 'OAuth2', 'Twitter', ...

Entonces Python entiende que para referirse a esos objetos, tiene que usar el nombre del espacio de nombres y el nombre del objeto.

Imagina que importas otro paquete que casualmente tiene un objeto ‘Twitter’ también:

>>> import otro_paquete
>>> dir(otro_paquete)
 {'Twitter', 'Facebook', 'Googleplus',...

¿Cómo puede Python usar uno u otro, si ambos se llaman igual?

Exacto. Usando el espacio de nombres.

>>> t1 = twitter.Twitter()
>>> t2 = otro_paquete.Twitter()

Hay otras formas alternativas de solucionar el problema, pero de momento lo dejamos así.  Vuelve a invocar el comando Twitter, pero con el prefijo correcto. Ah! Y pon el prefijo también a OAuth, que es otro objeto de la librería twitter.

>>> import twitter
>>> t = twitter.Twitter(
... auth=twitter.OAuth(token, token_key, con_secret, con_secret_key))
Traceback (most recent call last):
 File "<stdin>", line 2, in <module>
NameError: name 'token' is not defined
>>>

 

Vaya. Otro NameError. Parece que deberíamos saber qué es token (y posiblemente también token_key, con_secret, con_secret_key…).

Pues sí. Esos objetos son variables, y contienen las claves del proceso de autenticación. O deberían. Porque de momento nadie habló de autenticarse.

Pero eso ya es materia de otra entrada.

 

Twitter: conversaciones gráficas con Python (I)

Presentación

Os adelanto que este es un proyecto largo y entretenido.

La idea es tomar una conversación de Twitter y convertirla en algo así como ésto:

nolla

Es decir, mi intención es convertir la conversación lineal de Twitter en un árbol de nodos, un formato gráfico que permita seguir mejor el flujo de la conversación.

Bueno, de hecho, llevo meses jugando con el API de Twitter, he aprendido cómo funciona y cómo son las estructuras de datos que devuelve, y de pronto un tropiezo: ¿dónde está la primitiva para obtener una conversación?

La versión 1.1 del API de Twitter

Está claro que en Twitter la tienen, ya que sus clientes para móvil y web presentan todos los tuits de una conversación, anteriores y posteriores, sin ningún problema. Pero, por alguna razón, han preferido que esa API no sea pública. Sus razones tendrán.

He buscado en Google y tampoco hay respuestas. Para complicar más las cosas, el API de Twitter pasó de la versión 1 a la 1.1 en junio de 2013, con lo que muchos desarrollos dejaron de ser funcionales hace relativamente poco tiempo (escribo en agosto de 2014).

El cambio de versión tiene un efecto disuasorio añadido. Ahora es necesario autenticarse para poder usar una buena parte del API, y esta autenticación OAuth no es precisamente sencilla así de entrada. Creo que esto también desanima un poco a los aficionados.

En resumen, volviendo al asunto, la pregunta era ¿Se podrá obtener una conversación de Twitter de forma programática? La respuesta es que sí, y a lo largo de estos posts espero ser capaz de daros unas cuantas indicaciones para lograrlo.

 Ingredientes

En lo que sigue, usaremos varias piezas de software. Aquí las enumero, aunque volveré sobre el asunto según las vayamos necesitando.

No es necesario tenerlo todo ya. Para empezar necesitamos solo python, el instalador de paquetes pip y la librería twitter.

Mi sistema operativo es un Windows 7. Para algunas cosas (la codificación UTF-8, por ejemplo) me da la sensación de que Linux me habría complicado menos. Pero tampoco han sido problemas insuperables.

Documentación

Aquí os dejo también enlaces a las fuentes del conocimiento, Google aparte.

  • Referencia del API de Twitter. Describe el interfaz de Twitter. Se trata de peticiones HTML, pero el paquete twitter de Python usa casi la misma sintaxis, de modo que la documentación de Twitter nos sirve perfectamente.
  • Referencia de Python. La oficial. Mucha información pero muy buena.
  • Stackoverflow. Una web de preguntas y respuestas donde es raro no encontrar el problema que uno tiene en un momento dado. Cuando busco en Google, suelo mirar primero los enlaces a esta web.

Python, reverse() y los separadores de millares

Python como programación recreativa

Este verano de 2014 me ha dado por matar el tiempo programando en Python.

Python es un lenguaje de programación especial. Es sencillo de aprender, y de usar. Yo llevo programando desde que tuve mi primera calculadora programable, una TI-58 con display de diodos rojos (eso fue en 1.978), y he usado unos cuantos lenguajes de progamación en mi vida. Encontrarme con Python ha sido una agradable sorpresa.

En Python:

  • El aspecto formal del código determina la estructura. Los bloques anidados se tabulan hacia la derecha para distinguirlos del código principal.
  • La sintaxis procura huir de complejidades semánticas: comparativamente a otros lenguajes, es agradable de leer.
  • No se supone que el programador es torpe o malvado. Todos los objetos son públicos y visibles.  Se agradece que el lenguaje confíe en que uno hará buen uso de él.
  • Sugiere convenciones estilísticas, pero no obliga a adherirse a ellas. Es el propio programador el que acaba admitiendo que le conviene seguirlas. En resumen, es un lenguaje amable y nada paranoico.

Para un aficionado a la programación, un lenguaje sencillo y potente, amable y eficaz es un juguete, ni más ni menos. Y en esto gasto el verano, jugando.

Una tarea para entretenerse

Para ilustrar mi relación con Python, a continuación os enseño el código que me distrajo ayer, mientras veía en la tele El Club de los Poetas Muertos (resulta que ayer murió Robin Williams, DEP).

Se trata de formatear un número para que muestre los separadores de millares.

Es decir, escribir un codigo que convierta el número 66665583202 en la cadena ‘66.665.583.202’

En el intérprete de Python, cuyo prompt es >>>, escribo la cifra con la que voy a hacer pruebas. Al pulsar ENTER, me hace eco del valor:

>>> 66665583202
 66665583202

El algoritmo más sencillo para poner los puntos de millares, etc. consistiría en contar tres caracteres (empezando por la derecha), poner el punto, contar otros tres caracteres, poner otro punto, y así hasta terminar con todos los caracteres.

Cadenas

Para convertir el número en cadena, Python tiene la función str():

>>> str(66665583202)
 '66665583202'

Ahora el eco viene con comillas, lo que indica que ya tenemos una cadena. Para invertir la cadena, lo primero que viene a la cabeza a un programador es una función reverse(). Puesto que tenemos un objeto de cadena, podría éste tener un método que hiciese el servicio, ejecutando:

>>> str(66665583202).reverse()
 Traceback (most recent call last):
 File "<pyshell#6>", line 1, in <module>
 str(66665583202).reverse()
 AttributeError: 'str' object has no attribute 'reverse'

Pero ya vemos que no. Mal rollo. ¿Cómo se hace esto en Python? Aquí hay que estudiar una de las singularidades de Python, chocante al principio pero que cuanto más la usas más utilidad le encuentras: el troceado de cadenas (cómo tomar una parte de las mismas mediante índices). Unos cuantos ejemplos bastarán para pillarle el tranquillo:

>>> cadena = '0123456789'
>>> cadena[0]  # El primer elemento es el 0
'0'
>>> cadena[9]
'9'
>>> cadena[10]  # Buscar un elemento fuera de límites da error
Traceback (most recent call last):
 File "<pyshell#17>", line 1, in <module>
 cadena[10]
IndexError: string index out of range
>>> cadena[-1]  # Los índices negativos cuentan desde el final
'9'
>>> cadena[-2]
'8'
>>> cadena[-9]
'1'
>>> cadena[-len(cadena)]  # Podemos usar expresiones
'0'
# Sintaxis extendida para extraer varios caracteres
>>> cadena[4:8]  # Desde el caracter 4 al 8 (exclusive)
'4567'
>>> cadena[1:9]
'12345678'
>>> cadena[1:-2]  # Desde el 1 hasta el final menos los dos últimos
'1234567'
>>> cadena[1:9:2]  # Desde el 1 al 9 pero de 2 en 2
'1357'
>>> cadena[1:9:-2]  # Igual pero hacia atrás (no da nada)
''
>>> cadena[9:1:-2]  # Desde el 9 hasta el uno hacia atrás de 2 en 2 
'9753'
# No poner el índice significa desde el principio al final
>>> cadena[:]
'0123456789'
>>> cadena[::-1]  # Y poniendo de uno en uno hacia atrás... Voilà!
'9876543210'
>>>

Espero que estos ejemplos hayan dejado clara la sintaxis de troceado. Esta sintaxis se puede usar también en otros objetos que también son secuencias, como las listas o las tuplas.

El ejemplo que me interesa es el último: si indexo una cadena como [::-1], el resultado es la cadena invertida. Vamos a verlo

>>> str(66665583202)[::-1]
 '20238556666'

¡Bien! Esta parte ya está conseguida. Ahora me gustaría trocear la cadena resultante en bloques de tres caracteres. Para esto también podemos usar el troceado: Si a la cadena resultante de invertir la indexamos con [0:3], tendremos otra cadena con tres caracteres. A continuación pongo las distintas particiones que estoy buscando.

>>> str(66665583202)[::-1][0:3]
 '202'
 >>> str(66665583202)[::-1][3:6]
 '385'
 >>> str(66665583202)[::-1][6:9]
 '566'
 >>> str(66665583202)[::-1][9:12]
 '66'

Es interesante que, en el último caso, usar un índice fuera de rango no causa error sino que elegantemente devuelve solo los caracteres que queden. Cuando indexábamos un último carácter no era así, teníamos el error IndexError.

Iteradores y listas

En Python, una lista se crea escribiendo varias expresiones separadas por comas y dentro de corchetes:

>>> [1, 2, 3, 4]
[1, 2, 3, 4]

La lista que queremos construir, de hecho sería [‘202’, ‘385’, ‘566’, ’66’]. Vamos a ver cómo se hace con Python.

Una lista se puede crear de forma implícita ejecutando varias veces un código generador. La sintaxis para esto es [expresion(variable) for variable in lista] . Es decir, primero ponemos la función que vamos a evaluar, que presumiblemente dependerá de la variable del bucle, luego el nombre de la variable y la lista de valores que aquélla ha de recorrer.

Con unos ejemplos será más claro. Usaremos la función range() para generar los índices:

>>> # range(n) devuelve una lista de n números naturales
>>> [x for x in range(10)]  
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> [[x, x*x] for x in range(0, 9, 3)]  # Desde 0 a 9 de 3 en 3
[[0, 0], [3, 9], [6, 36]]

Este último ejemplo nos da pie para uno que nos sirva para el problema:

>>> [x for x in range(0, 10, 3)]
[0, 3, 6, 9]

Todo lo que hay que hacer es complicar la expresión para que nos salgan los cuatro trozos de cadena en lugar de los índices. Nuestra expresión origen es la cadena, primero invertida, luego troceada:

str(66665583202)    [::-1]    [i:i+3]

Vamos a meter esto en lugar de la x en la construcción [x for x in range(…)]. Además, en función de la longitud de la cadena a pelo (10), vamos a poner la función que nos da la longitud de esa cadena: len(str(66665583202))

>>> [str(66665583202)[::-1][i:i+3] for i in range(0, \     len(str(66665583202)), 3)]
 ['202', '385', '566', '66']

Concatenar elementos de una lista

Las cadenas tienen un método  llamado join(), que también tiene su singularidad. Como argumento tiene una lista, y su función es unir los elementos de la lista usando la cadena de referencia. Por ejemplo:

>>> "cadena".join[' Esto ', ' es ', ' una ', ' lista ']
 ' Esto cadena es cadena una cadena lista '

Para nuestro caso, ya tenemos la lista, sólo nos falta concatenarla con el carácter ‘.’ (punto).

>>> '.'.join([str(66665583202)[::-1][i:i+3] for i in range(0, \  len(str(66665583202)), 3)])
 '202.385.566.66'

Para acabar, toda la cadena debe reinvertirse para quedar al derecho otra vez:

>>> '.'.join([str(66665583202)[::-1][i:i+3] for i in range(0, \   len(str(66665583202)), 3)])[::-1]
 '66.665.583.202'

Epílogo 1

Una de los puntos fuertes de Python es la cantidad de módulos que los desarrolladores han ido creando a lo largo del tiempo.  Python tiene una larga historia, ya que empezó a desarrollarse en 1.989.

Sería raro que en todo este tiempo nadie se hubiera encontrado con nuestro problema, y más raro aún que no le hubieran buscado solución. Buscando en Google acerca del formato de cadenas en Python, encontré que las cadenas tienen un método format(), que hace el mismo trabajo que acabamos de desarrollar.

El formato de las cadenas puede verse en la documentación de Python (otro de sus puntos fuertes). El formato se especifica como una cadena con llaves y dos puntos; en caso de incluir una coma se indica que se desean separadores de millar.

>>> "{:,}".format(66665583202)
 '66,665,583,202'

El caso es que en España, los separadores son puntos. Esto es fácil de arreglar con la función replace() (siempre que no hayan decimales…)

>>> "{:,}".format(66665583202).replace(',','.')
 '66.665.583.202'
 >>> "{:,}".format(66665583202.344532).replace(',','.')
 '66.665.583.202.34453'

Epílogo 2

Esta contrariedad de los decimales me empuja a buscar otra solución mejor. Y ésta llega a través del paquete locale, que incluye todo lo referido a la configuración regional. La instrucción import nos permite acceder al espacio de nombres de dicho paquete, y la función setlocale() del mismo nos configura Python para trabajar con la configuración regional de la máquina.

>>> import locale
 >>> locale.setlocale(locale.LC_ALL,'')
 'Spanish_Spain.1252'
>>> locale.format("%d", 66665583202.344532, grouping=True)
 '66.665.583.202'  # d: entero
>>> locale.format_string("%f", 66665583202.344532, grouping=True)
 '66.665.583.202,344528'  # f: float
>>> locale.format("%.f", 66665583202.344532, grouping=True)
'66.665.583.202'  # .nf es float con n decimales. Si n no está, 0 decimales
>>> locale.format("%f", 66665583202, grouping=True)
 '66.665.583.202,000000'  # Aquí f pone los decimales a un entero

La solución que usaríamos en nuestro problema

>>> locale.format("%d", 66665583202, grouping=True)
 '66.665.583.202'

Epílogo 3

Otra de las características de Python es que existen varias maneras de hacer las cosas. En el caso de los formatos de cadenas, además de la sintaxis {:,} que vimos antes, existe la {:n}, que indica que se usen los formatos de la configuración regional. Para que funcione se debe hacer el setlocale() como en el caso anterior.

>>> '{:n}'.format(66668355202)
'66.668.355.202'

Y eso es todo por hoy. Hasta otra!