开发一个 Rss 阅读器 03 - 文章列表

·

2 min read

上一节,我增加了简陋的管理订阅的功能,不过阅读器真正的内容是文章,为了能让这个简陋的东西能工作起来,需要给它实现一个获取文章列表的功能。

抓取文章

抓取文章,这里我用到了两个库

# Gemfile
gem 'simple-rss', '~> 1.3', '>= 1.3.3'
gem 'httparty', '~> 0.20.0'

将其添加到 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 %>

image.png

只要点击这个养眼的按钮,就可以抓取最新的 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

这样就有一个简陋的列表页面了。

image.png

下一步

虽然有了简单的页面,但是功能是极其不完善的。

  • 每个订阅都需要手动更新
  • 每次在订阅里面触发更新,都有可能插入重复的内容到文章列表中
  • 没有显示文章摘要(我个人喜欢去原文阅读,给作者带去微小的流量),但是简单的摘要有助于在阅读前大致了解文章的内容。
  • 添加订阅太订单,需要填很多表单
  • 跑在 repl 公开的 repo 上面,所有人都可以查看并修改我的文章订阅,很不安全
  • ...

对我来说,下一步迫切需要更新的是前两条:自动更新以及去重。

关于自动更新,限于 replit 的功能,没有办法跑定时任务,虽然我有 Hacker Plan, 可以让应用一直运行并触发定时任务,但是如果想分享给网友使用的话,不能指望每个人都购买订阅,所以找一个相对廉价的替代文案是有必要的。