<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Ya he aprendido otra cosa :D &#187; Web scraping</title>
	<atom:link href="http://www.aureoares.es/category/programacion/web-scraping/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.aureoares.es</link>
	<description>por Áureo Ares</description>
	<lastBuildDate>Thu, 17 Jul 2014 15:48:10 +0000</lastBuildDate>
	<language>es-ES</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
		<item>
		<title>Filtros Bloom escalables.</title>
		<link>http://www.aureoares.es/filtros-bloom-escalables/</link>
		<comments>http://www.aureoares.es/filtros-bloom-escalables/#comments</comments>
		<pubDate>Mon, 07 Apr 2014 18:51:32 +0000</pubDate>
		<dc:creator><![CDATA[Áureo Ares]]></dc:creator>
				<category><![CDATA[Programación]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Web scraping]]></category>

		<guid isPermaLink="false">http://www.aureoares.es/?p=90</guid>
		<description><![CDATA[Cómo utilizar filtros Bloom escalables con Python. Funcionamiento y ejemplo de código.]]></description>
				<content:encoded><![CDATA[<p>Hace dos semanas publiqué un <a title="Filtros Bloom en web scraping." href="http://www.aureoares.es/filtros-bloom-en-web-scraping/">ejemplo de filtro Bloom básico</a>. En mis aventuras con web scraping el que realmente uso es una versión escalable ya que suele ser muy difícil estimar el número máximo de enlaces que voy a necesitar almacenar.</p>
<p>No existe (que yo sepa) un modo de aumentar el tamaño del array de bits de un filtro Bloom una vez que ya se han empezado a añadir elementos. El modo de implementar un filtro escalable no es ni más ni menos que utilizar más de un filtro a la vez.<span id="more-90"></span></p>
<p>Para ello, cada vez que un filtro se llena se crea uno nuevo de mayor capacidad. Los nuevos elementos se van añadiendo al último filtro creado y a la hora de buscar un elemento se busca en todos uno por uno.</p>
<p>La base del filtro escalable es por tanto muy sencilla. La mayor complicación sería escoger el modo de escalar la capacidad de los filtros, es decir, cómo de grande debe ser el nuevo en comparación con el anterior. En su momento me vino a la cabeza la posibilidad de que todos tengan el mismo tamaño e incluso de que fuesen cada vez más pequeños, pero hasta la fecha no se me ha ocurrido ningún extraño caso en el que pueda resultar útil.</p>
<p>Lo normal es utilizar una función lineal o exponencial. Yo he optado por permitir ambas ya que no era ningún esfuerzo pero en la práctica siempre me ha venido mejor la exponencial. Los cálculos son muy sencillos:</p><pre class="crayon-plain-tag"># initial_capacity = capacidad del primer filtro
# scale_factor = factor de aumento
# filters = array de filtros, len(filters) es el n&uacute;mero de filtros que ya haya
# Funci&oacute;n lineal:
capacity = int(initial_capacity * (scale_factor * len(filters)))
# Funci&oacute;n exponencial:
capacity = int(initial_capacity * (scale_factor ** len(filters)))</pre><p></p>
<p>&nbsp;</p>
<p>Añadir y buscar elementos también es muy sencillo:</p><pre class="crayon-plain-tag">def add(element):
    if filters[-1].is_full():
        # En el ejemplo del art&iacute;culo anterior, el filtro se creaba pasando como par&aacute;metros la capacidad y el margen de error.
        filters.append(BloomFilter(calc_next_capacity(), error_rate))
    filters[-1].add(element)
#
def lookup(string):
    for f in reversed(filters):
        if f.lookup(string): return True
    return False</pre><p>Lo único destacable es que al buscar es más eficiente recorrer los filtros en orden inverso (del último al primero), ya que de media se tardará menos en encontrar el elemento. Aunque en caso de no encontrarse la búsqueda tardará lo mismo se haga en un sentido u otro.</p>
<p>El código completo comentado se puede ver en <a title="Código completo en Google Code" href="https://code.google.com/p/python-scalable-bloom-filter/source/browse/" target="_blank">Google Code</a>.</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.aureoares.es/filtros-bloom-escalables/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Filtros Bloom en web scraping.</title>
		<link>http://www.aureoares.es/filtros-bloom-en-web-scraping/</link>
		<comments>http://www.aureoares.es/filtros-bloom-en-web-scraping/#comments</comments>
		<pubDate>Mon, 24 Mar 2014 12:37:32 +0000</pubDate>
		<dc:creator><![CDATA[Áureo Ares]]></dc:creator>
				<category><![CDATA[Programación]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Web scraping]]></category>

		<guid isPermaLink="false">http://www.aureoares.es/?p=24</guid>
		<description><![CDATA[Cómo utilizar filtros Bloom el Python, orientado al web scraping. Funcionamiento y ejemplo de código.]]></description>
				<content:encoded><![CDATA[<p>En mis aventuras con <a title="Definición de web scraping en Wikipedia" href="http://es.wikipedia.org/wiki/Web_scraping" target="_blank">web scraping</a> me encontré con un problema muy común al detectar automáticamente las URLs:</p>
<p>Cuando sabes lo que buscas es muy fácil fabricar un listado de URLs que necesitas revisar. Puede incluso ser una sola URL. Sin embargo, cuando necesitas ir detectando sobre la marcha nuevas URLs tienes que guardar un registro de las que ya has visitado. De lo contrario te encontrarás visitando las mismas páginas enlazadas entre sí una y otra vez hasta el fin de los tiempos.<span id="more-24"></span></p>
<p>Si sabemos que serán pocas las URLs a visitar, esto no es un gran problema. Simplemente nos guardamos una lista de las que vamos visitando y nos aseguramos de no repetir. El problema está cuando nos enfrentamos a sitios web muy grandes de los que necesitamos revisar una gran cantidad de páginas. Entonces eso de tener la lista de URLs en memoria se convierte rápidamente en una mala idea.</p>
<p>En estos casos, si las URLs en sí son relevantes (es decir, si cada URL es un dato que necesitas guardar por alguna otra razón) seguramente necesites una base de datos. Pero si lo único que necesitas saber es si ya has visitado antes una URL, los filtros Bloom son una opción increíblemente eficiente y rápida.</p>
<h2>Bonita historia, pero ¿qué es un filtro Bloom?</h2>
<p>No lo voy a explicar en profundidad ya que aparece muy bien explicado en la <a title="Definición de filtro Bloom en Wikipedia (en inglés)" href="http://en.wikipedia.org/wiki/Bloom_filter" target="_blank">Wikipedia (inglés)</a>, incluyendo muchas referencias interesantes al final del artículo. De modo que comentaré lo más importante.</p>
<p>Las características principales de los filtros Bloom son las siguientes:</p>
<ul>
<li>Tiene un cierto <strong>margen de error</strong> (falsos positivos), calculable y ajustable.</li>
<li>Al buscar un elemento, un resultado negativo significa que con toda seguridad <strong>no está</strong> en la lista.</li>
<li>Un resultado positivo significa que un elemento <strong>probablemente está</strong> en la lista (debido a la posibilidad de falsos positivos).</li>
<li><span style="line-height: 1.5em;">Requiere muy <strong>poco espacio</strong> en memoria y las consultas son muy rápidas.</span></li>
</ul>
<p>El funcionamiento es muy sencillo, para implementarlo necesitaremos:</p>
<ul>
<li>Encontrar una implementación ya realizada que nos guste (nos parezca apropiada) y podamos utilizar (licencia), en cuyo caso ya habremos terminado :D.</li>
</ul>
<p>o</p>
<ul>
<li>Una lista de elementos binarios (bit array).</li>
<li>Una o más funciones de hashing.</li>
</ul>
<p>En mi caso necesitaba una implementación en Python ya que es el lenguaje que suelo utilizar para las tareas de scraping. La implementación de <a title="Pybloom en GitHub" href="https://pypi.python.org/pypi/pybloom/1.0.2" target="_blank">pybloom</a> la verdad es que no me gustó nada y no encontré ninguna otra que me sirviese. De modo que opté por hacerme la mía.</p>
<p>La elección de la <strong>función de hashing</strong> es muy importante. El requisito indispensable es que sea <strong>uniforme</strong> (todos los posibles valores tienen la misma probabilidad de aparecer como resultado) y <strong>determinista</strong> (un mismo valor de entrada genera siempre el mismo valor de resultado). También es importante, aunque no imprescindible, que sea <strong>no criptográfica</strong>. Las funciones de hashing criptográficas están muy bien para otros usos, pero son más lentas (este es uno de los principales inconvenientes que le veo a pybloom). No soy ningún experto en hashing, pero leyendo un poco la que más me convence por el momento es <a title="MurmurHash3" href="https://pypi.python.org/pypi/mmh3" target="_blank">MurmurHash3</a>.</p>
<p>Resumiendo, para la implementación en Python utilicé lo siguiente:</p>
<p></p><pre class="crayon-plain-tag"># Requiere instalar previamente bitarray y mmh3 (murmurhash3)
# sudo pip install bitarray
# sudo pip install mmh3

from bitarray import bitarray
import mmh3
from math import log, ceil</pre><p></p>
<h2>Añadir un elemento al filtro:</h2>
<p>Para añadir un elemento realizaremos varios hashes del mismo. Se pueden utilizar distintas funciones de hashing o una sola función con diferentes semillas. Teniendo ya una función que me gusta y sin saber cuántos hashes diferentes voy a necesitar, me parece más lógico utilizar una sola con diferentes semillas:</p>
<p></p><pre class="crayon-plain-tag"># hash_count = n&uacute;mero de hashes a realizar.
# size = tama&ntilde;o del bitarray.
for seed in xrange(hash_count):
	position = mmh3.hash(element, seed) % size
	bit_array[position] = 1
element_count += 1</pre><p></p>
<p>Los hashes mmh3 son numéricos, de modo que se pueden ajustar al tamaño del bitarray calculando el &#8220;módulo&#8221; (resto de división) con el operador &#8220;%&#8221;. El resultado es la posición del bitarray que marcamos a 1.</p>
<h2>Buscar un elemento en el filtro:</h2>
<p>Buscar un elemento en el filtro es muy parecido, simplemente comprobamos las posiciones que corresponderían al elemento que estamos buscando.</p>
<p></p><pre class="crayon-plain-tag"># hash_count = n&uacute;mero de hashes a realizar.
# size = tama&ntilde;o del bitarray.
for seed in xrange(self.hash_count):
	position = mmh3.hash(element, seed) % self.size
	if self.bit_array[position] == 0:
		return False
return True</pre><p></p>
<div style="width: 659px" class="wp-caption aligncenter"><a href="http://www.aureoares.es/wp-content/uploads/bloom_filter.png"><img class="size-full " src="http://www.aureoares.es/wp-content/uploads/bloom_filter.png" alt="Ejemplo de filtro Bloom." width="649" height="233" /></a><p class="wp-caption-text">Imagen extraída de la Wikipedia, donde se muestra la idea básica de un filtro Bloom utilizando 3 hashes para cada elemento.</p></div>
<h2>El tamaño del bitarray y el número de hashes:</h2>
<p>La mayoría de implementaciones que encontré eran clases cuyo constructor recibía como parámetros el tamaño del bitarray y el número de hashes a utilizar. Aunque se puede utilizar de esta manera, en la práctica creo que es mucho más cómodo especificar lo que realmente me importa: el número de elementos que quiero guardar y el margen de error que estoy dispuesto a permitir.</p>
<p>Para esto es necesario calcular el tamaño del bitarray y el número de hashes necesarios para poder almacenar el número de elementos que queremos con un margen de error menor o igual al especificado. Las fórmulas y su explicación se pueden ver también en la Wikipedia, pero escritas en Python serían algo así:</p>
<p></p><pre class="crayon-plain-tag"># capacity = n&uacute;mero de elementos a guardar.
# error_rate = tasa de error, entre 0 (0%) y 1 (100%). Por ejemplo, 0.01 ser&iacute;a un 1%.
def calc_size():
	return int(ceil(- (float(capacity) * log(float(error_rate))) / (log(2))**2))

# Para calcular el n&uacute;mero de hashes tenemos que haber calculado primero el tama&ntilde;o del bitarray.
# size = tama&ntilde;o del bitarray.
def calc_hash_count():
	return int(ceil((float(size) / float(capacity)) * log(2)))</pre><p></p>
<p>Utilizo la función &#8220;ceil&#8221; para asegurarme de que se cumplen los requisitos (redondeando hacia arriba).</p>
<h2>Uniones e intersecciones:</h2>
<p>Algo que no he visto hasta la fecha en ninguna implementación de filtros Bloom, al menos en Python, son las operaciones de unión e intersección entre dos filtros (desde el punto de vista de teoría de conjuntos). Son increíblemente fáciles de implementar y en concreto la unión de filtros me ha resultado bastante útil.</p>
<p>Las intersecciones también pueden ser útiles, pero en mi caso (pocos o ningún elemento en común) los resultados no me han parecido lo bastante fiables.</p>
<p>Más abajo en el código final de la clase se puede ver la implementación de estas dos operaciones, pero es tan sencillo como realizar las operaciones a nivel de bit AND (&amp;) y OR (|) entre los dos bitarray.</p>
<h2>Mi implementación:</h2>
<p>Esta es la clase que me hice. No es la que uso actualmente ya que más tarde hice un filtro Bloom escalable, más avanzado, que detallaré en otro artículo ya que este me está quedando más grande de lo que esperaba.</p>
<p>Los filtros Bloom tienen una infinidad de utilidades. Cabe destacar que esta clase fue creada específicamente para cubrir mis necesidades, por lo que hay algunas decisiones de implementación (como permitir añadir más elementos aunque el filtro esté al máximo de capacidad o devolver un valor vacío si no se puede realizar una unión) que pueden no ser buenas en otros contextos.</p>
<p></p><pre class="crayon-plain-tag">#!/usr/bin/python
# -*- coding: utf-8 -*-
#

# Requires bitarray and mmh3 (murmurhash3)
# sudo pip install bitarray
# sudo pip install mmh3

from bitarray import bitarray
import mmh3
from math import log, ceil

class BloomFilter:

        def __init__(self, capacity, error_rate):
                if not capacity &gt; 0: raise ValueError(&quot;capacity must be &gt; 0&quot;)
                if not (0 &lt; error_rate &lt; 1): raise ValueError(&quot;error_rate must be between 0 and 1.&quot;)
capacity.
                self.capacity = capacity
                self.element_count = 0
                self.error_rate = error_rate
                self.size = self.calc_size()
                self.hash_count = self.calc_hash_count()
                self.bit_array = bitarray(self.size)
                self.bit_array.setall(0)

        def add(self, element):
                for seed in xrange(self.hash_count):
                        position = mmh3.hash(element, seed) % self.size
                        self.bit_array[position] = 1
                self.element_count += 1

        def lookup(self, element):
                for seed in xrange(self.hash_count):
                        position = mmh3.hash(element, seed) % self.size
                        if self.bit_array[position] == 0:
                                return False
                return True

        def union(self, b):
                if self.size != b.size: return None
                if self.hash_count != b.hash_count: return None
                result = BloomFilter(self.capacity, self.error_rate)
                result.bit_array = self.bit_array | b.bit_array
                result.element_count = self.element_count + b.element_count
                return result

        def intersection(self, b):
                if self.size != b.size: return None
                if self.hash_count != b.hash_count: return None
                result = BloomFilter(self.capacity, self.error_rate)
                result.bit_array = self.bit_array &amp; b.bit_array
                result.element_count = result.calc_element_count()
                return result

        def is_full(self):
                if self.element_count &lt; self.capacity: return False
                else: return True

        def calc_size(self):
                return int(ceil(- (float(self.capacity) * log(float(self.error_rate))) / (log(2))**2))

        def calc_hash_count(self):
                return int(ceil((float(self.size) / float(self.capacity)) * log(2)))

        def calc_error_rate(self, use_capacity = False):
                if use_capacity: n = float(self.capacity)
                else: n = float(self.element_count)
                return (1.0 - (1.0 - 1.0 / float(self.size)) ** (float(self.hash_count) * n)) ** float(self.hash_count)

        def calc_element_count(self):
                x = float(self.bit_array.count())
                return int(ceil(- (float(self.size) * log(1.0 - (x / float(self.size)))) / float(self.hash_count)))

        def __contains__(self, string):
                return self.lookup(string)</pre><p></p>
<p>El código completo comentado, incluyendo la versión escalable, está alojado en <a title="Código completo del filtro Bloom en Google Code" href="https://code.google.com/p/python-scalable-bloom-filter/source/browse/" target="_blank">Google Code</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.aureoares.es/filtros-bloom-en-web-scraping/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
