lunes, enero 29, 2007

Exceso de información, filtros bayesianos y DIY

Desde que leí esta entrada de manje, "Selección de noticias mediante filtro bayesiano" en la que presentaba su proyecto, lo pensé... ¿por que nadie ha hecho un lector al que lo entrenes y te filtre según lo has entrenado? Bueno, manje lo tiene hecho y se pude probar (es TU PERIODICO)

No estoy seguro de que un filtrado automático sea la solución al exceso de fuentes de información, además de tenerlo que entrenar, con la inversión en esfuerzo que eso supone. Pero a este respecto manje y los usuarios de TU PERIODICO nos pueden comentar más. Yo a nivel personal si que digo que me ha resultado más entretenido programar un miniscrí que entrenar el filtro y que sigo usando bloglines a pesar de haber alternativas seguramente mejores...

En fin, como me interesó la idea, al menos un rato :), pues me puse a hacerlo yo mismo... porqué no, ¿verdad? Total que me hice un script en Ruby, porque me apetecía, porque yo lo valgo y para ser buzzword compliant :) (No sé, a otros les ha servido de algo :)). Como no tengo ningún tiempo de seguir su desarrollo (y tampoco se para que...) lo pongo por aquí por si le sirve a alguien, aunque sea solo de mal ejemplo. Aviso que está hecho con la velocidad y el descuido de la pasión, con lo que no es de calidad de producción ni en la forma ni en la organización.

Permite leer feed por feed en modo interactivo o todo seguido y se va guardando las fechas de última lectura de las fuentes, con lo que en teoría debería mostrar solo los ítems nuevos. En la práctica, sospecho que a mitad por el parser de feeds que he usado, Ruby feedparser, a mitad por que los que los forman no tienen mucho cuidado (pero no lo he analizado en serio) hay fuentes que salen entradas ya leídas. El escrí permite ir llenando su "base de datos" de fuentes o importarlas de bloglines. Permite, claro, entrenarlo (con Classifier, que se instala a través de rubygems) y permite que la salida sea en modo texto, para esos frikis como yo a los que nos encanta la consolica, y claro, para todos aquellos que quieran procesar la salida...

Después de estos preámbulos, ahí va:


require 'rubygems'
require 'stemmer'
require 'classifier'
require 'yaml'

class SimpleFeed
attr_reader :url, :date
attr_writer :date
def initialize(url, date)
@url = url
@date = date
end
end

def serialize( c, name )
File.open( name, 'w' ) do |out|
YAML.dump( c, out )
end
end

require 'net/http'
require 'uri'

def fetch(uri_str, date, limit = 10)
#TODO: I should choose better exception.
raise ArgumentError, 'HTTP redirect too deep' if limit == 0

header={'If-Modified-Since' => date.to_s}
uri=URI.parse(uri_str)
http = Net::HTTP.new(uri.host, uri.port)
response=http.get(uri.path,header)
case response
when Net::HTTPSuccess then response
when Net::HTTPRedirection then fetch(response['location'], limit - 1)
else
puts "error getting " + uri_str + response
STDIN.gets()
response.error!
end
end

#const
Filter_file = "filter.yaml"
Feeds_file = "feeds.yaml"


#default options
add_feed = false
train = false
html_content = false
no_interactive = false
bloglines_import = false
bloglines_file = ''


ARGV.each_with_index do |arg,index|
case arg
when '--add_feed'
add_feed = true
when '--train'
train = true
when '--html_content'
html_content = true
when '--no_interactive'
no_interactive = true
when '--bloglines_import'
bloglines_import = true
bloglines_file = ARGV[index+1]
puts bloglines_file
end

end

#load filter
begin
filter =YAML::load_file( Filter_file )
rescue SystemCallError
filter= Classifier::Bayes.new 'Interesting', 'Uninteresting'
end


#load feeds
feeds = Array.new
if(bloglines_import == false)
begin
feeds = YAML::load_file(Feeds_file)
rescue SystemCallError
end
else
require 'rexml/document'
include REXML
file = File.new(bloglines_file)
doc = Document.new(file)
doc.root.each_element('body/outline/outline'){
|outline| feeds.push(SimpleFeed.new(outline.attributes['xmlUrl'],Time.at(0)))
}
end

if(add_feed == true)
print ('Insert a feed, please: ')
f=SimpleFeed.new(STDIN.gets().chop!,Time.at(0))
feeds.push(f)
end


require 'feedparser'
require 'feedparser/html2text-parser'

#iterate the feeds
feeds.each{ |sf|
puts 'Loading '+sf.url + ' ...'
begin
res=fetch(sf.url,sf.date)
s= res.body
rescue
puts "Error getting "+ sf.url
end

begin
#puts res['Date']
mod_date = res['Last-Modified']
puts mod_date
#URI::parse(sf.url), header
rescue
puts res
puts "Error: Web server doesn't implement date: "+ sf.url
end

begin
f = FeedParser::Feed::new(s)
if( f != nil and f.title!=nil )
puts(f.title)
else
puts("Title not available")
end
rescue
puts "Error parsing feed, Not valid: " + sf.url + "\n" +s
end
#Feeds are usually in temporal reverse order
if(f!=nil and f.items!=nil)
f.items.reverse_each {
|i|
#title, date, content
begin
if((i.date ==nil or sf.date==nil or i.date>sf.date) )
if(html_content == false and i.content != nil)

p = FeedParser::HTML2TextParser::new(true)
p.feed(i.content)
p.close
i.content = p.savedata
end
#TODO: -> content NULL
entry = ''
if i.title
entry+=i.title + " "# + i.date.to_s
end
if(i.content != nil)
entry+=i.content
end
puts(entry)
#puts ("Most recent date: " +sf.url+ " "+ i.date.to_s)
if(i.date!=nil)
sf.date=i.date
else
if (mod_date !=nil)
#If mod_date exists
sf.date=mod_date
end
end

if(train)
print(' (Interesting[Y/n]?)')
ans=STDIN.gets().chop!
print(ans)
if ans=='n'
filter.train_uninteresting(entry)
else
filter.train_interesting(entry)
end
else

puts(filter.classify(entry))# returns '[Un]interesting'
if(no_interactive==false)
STDIN.gets()
end
end
end
rescue
puts "Unknown Error"
end

}
end
}
serialize(filter, Filter_file)
serialize(feeds, Feeds_file)



La misma entrada en BP