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