極個人的プログラミング学習備忘録

HTML、CSS、PHP、JavaScript、Vueなどで学習したことをメモしていきます

PHPでフィードリーダー作ろう!3つの形式(RSS1.0、RSS2.0、ATOM)をサクッと取得

フィードっていろいろある

フィードは情報を配信するための形式のこと。RSS 1.0, 2.0, ATOMなど、いくつかの種類がある。それぞれ少しずつ形式や構造が違うけど、今回はこれらすべてに対応したリーダーを作る方法を紹介するよ!

それぞれのフィード、どうやって読む?

フィードって基本的にXML形式で書かれている。だから、SimpleXMLというPHPのライブラリを使って解析することができる。RSSやATOMそれぞれの特徴を理解しながら、タグを解析して欲しい情報を取り出してみよう。ちょっとしたコツで、どんなフィードもサクッと読めるようになるよ。

複数のサイトのフィードを効率よく取得する

複数のサイトのフィードを効率的に取得するため、curl_multi_* 関数を利用。また、負荷対策のため、10個ずつ処理するようにしている。

PHPフィードリーダーコード

<?php

/*
1. fetchFeeds($feedUrls)
この関数は、与えられたURLリストから、それぞれのフィードの内容を取得します。

引数:
$feedUrls: フィードを取得したいURLの配列。

戻り値:
フィードの内容を含む配列。

詳細:
関数はURLリストを受け取り、それをバッチ単位に分割します。各バッチに対して、cURLマルチハンドリングを使用してフィードの内容を効率的に一斉に取得します。全てのバッチの取得が完了したら、その結果を結合して返します。

*/
function fetchFeeds(array $urls) {
    $multiCurl = curl_multi_init();
    $curlArray = [];

    foreach ($urls as $i => $url) {
        $curlArray[$i] = curl_init($url);
        curl_setopt($curlArray[$i], CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curlArray[$i], CURLOPT_USERAGENT, 'CustomFeedParser/1.0');
        curl_setopt($curlArray[$i], CURLOPT_CONNECTTIMEOUT, 10);  // 接続のタイムアウトを10秒に設定
        curl_setopt($curlArray[$i], CURLOPT_TIMEOUT, 30);         // リクエスト全体の最大実行時間を30秒に設定

        curl_multi_add_handle($multiCurl, $curlArray[$i]);
    }

    $active = null;
    do {
        $status = curl_multi_exec($multiCurl, $active);
        if ($active) {
            curl_multi_select($multiCurl);
        }
    } while ($active && $status == CURLM_OK);

    $results = [];
    foreach ($curlArray as $id => $curl) {
        if (curl_errno($curl)) {
            $results[$id] = ['error' => curl_error($curl)];
        } else {
            $results[$id] = curl_multi_getcontent($curl);
        }
        curl_multi_remove_handle($multiCurl, $curl);
    }

    curl_multi_close($multiCurl);

    return $results;
}

/*
2. escape($data)
この関数は、文字列や配列内の文字列を安全にエスケープして、XSS攻撃から保護します。

引数:
$data: エスケープしたい文字列、またはその文字列を含む配列。

戻り値:
エスケープされた文字列、またはその文字列を含む配列。

詳細:
関数は再帰的に動作し、深い階層の配列内の文字列までエスケープを行います。エスケープには、htmlspecialchars関数を使用しています。

*/
function escape($data) {
    return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}

/*
3. parseFeeds($feeds)
この関数は、フィードの内容を解析して、エントリーのタイトルと日付を取得します。

引数:
$feeds: fetchFeeds関数で取得されるフィード内容の配列。

戻り値:
エントリータイトルと日付を含む配列。

詳細:
関数内では、SimpleXMLを使用してフィードの内容を解析します。RSS 1.0, RSS 2.0, ATOMの各形式を識別し、それぞれの形式に応じてタイトルと日付を抽出します。

*/
function parseFeeds($feedsContent) {
    $parsedFeeds = [];
    // libxmlのエラーレポートを有効にする
    libxml_use_internal_errors(true);

    foreach ($feedsContent as $content) {
        $xml = simplexml_load_string($content, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_PARSEHUGE | LIBXML_NOENT);
        if ($xml === false) {
            $parsedFeeds[] = ['error' => 'Failed to parse XML'];
            continue;
        }

        $feedType = '';
        if (isset($xml->channel)) {
            $feedType = 'rss2.0';
        } elseif (isset($xml->entry)) {
            $feedType = 'atom';
        } elseif (isset($xml->item)) {
            $feedType = 'rss1.0';
        }

        switch ($feedType) {
            case 'rss2.0':
                $feedData = [
                    'title'       => (string) $xml->channel->title,
                    'link'        => (string) $xml->channel->link,
                    'description' => (string) $xml->channel->description,
                    'items'       => []
                ];
                foreach ($xml->channel->item as $item) {
                    $date = isset($item->pubDate) ? (string) $item->pubDate : '';
                    $feedData['items'][] = [
                        'title'       => (string) $item->title,
                        'link'        => (string) $item->link,
                        'description' => (string) $item->description,
                        'date'        => $date
                    ];
                }
                break;
            case 'atom':
                $feedData = [
                    'title'       => (string) $xml->title,
                    'link'        => (string) $xml->link['href'],
                    'description' => (string) $xml->subtitle,
                    'items'       => []
                ];
                foreach ($xml->entry as $entry) {
                    $date = isset($entry->published) ? (string) $entry->published : '';
                    $feedData['items'][] = [
                        'title'       => (string) $entry->title,
                        'link'        => (string) $entry->link['href'],
                        'description' => (string) $entry->summary,
                        'date'        => $date
                    ];
                }
                break;
            case 'rss1.0':
                $feedData = [
                    'title'       => (string) $xml->title,
                    'link'        => (string) $xml->link,
                    'description' => (string) $xml->description,
                    'items'       => []
                ];
                foreach ($xml->item as $item) {
                    $date = isset($item->children('http://purl.org/dc/elements/1.1/')->date) 
                            ? (string) $item->children('http://purl.org/dc/elements/1.1/')->date
                            : '';
                    $feedData['items'][] = [
                        'title'       => (string) $item->title,
                        'link'        => (string) $item->link,
                        'description' => (string) $item->description,
                        'date'        => $date
                    ];
                }
                break;
            default:
                $feedData = ['error' => 'Unknown feed format'];
        }
        $parsedFeeds[] = $feedData;
    }
    return $parsedFeeds;
}

/*
4. processBatch($batchUrls)
この関数は、与えられたバッチのURLリス[f:id:onsen222:20230824123112j:plain]トから、それぞれのフィードの内容を取得します。

引数:
$batchUrls: フィードを取得したいURLのサブセット(バッチ単位でのリスト)。

戻り値:
バッチ内のURLから取得したフィードの内容を含む配列。

詳細:
この関数はfetchFeeds関数の内部で使用されます。バッチ内の各URLに対してcURLハンドルを初期化し、それらのハンドルを使用してフィードの内容を並行して取得します。

*/
function processBatch(array $batchUrls) {
    $feedsContent = fetchFeeds($batchUrls);
    $feedsData = parseFeeds($feedsContent);

    foreach ($feedsData as $feedData) {
        if (isset($feedData['error'])) {
            echo escape($feedData['error']);
        } else {
            echo "Feed Title: " . escape($feedData['title']) . "<br>";
            echo "Feed Link: " . escape($feedData['link']) . "<br>";
            //echo "Feed Description: " . escape($feedData['description']) . "<br><hr>";
            foreach ($feedData['items'] as $item) {
                echo "Item Title: " . escape($item['title']) . "<br>";
                echo "Item Link: " . escape($item['link']) . "<br>";
                //echo "Item Description: " . escape($item['description']) . "<br>";
                echo "Item Date: " . escape($item['date']) . "<br><hr>";
            }
        }
    }
}

$urls = [
    'FEED_URL_1',
    'FEED_URL_2',
    // ... 他のフィードURL、例えば100個以上
];

//サーバの負荷対策のため、10個ずつ処理
$batches = array_chunk($urls, 10);

foreach ($batches as $batchUrls) {
    processBatch($batchUrls);
    // 必要に応じて、ここで一時的に処理を遅延させることも考慮できます
    sleep(2); // 例: 2秒待機
}

以上です!
(このコードのサポートは一切できません。自己責任でご利用ください)