首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 生成百度 Sitemap 站點地圖:分片輸出、索引文件、百度移動適配完整實作

     編輯:PHP 相關知識

百度站長平台要求 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 格式(符合百度移動適配規範)

<?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>無需人工干預,實時生效

部署建議

  1. 建立 Cron:0 * * * * /usr/bin/php /path/to/sitemap_gen.php >/dev/null 2>&1
  2. 2. Nginx 靜態服務:location /sitemap/ { alias /var/www/html/sitemap/; } 3. 百度站長平台提交:https://www.example.com/sitemap/ask.xml 4. 監控:定期檢查 Index.txt 最後行 written 是否增長、文件大小是否正常
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved