利用 Laravel 花 2 小時擼一個 RSS 生成器

coding01發表於2018-04-07

利用 Laravel 花 2 小時擼一個 RSS 生成器

Wait no longer! Create RSS feeds for all websites you care about and read them from the comfort of your feed reader.

現在越來越多的網站都不支援 RSS 訂閱了,而作為 RSS 的忠實粉絲,還是希望有個工具可以將自己關注的網站內容聚合在一起,然後實時推送到手機上,及時獲取最新訊息和新聞動態。

所以今天就讓我們用 2 個小時,擼一個 RSS 生成器。

本文的主角仍然是 Laravel。

1. 搭建 Laravel 骨架

由於需要有一個後臺,新增我們關注的網站,所以我們還是沿用 laravel-admin 外掛。

// 1. 建立 Laravel 5.5版本專案

composer create-project --prefer-dist laravel/laravel:5.5 lrss

cd lrss

cp .env.example .env

php artisan key:generate

// 2. 使用 laravel-admin 外掛
composer require encore/laravel-admin "1.5.*"

php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider"

php artisan admin:install

複製程式碼

*注:*如出現問題:SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

解決方案:在 AppServiceProvider.php 加入預設字串長度

use Illuminate\Support\Facades\Schema;

public function boot()
{
    Schema::defaultStringLength(191);
}
複製程式碼

剛巧,我們想借助 Symfonys 提供的 DomCrawler 外掛,來解析網站的 xpath 資訊,發現 laravel-admin外掛有引入:

利用 Laravel 花 2 小時擼一個 RSS 生成器

2. 解析 XPath

之前有想借助 huginn 這個神器來生成我們的 RSS Feed,主要參看文章:讓所有網頁變成RSS —— Huginn git.huginn.cn/docs/%E8%AE…

但在實際使用中,發現一直出現 Huginn 無故當機,或者後臺 jobs 動不動就失敗。這才有了自己擼個工具的想法。

但 Huginn 給了我靈感,可以利用解析 XPath 來生成 RSS Feed。

建立 Xpath 控制器

為了驗證輸入的 XPath 資訊的準確性,我們可以參考 Huginn,

首先在 Huginn 測試 XPath 的效果,在建立 WebsiteAgent 介面,輸入如下資訊:

{
  "expected_update_period_in_days": "2",
  "url": "http://www.woshipm.com/",
  "type": "html",
  "mode": "on_change",
  "extract": {
    "title": {
      "xpath": "//div[@class=\"postlist-item u-clearfix\"]/div[2]/h2/a/text()",
      "value": "normalize-space(.)"
    },
    "desc": {
      "xpath": 
"//div[@class=\"postlist-item u-clearfix\"]/div[2]/p/text()",
      "value": "normalize-space(.)"
    },
    "url": {
      "xpath": "//div[@class=\"postlist-item u-clearfix\"]/div[2]/h2/a",
      "value": "@href"
    }
  }
}
複製程式碼

然後點 「Dry Run」即可測試:

利用 Laravel 花 2 小時擼一個 RSS 生成器

利用 Laravel 花 2 小時擼一個 RSS 生成器

最後根據 Huginn 填入的資訊,我們來建立 Xpath Controller

// bash
php artisan make:model Xpath -m

// migration
public function up()
{
    Schema::create('xpaths', function (Blueprint $table) {
        $table->increments('id');
        
        // url
        $table->string('url', 250);
        $table->string("urldesc", 250);
        
        // title
        $table->string('titlexpath', 250);
        $table->string('titlevalue', 100)
              ->nullable();

        // desc
        $table->string('descxpath', 250);
        $table->string('descvalue', 100)
               ->nullable();

        // url
        $table->string("preurl", 50)->nullable();
        
        $table->string('urlxpath', 250);
        $table->string('urlvalue', 100)
              ->nullable();
              
        $table->timestamps();
    });
}

// migrate
php artisan migrate

// 建立 admin/Controller
php artisan admin:make XpathController --model=App\\Xpath

// 建立 route
$router->resource('xpaths', XpathController::class);

// 加入到 admin 的 menu 中
// 略
複製程式碼

注: 可以參考之前的文章:推薦一個 Laravel admin 後臺管理外掛

CURD XPath

有了 laravel-admin 外掛,操作 XPath 資訊就好簡單了,直接看程式碼:

<?php

namespace App\Admin\Controllers;

use App\Xpath;

use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Facades\Admin;
use Encore\Admin\Layout\Content;
use App\Http\Controllers\Controller;
use Encore\Admin\Controllers\ModelForm;

class XpathController extends Controller
{
    use ModelForm;

    /**
     * Index interface.
     *
     * @return Content
     */
    public function index()
    {
        return Admin::content(function (Content $content) {

            $content->header('header');
            $content->description('description');

            $content->body($this->grid());
        });
    }

    /**
     * Edit interface.
     *
     * @param $id
     * @return Content
     */
    public function edit($id)
    {
        return Admin::content(function (Content $content) use ($id) {

            $content->header('header');
            $content->description('description');

            $content->body($this->form()->edit($id));
        });
    }

    /**
     * Create interface.
     *
     * @return Content
     */
    public function create()
    {
        return Admin::content(function (Content $content) {

            $content->header('header');
            $content->description('description');

            $content->body($this->form());
        });
    }

    /**
     * Make a grid builder.
     *
     * @return Grid
     */
    protected function grid()
    {
        return Admin::grid(Xpath::class, function (Grid $grid) {

            $grid->id('ID')->sortable();

            $grid->column('url');
            $grid->column('urldesc', "描述");

            $grid->column('titlexpath');
            $grid->column('titlevalue');

            $grid->column('descxpath');
            $grid->column('descvalue');

            $grid->column('preurl');
            $grid->column('urlxpath');
            $grid->column('urlvalue');

            $grid->created_at();
            $grid->updated_at();
        });
    }

    /**
     * Make a form builder.
     *
     * @return Form
     */
    protected function form()
    {
        return Admin::form(Xpath::class, function (Form $form) {

            $form->display('id', 'ID');

            // url
            $form->text('url', '連結')
                ->placeholder('請輸入解析的網址')
                ->rules('required|min:5|max:250');

            $form->text('urldesc', '一句話描述')
                ->placeholder('一句話描述')
                ->rules('required|min:5|max:250');

            // title
            $form->divide();
            $form->text('titlexpath', 'title xpath')
                ->placeholder('請輸入標題 xpath')
                ->rules('required|min:5|max:250');

            $form->text('titlevalue', 'title value 預設可以不填')
                ->default('')
                ->rules('max:100');

            // desc
            $form->divide();
            $form->text('descxpath', 'desc xpath')
                ->placeholder('請輸入詳情 xpath')
                ->rules('required|min:5|max:250');

            $form->text('descvalue', 'desc value,預設可以不填')
                ->default('')
                ->rules('max:100');

            // url
            $form->divide();
            $form->text('preurl', 'url 字首')
                ->placeholder('請輸入文章的url 字首')
                ->rules('max:50');

            $form->text('urlxpath', 'url xpath')
                ->placeholder('請輸入文章的url xpath')
                ->rules('required|min:5|max:250');

            $form->text('urlvalue', 'url value 預設可以不填')
                ->default('')
                ->rules('max:100');

            $form->divide();
            $form->display('created_at', 'Created At');
            $form->display('updated_at', 'Updated At');
        });
    }
}

複製程式碼

新增兩個網站資訊試試:

利用 Laravel 花 2 小時擼一個 RSS 生成器

利用 Laravel 花 2 小時擼一個 RSS 生成器

XPath 轉為 RSS Feed

1. 根據填入的 Xpath 資訊,解析內容:

public static function analysis(XpathModel $model) {
    $html = file_get_contents($model->url);

    $crawler = new Crawler($html);

    $titlenodes = $crawler->filterXPath($model->titlexpath);
    $titles = self::getValueByNodes($titlenodes, $model->titlevalue);

    $descnodes = $crawler->filterXPath($model->descxpath);
    $desces = self::getValueByNodes($descnodes, $model->descvalue);

    $urlnodes = $crawler->filterXPath($model->urlxpath);
    $urls = self::getValueByNodes($urlnodes, $model->urlvalue);

    return RssFeeds::feeds($model, $titles, $desces, $urls);
}

// 通過規則獲取 nodes 的值
public static function getValueByNodes(Crawler $crawler, $key = null) {
    return $crawler->each(function (Crawler $node) use ($key) {
        if (empty($key)) {
            return trim($node->text());
        } else {
            return $node->attr($key);
        }
    });
}
複製程式碼

2. 將獲得 title、desc 和 url 陣列裝入 Feed Item 中,構建 RSS。

public static function feeds(Xpath $xpath, $titles = [], $desces = [], $urls = []) {
    if (!empty($xpath->preurl)) {
        $preurl = $xpath->preurl;
        $urlss = collect($urls)->map(function ($url, $key) use ($preurl) {
            return $preurl.trim($url);
        });
    } else {
        $urlss = collect($urls);
    }
    return response()
        ->view('rss',
        [
            'xpath' => $xpath,
            'titles' => $titles,
            'desces' => $desces,
            'urls' => $urlss->toArray(),
            'pubDate' => Carbon::now()
        ])
        ->header('Content-Type', 'text/xml');
}
複製程式碼

3. 編寫 blade 模板

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
   <channel>
       <title>{{ $xpath->url or ' title' }}</title>
       <description>{{ $xpath->urldesc or '描述' }}</description>
       <link>{{ $xpath->url }}</link>
       <atom:link href="{{ url("/feed/$xpath->id") }}" rel="self" type="application/rss+xml"/>
       <pubDate>{{ $pubDate }}</pubDate>
       <lastBuildDate>{{ $pubDate }}</lastBuildDate>
       <generator>coding01</generator>
       @foreach ($titles as $key => $title)
           <item>
               <title>{{ $title }}</title>
               <link>{{ $urls[$key] }}</link>
               <description>{{ $desces[$key] }}</description>
               <pubDate>{{ $pubDate }}</pubDate>
               <author>coding01</author>
               <guid>{{ $urls[$key] }}</guid>
               <category>{{ $title }}</category>
           </item>
       @endforeach
   </channel>
</rss>
複製程式碼

4. 最終來看看效果吧,為每一個網站做成一個 RSS:

利用 Laravel 花 2 小時擼一個 RSS 生成器

RSS 實時訂閱

至此,當前的 Laravel 程式碼告一段落了,但為了達到及時推送內容到手機的目標,我藉助了兩個工具:

  1. Tiny Tiny RSS
  2. IFTTT + 釘釘

把製作好的 RSS 連結加入 Tiny Tiny RSS 上,每隔半個小時,更新一次,獲取最新的內容:

利用 Laravel 花 2 小時擼一個 RSS 生成器

然後藉助 IFTTT 繫結釘釘的群機器人 Webhook:

iftttout

最後在手機釘釘或者在 PC 上就能及時收到最新資訊和資訊了:

利用 Laravel 花 2 小時擼一個 RSS 生成器

總結

今天花了 2 個小時,主要是藉助 laravel-amin 和 symfony/dom-crawler 外掛來自己動手搭建一個 RSS Feed 生成工具Demo。

接下來還有待於繼續優化,如向 feed43.com/ 那樣,輸入 Web URL 就能生成 RSS Feed,又能根據實際需要自己設定更新時間等。

最後,程式碼以放在 github 上,可供參考: github.com/fanly/lrss

「未完待續」

相關文章