八卦新闻:用 RSS 读新闻

《Head First C 中文版》第 9 章 进程与系统调用第 416 页八卦新闻:用 RSS 读新闻

RSS 源是网站发布新闻的常用方式。RSS 源其实就是一个 XML 文件,里面有新闻的摘要和链接。当然,你完全有能力写一个直接从网页读取 RSS 文件的 C 程序,但这涉及一些你没有接触过的编程概念。为什么不找一个程序帮忙处理 RSS 文件呢?

RSS Gossip 是一个 Python 小脚本,它可以根据某个关键字在 RSS 源中查找新闻。你必须先安装 Python 才能运行这个脚本,一旦有了 Python 和 rssgossip.py,就可以像这样搜索新闻:

09-00

下面就是在我的 Arch Linux 64-bit 操作系统中的运行结果:

> python2 --version
Python 2.7.4
> python --version
Python 3.3.1

> export RSS_FEED=http://www.wking-china.com/xpjylc/rss/edition.rss

> python2 rssgossip.py 'China'
Why China involved in Africa's health
China reduces banking lifeline to N. Korea
China's youngest comrades 

> python2 rssgossip.py -u 'China'
Why China involved in Africa's health
    http://www.wking-china.com/xpjylc/2013/05/09/opinion/china-africa-health/index.html?eref=edition
China reduces banking lifeline to N. Korea
    http://www.wking-china.com/xpjylc/2013/05/08/business/china-north-korea-banking/index.html?eref=edition
China's youngest comrades 
    http://www.wking-china.com/xpjylc/2013/04/30/world/asia/china-young-communists/index.html?eref=edition

请注意以下几点:

  • 大多数 Linux 操作系统中已经预装了 Python 了,一般来说不需要另外再安装了。但是要注意,本书中的 rssgossip.py 脚本需要 Python2,不能使用 Python3。而现在很多 Linux 中已经是 Python3 了。如果是这样,就需要使用 python2 代替 python 。
  • RSS 链接是通过 RSS_FEED 环境变量传递给 rssgossip.py 脚本的。
  • 书中的 RSS 链接是作者编造的,实际上并不存在,需要替换为我们自己找到的 RSS 链接。
  • rssgossip.py 可带 -u 参数运行,将会显示新闻的 URL。这一点很重要,在后面我们将会用到。

下面就是 rssgossip.py 源程序。也可以到文末的参考资料[2]中下载。

import urllib
import os
import re
import sys
import string
import unicodedata
import getopt
from xml.dom import minidom

def usage():
    print("Usage:\npython rssgossip.py [-uh] <search-regexp>")

try:
    opts, args = getopt.getopt(sys.argv[1:], "uh", ["urls", "help"])
except getopt.GetoptError, err:
    print str(err)
    usage()
    sys.exit(2)

include_urls = False
for o, a in opts:
    if o == "-u":
        include_urls = True
    elif o in ("-h", "--help"):
        usage()
        sys.exit()
    else:
        assert False, "unhandled option"

searcher = re.compile(args[0], re.IGNORECASE)
for url in string.split(os.environ['RSS_FEED']):
    feed = urllib.urlopen(url)
    try:
        dom = minidom.parse(feed)
        forecasts = []
        for node in dom.getElementsByTagName('title'):
            txt = node.firstChild.wholeText
            if searcher.search(txt):
                txt = unicodedata.normalize('NFKD', txt).encode('ascii', 'ignore')
                print(txt)
                if include_urls:
                    p = node.parentNode
                    link = p.getElementsByTagName('link')[0].firstChild.wholeText
                    print("\t%s" % link)
    except:
        sys.exit(1)

案例研究:在浏览器中打开新闻

本书第 10 章 进程间通信第 444 页案例研究:在浏览器中打开新闻

假设你想在浏览器中打开 rssgossip.py 脚本找到的新闻链接,你将在父进程中运行程序,在子进程中运行 rssgossip.py 。需要创建管道,把 rssgossip.py 的输出和程序的输入连接起来。

代码熟食:在浏览器中打开网页

第 446 页代码熟食:在浏览器中打开网页

程序需要用机器上的浏览器打开网页,但不同操作系统与程序交互的方式不同,因此实现起来多少有些难度。

好在兼职演员已经为你写好了这部分代码,它能够在绝大多数系统上打开网页。但他们好像还有要事在身,所以只采用了最简单的实现方式:

void open_url(const char *url)
{
  char launch[255];
  sprintf(launch, "cmd /c start %s", url); // 在Windows上打开网页。
  system(launch);
  sprintf(launch, "x-www-browser '%s' &", url); // 在Linux上打开网页。
  system(launch);
  sprintf(launch, "open '%s'", url);       // 在Mac上打开网页。
  system(launch);
}

代码用了三条命令,它们分别可以在 Mac、Windows 和 Linux 上打开 URL 。每次都有两条命令会失败,但只要有一条成功就行了。

实际上,在我的 Linux 机器上,三条命令都失败了。这是怎么回事?原来, x-www-browser 一般用于 Ubuntu 等 Linux 发行版中。虽然 Ubuntu 很多人使用,而且我以前也用过,但现在我用的是 Arch Linux,它不支持 x-www-browser 。解决方案是使用 xdg-open 代替,它可用于大多数 Linux 操作系统。而且 xdg-open 不仅可以打开网页,而且可以打开操作系统支持的任意文件,这和上述程序中其它两个操作系统的做法一致。这样,open_url 函数也可以改名为 open_thing 函数。详细信息请参见文末的参考资料[3]。

连接管道

第 448 页连接管道:练习解答

大部分代码已经写好了,你只需要填写用管道连接父子进程的那部分。为了节约空间,我们去掉了 #include 语句、error() 和 open_url() 函数。

int main(int argc, char *argv[])
{
  char *phrase = argv[1];        // 下一行的 RSS 可以根据实际需要替换。
  char *vars[] = {"RSS_FEED=http://www.wking-china.com/xpjylc/rss/edition.rss", NULL};
  int fd[2];                     // 这个数组将保存管道描述符
  if (pipe(fd) == -1) {          // 创建管道,并把描述符保存在fd[0]和fd[1]中。
    error("Can't create pipe");  // 为了防止管道创建失败,需要检查pipe()的返回值。
  }
  pid_t pid = fork();
  if (pid == -1) {
    error("Can't fork process");
  }
  if (!pid) {       // 这里你在子进程中。
    dup2(fd[1], 1); // 把标准输出设为管道的写入端
    close(fd[0]);   // 子进程不会读取管道,所以我们将关闭读取端
    if (execle("/usr/bin/python2", "/usr/bin/python2", "rssgossip.py",
               "-u", phrase, NULL, vars) == -1) { // -u 让脚本显示新闻URL。
      error("Can't run script");
    }
  }
  dup2(fd[0], 0); // 从这里开始你就在父进程中。
  close(fd[1]);   // 关闭管道的写入端,因为父进程不需要向管道写数据。
  char line[255];
  while (fgets(line, 255, stdin)) { // 将从标准输入读取数据,因为管道连到了标准输入。
    if (line[0] == '\t')   // 如果line以Tab开头...     //上一行的stdin也可以用fd[0]。
      open_url(line + 1);  // ...就说明它是URL。
  }
  return 0;
}

注意, 在上述代码中:

  • 我们使用自己的 RSS 源替换了书中虚构的 RSS 源。
  • 使用 /usr/bin/python2 替换了 /usr/bin/python 。

夜未眠:还在为错误代码烦恼?

本书第 434 页夜未眠:还在为错误代码烦恼?

每次你在系统调用时都需要反反复复写那些错误处理代码。还犹豫什么!赶快使用我们的独家秘方,我们将向你展示如何重用错误代码,从此你将告别重复代码:

……

首先,需要把处理代码放到一个单独的 error() 函数中,然后把 return 语句换成 exit() 系统调用。

void error(const char *msg)
{
  fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  exit(1); // exit(1)会立刻终止程序,并把退出状态置1。
}

现在就可以把那些烦人的错误检查代码换成:

……

警告:每次程序执行只有一次调用 exit() 的机会,“程序突然结束恐惧症”患者慎用。

补上 include 语句

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>

编译代码并运行程序

本书第 449 页(使用我机器上的运行结果代替了书中的):

编译代码并运行程序,出现了:

> gcc news_opener.c -o news_opener
> ./news_opener 'China'
sh: cmd: 未找到命令
sh:行1: open: 未找到命令
sh: cmd: 未找到命令
sh:行1: open: 未找到命令
sh: cmd: 未找到命令
sh:行1: open: 未找到命令
已在现有的浏览器会话中创建新的窗口。
已在现有的浏览器会话中创建新的窗口。
已在现有的浏览器会话中创建新的窗口。
>

10-00

太棒了,程序工作了。

news_opener 程序在一个独立的进程中运行了 rssgossip.py,并让它显示找到新闻的 URL 。所有本来应该发送到屏幕上的输出现在通过管道重定向到 news_opener 父进程,news_opener 就可以在浏览器中打开新闻了。

管道是连接进程的好办法。现在你不但能够运行进程,控制它们的环境,而且还能获取进程的输出,这样就可以实现很多功能。任何一个能够在命令行中运行的程序你都可以在 C 代码中调用并控制它。

参考资料

  1. 《Head First C 中文版》
  2. GitHub: dogriffiths/rssgossip
  3. Stack Overflow: linux - Compatibility of x-www-browser
  4. Linux manual page: fork(2)
  5. Linux manual page: pipe(2)
  6. Linux manual page: exit(3)
  7. Python Programming Language - Official Website