<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>El hilo del laberinto (Publicaciones sobre Scraping web)</title><link>https://blog.jcea.es/</link><description></description><atom:link href="https://blog.jcea.es/categories/scraping-web.xml" rel="self" type="application/rss+xml"></atom:link><language>es</language><lastBuildDate>Mon, 01 Jun 2026 23:08:26 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Feed Slashdot: Modo "verboso" y peticiones HTTP incondicionales</title><link>https://blog.jcea.es/posts/20240727-slashdot_verbose_e_incondicionales.html</link><dc:creator>Jesús Cea Avión</dc:creator><description>&lt;div&gt;&lt;p&gt;Artículos previos sobre este proyecto para enviar por email las
novedades diarias de un &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://blog.jcea.es/posts/20191011-slashdot.html"&gt;Generar un email diario a partir de un feed RSS (Slashdot)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://blog.jcea.es/posts/20200330-slashdot.html"&gt;Mejoras a la hora de generar un email diario a partir de un feed RSS (Slashdot)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://blog.jcea.es/posts/20240416-pickle.html"&gt;Los peligros de volcar a "pickle" cualquier cosa&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://blog.jcea.es/posts/20240629-envio_gmail.html"&gt;"Nullmailer", "Return-Path" y GMail&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aunque el código va necesitando una refactorización y limpieza,
sigo haciendo pequeñas modificaciones y mejoras:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Modo &lt;em&gt;verboso&lt;/em&gt;: Normalmente cuando se invoca el programa hace su
trabajo sin más. Pero a veces nos interesa saber qué ha hecho,
si ha enviado un correo electrónico o no, por ejemplo, cuántas
entradas nuevas hay en el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Peticiones &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto"&gt;HTTP&lt;/a&gt; incondicionales: De vez en cuando el
&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; da un error &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto"&gt;HTTP&lt;/a&gt; 50x, indicando un
error interno. Cuando esto ocurre, el &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Sistema_operativo"&gt;Sistema Operativo&lt;/a&gt; de mi
servidor me envía un email indicando que el proceso ha fallado.
Ejecutándolo manualmente compruebo que cuando ocurren esos
errores el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; no ha cambiado desde la
petición anterior y que, de hecho, el problema se resuelve solo
cuando aparece una entrada nueva.&lt;/p&gt;
&lt;p&gt;Esto tiene toda la pinta de ser un bug en el código de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt;
y en realidad solo me afecta de forma cosmética (los correos
electrónicos de errores que me llegan por ser administrador de
la máquina que ejecuta este servicio), pero durante las pruebas
comprobé que hacer una petición &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto"&gt;HTTP&lt;/a&gt; incondicional
&lt;a class="footnote-reference" href="https://blog.jcea.es/posts/20240727-slashdot_verbose_e_incondicionales.html#incondicional" id="id1"&gt;[1]&lt;/a&gt; no produce un error, así que añadí esa
funcionalidad al programa.&lt;/p&gt;
&lt;table class="docutils footnote" frame="void" id="incondicional" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://blog.jcea.es/posts/20240727-slashdot_verbose_e_incondicionales.html#id1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;p class="first last"&gt;Cuando mi programa detecta una actualización del &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;
se queda con sus entradas y las siguientes peticiones no
vuelven a pedir el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; sin más, sino que envía al
servidor &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto"&gt;HTTP&lt;/a&gt; de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; lo que se llama una petición
condicional que básicamente dice: "envíame el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;
solo si ha habido algún cambio desde la versión anterior. Si
no no me mandes nada". Esto es ventajoso para todo el mundo,
pero parece que &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; falla de vez en cuando cuando en
realidad no ha habido un cambio en el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; y
simplemente tendría que haberme respondido con un código
&lt;a class="reference external" href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto"&gt;HTTP&lt;/a&gt; 304. Es así cómo debería ser y, de hecho, es lo que
hace el 99.99% del tiempo. Pero falla ese 0.01%...&lt;/p&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El parche es largo, pero fácil de entender:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.jcea.es/posts/20240727-slashdot_verbose_e_incondicionales.html"&gt;Leer más…&lt;/a&gt; (quedan 3 minutos de lectura)&lt;/p&gt;&lt;/div&gt;</description><category>Código Fuente</category><category>Feed RSS</category><category>HTTP</category><category>Python</category><category>Scraping web</category><category>Slashdot</category><guid>https://blog.jcea.es/posts/20240727-slashdot_verbose_e_incondicionales.html</guid><pubDate>Fri, 26 Jul 2024 23:49:00 GMT</pubDate></item><item><title>Mejoras a la hora de generar un email diario a partir de un feed RSS (Slashdot)</title><link>https://blog.jcea.es/posts/20200330-slashdot.html</link><dc:creator>Jesús Cea Avión</dc:creator><description>&lt;div&gt;&lt;p&gt;En &lt;a class="reference external" href="https://blog.jcea.es/posts/20191011-slashdot.html"&gt;Generar un email diario a partir de un feed RSS (Slashdot)&lt;/a&gt; publico un pequeño programa en &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Python"&gt;Python&lt;/a&gt;
para enviarme un correo electrónico diario con los artículos
publicados en &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; desde la última notificación.&lt;/p&gt;
&lt;p&gt;Este programa me ha servido bien durante años, pero había un
problema desconcertante: El programa se ejecuta por &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Cron_(Unix)"&gt;cron&lt;/a&gt; una vez
por hora y se supone que tendría que enviarme el resumen diario
entre las cuatro y cinco de la tarde. Normalmente era así, pero no
siempre. A veces el email se enviaba horas más tarde.&lt;/p&gt;
&lt;p&gt;La cosa no era grave, solo molesta, y nunca tenía tiempo para
investigarla. Cuando por fin me metí en harina, tardé un buen
ratillo en localizar el problema:&lt;/p&gt;
&lt;p&gt;El programa hace una petición &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto"&gt;HTTP&lt;/a&gt; condicional al &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; de
&lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; para no machacar el servidor y no trabajar nosotros
tampoco en vano si no ha habido cambios. La cuestión es que, si no
hay nada nuevo que procesar, el programa termina inmediatamente.
Es decir, no llega a la parte de "es hora de mandar el email". En
resumen, el correo electrónico se enviaba solo cuando era más
tarde de las 16:00 y, además, había entrado algún artículo nuevo
en el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Una vez determinada la causa última, la solución es simple:
procesar el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; aunque no haya novedades. La cosa parecía
sencilla, pero al final no lo fue tanto. No guardo el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;
original así que tengo que hacer chanchullos para que mi programa
no considere que ha perdido datos. La lógica es innecesariamente
compleja, el programa exige una refactorización urgente. Durante
el proceso detecté otro problema: si en un momento dado el
programa detecta que se han perdido datos, pero no ha llegado aún
el momento de enviar el correo electrónico, no nos enteramos.
Compliqué el código una vez más para que el estado "pérdida de
datos" sea persistente y solo se limpie cuando se ha enviado el
correo electrónico notificándolo.&lt;/p&gt;
&lt;p&gt;El resultado es un programa que funciona correctamente, pero
innecesariamente complejo y bastante frágil. Cuando se llega a
este estado urge refactorizar o, con un programa tan corto,
directamente reescribirlo por completo.&lt;/p&gt;
&lt;p&gt;Las diferencias respecto a la versión anterior son:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://blog.jcea.es/listings/slashdot-20181015-ddeb027b3f1c.diff.html"&gt;slashdot-20181015-ddeb027b3f1c.diff&lt;/a&gt;  &lt;a class="reference external" href="https://blog.jcea.es/listings/slashdot-20181015-ddeb027b3f1c.diff"&gt;(Código fuente)&lt;/a&gt;&lt;/p&gt;
&lt;pre class="code diff"&gt;&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-1"&gt;&lt;/a&gt;&lt;span class="gd"&gt;--- slashdot-20181015.py        2019-10-12 02:28:46.020075954 +0200&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-2"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+++ slashdot-ddeb027b3f1c.py    2021-02-11 20:18:38.341490217 +0100&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-3"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -1,7 +1,7 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-4"&gt;&lt;/a&gt; #!/usr/bin/env python3
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-5"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-6"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-7"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-# (c) 2018 Jesús Cea Avión - jcea@jcea.es - https://www.jcea.es/&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-8"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+# (c) 2018-2020 Jesús Cea Avión - jcea@jcea.es - https://www.jcea.es/&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-9"&gt;&lt;/a&gt; # This code is licensed under AGPLv3.
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-10"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-11"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-12"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -28,6 +28,7 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-13"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-14"&gt;&lt;/a&gt;         self._procesados = data['procesados']
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-15"&gt;&lt;/a&gt;         self._buckets = data['buckets']
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-16"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        self._solapamiento = data.get('solapamiento', True)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-17"&gt;&lt;/a&gt;         self._procesados_nuevos = set()
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-18"&gt;&lt;/a&gt;         self.etag = data['etag']
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-19"&gt;&lt;/a&gt;         self.modified = data['modified']
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-20"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -41,10 +42,14 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-21"&gt;&lt;/a&gt;             v['modified'] = self.modified
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-22"&gt;&lt;/a&gt;         return v
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-23"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-24"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    def notificado(self):&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-25"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        self._solapamiento = True&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-26"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        self.cambiado = True&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-27"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-28"&gt;&lt;/a&gt;     def solapamiento(self):
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-29"&gt;&lt;/a&gt;         if self.saved:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-30"&gt;&lt;/a&gt;             return self._solapamiento
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-31"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        return not self._procesados.isdisjoint(self._procesados_nuevos)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-32"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        return self._solapamiento and not self._procesados.isdisjoint(self._procesados_nuevos)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-33"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-34"&gt;&lt;/a&gt;     def update(self, guid, ts, title, link, summary):
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-35"&gt;&lt;/a&gt;         self._procesados_nuevos.add(guid)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-36"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -65,6 +70,10 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-37"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-38"&gt;&lt;/a&gt;         return self.update(guid, ts, title, link, summary)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-39"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-40"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    def feed304(self):&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-41"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        self._procesados_nuevos.update(self._procesados)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-42"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        return self.etag, self.modified&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-43"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-44"&gt;&lt;/a&gt;     def itera_antiguos(self, horas=16):
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-45"&gt;&lt;/a&gt;         ts_antiguo = datetime.datetime.now()
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-46"&gt;&lt;/a&gt;         ts_antiguo -= datetime.timedelta(seconds=horas * 60 * 60)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-47"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -99,6 +108,8 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-48"&gt;&lt;/a&gt;         if not self.cambiado:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-49"&gt;&lt;/a&gt;             return
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-50"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-51"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        self._solapamiento = self.solapamiento()&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-52"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-53"&gt;&lt;/a&gt;         procesados = self._procesados_nuevos
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-54"&gt;&lt;/a&gt;         if not purga:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-55"&gt;&lt;/a&gt;             procesados.update(self._procesados)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-56"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -106,6 +117,7 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-57"&gt;&lt;/a&gt;         with open(path + '.NEW', 'wb') as f:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-58"&gt;&lt;/a&gt;             data = {'procesados': procesados,
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-59"&gt;&lt;/a&gt;                     'buckets': self._buckets,
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-60"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+                    'solapamiento': self._solapamiento,&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-61"&gt;&lt;/a&gt;                     'etag': etag, 'modified': modified,
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-62"&gt;&lt;/a&gt;                     }
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-63"&gt;&lt;/a&gt;             pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-64"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -115,7 +127,6 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-65"&gt;&lt;/a&gt;         self.cambiado = False
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-66"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-67"&gt;&lt;/a&gt;         # Tras el save no deberíamos usar más este objeto
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-68"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        self._solapamiento = self.solapamiento()&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-69"&gt;&lt;/a&gt;         self.saved = True
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-70"&gt;&lt;/a&gt;         del self._procesados
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-71"&gt;&lt;/a&gt;         del self._procesados_nuevos
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-72"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -136,20 +147,21 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-73"&gt;&lt;/a&gt;     finally:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-74"&gt;&lt;/a&gt;         socket.setdefaulttimeout(timeout)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-75"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-76"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-    if feed.status == 304:&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-77"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        # print("Sin cambios")&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-78"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        return&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-79"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-80"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-    for entry in feed.entries:&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-81"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        summary = BeautifulSoup(entry["summary"], 'html.parser')&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-82"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        summary = list(summary.children)[0]&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-83"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        entradas.new(entry['id'], entry.updated_parsed,&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-84"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-                     entry['title'], entry['link'], summary)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-85"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-86"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-    no_solapamiento = ''&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-87"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-    if not entradas.solapamiento():&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-88"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        no_solapamiento = '&amp;lt;font size=+2&amp;gt;&amp;lt;b&amp;gt;HEMOS PERDIDO ENTRADAS&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;'&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-89"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-        no_solapamiento += '&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;\n'&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-90"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    if feed.status == 200:&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-91"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        etag, modified = feed.get('etag'), feed.get('modified')&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-92"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        for entry in feed.entries:&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-93"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+            summary = BeautifulSoup(entry["summary"], 'html.parser')&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-94"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+            summary = list(summary.children)[0]&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-95"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+            entradas.new(entry['id'], entry.updated_parsed,&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-96"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+                         entry['title'], entry['link'], summary)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-97"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    elif feed.status == 304:&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-98"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        # En el 304 no se manda ETAG ni Modified, así que coge los que comprobamos&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-99"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        # en la petición condicional.&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-100"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        etag, modified = entradas.feed304()&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-101"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    else:&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-102"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        raise RuntimeError(f'Estado: {feed.status}')&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-103"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-104"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    # Si es 304, procesamos lo viejo por si tiene que salir el email.&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-105"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-106"&gt;&lt;/a&gt;     html_desc = html_links = ''
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-107"&gt;&lt;/a&gt;     entradas_a_borrar = []
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-108"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -164,9 +176,14 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-109"&gt;&lt;/a&gt;     # lo cierto es que queremos actualizar el etag y el modified AUNQUE
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-110"&gt;&lt;/a&gt;     # no haya habido cambios en el feed. Esto es algo a mejorar en el futuro.
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-111"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-112"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-    entradas.save(feed.get('etag'), feed.get('modified'))&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-113"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    entradas.save(etag, modified)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-114"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-115"&gt;&lt;/a&gt;     if html_desc:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-116"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        no_solapamiento = ''&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-117"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        if not entradas.solapamiento():&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-118"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+            no_solapamiento = '&amp;lt;font size=+2&amp;gt;&amp;lt;b&amp;gt;HEMOS PERDIDO ENTRADAS&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;'&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-119"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+            no_solapamiento += '&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;\n'&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-120"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-121"&gt;&lt;/a&gt;         html_links = f'&amp;lt;ul&amp;gt;\n{html_links}\n&amp;lt;/ul&amp;gt;\n'
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-122"&gt;&lt;/a&gt;         html = f'''&amp;lt;html&amp;gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-123"&gt;&lt;/a&gt; &amp;lt;head&amp;gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-124"&gt;&lt;/a&gt;&lt;span class="gu"&gt;@@ -196,14 +213,17 @@&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-125"&gt;&lt;/a&gt;                        stdout=subprocess.PIPE,
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-126"&gt;&lt;/a&gt;                        stderr=subprocess.PIPE)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-127"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-128"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+        entradas.notificado()&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-129"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-130"&gt;&lt;/a&gt;     # XXX: Lo suyo sería grabar solo si hay cambios de verdad, pero
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-131"&gt;&lt;/a&gt;     # lo cierto es que queremos actualizar el etag y el modified AUNQUE
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-132"&gt;&lt;/a&gt;     # no haya habido cambios en el feed. Esto es algo a mejorar en el futuro.
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-133"&gt;&lt;/a&gt;     entradas = items()
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-134"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    etag, modified = entradas.feed304()  # XXX: Para evitar "no solapamiento"&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-135"&gt;&lt;/a&gt;     for guid, ts in entradas_a_borrar:
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-136"&gt;&lt;/a&gt;         entradas.borrar(guid, ts)
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-137"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-138"&gt;&lt;/a&gt;&lt;span class="gd"&gt;-    entradas.save(feed.get('etag'), feed.get('modified'), purga=False)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-139"&gt;&lt;/a&gt;&lt;span class="gi"&gt;+    entradas.save(etag, modified, purga=False)&lt;/span&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-140"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-141"&gt;&lt;/a&gt;
&lt;a name="rest_code_dc66038f25654613a8e9f88cd161359e-142"&gt;&lt;/a&gt; if __name__ == '__main__':
&lt;/pre&gt;&lt;p&gt;El código completo es el siguiente:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.jcea.es/posts/20200330-slashdot.html"&gt;Leer más…&lt;/a&gt; (quedan 5 minutos de lectura)&lt;/p&gt;&lt;/div&gt;</description><category>Código Fuente</category><category>Feed RSS</category><category>Privacidad</category><category>Python</category><category>Scraping web</category><category>Slashdot</category><guid>https://blog.jcea.es/posts/20200330-slashdot.html</guid><pubDate>Mon, 30 Mar 2020 16:52:00 GMT</pubDate></item><item><title>Generar un email diario a partir de un feed RSS (Slashdot)</title><link>https://blog.jcea.es/posts/20191011-slashdot.html</link><dc:creator>Jesús Cea Avión</dc:creator><description>&lt;div&gt;&lt;p&gt;Llevo décadas, literalmente, siguiendo &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt;. Es una de mis
fuentes de noticias de revisión diaria obligada. Se puede seguir a
través del &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;, pero el tráfico diario es alto y es fácil
perder noticias si pasas un par de días desconectado, porque, por
ejemplo, estás de vacaciones o de viaje. Afortunadamente,
&lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; proporciona la opción de recibir un único mensaje diario
con la recopilación de noticias del día anterior. Esto es muy
conveniente para leerlas cuando te va bien, no preocuparte de
perder noticias y, además, poder archivarlas en tu sistema de
correo para futuras búsquedas o referencias.&lt;/p&gt;
&lt;p&gt;Todo fue bien hasta el 23 de septiembre de 2018. Ese día me
dejaron de llegar los mensajes diarios de noticias sin ningún
motivo aparente &lt;a class="footnote-reference" href="https://blog.jcea.es/posts/20191011-slashdot.html#vuelve-a-funcionar" id="id1"&gt;[1]&lt;/a&gt;. Curiosamente, sí me
llegaban los emails de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; sobre promociones y publicidad.&lt;/p&gt;
&lt;p&gt;Revisé mi configuración &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Antispam"&gt;antispam&lt;/a&gt;, por si acaso mi servidor de
correo electrónico se estaba comiendo los mensajes, los &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Log_(inform%C3%A1tica)"&gt;logs&lt;/a&gt; del
servidor, etc., sin éxito. Preguntar online y revisar foros para
ver si alguien más tenía problemas fue infructuoso. Intentar
ponerme en contacto con la gente de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; fue imposible.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt;, sencillamente, ya no me enviaba los mensajes.&lt;/p&gt;
&lt;p&gt;Durante un tiempo aguanté leyendo el &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt; de forma diaria,
pero los fines de semana eran problemáticos porque tenía apagado
el ordenador un par de días y algunas noticias ya no entraban en
el último &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Rss"&gt;&lt;em&gt;feed&lt;/em&gt; RSS&lt;/a&gt;. Los lunes tocaba revisar noticias a mano.&lt;/p&gt;
&lt;p&gt;Tras pensarlo un poco y evaluar el coste/beneficio, decidí crear
un programa simple y mínimo para reemplazar el email de noticias
que me enviaba &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; a diario. El programa me costó una tarde
de trabajo y ha estado funcionando desde entonces sin ningún tipo
de problema.&lt;/p&gt;
&lt;table class="docutils footnote" frame="void" id="vuelve-a-funcionar" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label"&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="https://blog.jcea.es/posts/20191011-slashdot.html#id1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt; empezó a enviarme las noticias diarias de nuevo el 6
de abril de 2019. Así, sin más. ¿Qué ocurrió? Imagino que nunca
lo sabré. La cuestión es que ya tenía este reemplazo
funcionando durante meses, sin publicidad, sin depender de
terceros, sin comprometer mi privacidad...&lt;/p&gt;
&lt;p class="last"&gt;Sigo suscrito a las notificaciones diarias de &lt;a class="reference external" href="https://es.wikipedia.org/wiki/Slashdot"&gt;Slashdot&lt;/a&gt;, pero
ya ni siquiera abro esos emails. Tengo las mías y son mejores.&lt;/p&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;El código completo es el siguiente:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.jcea.es/posts/20191011-slashdot.html"&gt;Leer más…&lt;/a&gt; (quedan 17 minutos de lectura)&lt;/p&gt;&lt;/div&gt;</description><category>Código Fuente</category><category>Feed RSS</category><category>Privacidad</category><category>Python</category><category>Scraping web</category><category>Slashdot</category><guid>https://blog.jcea.es/posts/20191011-slashdot.html</guid><pubDate>Thu, 10 Oct 2019 23:43:00 GMT</pubDate></item></channel></rss>