百度站長平台要求 Sitemap 單文件不超 5 萬條 URL、體積 < 10MB,且支援移動適配標註。針對 70 萬條問答數據,本文實現分片生成(每片 5000 條)、索引文件自動維護、PC/移動雙端標註、低壓力增量抓取的完整方案。
SELECT ... WHERE id > $lastMaxId LIMIT 1000 游標式讀取,避免 OFFSET 深分頁性能劣化ask/0.xml ~ ask/N.xml,每文件 5000 條(5 次循環)ask.xml 為 Sitemap Index,引用所有分片Index.txt 記錄 文件名,最大ID,最小ID,已寫入條數,支援中斷續傳<?php
class SitemapGenerator {
private PDO $pdo;
private string $baseDir;
private string $baseUrl;
private int $batchSize = 1000; // 每次 DB 讀取
private int $maxPerFile = 5000; // 單 XML 文件上限
// URL 模板
private string $mobileTpl = 'https://m.example.com/ask/%d.html';
private string $pcTpl = 'https://www.example.com/ask/%d-p1.html';
public function __construct(PDO $pdo, string $baseDir, string $baseUrl) {
$this->pdo = $pdo;
$this->baseDir = rtrim($baseDir, '/') . '/';
$this->baseUrl = rtrim($baseUrl, '/') . '/';
}
public function generate(): void {
$indexFile = $this->baseDir . 'Index.txt';
$indexXmlFile = $this->baseDir . '../siteditu/ask.xml';
// 1. 讀取/初始化狀態
$state = $this->readState($indexFile);
$fileName = $state['fileName']; // 當前分片文件名(數字)
$written = $state['written']; // 當前分片已寫入條數
$lastMaxId = $state['lastMaxId']; // 上次讀取最大 ID
// 2. 計算本次讀取量
$need = $this->maxPerFile - $written;
$limit = min($this->batchSize, $need);
// 3. 讀取數據
$rows = $this->fetchQuestions($lastMaxId, $limit);
if (!$rows) {
echo "無新數據,生成結束";
return;
}
// 4. 構建 XML 內容
$xmlContent = '';
$minId = null;
$maxId = null;
foreach ($rows as $i => $row) {
if ($i === 0) $minId = $row['id'];
$maxId = $row['id'];
$lastmod = substr($row['last_time'], 0, 10); // Y-m-d
$mobileUrl = sprintf($this->mobileTpl, $row['id']);
$pcUrl = sprintf($this->pcTpl, $row['id']);
// 移動端 URL(含 mobile:mobile 標註)
$xmlContent .= $this->buildUrl($mobileUrl, $lastmod, true);
// PC 端 URL
$xmlContent .= $this->buildUrl($pcUrl, $lastmod, false);
}
// 5. 寫入分片文件
$filePath = $this->baseDir . "ask/{$fileName}.xml";
$this->writeXmlFile($filePath, $xmlContent, $written === 0);
// 6. 更新狀態
$newWritten = $written + count($rows) * 2; // 雙端各一條
$this->updateState($indexFile, [
'fileName' => $fileName,
'lastMaxId' => $maxId,
'lastMinId' => $minId,
'written' => $newWritten,
]);
// 7. 若分片滿,更新索引文件
if ($newWritten >= $this->maxPerFile) {
$this->updateIndexXml($indexXmlFile, $fileName);
// 下一分片狀態將在下次循環自動初始化
}
echo "分片 $fileName 寫入 " . count($rows) . " 條記錄({$newWritten} 條 URL),最大 ID: $maxId
";
}
private function fetchQuestions(int $lastMaxId, int $limit): array {
$sql = "SELECT id, last_time FROM questions WHERE id > ? ORDER BY id ASC LIMIT ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$lastMaxId, $limit]);
return $stmt->fetchAll();
}
private function buildUrl(string $url, string $lastmod, bool $isMobile): string {
$xml = "<url>\n";
$xml .= " <loc>" . htmlspecialchars($url) . "</loc>\n";
if ($isMobile) {
$xml .= " <mobile:mobile type=\"mobile\"/>\n";
}
$xml .= " <lastmod>$lastmod</lastmod>\n";
$xml .= " <changefreq>daily</changefreq>\n";
$xml .= " <priority>0.8</priority>\n";
$xml .= "</url>\n";
return $xml;
}
private function writeXmlFile(string $path, string $content, bool $isNew): void {
if ($isNew) {
$header = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$header .= '<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9" xmlns:mobile="https://www.baidu.com/schemas/sitemap-mobile/1/">' . "\n";
$footer = '</urlset>';
$full = $header . $content . $footer;
file_put_contents($path, $full, LOCK_EX);
} else {
// 插入到 </urlset> 之前
$xml = file_get_contents($path);
$xml = str_replace('</urlset>', $content . '</urlset>', $xml);
file_put_contents($path, $xml, LOCK_EX);
}
}
private function readState(string $indexFile): array {
if (!file_exists($indexFile)) {
@mkdir(dirname($indexFile), 0755, true);
file_put_contents($indexFile, '');
return ['fileName' => 0, 'lastMaxId' => 0, 'lastMinId' => 0, 'written' => 0];
}
$lines = file($indexFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!$lines) return ['fileName' => 0, 'lastMaxId' => 0, 'lastMinId' => 0, 'written' => 0];
$last = array_pop($lines);
[$fn, $max, $min, $written] = explode(',', $last);
return ['fileName' => (int)$fn, 'lastMaxId' => (int)$max, 'lastMinId' => (int)$min, 'written' => (int)$written];
}
private function updateState(string $indexFile, array $state): void {
$line = "{$state['fileName']},{$state['lastMaxId']},{$state['lastMinId']},{$state['written']}\n";
$lines = file($indexFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines && $state['written'] <= $this->maxPerFile) {
// 更新最後一行
$lines[count($lines)-1] = trim($line);
} else {
// 新增行
$lines[] = trim($line);
}
file_put_contents($indexFile, implode("\n", $lines) . "\n", LOCK_EX);
}
private function updateIndexXml(string $indexXmlFile, int $fileName): void {
$sitemapUrl = $this->baseUrl . "ask/setmapxml/{$fileName}.xml";
$lastmod = date('Y-m-d');
$entry = "<sitemap>\n <loc>$sitemapUrl</loc>\n <lastmod>$lastmod</lastmod>\n</sitemap>\n";
if (!file_exists($indexXmlFile)) {
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n<sitemapindex>\n</sitemapindex>";
file_put_contents($indexXmlFile, $xml);
}
$xml = file_get_contents($indexXmlFile);
$xml = str_replace('</sitemapindex>', $entry . '</sitemapindex>', $xml);
file_put_contents($indexXmlFile, $xml, LOCK_EX);
}
}
// --- 使用方式 ---
$pdo = new PDO('mysql:host=localhost;dbname=qa;charset=utf8mb4', 'user', 'pass');
$gen = new SitemapGenerator($pdo, '/var/www/html/sitemap/', 'https://www.example.com');
// 可放入 Cron 每小時執行一次,或瀏覽器分批觸發
$gen->generate();
?>
sitemap/
├── Index.txt # 狀態文件
│ 內容:0,3146886,3145887,5000
│ 1,3148886,3147887,1000
├── ask/
│ ├── 0.xml # 5000 條 URL(含 PC+移動)
│ ├── 1.xml
│ └── ...
└── ../siteditu/
└── ask.xml # Sitemap Index
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex>
<sitemap>
<loc>https://www.example.com/ask/setmapxml/0.xml</loc>
<lastmod>2024-01-15</lastmod>
</sitemap>
...
</sitemapindex>
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:mobile="https://www.baidu.com/schemas/sitemap-mobile/1/">
<url>
<loc>https://m.example.com/ask/7460.html</loc>
<mobile:mobile type="mobile"/>
<lastmod>2023-01-11</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.example.com/ask/7460-p1.html</loc>
<lastmod>2023-01-11</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
...
</urlset>
| 優化 | 實現 | 收益 |
|---|---|---|
| 游標式分頁 | WHERE id > $lastMaxId 替代 OFFSET | 深分頁性能 O(1),不隨數據增長而劣化 |
| 雙端合一 | 單次讀取同時生成 PC/移動 URL | 減半 DB 查詢、文件 I/O |
| 狀態文件續傳 | Index.txt 記錄進度 | 中斷重啟無損失,支援增量更新 |
| 分片尺寸可控 | 5000 條/文件,遠低於 5萬上限 | 留足冗餘,單文件解析極快 |
| 索引自動維護 | 分片滿時自動追加 <sitemap> | 無需人工干預,實時生效 |
0 * * * * /usr/bin/php /path/to/sitemap_gen.php >/dev/null 2>&1location /sitemap/ { alias /var/www/html/sitemap/; }
3. 百度站長平台提交:https://www.example.com/sitemap/ask.xml
4. 監控:定期檢查 Index.txt 最後行 written 是否增長、文件大小是否正常