RSS(Really Simple Syndication)是內容分發的標準格式,廣泛用於博客、新聞站點、播客等場景。PHP 內置的 XMLReader、SimpleXML、DOMDocument 以及 PEAR 的 XML_RSS 都能處理 RSS。本文涵蓋解析外部 RSS、生成自定義 RSS 兩大核心需求,提供現代最佳實踐代碼。
<?php
function parseRss(string $url, int $limit = 20): array {
// 設置超時,防止掛起
$context = stream_context_create([
'http' => ['timeout' => 10, 'user_agent' => 'PHP RSS Parser']
]);
$xml = simplexml_load_file($url, 'SimpleXMLElement', LIBXML_NOCDATA, $context);
if (!$xml) return [];
$items = [];
foreach ($xml->channel->item as $item) {
$items[] = [
'title' => (string)$item->title,
'link' => (string)$item->link,
'description' => (string)$item->description,
'pubDate' => isset($item->pubDate) ? (string)$item->pubDate : '',
'guid' => isset($item->guid) ? (string)$item->guid : '',
'author' => isset($item->author) ? (string)$item->author : '',
'category' => isset($item->category) ? (string)$item->category : '',
// 媒體附件(播客、圖片)
'enclosure' => isset($item->enclosure) ? [
'url' => (string)$item->enclosure['url'],
'type' => (string)$item->enclosure['type'],
'length' => (string)$item->enclosure['length']
] : null
];
if (count($items) >= $limit) break;
}
return $items;
}
// 使用示例
$articles = parseRss('https://www.php.net/feed.atom'); // PHP 官網也支持 Atom
foreach ($articles as $art) {
echo "{$art['pubDate']} - {$art['title']}
";
}
<?php
function parseRssLarge(string $url): Generator {
$reader = new XMLReader();
$reader->open($url);
$inItem = false;
$current = [];
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT) {
if ($reader->localName === 'item') {
$inItem = true;
$current = [];
} elseif ($inItem && in_array($reader->localName, ['title', 'link', 'description', 'pubDate', 'guid'])) {
$reader->read(); // 移動到文本節點
$current[$reader->localName] = $reader->value;
}
} elseif ($reader->nodeType === XMLReader::END_ELEMENT && $reader->localName === 'item') {
$inItem = false;
yield $current;
}
}
$reader->close();
}
// 逐條處理,內存常數級
foreach (parseRssLarge('https://example.com/large-feed.xml') as $item) {
// 處理單條,如入庫
saveToDatabase($item);
}
使用 DOMDocument 構建結構化 XML,確保輸出合規、編碼正確、可擴展。
<?php
class RssGenerator {
private $dom;
private $channel;
public function __construct(array $config) {
$this->dom = new DOMDocument('1.0', 'UTF-8');
$this->dom->formatOutput = true; // 可讀性縮進
$rss = $this->dom->createElement('rss');
$rss->setAttribute('version', '2.0');
$rss->setAttribute('xmlns:atom', 'https://www.w3.org/2005/Atom');
$rss->setAttribute('xmlns:content', 'https://purl.org/rss/1.0/modules/content/');
$this->dom->appendChild($rss);
$channel = $this->dom->createElement('channel');
$rss->appendChild($channel);
$this->channel = $channel;
// 必填字段
$this->addElement('title', $config['title']);
$this->addElement('link', $config['link']);
$this->addElement('description', $config['description']);
$this->addElement('language', $config['language'] ?? 'zh-CN');
$this->addElement('pubDate', date('r'));
$this->addElement('lastBuildDate', date('r'));
$this->addElement('generator', 'Custom PHP RSS Generator');
// Atom 自引用鏈接(SEO 友好)
$atomLink = $this->dom->createElement('atom:link');
$atomLink->setAttribute('href', $config['self_link'] ?? $config['link']);
$atomLink->setAttribute('rel', 'self');
$atomLink->setAttribute('type', 'application/rss+xml');
$channel->appendChild($atomLink);
}
public function addItem(array $item): self {
$elem = $this->dom->createElement('item');
$this->addChild($elem, 'title', $item['title']);
$this->addChild($elem, 'link', $item['link']);
$this->addChild($elem, 'guid', $item['guid'] ?? $item['link'], ['isPermaLink' => 'false']);
$this->addChild($elem, 'description', $item['description'], [], true); // CDATA
$this->addChild($elem, 'pubDate', $item['pubDate'] ?? date('r'));
if (!empty($item['author'])) {
$this->addChild($elem, 'author', $item['author']);
}
if (!empty($item['category'])) {
foreach ((array)$item['category'] as $cat) {
$this->addChild($elem, 'category', $cat);
}
}
if (!empty($item['enclosure'])) {
$enc = $this->dom->createElement('enclosure');
$enc->setAttribute('url', $item['enclosure']['url']);
$enc->setAttribute('type', $item['enclosure']['type']);
$enc->setAttribute('length', $item['enclosure']['length']);
$elem->appendChild($enc);
}
// 支持全文內容模塊
if (!empty($item['content'])) {
$content = $this->dom->createElement('content:encoded');
$content->appendChild($this->dom->createCDATASection($item['content']));
$elem->appendChild($content);
}
$this->channel->appendChild($elem);
return $this;
}
public function output(): void {
header('Content-Type: application/rss+xml; charset=UTF-8');
header('Cache-Control: public, max-age=3600');
echo $this->dom->saveXML();
}
private function addElement(string $name, string $value, array $attrs = []): void {
$el = $this->dom->createElement($name, htmlspecialchars($value, ENT_XML1, 'UTF-8'));
foreach ($attrs as $k => $v) $el->setAttribute($k, $v);
$this->channel->appendChild($el);
}
private function addChild(DOMElement $parent, string $name, string $value, array $attrs = [], bool $cdata = false): void {
$el = $this->dom->createElement($name);
foreach ($attrs as $k => $v) $el->setAttribute($k, $v);
if ($cdata) {
$el->appendChild($this->dom->createCDATASection($value));
} else {
$el->appendChild($this->dom->createTextNode($value));
}
$parent->appendChild($el);
}
}
// 使用示例
$rss = new RssGenerator([
'title' => '技術博客',
'link' => 'https://blog.example.com',
'description' => '分享 PHP、Laravel、架構設計等技術文章',
'language' => 'zh-CN',
'self_link' => 'https://blog.example.com/rss.xml'
]);
// 從數據庫獲取文章
$articles = $pdo->query('SELECT id, title, slug, excerpt, content, created_at FROM posts WHERE status=1 ORDER BY created_at DESC LIMIT 20')->fetchAll();
foreach ($articles as $art) {
$rss->addItem([
'title' => $art['title'],
'link' => "https://blog.example.com/post/{$art['slug']}",
'guid' => "post-{$art['id']}",
'description' => $art['excerpt'],
'content' => $art['content'], // 全文
'pubDate' => date('r', strtotime($art['created_at'])),
'category' => ['PHP', '後端開發'],
'author' => 'author@example.com (作者名)'
]);
}
$rss->output();
date('r') 或 DateTime::RFC2822,如 Mon, 15 Jan 2024 10:30:00 +0800。description、content:encoded 使用 CDATA 避免 HTML 實體破壞 XML 結構。isPermaLink="false" + 業務唯一 ID(如 post-123)。ETag/Last-Modified,解析端發送 If-None-Match/If-Modified-Since 減少流量。| 現象 | 可能原因 | 解決 |
|---|---|---|
| 輸出亂碼 | 文件編碼非 UTF-8 或未發送正確 Header | 文件存為 UTF-8 無 BOM,header('Content-Type: application/rss+xml; charset=UTF-8') |
| XML 解析錯誤 | 內容包含非法字符(如控制字符) | preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/', '', $str) 清洗 |
| 圖片不顯示 | description 為純文本或相對路徑 | 使用 content:encoded 放全文 HTML,圖片用絕對 URL |
| 訂閱器報錯 | 缺少必要字段或命名空間 | 參考 W3C Feed Validator 驗證 |
RSS 雖然「老派」,但在內容分發、播客、Newsletter、跨站聚合等場景仍不可替代。掌握 PHP 處理 RSS 的正確姿勢,能為你的產品增加一條穩定的內容觸達通道。