Pasarela RSS -> Twitter que también sube imágenes asociadas
Hace un año publiqué un artículo sobre cómo enviar las actualizaciones de tu blog directamente a Twitter: Ten tu propia pasarela RSS -> Twitter. Ese artículo es imprescindible para entender este. Léelo primero.
El código que publiqué en Ten tu propia pasarela RSS -> Twitter ha funcionado bien durante este tiempo pero, consciente de que la gente reacciona mejor a los mensajes de Twitter cuando tienen imágenes asociadas, quería añadirle esa funcionalidad. Añadir la capacidad de publicar imágenes.
Hay varios ejemplos en Internet sobre cómo hacerlo pero usan el API antiguo de Twitter, marcado como obsoleto. La librería que utilizo soporta la versión nueva del API, pero su uso no estaba documentado por ningún sitio.
Esto está solucionado con un poquito de presión del pesado de siempre :-) :
- Issue #279: Update examples to use "media/upload()" instead of the deprecated "update_with_media()".
Con una librería a la altura y la documentación actualizada, ya solo queda picar código.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 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 174 175 176 177 178 179 180 181 |
#!/usr/local/bin/python3 # 20xx-2015 jcea@jcea.es - http://www.jcea.es/ # Código liberado como Dominio Público. # Haz con él lo que quieras. import feedparser from bs4 import BeautifulSoup import urllib.request, urllib.parse import pickle import os, os.path import socket import fcntl import sys import functools path = '/home/blog-twitter/data/' persist = 'ya_publicados.pickle' def lock(f) : @functools.wraps(f) def wrapper() : # Si no puede, falla inmediatamente lock = open(persist+'.lock', 'a') fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) try : return f() finally : # El lock se libera automáticamente al terminar # pero así no dejamos basurilla en el directorio lock.close() os.remove(persist+".lock") return wrapper def drop_privileges() : os.chdir(path) os.setgroups([60001]) # Nobody os.setgid(60001) os.setuid(60001) def get_imgs_to_publish(url) : pagina = urllib.request.urlopen(url) if pagina.status != 200 : raise RuntimeError('La página %s devuelve el estado %s' %(url, pagina.status)) pagina = pagina.read() pagina = BeautifulSoup(pagina) pagina = pagina.select('.entry-content') if len(pagina) != 1 : raise RuntimeError('Formato inesperado') pagina = pagina[0] total_num_imgs = pagina('img') relevant_imgs = pagina('img', class_ = 'to-twitter') imgs = [] if total_num_imgs : if len(relevant_imgs) == 0 : raise RuntimeError('Tenemos varias imágenes pero ninguna marcada ' 'para ser publicada') for img in relevant_imgs : img_url = urllib.parse.urljoin(url, img['src']) imgs.append(img_url) return imgs def upload_images(images, auth) : import twitter # Solo importa si lo vamos a necesitar # La imagen debe ser GIF, PNG o JPEG y medir menos de 3Mbytes. # XXX: Buscar una referencia online de esto. Twitter_upload = twitter.Twitter(domain='upload.twitter.com', auth=auth) ids = [] for img in images : print('Subiendo imagen', img) img = urllib.request.urlopen(img) if img.status != 200 : raise RuntimeError('Intentar descargar la imagen %s devuelve el ' 'estado %s' %(img.url, img.status)) img = img.read() ids.append(str(Twitter_upload.media.upload(media=img)['media_id'])) return ids def get_auth() : import twitter # Solo importa si lo vamos a necesitar # Autenticacion OAUTH para TWITTER, segun los detalles de # http://jmillerinc.com/2010/05/31/twitter-from-the-command-line-in-python-using-oauth/ # http://github.com/sixohsix/twitter/blob/master/twitter/ircbot.py consumer_key = "XXXXXX" consumer_secret = "XXXXXX" # Podemos crear un nuevo cliente en twitter y pedir que nos # genere unos token de acceso directamente, que podemos usar ya sin mas. if not os.path.exists('oauth_token_file') : from twitter.oauth_dance import oauth_dance oauth_dance('FEED RSS de http://blog.jcea.es/', consumer_key, consumer_secret, "oauth_token_file") oauth_token, oauth_secret = \ twitter.oauth.read_token_file('oauth_token_file') auth = twitter.OAuth(oauth_token, oauth_secret, consumer_key, consumer_secret) return auth @lock def do() : socket.setdefaulttimeout(60) ya_publicado = set() if os.path.exists(persist) : ya_publicado = pickle.load(open(persist, 'rb')) feed = feedparser.parse('https://blog.jcea.es/rss.xml') assert feed.status == 200 assert feed.encoding == 'utf-8' limpieza = set([i.id for i in feed.entries]) ya_publicado.intersection_update(limpieza) # Limpieza de post antiguos a_publicar = [i for i in feed.entries if i.id not in ya_publicado] for entrada in a_publicar : import twitter # Solo importa si lo vamos a necesitar auth = get_auth() twitter_API = twitter.Twitter(auth=auth) imgs = get_imgs_to_publish(entrada.link) if len(imgs) : imgs_ids = upload_images(imgs, auth = auth) else : imgs_ids = [] # El 1 es por el espacio entre titulo y url # El acortamiento de t.co mide 22 bytes para HTTP y 23 bytes para HTTPS. # el 4 es por el ' #fb' l = 140-1-23-4 params = {} if len(imgs_ids) : params['media_ids'] = ','.join(imgs_ids) # El enlace a las imágenes, aunque haya varias, # y el espacio previo. l -= 23+1 titulo = entrada.title if len(titulo) > l : titulo = titulo[:l-3]+'...' # El acortamiento a 't.co' es automático y transparente, al # publicar en twitter. titulo = titulo+' '+entrada.link+' #fb' params['status'] = titulo try : twitter_API.statuses.update(**params) except twitter.api.TwitterHTTPError as e: import json e = json.loads(e.response_data.decode('utf-8')) if e['errors'][0]['message'] != 'Status is a duplicate.' : raise print(titulo) ya_publicado.add(entrada.id) f = open(persist+'.new', 'wb') pickle.dump(ya_publicado, f) f.flush() os.fsync(f) f.close() os.rename(persist+'.new', persist) if __name__ == '__main__' : drop_privileges() do() |
Este código mide el doble que el original. No voy a explicarlo entero; debes leer primero Ten tu propia pasarela RSS -> Twitter.
El código se ha reestructurado por completo para poder importarlo sin efectos colaterales [1]. En particular, se podría llamar varias veces en el mismo programa. La mayor complicación es la gestión del bloqueo para evitar lanzar varias actualizaciones simultáneas. Para ello creo un decorador en las líneas 21-35 y lo utilizo en la línea 109.
[1] |
En realidad el programa sí tiene algunos efectos colaterales, como la dependencia de ser ejecutado en un directorio concreto, el cambio de usuario/grupo o el timeout por defecto. Cambiaré el código cuando lo necesite. |
Como en la versión anterior, repaso el feed RSS de este blog. Hay dos cambios a destacar.
El primero son las líneas 133-137. Deciden si la entrada del blog tiene imágenes a publicar.
El segundo cambio son las líneas 144-149 y la línea 158. Si no hay imágenes, publicamos como siempre. Pero si hay imágenes debemos prepararlas para subirlas y debemos tener en cuenta que subir imágenes consume 24 caracteres adicionales del Twitter (23 de la URL y uno del espacio previo). Es decir, si publicamos imágenes perdemos 24 caracteres del texto posible. Es importante destacar el hecho de que solo se añade una URL al Twitter, subamos una imágen o subamos cuatro.
Decidir si hay que publicar imágenes y hacerse con ellas es la parte novedosa e interesante.
La rutina get_imgs_to_publish() en las líneas 43-65 descarga un artículo del blog, analiza su contenido y localiza las imágenes en el cuerpo, si las hay. Para masticar el HTML utilizamos la magnífica librería Beautiful Soup 4. Si no hay imágenes, no hay nada que hacer. Si encontramos imágenes, al menos una de ellas debe tener asignada la clase CSS to-twitter. Esta clase no hace nada en absoluto a nivel de CSS, simplemente sirve como un marcador. Las imágenes marcadas con esa clase CSS se deben publicar en Twitter. Si hay imágenes, al menos una de ellas debe tener esa clase CSS. He hecho esto así para evitar equivocaciones. Por supuesto tú puedes cambiar el código como quieras.
Obtener las URLs de las imágenes que nos interesan no es trivial. Pueden ser referencias absolutas, relativas o estar incluso en otro servidor diferente. Nos olvidamos de todas estas complejidades utilizando la librería estándar urllib.parse (línea 62). Agradece poder contar con estas ayudas de serie.
Una vez que tenemos las imágenes a publicar las descargamos una a una y las subimos al servidor upload.twitter.com en la rutina upload_images() (líneas 66-84). Podemos subir hasta cuatro imágenes del tipo GIF, JPEG o PNG. Deben medir menos de 3MBytes. [2]
[2] | Estas limitaciones las he leído por allí y por allá, en páginas sueltas. Me gustaría leer documentación oficial al respecto. |
Si conseguimos subir las imágenes correctamente recogemos los identificadores que nos da Twitter, los añadimos a los datos que adjuntamos al subir el Twitter (línea 146) y tenemos en cuenta que Twitter añadirá -implícita y automáticamente- una URL a nuestro mensaje (líneas 147-149).
¿Cómo marcamos una imagen con la clase CSS to-twitter?. Dependerá de tu software. En mi caso, en este blog, uso Nikola y sería algo así:
.. image:: ocsp.jpg :align: center :class: to-twitter
Podemos indicar varias clases CSS separándolas con espacios así que podemos seguir usando la decoración CSS que queramos para esas imágenes. Bastará con añadir to-twitter a las clases CSS que estemos usando para esas imágenes.
Un último detalle: Twitter también permite publicar vídeo. El código no necesita apenas cambios, salvo la búsqueda de las etiquetas HTML relevantes y la extracción de sus datos. Si usamos la etiqueta HTML 5 video el cambio sería trivial. Esto queda como ejercicio para el lector :).