开发一个 Rss 阅读器 03 - 文章列表
上一节,我增加了简陋的管理订阅的功能,不过阅读器真正的内容是文章,为了能让这个简陋的东西能工作起来,需要给它实现一个获取文章列表的功能。
抓取文章
抓取文章,这里我用到了两个库
# Gemfile
gem 'simple-rss', '~> 1.3', '>= 1.3.3'
gem 'httparty', '~> 0.20.0'
- httparty - 用于获取 xml 内容
- simple-rss 用于解析 xml 内容
将其添加到 Gemfile 后进行 bundle install
在 Channel model 里面,添加一个方法 fetch_items
利用上面的两个库,获取订阅的链接并进行解析。
resp = HTTParty.get(xml_link)
rss = SimpleRSS.parse(resp.body)
然后循环读取文章信息,插入到数据库,最后在末尾更新一下抓取时间。
rss.items.each do |item|
items.create!(title: item.title, ...)
end
update!(fetched_at: DateTime.current)
不过直接使用 item.title 等内容创建数据库记录时,遇到了一个编码的错误:
Encoding::UndefinedConversionError ("\xE5" from ASCII-8BIT to UTF-8):
app/models/channel.rb:10:in `block (2 levels) in fetch_items'
app/models/channel.rb:9:in `each'
app/models/channel.rb:9:in `block in fetch_items'
app/models/channel.rb:8:in `fetch_items'
app/controllers/channels_controller.rb:59:in `fetch'
这个就先不仔细研究了,先简单用 force encoding 解决,以后有空了再看 😂
item.title.force_encoding("UTF-8")
Channel Model 完整代码如下
# app/models/channel.rb
class Channel < ApplicationRecord
belongs_to :category
has_many :items
def fetch_items
resp = HTTParty.get(xml_link)
rss = SimpleRSS.parse(resp.body)
transaction do
rss.items.each do |item|
items.create!(
title: encode_data(item.title),
link: item.link,
description: encode_data(item.description),
content: encode_data(item.content),
published_at: item.pubDate.presence || item.published
)
end
update!(fetched_at: DateTime.current)
end
end
private
def encode_data(data)
data.force_encoding('utf-8') if data.present?
end
end
接下来简单的添加个触发抓取的请求
#app/controllers/channels_controller.rb
class ChannelsController < ApplicationController
before_action :set_channel, only: %i[show edit update destroy fetch]
# ...
def fetch
@channel.fetch_items
respond_to do |format|
format.html { redirect_to channels_url, notice: 'Channel was successfully fetched.' }
format.json { head :no_content }
end
end
# ...
end
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :channels do
member do
post :fetch
end
end
# ...
end
以及查看页面的抓取按钮(没有错,现在先手动点击更新)
<!-- app/views/channels/show.html.erb -->
<%= button_to "Fetch", [:fetch, @channel], method: :post %>
只要点击这个养眼的按钮,就可以抓取最新的 rss 内容并插入到数据库中。
文章列表
数据有了,列表就简单了,就先粗暴的使用表格吧,加入订阅、标题、发布日期三列,循环显示内容。
<!-- app/views/items/index.html.erb -->
<p style="color: green"><%= notice %></p>
<h1>Items</h1>
<table>
<thead>
<tr>
<th>Channel</th>
<th>Title</th>
<th>PubDate</th>
</tr>
</thead>
<tbody>
<% @items.each do |item| %>
<tr>
<td>
<%= item.channel.title %>
</td>
<td>
<%= link_to item.title, item.link, target: '_blank' %>
</td>
<td>
<%= item.published_at %>
</td>
</tr>
<% end %>
</tbody>
</table>
链接使用 target="_blank"
是因为想要文章直接在新页面打开原文。
<%= link_to item.title, item.link, target: '_blank' %>
这里有一个小的地方需要改,就是获取文章列表的请求,需要预加载一下关联的 channel 以避免 N+1 问题(程序员的坚持),并且按照发布时间倒序排列。
# app/controllers/items_controller.rb
class ItemsController < ApplicationController
def index
@items = Item.includes(:channel).order(published_at: :desc)
end
# ...
end
这样就有一个简陋的列表页面了。
下一步
虽然有了简单的页面,但是功能是极其不完善的。
- 每个订阅都需要手动更新
- 每次在订阅里面触发更新,都有可能插入重复的内容到文章列表中
- 没有显示文章摘要(我个人喜欢去原文阅读,给作者带去微小的流量),但是简单的摘要有助于在阅读前大致了解文章的内容。
- 添加订阅太订单,需要填很多表单
- 跑在 repl 公开的 repo 上面,所有人都可以查看并修改我的文章订阅,很不安全
- ...
对我来说,下一步迫切需要更新的是前两条:自动更新以及去重。
关于自动更新,限于 replit 的功能,没有办法跑定时任务,虽然我有 Hacker Plan, 可以让应用一直运行并触发定时任务,但是如果想分享给网友使用的话,不能指望每个人都购买订阅,所以找一个相对廉价的替代文案是有必要的。