内容


通过 YQL 和 PHP 构建 web 应用程序,第 2 部分

使用 PHP 和 YQL 来检索并合并来自多个 web 服务的数据

Comments

简介

在本系列第一部分,我向您介绍了 YQL,Yahoo! Query Language,该语言提供一个统一的、类似 SQL 的接口来检索来自多个 web 服务 APIs 的数据。我展示了如何从一个 PHP 应用程序访问 YQL 服务,提供了一些示例来说明 YQL 如何允许您利用 SQL 类型的结构来过滤、排列和链接查询结果。

在第二篇、也是本系列的最后一篇中,我深入探究了 YQL,并展示了如何从 PHP 应用程序内使用 YQL 来添加、编辑和删除第三方 web 服务上的数据。我还展示了如何使用 YQL 搜索和检索 HTML 页面上的数据以及 RSS 和 Atom 等结构化 XML 格式的数据。

使用 YQL 添加数据

正如在前一篇文章中所看到的,YQL 允许您使用 SELECT 语句提取来自第三方 web 服务的数据。但是,SELECT 并非 YQL 支持的惟一 SQL 语句;您还可以使用 INSERT、DELETE 和 UPDATE 查询、运用相同的 SQL 语法操作第三方服务上的数据。

要了解其运作方式,请查看 清单 1,其中使用一个 INSERT 查询在 Wordpress.com 上将一篇博客文章添加到用户的博客上。要运行这示例,您必须用有效的 Wordpress.com 帐户凭据替换 BLOGNAME、BLOGUSER 和 BLOGPASS 变量;您可以注册一个免费 Wordpress.com 博客来获取这些(参见 参考资料 获取链接)。

清单 1. 使用 YQL INSERT 查询添加数据
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute YQL query
// add new post to Wordpress blog using POST
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->env('store://datatables.org/alltableswithkeys');  
  $client->q("INSERT INTO wordpress.post 
    (title, description, blogurl, username, password) VALUES 
    ('Hello world', 'This is my first blog post with YQL...woohoo!', 
    'http://BLOGNAME.wordpress.com', 'BLOGUSER', 'BLOGPASS')");       
  $result = $client->post();
  echo 'Entry posted with ID: ' . $result->results->postid;
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
    exit;
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
    exit;
}
?>

清单 1 所示,使用 YQL 执行一个 INSERT 查询与在任何兼容 SQL 的 RDBMS 上执行 INSERT 查询一样。您必须指定要插入数据的表名,后面紧接一列键和值。YQL 使用表定义将查询数据转化成一个 XML 编码的数据包,之后通过 POST 将该数据包提交给 Wordpress.com web 服务。响应以一个标准 YQL 结果文档的形式返回给客户端,该文档包含新插入文章的 ID。

图 1 展示了 Wordpress.com 博客上新增的文章。

图 1. 使用 YQL 添加的博客文章
使用 YQL 添加的博客文章屏幕截图
使用 YQL 添加的博客文章屏幕截图

清单 2 提供 清单 1 的一个更交互式的版本,允许用户直接将内容输入到一个 web 表单,并将该内容转化为一篇 Wordpress.com 博客文章。

清单 2. 使用 YQL 交互式地添加博客文章
<html>
  <head></head>
  <body> 
    <h2>Post Blog Entry</h2> 
    <form method="post" action="<?php echo htmlentities($_SERVER['PHP_SELF']); ?>">
      <div>
        Title: <br/>
        <input type="text" name="title" size="40" /> 
      </div>
      <div>
        Body: <br/>
        <textarea name="body" rows="5" cols="30"></textarea>
      </div>    
      <div>
        Blog URL: <br/>
        http:// <input type="text" name="prefix" size="40" /> .wordpress.com
      </div>
      <div>
        Username: <br/>
        <input type="text" name="user" size="40" />
      </div>
      <div>
        Password: <br/>
        <input type="text" name="pass" size="40" />
      </div>
      <input type="submit" name="submit" value="Post" />    
    </form>
    <?php
    if (isset($_POST['submit'])) {
      // validate input (omitted for brevity)
      $title = $_POST['title'];
      $body = $_POST['body'];
      $blog = 'http://' . $_POST['prefix'] . '.wordpress.com';
      $user = $_POST['user'];
      $pass = $_POST['pass'];

      // set up Zend auto-loader
      // load Zend REST client classes
      require_once 'Zend/Loader.php';
      Zend_Loader::loadClass('Zend_Rest_Client');

      // execute YQL query
      try {
        $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
        $client->env('store://datatables.org/alltableswithkeys');  
        $client->q(
          "INSERT INTO wordpress.post (title, description, blogurl, username, password) 
          VALUES ('$title', '$body', '$blog', '$user', '$pass')");       
        $result = $client->post();
      } catch (Zend_Rest_Client_Exception $e) {
          echo "Client error: " . $e->getResponse();
          exit;
      } catch (Exception $e) {
          echo "Error: " . $e->getMessage();
          exit;
      }

      // iterate over query result set
      echo 'Entry posted with ID: ' . $result->results->postid;
    }
    ?>
  </body>      
</html>

图 2 展示显示给用户的 web 表单以及一些输入。

图 2. 用于交互式地向 Wordpress.com 添加博客文章的一个 web 表单
用于交互式地向 Wordpress.com 添加博客文章的一个 web 表单屏幕截图
用于交互式地向 Wordpress.com 添加博客文章的一个 web 表单屏幕截图

图 3 显示提交表单之后 Wordpress.com 上的博客文章。

图 3. Wordpress.com 上新增的博客文章
Wordpress.com 上新增的博客文章屏幕截图
Wordpress.com 上新增的博客文章屏幕截图

使用 YQL 更新和删除数据

与使用 INSERT 一样,您也可以使用 UPDATE 和 DELETE 语句。比如,清单 3 使用一个 UPDATE 查询来更新 清单 1 中新创建的文章。

清单 3. 使用 YQL UPDATE 查询修改数据
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute YQL query
// update post on Wordpress blog
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->env('store://datatables.org/alltableswithkeys');  
  $client->q(
    "UPDATE wordpress.post SET title='Updated blog post', 
    description='Look, ma, I updated my blog post via YQL', publish='true' 
    WHERE blogurl = 'http://BLOGNAME.wordpress.com' AND 
    username = 'BLOGUSER' AND password = 'BLOGPASS' AND postid = '5'");       
  $result = $client->post();  
  if ($result->methodResponse->params->param->value->boolean == 1) {
    echo 'Post successfully updated';    
  } else {
    throw new Exception ('Could not delete post');  
  }
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
    exit;
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
    exit;
}
?>

与任何数据库 UPDATE 查询一样,您必须向 YQL 查询传递要更新的字段列表,以及其新值和将更新限制为一组记录的一个 WHERE 子句。在 清单 3 中,WHERE 子句包含用户凭据以及要更新的文章 ID。

图 4 展示 Wordpress.com 博客上已更新的文章。

图 4. 通过 YQL 修改的博客文章
通过 YQL 修改的博客文章屏幕截图
通过 YQL 修改的博客文章屏幕截图

清单 4 展示如何使用 YQL 删除一篇文章。

清单 4. 使用 YQL DELETE 查询删除数据
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute YQL query
// delete post from Wordpress blog
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->env('store://datatables.org/alltableswithkeys');  
  $client->setConfig(array('timeout' => 30)); 
  $client->q(
    "DELETE FROM wordpress.post WHERE blogurl = 'http://BLOGNAME.wordpress.com' 
    AND username = 'BLOGUSER' AND password = 'BLOGPASS' AND postid = '5'");       
  $result = $client->post();  
  if ($result->methodResponse->params->param->value->boolean == 1) {
    echo 'Post successfully deleted';    
  } else {
    throw new Exception ('Could not delete post');  
  }
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
    exit;
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
    exit;
}
?>

注意,在撰写本文时,Wordpress.com 的 YQL 社区表不包含 UPDATE 和 DELETE 功能支持,不过一个补丁正在开发中。如果运行前几个清单有问题,您可以引用作者的这些补丁版本的表,修改 YQL 查询字符串,以通过一个 USE 子句引用自定义表。示例如下:

USE 'http://github.com/vikram0/yql-tables/raw/master/wordpress/wordpress.post.xml' 
AS wordpress.post; 
DELETE FROM wordpress.post WHERE blogurl = 'http://BLOGNAME.wordpress.com' 
AND username = 'BLOGUSER' AND password = 'BLOGPASS' AND postid = '5'

在本例中,您可能还需要调整客户端超时,如下所示:

  $client->setConfig(array('timeout' => 30));

示例应用程序:云购物车

在掌握了所有这些背景信息之后,让我们来看一下如何使用 YQL 数据修改功能来构建一个示例应用程序。在本例中您的目标 web 服务是 Payvment(参见 参考资料 获取链接),即一个 “云中购物车” 实现。顾名思义,Payvment 允许您使用一个 REST API 创建、编辑和修改购物车,这样就可以很容易地将购物车功能整合到一个 web 应用程序中。Payvment 还使一个社区开放数据表可供 YQL 使用,支持您使用 YQL(而非访问 Payvment REST API)来执行购物车操作。

考虑到这一点,让我们组合一个示例应用程序,使用 YQL 和 PHP 来构建一个云中的购物车。该应用程序定义一个基本产品目录,含有每个 SKU 的价格和说明,允许用户使用 YQL INSERT 查询添加这些商品到其 Payvment 车。用户还可以使用 UPDATE 或 DELETE 查询修改数量或从购物车中删除商品。您可以使用一个 SELECT 查询随时检索购物车的当前内容(包含小计)。

清单 5 列出了完整的代码。

清单 5. 使用 Payvment YQL 社区表构建的购物车
<?php
// set API keys and constants
define (PAYVMENT_API_KEY, 'YOUR-API-KEY');
define (PAYVMENT_ID, 'YOUR-ID');

// define simple product catalog
$catalog = array(
  '167' => array('title' => 'Flying hammer', 'price' => 35.99),
  '543' => array('title' => 'Lightsaber', 'price' => 76.99),
  '129' => array('title' => 'Sonic screwdriver', 'price' => 10.99)
);

// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// start session
session_start();

try {
  // check if a cart already exists, if not, get a new cart ID
  if (!isset($_SESSION['cart'])) {
    $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
    $query = "USE 'http://github.com/vikram0/yql-tables/raw/master/shoppingcart/
              shoppingcart.YourProducts.xml' AS shoppingcart; 
              SELECT * FROM shoppingcart WHERE apiKey='" . PAYVMENT_API_KEY . "'";
    $client->q($query);       
    $result = $client->get();
    if ($result->results->cartfeed->status->StatusCode != 200) {
      throw new Exception ('ERROR: Cannot get shopping cart');  
    } 
    $_SESSION['cart'] = (string) $result->results->cartfeed->results->CartID;
  }

  // check for add/edit/delete operations
  switch(strtolower($_REQUEST['op'])) {
    // add = INSERT
    case 'add':
      $id = $_REQUEST['id'];
      $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
      $query = 
        "USE 'http://github.com/vikram0/yql-tables/raw/master/shoppingcart/
        shoppingcart.YourProducts.xml' AS shoppingcart; 
        INSERT INTO shoppingcart (apiKey, cartID, payvmentID, itemID, itemName, 
        itemPrice) VALUES ('" . PAYVMENT_API_KEY . "', '" . 
        $_SESSION['cart'] . "', '" . PAYVMENT_ID . "', '$id', '" . 
        $catalog[$id]['title'] . "', '" . $catalog[$id]['price'] . "')";
      $client->q($query);       
      $result = $client->post();
      if ($result->results->cartfeed->status->StatusCode != 200) {
        throw new Exception ('ERROR: Cannot add item to shopping cart');  
      } 
      break;  
    // remove = DELETE
    case 'remove':
      $id = $_REQUEST['id'];
      $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
      $query = 
        "USE 'http://github.com/vikram0/yql-tables/raw/master/shoppingcart/
        shoppingcart.YourProducts.xml' AS shoppingcart; 
        DELETE FROM shoppingcart WHERE apiKey = '" . PAYVMENT_API_KEY . "' 
        AND cartID = '" . $_SESSION['cart'] . "' AND itemID = '" . $id . "' 
        AND retailerID = '" . PAYVMENT_ID . "'";
      $client->q($query);       
      $result = $client->post();
      if ($result->results->cartfeed->status->StatusCode != 200) {
        throw new Exception ('ERROR: Cannot remove item from shopping cart');  
      }     
      break;  
    // update = UPDATE
    case 'update':
      $id = $_REQUEST['id'];
      $qty = $_REQUEST['qty'];
      $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
      $query = 
        "USE 'http://github.com/vikram0/yql-tables/raw/master/shoppingcart/
        shoppingcart.YourProducts.xml' AS shoppingcart; 
        UPDATE shoppingcart SET itemQty = $qty WHERE apiKey = '" . PAYVMENT_API_KEY . "' 
        AND cartID = '" . $_SESSION['cart'] . "' AND itemID = '" . $id . "' 
        AND retailerID = '" . PAYVMENT_ID . "'";
      $client->q($query);       
      $result = $client->post();
      if ($result->results->cartfeed->status->StatusCode != 200) {
        throw new Exception ('ERROR: Cannot update item in shopping cart');  
      }         
      break;      
  }

  // get current cart contents (summary and individual items)
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $query = 
    "USE 'http://github.com/vikram0/yql-tables/raw/master/shoppingcart/
    shoppingcart.YourProducts.xml' AS shoppingcart; 
    SELECT * FROM shoppingcart WHERE apiKey='" . PAYVMENT_API_KEY . "' 
    AND cartID='" . $_SESSION['cart'] . "'";
  $client->q($query);       
  $result = $client->get();
  if ($result->results->cartfeed->status->StatusCode == 200) {
    $cart_summary = $result->results->cartfeed->results;
    $cart_items = $result->results->cartfeed->items->storeitems;
  } else {
    throw new Exception ('ERROR: Cannot get shopping cart contents');  
  }  
} catch (Exception $e) {
  die($e->getMessage());  
}
?>
<html>
  <head>
    <style type="text/css">
      table {
                 border-width: 1px;
                 border-spacing: 2px;
                 border-style: outset;
                 border-color: red;
                 border-collapse: collapse;
                 background-color: white;
      }
      table th {
                 border-width: 1px;
                 padding: 4px;
                 border-style: inset;
                 border-color: red;
                 background-color: white;
                 -moz-border-radius: 0px 0px 0px 0px;
        font-weight: bolder;
      }
      table td {
                 border-width: 1px;
                 padding: 4px;
                 border-style: inset;
                 border-color: red;
                 background-color: white;
                 -moz-border-radius: 0px 0px 0px 0px;
      }
      </style>
  </head>
  <body>    
    <h2>Current Shopping Cart</h2>
    <table>
      <tr>
        <th>Item</th>
        <th>Price</th>
        <th>Quantity</th>
        <th>Subtotal</th>
        <th></th>
        <th></th>
      </tr>
      <?php foreach ($cart_items->item as $i): // display current cart ?>
      <form method="post">
      <input type="hidden" name="id" value="<?php echo $i->ItemID; ?>" />
      <tr>
        <td><?php echo $i->ItemName; ?></td>
        <td><?php echo $i->ItemPrice; ?></td>
        <td><input type="text" name="qty" size="3" 
          value="<?php echo $i->ItemQty; ?>" /></td>
        <td><?php echo $i->ItemQtyPrice; ?></td>
        <td><input type="submit" name="op" value="Update" /></td>
        <td><input type="submit" name="op" value="Remove" /></td>
      </tr>  
      </form>
      <?php endforeach; ?>
      <tr>
        <td colspan="3">Total</td>
        <td><?php echo $cart_summary->GrandTotal; ?></td>  
        <td colspan="2"></td>
      </tr>
    </table>  

    <h2>Product Catalog</h2>
    <table>
      <tr>
        <th>Title</th>
        <th>Price</th>
        <th></th>
      </tr>
      <?php foreach ($catalog as $id => $item): // display product catalog ?>
      <form method="post">
      <input type="hidden" name="id" value="<?php echo $id; ?>" />
      <tr>
        <td><?php echo $item['title']; ?></td>
        <td><?php echo $item['price']; ?></td>
        <td><input type="submit" name="op" value="Add" /></td>
      </tr>
      </form>
      <?php endforeach; ?>
    </table>
  </body>
</html>

这段代码乍看起来让人生畏,但是它实际上比较简单。下面是对 清单 5 中关键部分的细分:

  • 脚本的前几行定义用户的 Payvment API 键和惟一用户标识符。这些令牌可从 Payvment 站点(参见 参考资料 获取链接)免费获取(注册后)。在这几行代码之后,脚本将一个简单产品目录定义为一个数组,以数组键为 SKUs,而对应的值存放产品信息。该产品目录被格式化为一个 HTML 表,带有可供用户添加商品到其购物车的控件。
  • 接下来,脚本初始化一个会话,检查会话是否已经包含一个购物车 ID。这个 ID 旨在跨 Payvment API 惟一标识用户的购物车,且它必须包含在大部分 API 调用中。如果会话中有购物车 ID,脚本继续下一步;如果没有,它制定并执行一个返回新购物车 ID 的虚拟 SELECT 查询,然后将该 ID 保存到会话。
  • 之后脚本检查看当前请求是否包含一个添加、编辑或删除操作,由 $_REQUEST['op'] 变量表示。根据所请求的操作,Zend_Rest_Client 使用 YQL 对 Payvment 数据执行一个 INSERT、UPDATE 或 DELETE 查询。注意,每个查询都包含 AIP 键、用户 ID,有时还包含被修改或删除的商品的 ID。
  • 最后,脚本执行一个 SELECT 查询来检索购物车的当前内容。然后该信息被格式化为一个可显示的 HTML 表,连同用于修改数量或删除商品的适当控件。注意,Payvment API 根据添加商品到购物车时提供的信息自动计算商品小计和购物车总量。

注意,在撰写本文时,Payvment 服务的 YQL 社区表使用一个过期的 REST API 端点,不过一个补丁正在开发中。上一个清单利用作者的补丁版本的表,在 Github(参见 参考资料)中有提供。

图 5 展示运行中的应用程序。

图 5. 基于云的购物车
基于云的购物车和产品目录屏幕截图
基于云的购物车和产品目录屏幕截图

通过 YQL 使用 INSERT、DELETE 和 UPDATE 查询时有一个要点需要注意。支持这些操作的大部分 web 服务都需要一个用户名、密码和/或 AIP 键来执行数据修改;通常您可以从每个服务的网站免费获取这些凭据。在这些情况下,添加或修改数据的程序类似于前面的清单,其中凭据构成 YQL 查询的一部分,通过 Zend_Rest_Client 被传递给 YQL 查询。

对于需要 OAuth 身份验证的服务,最好使用一个支持 OAuth 的工具包,比如 Yahoo! Open Social SDK,而非 Zend_Rest_Client,因为它为您处理 OAuth 身份验证的所有细节。您可以在 参考资料 中找到对这个 SDK 的链接,以及到其他文档的链接。

为 YQL 结果分页

在本文的前半部分,您了解了 YQL 如何过滤和排列搜索结果,使用 WHERE 子句支持和实用功能来对结果集进行排序、计数,并删除重复结果。不过这些只是一部分;YQL 还能够通过本地远程 分页限值来限制结果集中的记录数。

一个远程 限值控制 YQL 从第三方 Web 服务检索的记录的数量;一个本地 限值控制 YQL 返回给调用应用程序的记录的数量。

如果没有指定限值,YQL 通常会返回 10 条记录。要修改这个数目,在圆括号中向查询传递一个限值,比如,下面的示例将远程限值设置为 15 条记录:

SELECT * FROM music.artist.popular (15)

要返回所有记录,将限值设定为 0,如下所示:

SELECT * FROM music.artist.popular (0)

本地限值通过 LIMIT 和 OFFSET 子句执行,其运作方式与 SQL 语句很像:LIMIT 子句定义要返回的记录数,而 OFFSET 子句指定起始偏移值。下面的示例返回结果集中的第 5 到 9 条记录:

SELECT * FROM music.artist.popular (15) LIMIT 5 OFFSET 5

拥有这些控制之后,使用像 Zend_Paginator 这样的分页小部件就可以相当容易地为 YQL 结果实现简单的分页系统。比如,清单 6 展示一种为 YQL 结果分页的方法:

清单 6. 使用 Zend_Paginator 为 YQL 结果分页
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');
Zend_Loader::loadClass('Zend_Paginator');
Zend_Loader::loadClass('Zend_Paginator_Adapter_Array');

try {
  // execute YQL query
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->q("SELECT name FROM music.artist.popular (0)"); 
  $result = $client->get();
  $data = array();
  foreach ($result->results->Artist as $artist) {
    $data[] = (string)$artist['name'];  
  }

  // initialize pager with data set
  $pager = new Zend_Paginator(new Zend_Paginator_Adapter_Array($data));

  // set page number from request
  $currentPage = isset($_GET['p']) ? (int) htmlentities($_GET['p']) : 1;
  $pager->setCurrentPageNumber($currentPage);

  // set number of items per page from request
  $itemsPerPage = isset($_GET['c']) ? (int) htmlentities($_GET['c']) : 10;
  $pager->setItemCountPerPage($itemsPerPage);

  // get pages
  $pages = $pager->getPages();

  // create page links
  $pageLinks = array();
  $separator = ' | ';
  for ($x=1; $x<=$pages->pageCount; $x++) {
    if ($x == $pages->current) {
      $pageLinks[] = $x;      
    } else {
      $q = http_build_query(array('p' => $x, 'c' => $itemsPerPage));
      $pageLinks[] = "<a href=\"?$q\">$x</a>";  
    }
  } 

} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

// iterate over current page set
echo '<h2>Popular Artists</h2>';
$ctr = $pages->firstItemNumber;
foreach ($pager->getCurrentItems() as $item) {
  echo  $ctr . '. ' . $item . '<br/>';  
  $ctr++;
}

// print page links
echo '<div>';
echo 'Pages:' . implode($pageLinks, $separator);
echo '</div>';
?>

清单 6 首先载入 Zend_Paginator 组件。顾名思义,这是一个现成的 Zend Framework 组件,您可以用它来为大型数据集分页。它还提供一套现成的方法来生产页面链接,便于用户浏览结果集的不同页面。

加载 Zend_Paginator 之后,清单 6 执行一个无限制的 SELECT 查询来检索完整的 YQL 结果集。然后将返回的数据报告给一个 PHP 数组,并将其传递给 Zend_Paginator 数组数据源,该数据源负责提取当前页面所需的数组子集。Zend_Paginator 还负责生成页面导航链接,从而允许用户向前或向后浏览结果集。

该方法不是为 YQL 结果分页最有效的方式,您应当避免在生产环境中使用该示例。清单 6 必须重新对每个请求执行 YQL 查询,向分页练习添加时间和性能成本。一种更有效的方法是将 YQL 结果集存储在一个服务器端缓存,并从那里为每个请求检索数据,而不是从 YQL 服务请求它。清单 7 展示了这种改进的方法,使用 Zend_Cache 将 YQL 结果保存在一个服务器端文件缓存中。

清单 7. 使用 Zend_Paginator 和 Zend_Cache 的一种更有效的分页方法
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');
Zend_Loader::loadClass('Zend_Paginator');
Zend_Loader::loadClass('Zend_Cache');
Zend_Loader::loadClass('Zend_Paginator_Adapter_Array');

try {

  // set up cache
  $front = array(
     'lifetime' => 2000, 
     'automatic_serialization' => true
  );
  $back = array(
      'cache_dir' => './' 
  );
  $cache = Zend_Cache::factory('Core', 'File', $front, $back);

  // look for YQL data in cache and use if available
  // if not, execute YQL query, get fresh result set and save to cache
  if(!$data = $cache->load('yql')) {     
    // execute YQL query
    // get total count of items
    $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
    $client->q("SELECT name FROM music.artist.popular (0)"); 
    $result = $client->get();
    $data = array();
    foreach ($result->results->Artist as $artist) {
      $data[] = (string)$artist['name'];  
    }
    $cache->save($data, 'yql');
  }

  // initialize pager with data set
  $pager = new Zend_Paginator(new Zend_Paginator_Adapter_Array($data));

  // set page number from request
  $currentPage = isset($_GET['p']) ? (int) htmlentities($_GET['p']) : 1;
  $pager->setCurrentPageNumber($currentPage);

  // set number of items per page from request
  $itemsPerPage = isset($_GET['c']) ? (int) htmlentities($_GET['c']) : 10;
  $pager->setItemCountPerPage($itemsPerPage);

  // get pages
  $pages = $pager->getPages();

  // create page links
  $pageLinks = array();
  $separator = ' | ';
  for ($x=1; $x<=$pages->pageCount; $x++) {
    if ($x == $pages->current) {
      $pageLinks[] = $x;      
    } else {
      $q = http_build_query(array('p' => $x, 'c' => $itemsPerPage));
      $pageLinks[] = "<a href=\"?$q\">$x</a>";  
    }  
  } 

} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

// iterate over current page set
echo '<h2>Popular Artists</h2>';
$ctr = $pages->firstItemNumber;
foreach ($pager->getCurrentItems() as $item) {
  echo  $ctr . '. ' . $item . '<br/>';  
  $ctr++;
}

// print page links
echo '<div>';
echo 'Pages:' . implode($pageLinks, $separator);
echo '</div>';
?>

清单 7 背后的基本逻辑很简单:对于每个请求,脚本检查缓存看 YQL 结果是否已经存在。如果是,缓存的副本被用于填充 Zend_Paginator 数据数组,分页照常继续。仅当无法找到缓存副本或缓存副本过期时才执行 YQL 查询,结果再次保存到缓存以备将来使用。

作为一种替代解决方案,您也可以考虑将结果数据存储在客户端变量中,并在用户在页面间移动时使用 JavaScript 动态填充页面容器。

图 6 展示了输出:

图 6. 经过分页的 YQL 结果
经过分页的 YQL 结果屏幕截图(当红艺人名单)
经过分页的 YQL 结果屏幕截图(当红艺人名单)

执行多个查询

YQL 还提供大量额外实用工具表,可用于各种用途,比如同时运行多个查询或解析外部提要。为了说明,清单 8 使用特殊的 ‘yql.query.multi’ 表来同时运行三个查询:

清单 8. 执行一个 YQL 多查询
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute multiple YQL queries
// combine results
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $q=<<<EOF
    SELECT * 
    FROM yql.query.multi 
    WHERE queries="
      SELECT * FROM geo.places WHERE text='london';
      SELECT * FROM geo.places WHERE text='hong kong';
      SELECT * FROM geo.places WHERE text='tanzania'
    "
EOF;
  $client->q($q);
  $result = $client->get();
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

// iterate over query result set
foreach ($result->results->results as $result) {
  foreach ($result->place as $place) {
    echo '<div>';
    echo '<strong>' . $place->name . ', ' . 
      $place->country . ' (' . $place->placeTypeName . 
      ') </strong> <br/>';
    echo 'Latitude:' . $place->centroid->latitude . 
    ' Longitude:' . $place->centroid->longitude;
    echo '</div>';    
  }
}  
?>

清单 8 包含三个独立的 YQL 查询,每个查询都从 geo.places 表搜寻有关不同城市的信息。为了更高效,这三个查询被包装成针对 yql.query.multi 表的一个查询,每个查询的结果都被合并到一个结果文档中。以这种方式执行一个多查询具有一些好处:代码实现更高可读性,而且如果所有查询都针对一个表,查询速度会大大提高,因为 YQL 使用内部缓存来避免多次读取相同的表定义文件。

图 7 展示清单 8 的输出。

图 7. YQL 多查询的合并结果
YQL 多查询的合并结果屏幕截图(条目包括城镇、国家、维度和经度)
YQL 多查询的合并结果屏幕截图(条目包括城镇、国家、维度和经度)

解析 XML 提要

另一套实用工具表简化对标准 XML 格式文档(比如 RSS 或 Atom 提要)中数据的提取和解析。例如,清单 9 展示如何使用 YQL 解析一个外部 RSS 提要并使用 rss 表从其中提取信息子集:

清单 9. 使用 YQL 解析 RSS 提要
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute YQL query
// get 5 top stories from Google News RSS feed
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->q("SELECT title FROM rss (5) 
    WHERE url='http://news.google.com/news?ned=us&topic=h&output=rss'");
  $result = $client->get();
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

// iterate over query result set
echo '<h2>Top Stories</h2>';
foreach ($result->item as $item) {
  echo '<div>' . $item->title . '</div>';
}  
?>

图 8 展示了输出。

图 8. Google 新闻故事列表,使用 YQL 从一个 RSS 提要中提取
Google 新闻故事列表屏幕截图,使用 YQL 提取自一个 RSS 提要
Google 新闻故事列表屏幕截图,使用 YQL 提取自一个 RSS 提要

同样地,您可以使用一个 xml 表向任何 XML 文档应用 YQL 过滤器,结合使用 XPath 检索特定的节点集。清单 10 展示该技术的一个示例。

清单 10. 使用 YQL 解析 XML 文档
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute YQL query
// get 5 top stories from Google News RSS feed
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->q("SELECT title, link FROM xml (5) 
    WHERE url='http://news.google.com/news?ned=us&topic=h&output=rss' 
    AND itemPath='//item'");
  $result = $client->get();
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

// iterate over query result set
echo '<h2>Top Stories</h2>';
foreach ($result->item as $i) {
  echo '<div><a href="' . $i->link . '">' . $i->title . 
  '</a></div>';
}  
?>

清单 10 使用一个 SELECT 查询来解析一个外部 XML 文档,并对结果应用一个 XPath 表达式来选取节点的特定子集。以通用的方式向 XML 数据应用类似 SQL 的语法和过滤器的这种能力,可以极大地减轻从完整的 XML 文档树提取信息的工作。

图 9 展示了输出。

图 9. Google 新闻故事列表,使用 YQL 从一个 XML 提要中提取
Google 新闻故事列表屏幕截图,使用 YQL 提取自一个 XML 提要
Google 新闻故事列表屏幕截图,使用 YQL 提取自一个 XML 提要

从 HTML 页面提取数据

YQL 还可以从 HTML web 页面提取数据。YQL 包含一个 html 实用工具表,您可以将它作为一个过滤器来从一个 HTML 页面挖出特定的数据。XPath 提供从 HTML 文档树定位和检索单个节点或节点集所需的表达式语言。

为说明这是如何工作的,假设您希望获取印度各邦和中央直辖区的完整列表。该信息在印度官方政府网站中以格式化的 HTML 表予以提供(图 10)。查看 图 10 的放大图

图 10. 包含印度各邦相关数据的一个 web 页面的 HTML 源
包含印度各邦相关数据的一个 web 页面的 HTML 源屏幕截图
包含印度各邦相关数据的一个 web 页面的 HTML 源屏幕截图

使用 YQL,仅需一个 SELECT 查询即可从源页面提取数据到 YQL 结果集中,如 清单 11 所示。

清单 11. 使用 YQL 解析 HTML 文档
<?php
// set up Zend auto-loader
// load Zend REST client classes
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Rest_Client');

// execute YQL query
// scrape GOI page to get list of Indian states
try {
  $client = new Zend_Rest_Client('http://query.yahooapis.com/v1/public/yql');
  $client->q("SELECT content FROM html 
    WHERE url='http://india.gov.in/knowindia/state_uts.php' 
    AND xpath='//ul[@class=\'sec1\']/li/a'");

  $result = $client->get();
} catch (Zend_Rest_Client_Exception $e) {
    echo "Client error: " . $e->getResponse();
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
// iterate over query result set
echo '<h2>List of States in India</h2>';
echo '<ul>';
foreach ($result->a as $state) {
  echo "<li>$state</li>";
}
echo '</ul>';
?>

图 11 展示了输出。

图 11. 印度各邦列表,使用 YQL 提取自一个 web 页面
印度各邦列表屏幕截图,使用 YQL 提取自一个 web 页面
印度各邦列表屏幕截图,使用 YQL 提取自一个 web 页面

结束语

显然,使用 YQL 可以做的很多,不仅仅是从一个或多个 web 服务提取数据。通过 YQL,可以使用标准的 INSERT、UPDATE 和 DELETE 查询添加和修改第三方 web 应用程序中的数据,这种能力便于创建从多个数据源读写数据的基于云计算的应用程序。YQL 还提供大量实用工具表,可以使用 YQL XPath 组合过滤器从 RSS、Atom、CSV 和 HTML 等流行格式的文档中提取信息。

YQL 目前正处于积极开发中,因此预计在将来会有更多益处。请随意对 YQL 进行试验,或甚至在您开始将 YQL 集成到 PHP 应用程序时将您自己的数据表添加到社区存储库。祝编程愉快!


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source
ArticleID=606059
ArticleTitle=通过 YQL 和 PHP 构建 web 应用程序,第 2 部分
publish-date=01042011