querySelectorでIEでもそれ以外でもネームスペース付きのXMLからデータを取得する方法

前のページ「Google Feed APIを使わずXMLHttpRequestでクロスドメインのRSSを取得する方法 まとめ」でJavaScriptのXMLHttpRequestを使ってRSSを取得するまで書きました。取得したRSSはネームスペース付きのXMLなので、どうやってデータにアクセスすればいいのかわかりませんでしたが、このページではそれを解決する方法をまとめています。jQueryは使っていません。

document.evaluateはIEで動かない

最近はIEで見ても、その他のブラウザで見ても同じに見えるので油断していました。

ググってみると「JavaScriptでデータを走査する時、namespaceを解決するにはdocument.evaluateを使う」というような感じだったので、それを参考にスクリプトを書いてGoogle Chromeで試したらちゃんと動いたんですけど、InternetExplorerでは動きません。document.evaluateはIEで実装していないらしいです。

という事はIEだけ別のソースを書かなくてはいけないのか?と思ったんですけど、querySelectorだけで両方対応出来ました。

responseTextではなくresponseXMLを使う

XMLHttpRequestで取得したデータをresponseTextで取って、それを次のようにHTMLにしてquerySelectorをかけてもうまくいきません。正確に言うと、これで取れるデータもあるんですが取れないデータがあります。

HTMLの受け皿に、下ではdivを使っていますが、ちゃんとそれ用のobjectがあったような気もします。もし、それをご存知ならそちらを使われた方がよろしいかと。。

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200){
        /* Bad example */
        var div = document.createElement('div');
        div.innerHTML = xhr.responseText;
        var items = div.querySelectorAll('item');
    }
};

xhr.open('GET', rss_url, true);
xhr.send('');

そうではなく、responseXMLにquerySelectorをかければ、namespaceがあってもデータが取得できます。

var items = xhr.responseXML.querySelectorAll('item');

namespaceがついたタグの指定方法

namespaceのついたタグを抽出する時は、querySelectorの引数に、namespaceを除いたタグ名を指定すればいいみたいです。

次のRSSから<dc:date>の中身を抜き出す場合を例にします。

<rdf:RDF xmlns:rdf="" xmlns:dc="" xmlns:admin="" xmlns:content="" xmlns="">
    <channel rdf:about="">
        <title></title>
        <link></link>
        <description></description>
        <dc:language></dc:language>
        <items>
            <rdf:Seq>
                <rdf:li rdf:resource=""/>
                <rdf:li rdf:resource=""/>
            </rdf:Seq>
        </items>
    </channel>
    <item rdf:about="">
        <link></link>
        <title></title>
        <description>
            ...
        </description>
        <dc:subject></dc:subject>
        <dc:creator></dc:creator>
        <dc:date>yyyy-mm-ddT00:00:00+09:00</dc:date>
        <content:encoded>
            <![CDATA[ ... ]]>
        </content:encoded>
    </item>
    <item rdf:about="">
        <link></link>
        <title></title>
        <description>
            ...
        </description>
        <dc:subject></dc:subject>
        <dc:creator></dc:creator>
        <dc:date>yyyy-mm-ddT00:00:00+09:00</dc:date>
        <content:encoded>
            <![CDATA[ ... ]]>
        </content:encoded>
    </item>
</rdf:RDF>

先ほどの

var items = xhr.responseXML.querySelectorAll('item');

これでitemの一覧は配列化されているので、それをループして各itemの中から日付を抽出したい場合はnamespaceのdcとコロンを取り除いたタグ名「date」でエレメントが取得できます。

var items = xhr.responseXML.querySelectorAll('item');

for(var i = 0; i < items.length; i++){
    var date = items[i].querySelector('date');
    alert(date.textContent); // yyyy-mm-ddT00:00:00+09:00
}

属性の取得はgetAttribute

例えば、itemタグのrdf:aboutを取得するには、items[i].about としてもダメでした。この場合はgetAttribute()に取得したい属性名を指定するんですが、こちらの場合はnamespaceも必要みたいです。

// <item rdf:about="http://example.com"></item> からrdf:aboutの値を抽出

var items = xhr.responseXML.querySelectorAll('item');

for(var i = 0; i < items.length; i++){
    alert(items[i].about);                     // undefined
    alert(items[i].getAttribute('about'));     // null
    alert(items[i].getAttribute('rdf:about')); // http://example.com
}

<![CDATA[]]>の中のテキスト抽出にはtextContentを使う

<![CDATA[]]>セクションのテキストを取り出すには、innerHTMLではなくtextContentを使います。

正確に言うと、innerHTMLでも取れなくはないですが、<![CDATA[ と ]]> のタグも含まれたテキストになります。

textContentだと、<![CDATA[ と ]]> のタグがついていません。

参考ページ「Node.textContent – Web API インターフェイス | MDN

CDATAの中は例えHTMLのソースだとしても、あくまでテキストです。そのHTML文の中にあるエレメントを抽出するにはそのソースを元にHTML化します。

例えば、<content:encoded>タグの中にあるブログの各ページの本文から<img />タグの個数を調べるには、次のような方法があります。

var items = xhr.responseXML.querySelectorAll('item');

for(var i = 0; i < items.length; i++){
    var src = items[i].querySelector('encoded').textContent;

    var div = document.createElement('div');
    div.innerHTML = src;

    var img = div.querySelectorAll('img');
    alert(img.length);
}

HTMLの受け皿に、上ではdivを使っていますが、ちゃんとそれ用のobjectがあったような気もします。もし、それをご存知ならそちらを使われた方がよろしいかと。。

XMLHttpRequestで取得したデータを一覧表示するサンプル

以下は取得したいRSSのURLを入力すると、その結果を記事内の最初の画像付きで5個表示させるサンプルです。前のページ「Google Feed APIを使わずXMLHttpRequestでクロスドメインのRSSを取得する方法 まとめ」の方法でクロスドメインに対応しています。

ただしこのソースでは簡略化のためAtomには未対応です。

Atomにも対応させるには下のソースのcallback関数を変えるだけです。このページの一番下に「RSSとAtom両対応版のcallback関数」を置いておきます。

<!-- HTML部分 -->

<p id="sample_form">
<input type="text" name="sample" value="" placeholder="RSSのURLを入力して送信して下さい" />
<input type="button" value="送信" />
</p>

<div id="result"></div>
<!-- JavaScript部分 -->

<script>

// 絵文字を除外する
// imgタグが20ピクセル以下に指定されていたらfalse
// heightもwidthも未指定または20ピクセル以上ならtrue
function image_size_check(img){
    if(img.height && 20 >= img.height){
        return false;
    }

    if(img.width && 20 >= img.width){
        return false;
    }

    return true;
}

// 本文中から画像を抽出
function get_image(content, link){
  // contentの中のimgタグを全部取得
  var div = document.createElement('div');
  div.innerHTML = content;
  var images = div.querySelectorAll('img');

  for(var n = 0; n < images.length; n++){
    // imagesの中で最初に見つかった20ピクセル以上の画像を抽出
    // 20ピクセルは絵文字を除くため
    if(image_size_check(images[n])){
      // ブラウザによってはスラッシュ一つで始まる画像のURLが
      // 開いているページのものになってしまうのでその対策
      var pattern = /^(\S+?:\/)?\/([^\/\s]+).*$/;
      var location_domain = window.location.href.replace(pattern, '$2');
      var origin_domain = link.replace(pattern, '$2');
      var src = images[n].src.replace(location_domain, origin_domain);

      return '<p><img src="' + src + '" alt="画像" /></p>';
    }
  }

  return '<p></p>';
}

// callback本体
function callback(rss){
  var max_count = 5; // 5個まで表示
  var items = rss.querySelectorAll('item');
  var html  = '';

  for(var i = 0; i < items.length && i < max_count; i++){
    var link    = items[i].querySelector('link').textContent;
    var title   = items[i].querySelector('title').textContent;
    var date    = items[i].querySelector('date').textContent;
    var content = items[i].querySelector('encoded').textContent;
    var img     = get_image(content, link);

    date = date.replace(/^(\d{4})\-(\d{2})\-(\d{2})T.+$/,'$1年$2月$3日');

    html += '<li>' + img 
         + '<p><a href="' + link + '">' + title + '</a></p>'
         + '<p>' + date + '</p>'
         + '</li>';
  }

  document.querySelector('#result').innerHTML = '<ul>' + html + '</ul>';
}

function get_rss(url){
    var rss_server = 'ここに自作RSS中継サーバーのURL http://example.com/';

    var pattern       = /^(\S+?:\/)?\/([^\/\s]+).*$/;
    var server_domain = rss_server.replace(pattern, '$2');
    var location      = window.location.href.replace(pattern, '$2');

    /*
     指定されたRSSのドメインが閲覧中のページのドメインと
   同じならそのままRSSが取得できるので
   違う場合のみ中継サーバーを使う。
    */
    if(server_domain != location){
        url = rss_server + '?rss=' + encodeURIComponent(url);
    }

    if(window.XDomainRequest){
        var xhr = new XDomainRequest();
        xhr.onload  = function(){callback(this.responseXML);};
        xhr.onerror = function(){}
        xhr.open('GET', url, true);
        xhr.send('');
    }else if(window.XMLHttpRequest){
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
            if(this.readyState == 4 && this.status == 200){
                callback(this.responseXML);
            }
        };
        xhr.open('GET', url, true);
        xhr.send('');
    }
}

var form = document.querySelectorAll('#sample_form input');

form[1].onclick = function(){
    get_rss(form[0].value);
}
</script>

下のフォームにRSSかAtomのURLを入力して送信ボタンを押すと最新記事一覧を(画像があるページではページ内の画像付きで)5個表示します。クロスドメイン対応です。

注)
エラーの時はエラーメッセージは表示せず、何もしません。
bloggerのURLはリダイレクトされるので、リダイレクト先のURLを入力しない場合は取得しません。

ちなみにCSSは次の通りです。

CSSの下にRSSとAtom両対応版のcallback関数を置いておきます。callback以外は上と同じです。

<!-- CSS部分 -->

<style>
#result {
 margin:0;
 padding:0;
 border:none;
 width:400px;
 background:transparent;
}

#result ul,
#result li {
 list-style-type:none !important;
 background:transparent;
}

#result li {
 padding-bottom:0.5em;
 margin-bottom:0.5em;
 border-bottom: 2px solid #ddd;
}

#result li:last-child {
 border-bottom:none;
}

#result li:after {
 content: ".";
 display: block;
 height: 0;
 font-size:0;
 clear: both;
 visibility:hidden;
}

#result img {
 width:100%;
 height:auto;
}

#result a {
 text-decoration:none;
 line-height:1.1em;
 background:transparent;
}

#result p {
 margin:0;
 padding:0;
 line-height:1em;
 background:transparent;
}

#result p:nth-child(1) {
 width:80px;
 float:left;
 margin-right:5px;
}

#result p:nth-child(2) {
 display:block;
 overflow:hidden;
}

#result p:nth-child(3) {
 line-height:1.4em;
 text-align:right;
 font-size:90%;
}

#sample_form input:nth-child(1) {
 width:400px;
}
</style>
<!-- RSSとAtom両対応版のcallback関数 -->

<script>
function callback(xml, content_type){
  // HTMLの生成関数(callbackの外に書いてもOK)
  function make_html(link, title, date, img){
    return '<li>' + img 
      + '<p><a href="' + link + '">' + title + '</a></p>'
      + '<p>' + date + '</p>'
      + '</li>';
  }

  var count_max = 5;
  var i = 0;
  var html  = '';

  var items = xml.querySelectorAll('item');

  if(items.length){
    if(items[0].querySelector('date')){
      // RSS
      // Seesaa FC2 livedoor(livedoorはRSSとAtomがある)
      for(var i = 0; i < items.length && i < count_max; i++){
        var link    = items[i].querySelector('link').textContent;
        var title   = items[i].querySelector('title').textContent;
        var date    = items[i].querySelector('date').textContent;
        var content = items[i].querySelector('encoded').textContent;
        var img     = get_image(content, link);

        date = date.replace(/^(\d{4})\-(\d{2})\-(\d{2})T.+$/,'$1年$2月$3日')
        html += make_html(link, title, date, img);
      }
    }else{
      // Atom
      // WordPress blogger
      // ただしbloggerのURLはリダイレクトされるので
      // リダイレクト先のURLを入力しない場合は取得しない
      for(var i = 0; i < items.length && i < count_max; i++){
        var link    = items[i].querySelector('link').textContent;
        var title   = items[i].querySelector('title').textContent;
        var date    = items[i].querySelector('pubDate').textContent;
        var thumb   = items[i].querySelector('thumbnail');
        var content = items[i].querySelector('encoded');
        var img     = '<p></p>';

        date = date.replace(/^\S+?,\s+([0-9]+)\s+(\S+)\s+([0-9]+)\s.+$/,function(all, g1, g2, g3){
          var m = {
            'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
            'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
          };

          return g3 + '年' + m[g2] + '月' + g1 + '日';
        });

        if(thumb){
          var src = thumb.getAttribute('url');
          img = '<p><img src="' + src + '" alt="画像" /></p>';
        }else if(content){
          img = get_image(content.textContent, link);
        }

        html += make_html(link, title, date, img);
      }
    }
  }else{
    // livedoorのAtom
    items = xml.querySelectorAll('entry');

    for(var i = 0; i < items.length && i < count_max; i++){
      var link    = items[i].querySelector('link').getAttribute('href');
      var title   = items[i].querySelector('title').textContent;
      var date    = items[i].querySelector('issued').textContent;
      var content = items[i].querySelector('content').textContent;
      var img     = get_image(content, link);

      date = date.replace(/^(\d{4})\-(\d{2})\-(\d{2})T.+$/,'$1年$2月$3日')
      html += make_html(link, title, date, img);
    }
  }

  document.querySelector('#result').innerHTML = '<ul>' + html + '</ul>';
}
</script>

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です