前のページ「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>