Crawling данных

Для миграции legacy данных потребовалось написать небольшой crawler.

В качестве парсера был выбран nokogiri, а для получения файла обычная open-uri библиотека.

Оснавная проблема с которой столкнулся это неправильная работа парсера с кодировками, отличными от utf-8. После недолгих поисков и изучения проблемы, было решено получать страницу вручную, конвертировать с помощью iconv, а потом уже передавать это дело парсеру.

Итак к самой задаче. Есть каталог изделий распределенный по категориям. Сначала нужно получить список всех ссылок, далее получить более детальную информацию о конкретном товаре и составить список аттрибутов товара:


require 'rubygems'
require 'nokogiri'
require 'open-uri'

class ItemsCrawler

def crawl
results = []
get_categories_urls.each do |url|
data = extract_data(url)
results << data if data
end
results
end

private
def get_categories_urls
links = []
doc = Nokogiri::HTML(open("#{HOST}/catalogue"))
doc.css('div.list a').each do |link|
links << link['href'] if link['href'].include?('htm')
end
links
end

def extract_data(url)
puts "Getting - #{HOST+url}"
html = open(HOST+url).read
html = Iconv.iconv('utf-8', 'cp1251', html)

# Нужно заменить, иначе nokogiri его трактует как windows-1251
doc = Nokogiri::HTML(html[0].gsub('charset=windows-1251"', 'charset=utf-8"'))

# Далее получение аттрибутов с помощью xpath или css селектора

title = doc.search('title').first.contet
description = doc.search('div.content').first.inner_html # Получение содержания со всеми тегами
{:title => title, :description => description}
end


Вот в принципе и все, получился довольно простой парсер.

Просмотр request в rails

Появилась необходимость просмотреть body запроса который приходит в rails приложение. Для этого подключил rack.

Стартуем сервер:

Rack::Handler::WEBrick.run
lambda { |env| raise env['rack.input'].read.inspect }, :Port => 1234

Тестируем:

curl --header "Content-Type: text/xml" --data @aaa.xml http://localhost:1234/



Больше ресурсов о rack:
http://remi.org/2009/02/19/rack-basics.html

Определение страны по ip адресу

Довольно простое и быстрое решение геолокации.


=> require 'open-uri'
>> []
=> open("http://api.wipmania.com/#{ip_address}?yoursite.com").read
>> "LV"


Дополнительная информация тут.

Можно также все это дело реализовать у себя. wipmania предоставляет свою базу как в sql, cidr так и в txt формате.

Анализ текстовых докуметов. Получение ключевых слов.

Появилась интересная задачка по вычленению ключевых слов из текстового документа. Необходимо разработать такой модуль, который на входе получает текстовой файл, а на выходе выдает ключевые слова которые могли бы дать представление о чем идет речь в документе, с помощью ключевых слов.

К примеру, есть статья о хокейном матче между динамо рига - динамо москва, ключевыми словами должны быть: хоккей, динамо рига, динамо москва, матч, чемпионат.

Существует несколько алгоритмов, основанных на алгоритмах обработки естественного языка, но они довольно сложные и не укладываются в временные рамки. К тому же алгоритмы для каждого языка отличаются.

Поэтому было решено для начала разработать такой алгоритм только для анлг языка, на основании описанного тут алгоритма.

Алгоритм включает следующие шаги:

0. Определение языка

1. Stemming, на основании алгоритма Потера

2. Построение frequency таблицы

3. Кластеризация часты термов

4. Вычисление совстречаемости термов

5. Вычисление хи-квадрата для термов

6. Выбор терма с наибольшим значением хи-квадрата - это и будет ключевое слово


Далее каждый шаг рассматривается детально.

0. Для определения языка можно поставить гем: whatlanguage. Работает он предельно просто:
>> "Je suis un homme".language
=> :french

1. Для стеминга существует несколько алгоритвом реализованных в руби
* Номер раз
* Номер два

Второй вариант написан на C и автор утверждает что он быстрее. Использовать это очень просто:

$ script/console
=> 'running'.stem
>> 'run'

2. Построение таблицы частот

Тут я реализовал простенький алгоритм, в дальнейшем его планирую переписать на с:


class FrequencyHash < Hash

def occur(key)
if include?(key)
store(key, self[key] + 1)
else
store(key, 1)
end
end

def frequent_keys(min_frequency)
self.keys.reject{|key| self[key] < min_frequency}.sort{|a,b| self[b] <=> self[a] && a <=> b}
end

end

require 'stemmer'
require 'whatlanguage'

module KeywordsHelper
MIN_LENGHT = 3

def get_frequences(full_text)
words = FrequencyHash.new
full_text.split(/\W/).each do |word|
word = word.stem
words.occur(word) if word.length >= MIN_LENGHT
end
words.frequent_keys(2)
end

end


Далее переходим к шагу 3...

To be continued...

Блокировка пользователей по ip адресу

Не так давно на один из моих сайтов стали проведить DDoS атаки, отсылая с разных ip адресов запросы на однин и тот же адрес. Количество запросов не такое большое, но сам action довольно тяжеловесный и занимает от 5 до 30 секунд.

Собственно поэтому и решил написать небольшую надстройку, для занесения нежелательных ip адресов в black-list.

У всех запросов была одна закономерность, запрос вываливался при валидации токена формы verify_authenticity_token. Поэтому и было решено начать оттуда.

Первое что было сделано, это логгирование "плохих" запросов (config/initializers/forgery_protection.rb):


module ActionController
module RequestForgeryProtection

def verify_authenticity_token
verified_request? || block_user! || raise(ActionController::InvalidAuthenticityToken)
end

def block_user!
BlockedIp.unverified_request!(request.remote_ip)
end

end

end


Далее собственно модель с тремя аттрибутами (ip адрес, количество попыток, статус) (apps/models/blocked_ip):

'
class BlockedIp < ActiveRecord::Base

before_save :change_status

STATUS_NONE = 'none'
STATUS_BLOCK = 'block'
STATUS_BLOCKED = 'blocked'

ALLOWED_ATEMTPS = 5

named_scope :pending_for_block, {:conditions => ["status = ?", STATUS_BLOCK]}

def BlockedIp.unverified_request!(ip_address)
ip = BlockedIp.find_by_ip_address(ip_address)
ip ||= BlockedIp.new(:ip_address => ip_address)
ip.violation!
false
end

def violation!
self.attempts += 1
self.save
end

def block!
`iptables -I INPUT -s #{self.ip_address} -j DROP`
self.update_attribute(:status, STATUS_BLOCKED)
AdminNotifier.deliver_ip_blocked(self)
self.save
end

private
def change_status
if self.attempts > ALLOWED_ATEMTPS and self.status != STATUS_BLOCKED
self.status = STATUS_BLOCK
end
end

end


Тут все довольно просто, есть три статуса:

* none - пока ничего страшного
* block - кандидат на блокирование
* blocked - заблокирован

Ну и последнее, так как команда iptables должна выполнятся в режиме root'a, то самым простым решением было написать rake таск и запускать его из рутового кронтаба:

$ sudo crontab -e
*/10 * * * * cd /your/apps/here && RAILS_ENV=production rake

И собственно сам rake таск:


namespace :maintenance do

desc "Block ips"
task :block_ips => :environment do
BlockedIp.pending_for_block.each do |ip|
ip.block!
end
end

end


В принципе при больших нагрузках можно снизить планку ALLOWED_ATEMTPS, но появляется вероятность отсеить не того пользователя.