从远程 RSS 文件抓取标题

本文中,Nick 介绍了如何检索连锁内容并转换成网站上的标题。因为这类提示没有正式的格式,聚集器经常面临支持多种格式的困难,因此 Nick 还介绍了如何用 XSL 转换来更简单地处理多重连锁文件格式。

随着 Weblog 的普及,信息过载越来越严重。现在读者要比以前浏览更多的站点,按常规的方式全部访问基本上是不可能的。部分问题可以通过内容连锁来解决,即站点把它的标题和基本信息放在单独的 提示中。当今,多数提示都使用一种称为 RSS 的 XML 格式,尽管在使用中有不同的变体,甚至还有一种潜在的竞争格式。

本文介绍了如何利用 Java 技术来检索连锁提示的内容,确定其类型并把它转换成 HTML 格式以显示在 Web 站点上。这个过程包括五个步骤:

  1. 检索 XML 提示
  2. 分析提示
  3. 确定正确的转换
  4. 执行转换
  5. 显示结果

本文记录了一个 Java Server Page (JSP)的创建过程,它检索远程提示并使用 Java bean 和 XSLT 进行转换,然后把新转换的信息合并到 JSP 页面中。但是,这个概念实际上适用于任何 Web 环境。

源文件

根据您询问的对象不同,RSS 可能代表 RDF 站点摘要、丰富站点摘要或者其他不那么贴切的缩写词。无论如何,通常使用的 RSS 不少于四种版本,从相当简单的 0.91 版(不包括命名空间,并且对内容进行了某些限制)到 2.0 版(它包含了所有以前的版本,包括 0.91 版——因此有效的 0.91 版文件也是有效的 2.0 版文件,并且允许使用命名空间)。因为支持命名空间,所以 2.0 版允许连锁出版商向提示中增加元素,只要这些元素属于不同的命名空间即可。有些连锁出版商利用这种能力增加使用资源定义格式(RDF)的信息。

一个简单的 RSS 2.0 文件看起来类似于下面这个提示,它取自 Adam Curry 的 weblog (请参阅 参考资料):



清单 1. RSS 2.0 消息示例
<?xml version="1.0"?>
<rss version="2.0">
 <channel>
  <title>Adam Curry: Adam Curry's Weblog</title>
  <link>http://www.blognewsnetwork.com/members/0000001/</link>
  <description>News and Views from Adam Curry</description>
  <language>en-us</language>
  <copyright>Copyright 2003 Adam Curry</copyright>
  <lastBuildDate>Thu, 24 Jul 2003 09:26:48 GMT</lastBuildDate>
  <docs>http://backend.userland.com/rss</docs>
  <generator>Radio UserLand v8.0.9b2</generator>
  <managingEditor>adam@curry.com</managingEditor>
  <webMaster>adam@curry.com</webMaster>
  <item>
   <title>weblog at work again</title>
   <link>
   http://www.blognewsnetwork.com/members/0000001/2003/07/24.html#a4158
   </link>
   <description><a href="http://radio.weblogs.com/0001014/images/2003/07/24/ad
amwheely.jpg"><img src="http://radio.weblogs.com/0001014/images/2003/07/24/
adamwheely.jpg" width="250" height="187.5" border="0" align="right" hspace="15" v
space="5" alt="A picture named adamwheely.jpg"></a>A few days ago I aske
d if anyone had taken pictures of me at the annual ...</description>
   <guid>
   http://www.blognewsnetwork.com/members/0000001/2003/07/24.html#a4158
   </guid>
   <pubDate>Thu, 24 Jul 2003 09:21:25 GMT</pubDate>
  </item>
  <item>
   <title>teens trouble with web</title>
   <link>
   http://www.blognewsnetwork.com/members/0000001/2003/07/23.html#a4156
   </link>
   <description>According to a report from Northumbria University, most teenagers
 lack the <a href="http://www.web-user.co.uk/news/news.php?id=33621">inform
ation gathering skills</a> needed for using the internet efficiently. This 
sounds like it shouldn't be happening in ...</description>
   <guid>
   http://www.blognewsnetwork.com/members/0000001/2003/07/23.html#a4156
   </guid>
   <pubDate>Wed, 23 Jul 2003 17:36:23 GMT</pubDate>
  </item>
...
 </channel>
</rss>

要把这个提示转化成 HTML,可以使用 XSL 转换来处理它。





回页首


基本的样式表

最终目标是生成 HTML 文本,以便在另一个信息页面内以有组织的方式显示信息,如链接列表。实际的 HTML 输出应该类似于:



清单 2. 输出的 HTML
        <h2>Adam Curry: Adam Curry's Weblog
        </h2>
<h3>News and Views from Adam Curry
        </h3>
<ul>
<li>
  <a 
href="http://www.blognewsnetwork.com/members/0000001/2003/07/24.html#a4158">weblog 
at work again</a>
  <p><a href="http://radio.weblogs.com/0001014/images/2003/07/24/adamwheely.jpg">
<img src="http://radio.weblogs.com/0001014/images/2003/07/24/adamwheely.jpg" 
width="250" height="187.5" border="0" align="right" hspace="15" vspace="5" alt="A 
picture named adamwheely.jpg"></a>A few days ago I asked if anyone had taken 
pictures of me at the annual ...
        </li>
<li>
  <a 
href="http://www.blognewsnetwork.com/members/0000001/2003/07/23.html#a4156">teens 
trouble with web</a>
  <p>According to a report from Northumbria University, most teenagers lack the 
<a href="http://www.web-user.co.uk/news/news.php?id=33621">information gathering 
skills</a> needed for using the internet efficiently. This sounds like it 
shouldn't be happening in ...
        </li>
...
        </ul>
      

创建 XML 的该 HTML 输出需要一个 XSLT 样式表:



清单 3. 简单的样式表
<?xml version="1.0"?> 
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/">
  <xsl:apply-templates select="//channel"/>
  <ul>
    <xsl:apply-templates select="//item"/>
  </ul>
</xsl:template>
<xsl:template match="channel">
    
<xsl:apply-templates select="../image"/>
 <h2><xsl:value-of select="title"/></h2>
 <h3><xsl:value-of select="description"/></h3>
</xsl:template>
<xsl:template match="item">
  <li>
    <xsl:element name="a">
      <xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute>
      <xsl:value-of select="title" />
    </xsl:element>
    <p><xsl:value-of disable-output-escaping="yes" select="description" /></p>
  </li>
</xsl:template>
<xsl:template match="image">
  <xsl:element name="img">
    <xsl:attribute name="src"><xsl:value-of select="url"/></xsl:attribute>
    <xsl:attribute name="style">float:left; padding: 10px;</xsl:attribute>
  </xsl:element>
</xsl:template>
<xsl:template match="language">
</xsl:template>
</xsl:stylesheet>
<?xml version="1.0"?> 
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/">
  <xsl:apply-templates select="//channel"/>
  <ul>
    <xsl:apply-templates select="//item"/>
  </ul>
</xsl:template>
<xsl:template match="channel">
    
<xsl:apply-templates select="../image"/>
 <h2><xsl:value-of select="title"/></h2>
 <h3><xsl:value-of select="description"/></h3>
</xsl:template>
<xsl:template match="item">
  <li>
    <xsl:element name="a">
      <xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute>
      <xsl:value-of select="title" />
    </xsl:element>
    <p><xsl:value-of disable-output-escaping="yes" select="description" /></p>
  </li>
</xsl:template>
<xsl:template match="image">
  <xsl:element name="img">
    <xsl:attribute name="src"><xsl:value-of select="url"/></xsl:attribute>
    <xsl:attribute name="style">float:left; padding: 10px;</xsl:attribute>
  </xsl:element>
</xsl:template>
<xsl:template match="language">
</xsl:template>
</xsl:stylesheet>

页面的实际形式完全取决您所选择的要包括的数据。在这个例子中,只是创建了一个项目符号列表,带有链接回到最初源点的标题(如果有的话)和每个起源的描述。

要实际执行转换,需要创建一个 JSP 页面。





回页首


基本的 JSP 页面

存在无数种转换 XML 数据的方法。本文中将说明如何创建一个 JSP 页面,让它把一个提示交给 Java bean 进行转换。这个 bean 创建一个静态文件,JSP 页面把它结合到页面中。(到下一节“缓冲”就会明白使用静态文件的原因了。)

页面本身相当简单:



清单 4. JSP 页面
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
        <jsp:useBean id="rssBean" scope="request" class="RSSProcessor">
<% 
   rssBean.setRSSFile(
        "http://wolk.datashed.net/users/adam@curry.com/curryCom.xml");
 %>
</jsp:useBean>
<html>
<head>
   <title>Syndicated Feeds</TITLE>
</head>
<body>
           <jsp:include page="headlines.html" flush="true"/>
</body>
</html>
      

这里只需要创建一个 RSSProcessor 类的实例。因为已经把它包含在了 useBean 元素中,当创建该对象时将执行 setRSSFile() 方法。这个方法创建 headlines.html 页面,然后由 JSP 页面把它结合到输出中。

接下来,要创建执行转换的 bean。





回页首


转换文件

Java bean 只不过是带有 getset 方法的 Java 类。在这个例子中,set 方法, setRSSFile() 也包括对那个文件执行转换的代码:



清单 5. 转换提示
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileOutputStream;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
public class RSSProcessor {
  public RSSProcessor(){ }
  String _RSSFile;
   
  public String getRSSFile(){
       return _RSSFile;
  }
  public void setRSSFile(String fileName){
       try {
            
          StreamSource source = new StreamSource(fileName);
          StreamSource finalStyle = new StreamSource("final.xsl");
          String outputURL = "headlines.html";
          StreamResult result = new StreamResult(new 
                                    FileOutputStream(outputURL));
          TransformerFactory transFactory = TransformerFactory.newInstance();
          Transformer transformer = transFactory.newTransformer(finalStyle);   
          transformer.transform(source, result);
       } catch (Exception e) {
           e.printStackTrace();        
       }
  }
   
}

这个方法只需要一个输入源——恰好是远程 RSS 提示,使用 final.xsl 样式表把它转换成 headlines.html 文件。

事情的主要步骤是:检索文件、转换、显示结果。但实际应用中还有其他的问题需要考虑。





回页首


适应多重格式

如果所有的 RSS 文件都像这个例子一样,就万事大吉了。不幸的是,情况并非如此。不同的提供商和工具包可能产生另外的信息,或者用 RDF 信息或者其他命名空间化的模块代替核心信息,由于这种种的变化导致人们抱怨支持 RSS 太复杂了。但是通过利用 XSL 转换,不一定非要如此。

比方说,一个 RSS 2.0 提示可能还包括 RDF 信息,像下面这个取自 Typographica 的提示那样:



清单 6. 摘自带有 RDF 的 RSS 2.0 消息示例
<?xml version="1.0" encoding="iso-8859-1"?>
<rss version="2.0" 
            xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    xmlns:admin="http://webns.net/mvcb/"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Typographica</title>
    <link>http://typographi.ca/</link>
    <description>A daily journal of typography featuring news, observations, 
and open commentary on fonts and typographic design.</description>
            <dc:language>en-us</dc:language>
    <dc:creator>Stephen Coles</dc:creator>
    <dc:rights>Copyright 2003</dc:rights>
    <dc:date>2003-07-24T00:00:52-08:00</dc:date>
    <admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=2.63" />
    <admin:errorReportsTo rdf:resource="mailto:scoles@gomakecontact.com" />
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
    <item>
      <title>Hot and Cold Fonts</title>
      <link>http://typographi.ca/000643.php</link>
      <description>LettError have developed a multiple master font 
for the Design Institute of the University of Minnesota that varies 
along three...</description>
      <guid isPermaLink="false">643@http://typographi.ca/</guid>
      
        <content:encoded><![CDATA[<p><a href="http://www.letterror.com/">
LettError</a> have developed a multiple master font for the 
<a href="http://design.umn.edu/">Design Institute</a> of the University of 
Minnesota that varies along three dimensions: formality, informality, and 
"weirdness." (It's apparently possible to be 100% formal and 100% informal at 
the same time.)  As the New York Times...]]></content:encoded>
      <dc:subject></dc:subject>
      <dc:date>2003-07-24T00:00:52-08:00</dc:date>
    </item>
    <item>
      <title>Textura Digita</title>
      <link>http://typographi.ca/000642.php</link>
      <description>CNN reports that the Gutenberg Bible is now available 
on the web via the Ransom Center at the University of...</description>
      <guid isPermaLink="false">642@http://typographi.ca/</guid>
              <content:encoded><![CDATA[<p><a href=
"http://www.cnn.com/2003/TECH/internet/07/23/digital.scripture.ap/index.html">
CNN reports</a> that the Gutenberg Bible is now available on the web via the 
<a href="http://www.hrc.utexas.edu/exhibitions/permanent/gutenberg/">Ransom 
Center</a> at the University of Texas.</p>
...]]></content:encoded>
      <dc:subject></dc:subject>
      <dc:date>2003-07-23T13:16:15-08:00</dc:date>
    </item>
    <item>
      <title>Fight! Fight! Fight!</title>
      <link>http://typographi.ca/000640.php</link>
      <description>Angry because you had to miss TypeCon ’03? 
Work out that aggression with Helvetica vs. Arial....</description>
      <guid isPermaLink="false">640@http://typographi.ca/</guid>
              <content:encoded><![CDATA[<p>Angry because you had to miss 
<a href="http://www.typecon2003.com/">TypeCon ’03</a>? Work out that 
aggression with <a href="http://www.engagestudio.com/helvetica/">Helvetica vs. 
Arial</a>.</p>]]></content:encoded>
      <dc:subject></dc:subject>
      <dc:date>2003-07-22T08:52:36-08:00</dc:date>
    </item>
...
  </channel>
</rss>
      

注意,这个提示实际上包含两种不同的内容描述。第一个在 description 元素中,第二个在 encoded 元素中, encoded 元素是 http://purl.org/rss/1.0/modules/content/ 命名空间的一部分。这里可以看出,不同提示处理信息的方式不同。Adam Curry 的 blog 只对链接这样的信息编码并放到 description 元素中,而 Typographica(或者更准确地说是生成 Typographica 提示的工具包)在 description 元素中提供无标记的版本,并使用 CDATA 构造在 encoded 元素中提供完整的版本。

尽管为了利用各种额外的信息,为每种提示类型创建定制的表示是可取的,但从应用程序开发的观点来看是不实际的。但这并不意味着必须放弃。相反,可以创建针对不同提示的转换把它们转换成标准格式,然后交给最终的转换。

比如,可以创建一个以 RSS 2.0 样式表为参数的样式表,如果发现 encoded 元素,则用它代替所有的 description 元素:



清单 7. 转换 RDF 信息
<?xml version="1.0"?> 
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<rss>
  <channel>
     <xsl:apply-templates select="rss/channel" />
  </channel>
</rss>
</xsl:template>
<xsl:template match="title|link|/rss/channel/description|image|text()">
   <xsl:copy-of select="." />
</xsl:template>
<xsl:template match="item" >
   <item>
      <title><xsl:value-of select="title" /></title>
      <link><xsl:value-of select="link" /></link>
      <description><xsl:value-of select="description" /></description>
   </item>
</xsl:template>
<xsl:template match="item[encoded]" >
   <item>
      <title><xsl:value-of select="title" /></title>
      <link><xsl:value-of select="link" /></link>
      <description><xsl:value-of select="encoded" /></description>
   </item>
</xsl:template>
    
</xsl:stylesheet>

这个样式表制作最终样式表需要的副本,比如通道的标题和描述,并复制带有相应描述信息的 item

现在只需要把这个新文档交给最终转换:



清单 8. 串接转换
...
        import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.dom.DOMResult;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
public class RSSProcessor {
...
 public void setRSSFile(String fileName){
      try {
         StreamSource 
        interimSource = new StreamSource(fileName);
            
         
        String XSLSheetName = "2.0.xsl";
       StreamSource style = new StreamSource(XSLSheetName);
       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
       DocumentBuilder db = dbf.newDocumentBuilder();
       Document interimDoc = db.newDocument();
       DOMResult interimResult = new DOMResult(interimDoc);
         TransformerFactory transFactory = TransformerFactory.newInstance();
         
        Transformer interimTransformer = null;
       interimTransformer = transFactory.newTransformer(style);
       interimTransformer.transform(interimSource, interimResult);
       DOMSource source = new DOMSource(interimDoc);
         StreamSource finalStyle = new StreamSource("final.xsl");
         String outputURL = "headlines.html";
         StreamResult result = new StreamResult(new 
                                   FileOutputStream(outputURL));
         Transformer transformer = transFactory.newTransformer(finalStyle);   
         transformer.transform(source, result);
      } catch (Exception e) {
          e.printStackTrace();
      }
 }
}
      

花点时间看看这个步骤。首先,创建了一个过渡转换,它接受最初的提示并按照 清单 7 中 名为 2.0.xsl 的过渡样式表转换它。第一次转换的结果没有进入文件,而是进入一个 DOM Document 对象,然后把它作为源传递给第二次转换。

过渡样式表的名称 2.0.xsl 是经过深思熟虑的。根据版本号命名可以创建更灵活的系统。





回页首


选择版本

只要允许不同的格式,实际上就可以创建一个在处理之前检查提示版本的系统。毕竟只有 RSS 1.0 和 2.0 提示可以包含 RDF 元素,因此没有必要处理其他的提示。但是如何知道所用的版本呢?

要解决这个问题,可以装载真正的提示并分析,然后利用分析的结果设置正确的样式表。



清单 9. 选择样式表
...
        import org.xml.sax.InputSource;
import org.w3c.dom.Element;
public class RSSProcessor {
...
   public void setRSSFile(String fileName){
         try {
            
            
        InputSource docFile = new InputSource (fileName);
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            
        Document inputDoc = db.parse(docFile);
           Element rss = inputDoc.getDocumentElement();
           String version = null;
           if (rss.getNodeName().equals("rss")){
               version = rss.getAttribute("version");
               if (version == null) {
                  version = "0.91";
               }
           } else if (rss.getNodeName().equals("feed")){
              version = "echo";
           }
            
            String XSLSheetName = 
        version+".xsl";
            StreamSource style = new StreamSource(XSLSheetName);
            
        DOMSource interimSource = new DOMSource(inputDoc);
            Document interimDoc = db.newDocument();
            DOMResult interimResult = new DOMResult(interimDoc);
            TransformerFactory transFactory = TransformerFactory.newInstance();
            Transformer interimTransformer =
         null;
           if (version.equals("0.91")){
              interimTransformer = transFactory.newTransformer();
           } else {
               interimTransformer = transFactory.newTransformer(style);
           }
            interimTransformer.transform(interimSource, interimResult);
            DOMSource source = new DOMSource(interimDoc);
            StreamSource finalStyle = new StreamSource("final.xsl");
            String outputURL = "headlines.html";
            StreamResult result = new StreamResult(new 
                                      FileOutputStream(outputURL));
            Transformer transformer = transFactory.newTransformer(finalStyle);   
            transformer.transform(source, result);
         } catch (Exception e) {
             e.printStackTrace();
        
         }
   }
  
}
      

在这个例子中,加载了提示并检查它的 RSS 版本,然后使用这个版本号作为文件名。好处是如果发布了新的 RSS 版本,就可以通过添加新的样式表来扩展应用程序。注意,我已经增加了对 Echo、Atom 或其他任何可能被调用的 RSS 竞争者的检查,您可以根据变化调整对它的支持,只需修改 echo.xsl 样式表就可以了。

好处是这个过渡样式表是完全通用的。"2.0 - .91" 的样式表可以用于任何提示类型、任何地方,无论支持一个版本还是一百个版本,只要编辑 final.xsl 就可以改变最后的输出。

final.xsl 样式表是为简单的 0.91 版提示而设计的,因此如果处理的是这一版本的提示,就可以忽略过渡转换中的样式表。这就形成了 恒等转换,文档按原来的样子输出。

上述方法解决了多重版本的问题,但是还有一个问题需要考虑:并发。





回页首


缓存提示

这个系统将在个人服务器上工作得很好,因为只有您访问它,但在现实世界中,每当有人需要阅读提示时就把它拖走是不实际的(也是粗暴的)。相反,需要构造一个带有某种时间延迟机制的系统,如果提示最近被取走了,则使用已有的 headlines.html 文件。

为此,可以利用 Java 应用程序的性质。表示提示最后被取走的时间的 static 变量应该对 RSSProcessor 类的所有实例是一个常数,因此可以在实际拖走提示之前将当前时间与这个变量进行比较:



清单 10. 选择样式表
        import java.util.Date;
public class RSSProcessor {
...
           static Date _LastUpdated = new Date();
   public Date getLastUpdated(){
         return _LastUpdated;
   }
      
   public void setRSSFile(String fileName){
              Date now = new Date();
      long diff = now.getTime() - _LastUpdated.getTime();
      double interval = .5;
      if ((diff == 0) || (diff > (interval * 60 * 1000))){
           _LastUpdated = now;
     
         try {
            
            InputSource docFile = new InputSource (fileName);
...
            Transformer transformer = transFactory.newTransformer(finalStyle);   
            transformer.transform(source, result);
         } catch (Exception e) {
             e.printStackTrace();
         }
              }  
   }
   
}
      

服务器第一次实例化 RSSProcessor 时, _LastUpdated 使用当前日期实例化。(本质上)在同一时刻,服务器执行 setRSSFile() 方法,因为当前时间和 _LastUpdated 时间的差是0,于是进行转换。

下一次有人调用该页面时,将会创建 RSSProcessor 的一个新实例,但是因为 _LastUpdated 是静态的,所以新实例看到的是 _LastUpdated 的现有值而不是初始化这个变量。 interval 用分钟计量,而 _LastUpdated 和当前时间的差以毫秒计算。如果经过的时间数小于 interval,则什么也不发生。 headlines.html 文件没有更新,因此服务器还是使用原来的那一个文件。

另一方面,如果超过了 interval, _LastUpdated 取得当前时间,并传递给后续的任何 RSSProcessor 对象,bean 取一个提示的新副本进行转换。





回页首


结束语

本文说明了如何创建连锁提示的阅读器,以检索单个远程提示,使用 XSLT 进行转换,并作为 Web 页的一部分来显示。通过使用 XSLT 样式表,该系统还可以适应多重提示类型。

应用程序使用 DOM Document 分析提示并确定适当的样式表,还可以通过把部分逻辑移到外部样式表中进一步扩展。也可以调整系统使它获取多个提示——可能根据用户的选择,为每个提示创建单独的缓冲文件。类似地,可以让用户决定提示检索的时间间隔。



参考资料



关于作者

 

Nicholas Chase, Studio B 的作者,参与了包括 Lucent Technologies、Sun Microsystems、Oracle 和 Tampa Bay Buccaneers 在内的多家公司的网站开发。Nick 曾是一名高中物理教师、低级放射性废物设施管理员、在线科幻小说杂志编辑、多媒体工程师和 Oracle 讲师。最近,他是佛罗里达州 Clearwater 的 Site Dynamics Interactive Communications 的首席技术官。他写了四本有关 Web 开发的书,包括 XML Primer Plus(Sams)。他乐于倾听读者的意见,可以通过 nicholas@nicholaschase.com 与他联系。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值