Статьи

Написание агрегатора каналов с Синатрой

Кто не любит закаты? Из всех сайтов, которые вы видели о закате, вы чувствуете, что ни один не делает это правильно. Вы решаете сделать лучший сайт проклятых закатов в Интернете. Есть куча фотографий заката. Можем ли мы использовать общедоступные изображения, чтобы наполнить этот новый удивительный сайт? Мы создадим сайт, пока юристы выяснят последний вопрос.

Дети все еще говорят об API?

Кто-то может подумать: «Могу поспорить, у Flickr есть отличные фотографии заката». Да, действительно. Мы выберем эту группу . Внизу страницы вы увидите канал RSS. Можем ли мы прочитать этот канал и показать изображения и отдать должное нужным людям?

Да, да, мы можем.

Давайте использовать наших друзей Синатра для нашего сайта и стойки / тест для тестирования.

Установите Синатру и стойку / тестируйте

gem install sinatra
gem install rack-test

Мы создадим feed_aggregator и тестовую папку внутри него. Пока мы занимаемся этим, создайте пустой основной и тестовый файл тоже.

 $ mkdir feed_aggregator 
$ mkdir feed_aggregator/test 
$ touch feed_aggregator/test/feed_aggregator_test.rb

Возможно, наш первый тест — убедиться, что приложение запускается. В тестовом файле давайте добавим

 require '../main'  
require 'test/unit'  
require 'rack/test'  
 
ENV['RACK_ENV'] = 'test'
 
class FeedAggregatorTest < Test::Unit::TestCase
  include Rack::Test::Methods
 
  def app
    Sinatra::Application
  end
 
  def test_it_says_feed_aggregator
    get '/'
    assert last_response.ok?
    assert_equal 'Feed Aggregator', last_response.body
  end
 
end

Запустите тест.

 test$ ruby feed_aggregator_test.rb 
<internal:lib/rubygems/custom_require>:29:in `require': no such file to load -- ../main (LoadError)
	from <internal:lib/rubygems/custom_require>:29:in `require'
	from feed_aggregator_test.rb:1:in `<main>'

Да! Мы провалили тест. Там написано, что основного файла нет. Идите и сделайте этот файл.

 feed_aggregator $ touch main.rb
 test$ ruby feed_aggregator_test.rb 
Loaded suite feed_aggregator_test
Started
E
Finished in 0.001054 seconds.
 
  1) Error:
test_it_says_feed_aggregator(FeedAggregatorTest):
NameError: uninitialized constant FeedAggregatorTest::Sinatra
    feed_aggregator_test.rb:11:in `app'
    /Users/johnivanoff/.rvm/gems/ruby-1.9.2-p180@feed_aggregator/gems/rack-test-0.6.2/lib/rack/test/methods.rb:31:in `build_rack_mock_session'
    /Users/johnivanoff/.rvm/gems/ruby-1.9.2-p180@feed_aggregator/gems/rack-test-0.6.2/lib/rack/test/methods.rb:27:in `rack_mock_session'
    /Users/johnivanoff/.rvm/gems/ruby-1.9.2-p180@feed_aggregator/gems/rack-test-0.6.2/lib/rack/test/methods.rb:42:in `build_rack_test_session'
    /Users/johnivanoff/.rvm/gems/ruby-1.9.2-p180@feed_aggregator/gems/rack-test-0.6.2/lib/rack/test/methods.rb:38:in `rack_test_session'
    /Users/johnivanoff/.rvm/gems/ruby-1.9.2-p180@feed_aggregator/gems/rack-test-0.6.2/lib/rack/test/methods.rb:46:in `current_session'
    feed_aggregator_test.rb:15:in `test_it_says_feed_aggregator'
 
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

Давайте добавим код для приложения с надписью «Агрегатор каналов».

 require 'sinatra'
 
get '/' do
  "Feed Aggregator"
end

Как вы думаете, тест пройдет сейчас? Давай выясним.

 test$ ruby feed_aggregator_test.rb 
Loaded suite feed_aggregator_test
Started
.
Finished in 0.063198 seconds.
 
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Тест проходит.

Теперь давай возьмем этот канал. Поскольку мы разрабатываем тесты, было бы неплохо иметь фиксированный канал RSS. Идите вперед и создайте для них каталог и пустой XML-файл.

 test$ mkdir fixtures 
test$ touch fixtures/feed.xml

Отлично. Давайте скопируем источник канала в файл feed.xml . Если мы проведем тестирование в режиме реального времени, записи могут измениться и сделать его очень разочаровывающим во время тестирования. Кому это нужно?

 <?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
	    xmlns:media="http://search.yahoo.com/mrss/"
	    xmlns:dc="http://purl.org/dc/elements/1.1/"
	    xmlns:creativeCommons="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html"
	    	    xmlns:flickr="urn:flickr:user" >
	<channel>
 
 
		<title>Flickr's Best Sunsets Pool</title>
		<link>http://www.flickr.com/groups/flickrsbestsunsets/pool/</link>
... Content elided ...

(Примечание. Весь XML-файл можно найти в этом разделе .
ВОТ ЭТО ДА! С чего начать? Почему бы нам не поискать ссылки, чтобы человек мог кликнуть на оригинальную фотографию на Flickr?

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

Это звучит как потрясающий тест. Запишите это.

 def test_find_the_link
  feed = File.read('fixtures/feed.xml')
  items = parse feed
  item = items.first
  link = 'http://www.flickr.com/photos/mattcaustin/8205498382/in/pool-1373979@N22'
  assert_equal item[:link], link
end

Где я взял текст для переменной ссылки? Я скопировал ссылку с первого пункта в приспособлении. Скоро вы увидите, как это используется.

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

Давайте запустим тест. Я надеюсь, что это не удастся.

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

E.

Finished tests in 0.025705s, 77.8059 tests/s, 77.8059 assertions/s.

  1) Error:
test_find_the_link(FeedAggregatorTest):
NoMethodError: undefined method `parse' for #<FeedAggregatorTest:0x007ff7412ac390>
    feed_aggregator_test.rb:24:in `test_find_the_link'

2 tests, 2 assertions, 0 failures, 1 errors, 0 skips

Нет метода для разбора

Идите дальше и создайте этот метод в файле main.rb.

 require 'sinatra'

def parse  
end

get '/' do
  "Feed Aggregator"
end

Повторный тест.

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

E.

Finished tests in 0.251661s, 7.9472 tests/s, 7.9472 assertions/s.

  1) Error:
test_find_the_link(FeedAggregatorTest):
ArgumentError: wrong number of arguments (1 for 0)
    /Users/john/Dropbox/feed_aggregator/main.rb:4:in `parse'
    feed_aggregator_test.rb:23:in `test_find_the_link'

2 tests, 2 assertions, 0 failures, 1 errors, 0 skips

Мы не передавали никаких аргументов. Это нормально, так как мы хотели решить только последнюю ошибку. Что вы делаете, чтобы сделать этот пропуск? Вернуться в файл main.rb

 require 'sinatra'

def parse feed
end

get '/' do
  "Feed Aggregator"
end

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

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

E.

Finished tests in 0.027235s, 73.4349 tests/s, 73.4349 assertions/s.

  1) Error:
test_find_the_link(FeedAggregatorTest):
NoMethodError: undefined method `first' for nil:NilClass
    feed_aggregator_test.rb:24:in `test_find_the_link'

2 tests, 2 assertions, 0 failures, 1 errors, 0 skips

Это действительно так. Теперь нам нужен «первый» метод.

Для того, чтобы добраться до первого, нам нужно будет просмотреть XML, чтобы получить первую ссылку.

Теперь, как можно проанализировать XML, чтобы найти эту ссылку? Мне нравится (Нокогири) [http://nokogiri.org]. У вас установлен этот драгоценный камень? Давайте проверим. Ваш вывод может выглядеть иначе, чем мой.

 test$  gem list --local -d noko

*** LOCAL GEMS ***

Видимо, нет. Если вы этого не сделаете, идти вперед и установить его.

 test$ sudo gem install nokogiri

Кроме того, увидев в терминале, что он успешно установлен, как вы можете проверить, установлен ли он?

 test$  gem list --local -d noko

*** LOCAL GEMS ***

nokogiri (1.5.5)
    Authors: Aaron Patterson, Mike Dalessio, Yoko Harada, Tim Elliott
    Rubyforge: http://rubyforge.org/projects/nokogiri
    Homepage: http://nokogiri.org
    Installed at: /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator

    Nokogiri (鋸) is an HTML, XML, SAX, and Reader parser

Где были мы? Разбор XML-документа. Вам нужно загрузить корм в Нокогири. Затем вы можете просмотреть каждый элемент, получить ссылку и сохранить их в хеше. Не забудьте вернуть предметы.

 require 'sinatra'
require 'nokogiri'

def parse feed
  doc = Nokogiri::XML feed
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item
  end
end

get '/' do
  "Feed Aggregator"
end

У вас есть хорошее чувство по этому поводу? Идите вперед и запустите тест. Вы не забыли включить nokogiri в main.rb?

 test$ ruby feed_aggregator_test.rb
Loaded suite feed_aggregator_test
Started
..
Finished in 0.112374 seconds.

2 tests, 3 assertions, 0 failures, 0 errors, 0 skips

Snoopy Dance Можем ли мы сказать это?

Нам нужно получить миниатюру дальше. Где это в приспособлении?
Ты нашел это? Это атрибут в теге. Идите вперед и напишите тест для этого. Это довольно близко к первому.

 def test_find_the_thumbnail_image
  feed = File.read('fixtures/feed.xml')
  items = parse feed
  item = items.first
  thumbnail = 'http://farm9.staticflickr.com/8488/8205498382_4e5ed09a62_s.jpg'
  assert_equal item[:thumbnail], thumbnail
end

Запустите тест.

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

.F..

Finished tests in 0.053503s, 74.7622 tests/s, 93.4527 assertions/s.

  1) Failure:
test_find_the_thumbnail_image(FeedAggregatorTest) [feed_aggregator_test.rb:42]:
<nil> expected but was
<"http://farm9.staticflickr.com/8488/8205498382_4e5ed09a62_s.jpg">.

4 tests, 5 assertions, 1 failures, 0 errors, 0 skips

Это было ожидаемо. Давайте подумаем об этом. Нам нужно значение атрибута media:thumbnail Как насчет этого?

 require 'sinatra'
require 'nokogiri'

def parse feed
  doc = Nokogiri::XML feed
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item[:thumbnail] = doc_item.at('media:thumbnail').attr('url').value
    item
  end
end

get '/' do
  "Feed Aggregator"
end

Это имеет смысл, поскольку URL-адрес, который нам нужен, является атрибутом этого узла. Идите и попробуйте.

 test john$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

EE.

Finished tests in 0.031776s, 94.4109 tests/s, 62.9406 assertions/s.

  1) Error:
test_find_the_link(FeedAggregatorTest):
NoMethodError: undefined method `attr' for nil:NilClass
    /Users/john/Dropbox/feed_aggregator/main.rb:10:in `block in parse'
    /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:239:in `block in each'
    /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:238:in `upto'
    /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:238:in `each'
    /Users/john/Dropbox/feed_aggregator/main.rb:7:in `map'
    /Users/john/Dropbox/feed_aggregator/main.rb:7:in `parse'
    feed_aggregator_test.rb:23:in `test_find_the_link'

  2) Error:
test_find_the_thumbnail_image(FeedAggregatorTest):
NoMethodError: undefined method `attr' for nil:NilClass
    /Users/john/Dropbox/feed_aggregator/main.rb:10:in `block in parse'
    /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:239:in `block in each'
    /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:238:in `upto'
    /Users/john/.rvm/gems/ruby-1.9.3-p194@feed_aggregator/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:238:in `each'
    /Users/john/Dropbox/feed_aggregator/main.rb:7:in `map'
    /Users/john/Dropbox/feed_aggregator/main.rb:7:in `parse'
    feed_aggregator_test.rb:31:in `test_find_the_thumbnail_image'

3 tests, 2 assertions, 0 failures, 2 errors, 0 skips

Ну, это не удалось. undefined метод ‘attr’ для nil: NilClass Не удается найти узел <media:thumbnail> Если вы посмотрите на верхнюю часть RSS-канала, то увидите, что используется пространство имен мультимедиа. Подробнее о пространствах имен.

Оказывается, с Nokogiri, вы можете просто использовать символ трубы, чтобы указать поиск пространства имен. идти вперед и поменять местами двоеточие и заменить его трубкой в ​​строке миниатюр.

 require 'sinatra'
require 'nokogiri'

def parse feed
  doc = Nokogiri::XML feed
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item[:thumbnail] = doc_item.at('media|thumbnail').attr('url')
    item
  end
end

get '/' do
  "Feed Aggregator"
end

Дай попробовать. перезапустить тесты

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

...

Finished tests in 0.045266s, 66.2749 tests/s, 88.3665 assertions/s.

3 tests, 4 assertions, 0 failures, 0 errors, 0 skips

Потрясающие. Возможно, нам следует использовать заголовок картинки тоже. Идите вперед и напишите тест для этого. Я буду ждать.

Законченный? Вот что я сделал.

 def test_find_the_title
  feed = File.read('fixtures/feed.xml')
  items = parse feed
  item = items.first
  title = 'An Evening at Shell Beach'
  assert_equal item[:title], title
end

Опять же, мы используем заголовок из первого пункта нашего прибора. Запустите тест.

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

.F.

Finished tests in 0.078211s, 38.3578 tests/s, 51.1437 assertions/s.

  1) Failure:
test_find_the_title(FeedAggregatorTest) [feed_aggregator_test.rb:34]:
<nil> expected but was
<"An Evening at Shell Beach">.

3 tests, 4 assertions, 1 failures, 0 errors, 0 skips

Нам нужно искать название. Как бы вы добавили это в метод разбора?

 require 'sinatra'
require 'nokogiri'

def parse feed
  doc = Nokogiri::XML feed
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item[:thumbnail] = doc_item.at('media|thumbnail').attr('url')
    item[:title] = doc_item.at('title').text
    item
  end
end

get '/' do
  "Feed Aggregator"
end

Запустите тест.

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

....

Finished tests in 0.062725s, 63.7704 tests/s, 79.7130 assertions/s.

4 tests, 5 assertions, 0 failures, 0 errors, 0 skips

Круто, но давайте посмотрим что-нибудь на веб-странице. Мы хотим результаты в браузере.

Для простоты я буду использовать erb для создания веб-страницы.

 require 'sinatra'
require 'nokogiri'


def parse feed
  doc = Nokogiri::XML feed
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item[:thumbnail] = doc_item.at('media|thumbnail').attr('url')
    item[:title] = doc_item.at('title').text
    item
  end
end

get '/' do
  erb :index
end

__END__

@@index
<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="user-scalable=yes, width=device-width" />
<title>Lovely Sunsets</title> 
</head>
<body>
  <h1>Feed Aggregator</h1>
</body>
</html>

Так как мы внесли некоторые изменения, мы должны повторить тесты.

 test$ ruby feed_aggregator_test.rb 
Run options: 

# Running tests:

...F

Finished tests in 0.251257s, 15.9200 tests/s, 19.8999 assertions/s.

  1) Failure:
test_it_says_feed_aggregator(FeedAggregatorTest) [feed_aggregator_test.rb:18]:
<"Feed Aggregator"> expected but was
<"<!DOCTYPE html>n<html>n  <head>n  <meta charset="UTF-8">n  <meta name="viewport" content="user-scalable=yes, width=device-width" />n<title>Lovely Sunsets</title> n</head>n<body>n  <h1>Feed Aggregator</h1>n</body>n</html>n">.

4 tests, 5 assertions, 1 failures, 0 errors, 0 skips

К сожалению. Идите и исправьте это.

 def test_it_says_feed_aggregator
  get '/'
  assert last_response.ok?
  assert_match 'Feed Aggregator', last_response.body
end

Мы следим за тем, чтобы «Агрегатор кормов» был на странице.

Теперь, когда тесты проходят, давайте двигаться дальше. Вы перезапустили тест, верно? Мы добавим URL фида, а затем проанализируем нашу информацию.

 require 'sinatra'
require 'nokogiri'

feed = File.read('test/fixtures/feed.xml')

def parse feed
  doc = Nokogiri::XML feed
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item[:thumbnail] = doc_item.at('media|thumbnail').attr('url')
    item[:title] = doc_item.at('title').text
    item
  end
end

get '/' do
  @pictures = parse feed
  erb :index
end

__END__

@@index
<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="user-scalable=yes, width=device-width" />
<title>Lovely Sunsets</title> 
</head>
<body>
  <h1>Feed Aggregator</h1>
  <dl>
    <% @pictures.each do |picture| %>
      <dt><a href="<%= picture[:link] %>"><%= picture[:title] %></a></dt>
      <dd><img src="<%= picture[:thumbnail] %>" /></dd>
    <% end %>
  </dl>
</body>
</html>

Вы можете заметить, что я имею в виду тестовое устройство вместо RSS-канала. Опять же, я не хочу постоянно запрашивать их сервер во время разработки.

Идите вперед, запустите сервер и проверьте прекрасную работу в вашем браузере.

Все выглядит хорошо. Давайте использовать реальные данные. Как бы вы подключили это, чтобы получить канал от Flickr? Да, давайте добавим open-uri в файл main.rb и затем nokogiri откроет файл.

 require 'sinatra'
require 'nokogiri'
require 'open-uri'

feed = 'http://api.flickr.com/services/feeds/groups_pool.gne?id=1373979@N22&lang=en-us&format=rss_200'

def parse feed
  doc = Nokogiri::XML(open(feed))
  doc.search('item').map do |doc_item|
    item = {}
    item[:link] = doc_item.at('link').text
    item[:thumbnail] = doc_item.at('media|thumbnail').attr('url')
    item[:title] = doc_item.at('title').text
    item
  end
end

get '/' do
  @pictures = parse feed
  erb :index
end

__END__

@@index
<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="user-scalable=yes, width=device-width" />
<title>Lovely Sunsets</title> 
</head>
<body>
  <h1>Lovely Sunsets</h1>
  <dl>
    <% @pictures.each do |picture| %>
      <dt><a href="<%= picture[:link] %>"><%= picture[:title] %></a></dt>
      <dd><img src="<%= picture[:thumbnail] %>" /></dd>
    <% end %>
  </dl>
</body>
</html>

Запустите его и просмотрите в браузере http://127.0.0.1:4567/

Сладкий. Теперь вы можете добавить обработку ошибок, возможно, кеширование или несколько каналов.

Если вы хотите увидеть статью по одному из них, дайте нам знать.

Приветствия.