RSS

RSS 指 Really Simple Syndication(真正简易联合)。

RSS 使您有能力聚合(syndicate)网站的内容 RSS 定义了非常简单的方法来共享和查看标题和内容 RSS 文件可被自动更新 RSS 允许为不同的网站进行视图的个性化 RSS 使用 XML 编写

语法

RSS 2.0 的语法很简单,也很严格。

RSS 如何工作

RSS 用于在网站间分享信息。 使用 RSS,您在名为聚合器的公司注册您的内容。 步骤之一是,创建一个 RSS 文档,然后使用 .xml 后缀来保存它。然后把此文件上传到您的网站。接下来,通过一个 RSS 聚合器来注册。 每天,聚合器都会到被注册的网站搜索 RSS 文档,校验其链接,并显示有关 feed 的信息,这样客户就能够链接到使他们产生兴趣的文档。

RSS 实例

RSS 文档使用一种简单的自我描述的语法。

<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">

<channel>
  <title>W3Schools Home Page</title>
  <link>http://www.w3schools.com</link>
  <description>Free web building tutorials</description>
  <item>
    <title>RSS Tutorial</title>
    <link>http://www.w3schools.com/rss</link>
    <description>New RSS tutorial on W3Schools</description>
  </item>
  <item>
    <title>XML Tutorial</title>
    <link>http://www.w3schools.com/xml</link>
    <description>New XML tutorial on W3Schools</description>
  </item>
</channel>

</rss>

文档中的第一行:XML 声明 - 定义了文档中使用的 XML 版本和字符编码。此例子遵守 1.0 规范,并使用 ISO-8859-1 (Latin-1/West European) 字符集。 下一行是标识此文档是一个 RSS 文档的 RSS 声明(此例是 RSS version 2.0)。

下一行含有 <channel> 元素。此元素用于描述 RSS feed。
<channel> 元素有三个必需的子元素:
<title> - 定义频道的标题。(比如 w3school 首页)
<link> - 定义到达频道的超链接。(比如 www.w3school.com.cn)
<description> - 描述此频道(比如免费的网站建设教程)
每个 <channel> 元素可拥有一个或多个 <item> 元素。
每个 <item> 元素可定义 RSS feed 中的一篇文章或 "story"。
<item> 元素拥有三个必需的子元素:
<title> - 定义项目的标题。(比如 RSS 教程)
<link> - 定义到达项目的超链接。(比如 http://www.w3school.com.cn/rss)
<description> - 描述此项目(比如 w3school 的 RSS 教程)
最后,后面的两行关闭 <channel> 和 <rss> 元素。

注释

在 RSS 中书写注释的语法与 HTML 的语法类似:

<!-- This is an RSS comment -->

因为 RSS 也是 XML,请记住:

  • 所有的元素必许拥有关闭标签

  • 元素对大小写敏感

  • 元素必需被正确地嵌套

  • 属性值必须带引号

RSS <channel>

元素可描述 RSS feed。

以下三个元素是必须的子元素:

<title> - 定义频道的标题。(比如 w3school 首页)
<link> - 定义到达频道的超链接。(比如 www.w3school.com.cn)
<description> - 描述此频道(比如免费的网站建设教程)
ELEM Desc
<category> 可选的。为 feed 定义所属的一个或多个种类。
<cloud> 可选的。注册进程,以获得 feed 更新的立即通知。
<copyright> 可选。告知版权资料。
<description> 必需的。描述频道。
<docs> 可选的。规定指向当前 RSS 文件所用格式说明的 URL。
<generator> 可选的。规定用于生成 feed 的程序。
<image> 可选的。在聚合器呈现某个 feed 时,显示一个图像。
<language> 可选的。规定编写 feed 所用的语言。
<lastBuildDate> 可选的。定义 feed 内容的最后修改日期。
<link> 必需的。定义指向频道的超链接。
<managingEditor> 可选的。定义 feed 内容编辑的电子邮件地址。
<pubDate> 可选的。为 feed 的内容定义最后发布日期。
<rating> 可选的。feed 的 PICS 级别。
<skipDays> 可选的。规定忽略 feed 更新的天。
<skipHours> 可选的。规定忽略 feed 更新的小时。
<textInput> 可选的。规定应当与 feed 一同显示的文本输入域。
<title> 必需的。定义频道的标题。
<ttl> 可选的。指定从 feed 源更新此 feed 之前,feed 可被缓存的分钟数。
<webMaster> 可选的。定义此 feed 的 web 管理员的电子邮件地址。

RSS <item>

元素可定义 RSS feed 中的一篇文章或 “story”。

以下三个元素是必须的子元素:

<title> - 定义频道的标题。(比如 w3school 首页)
<link> - 定义到达频道的超链接。(比如 www.w3school.com.cn)
<description> - 描述此频道(比如免费的网站建设教程)
ELEM Desc
<author> 可选的。规定项目作者的电子邮件地址。
<category> 可选的。定义项目所属的一个或多个类别。
<comments> 可选的。允许项目连接到有关此项目的注释(文件)。
<description> 必需的。描述此项目。
<enclosure> 可选的。允许将一个媒体文件导入一个项中。
<guid> 可选的。为项目定义一个唯一的标识符。
<link> 必需的。定义指向此项目的超链接。
<pubDate> 可选的。定义此项目的最后发布日期。
<source> 可选的。为此项目指定一个第三方来源。
<title> 必需的。定义此项目的标题。

实例

将自己的网站生成RSS。

1) 直接通过 网络爬虫扒取自己的网站, 样式未抓取, 懒得做。

此法跳过。

2) 利用 JAR 自己构建。

文章放在本地, 全部为 markdown 编写。

  • 依赖jar

<properties>
    <rsslibj.version>1.0RC2</rsslibj.version>
    <exml.version>7.0</exml.version>
</properties>


<dependencies>
    <dependency>
        <groupId>rsslibj</groupId>
        <artifactId>rsslibj</artifactId>
        <version>${rsslibj.version}</version>
    </dependency>
    <dependency>
        <groupId>exml</groupId>
        <artifactId>exml</artifactId>
        <version>${exml.version}</version>
    </dependency>
</dependencies>
  • 随便写的测试类

注意:

  1. 因为代码的实现比较粗糙, 会导致文章中的 site.url 会被替换成为真正的网络地址(http://houbb.github.io)。所以本文的RSS看起来应该有些错误。

  2. 文章中涉及到的常量替换不必特别关注, 大多是由于jekyll和markdown的特殊性导致的。

/**
 * FileUtil Tester.
 *
 * @author houbinbin
 * @version 1.0
 * @since 02/14/2017
 */
public class RssBuildTest {

    /**
     * 读取此根路径下的所有文件
     */
    private static final String BASE_PATH = "/Users/houbinbin/IT/code/houbb.github.io/_posts";

    /**
     * 获取所有 markdown 文件列表
     * @since 1.7
     * @return
     */
    public static List<String> getAllMdFileList() {
        List<String> fileList = new LinkedList<>();
        Path path = Paths.get(BASE_PATH);

        try(DirectoryStream<Path> pathDirectoryStream = Files.newDirectoryStream(path,
                BlogMDConstant.MD_FILE_PATTERN)) {
            for(Path p : pathDirectoryStream) {
                File file = p.toFile();
                fileList.add(file.getAbsolutePath());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return fileList;
    }

    /**
     * 获取文章正文
     * @param fullContent
     * @return
     */
    public static final String getArticleContent(String fullContent) {
        String result;
        int start = fullContent.indexOf(BlogMDConstant.INDEX_LIST);
        /**
         * 如果有 这个标签
         * 则将目录索引截取掉。
         */
        if(start > 0) {
            result = fullContent.substring(start+BlogMDConstant.INDEX_LIST.length());
        } else {
            int secondIndex = getSecondThreeDashEndIndex(fullContent);  //--- 文章头结尾
            result  = fullContent.substring(secondIndex);
        }

        return result;
    }

    /**
     * 获取第二个 --- 的结束下标。
     * 后面是正文 开始的地方
     * @return
     */
    private static final int getSecondThreeDashEndIndex(String fullContent) {
        int index = fullContent.indexOf(BlogMDConstant.THREE_DASH, BlogMDConstant.THREE_DASH.length());
        return index;
    }


    /**
     * 根据文章URL构建真实的 RSS item 对象
     * @param articleURL 文章的本地URL
     * @return
     * @throws ParseException
     */
    private static Item buildItemForArticleWithJAR(String articleURL) throws ParseException {
        String content = FileUtil.getFileContent(articleURL);


        String title = BlogMDConstant.getForArticleTitle(content, BlogMDConstant.TITLE_PATTERN);
        String pubDate = BlogMDConstant.getForArticleTitle(content, BlogMDConstant.DATE_PATTERN);
        String published = BlogMDConstant.getForArticleTitle(content, BlogMDConstant.PUBLISHED_PATTERN);
        String description = getArticleContent(content);
        description = description.replace(BlogMDConstant.MD_ROOT_PATH, BlogMDConstant.NET_ROOT_PATH);   //替换为真实网络路径
        pubDate = pubDate.replace(BlogMDConstant.ARTICLE_TITLE_APPEND_TIME, BlogMDConstant.EMPTY);  //移除最后的时间添加字符串
        if(!BlogMDConstant.isPublished(published)) {
            return null;
        }

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(BlogMDConstant.DATE_TIME_FORMAT);
        Date date = simpleDateFormat.parse(pubDate);
        String netArticleUrl = buildArticleNetURL(articleURL);
        Item item = new Item();
        item.setTitle(title);
        item.setLink(netArticleUrl);
        item.setDcDate(date);
        item.setDescription(MarkdownUtil.markdownToHtml(description));
        return item;
    }


    /**
     * 根据本地地址构建
     * 1) 网络根地址+URL生成规则构建的URL
     * @param articleURL
     * @return
     */
    private static String buildArticleNetURL(String articleURL) {
        //// TODO: 17/2/15 实现
        return articleURL;
    }

    public static void main(String[] args) throws ParseException, IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {

        Channel channel = new Channel();
        channel.setTitle("Echo blog");
        channel.setLink("http://houbb.github.io/");
        String rssName = "blog.xml";

        List<String> stringList = getAllMdFileList();
        for(String articlePath : stringList) {
            Item item = buildItemForArticleWithJAR(articlePath);
            if(null == item) {
                continue;
            }
            channel.addItem(item);
        }

        File file = new File(rssName);

        if (!file.exists()) {
            file.createNewFile();
        }

        PrintWriter writer = new PrintWriter(new FileWriter(file));
        writer.write(channel.getFeed("rss"));
        writer.flush();
        writer.close();
    }
}

常量类:

/**
 * @author houbinbin
 * @version 1.0
 * @on 17/2/14
 * @since 1.7
 */
public final class BlogMDConstant {

    /**
     * 空字符串
     */
    public static final String EMPTY = "";

    /**
     * 文章题头 后面添加的时间 字符串
     */
    public static final String ARTICLE_TITLE_APPEND_TIME = " +0800";

    public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

    /**
     * markdown 文件正则匹配
     */
    public static final String MD_FILE_PATTERN = "*.md";

    /**
     * 换行符
     */
    public static final String RETURN_LINE = "\n";

    /**
     * 三个横线
     * 主要是markdown文件头
     */
    public static final String THREE_DASH = "---";

    /**
     * 标题
     */
    public static final String TITLE_PATTERN = "title:";

    /**
     * 日期
     */
    public static final String DATE_PATTERN = "date:";

    /**
     * 是否发布
     */
    public static final String PUBLISHED_PATTERN = "published:";


    /**
     * 文章的内容标题索引
     */
    public static final String INDEX_LIST = "* any list\n" +
            "{:toc}";

    /**
     * markdown 文件中 的默认根路径
     */
    public static final String MD_ROOT_PATH = "";

    /**
     * 网络真实  根路径
     */
    public static final String NET_ROOT_PATH = "http://houbb.github.io";



    /**
     * 当前文章是发布的
     * @param result
     * @return
     */
    public static final boolean isPublished(String result) {
        return "true".equalsIgnoreCase(result);
    }


    /**
     *
     layout: post
     title: RSS
     date:  2017-02-12 00:17:37 +0800
     categories: [Tool]
     tags: [rss]
     published: true
     * 获取文章表头对应的内容
     * 比如: 输入 "date:"
     * 则返回对应的时间字符串。
     * @param content 文章全文
     * @param pattern 对应的表头
     * @return
     */
    public static final String getForArticleTitle(final String content, final String pattern) {

        int startIndex = content.indexOf(pattern);
        int endIndex = content.indexOf(RETURN_LINE, startIndex);
        String lineContent = content.substring(startIndex, endIndex);
        int startSplitIndex = lineContent.indexOf(":")+1;  //时间这一栏
        String result = lineContent.substring(startSplitIndex);
        return result.trim();
    }
}