作るに至った経緯
GASでのWEBスクレイピングといえば、送られてきたhtmlソースをただの文字列として受け取り、正規表現やmatch、split、indexOfなどで無理矢理目的の要素・テキストを削り出してくるもの。一応スクレイピング用のライブラリもあるが、やっていることは対して変わらない。
「我々が求めるものはPythonのbs4のような、CSSセレクタなどで簡単に要素を見つけ出し、情報を抽出するものではなかろうか?」
そんなこんなで火が付いた。作ったプロトタイプもGoogle Chromeの開発者ツール上では、少々レスポンスが遅いもののキチンと想定道理に動いていたから、やれる自信はあった。再帰呼び出しの回数が多すぎて、どうやってもGASでは動かないと気付いた時、私は燃え尽きた。
どういうライブラリだったか
CSSセレクタベースで要素を見つけるもの。最初は親・子孫要素や属性([attr="hogehoge"])などにも対応させていたが、少しでも再帰呼び出しを減らすため、どんどんオプションを削られ、しまいには親要素からの検索も露と消えた。まあ結局動かなかったけど。というわけでこのライブラリ(笑)は、Google Chromeの開発者ツール上でしか、筆者は正常に動かしたことがない。悲しい。せっかく作ったのに。
呼び出し
varh=HTMLparser(`
<html>
<head></head>
<body>
<p class="cls">htmlソーステキスト</p>
...
</body>
</html>
`);vartag_p=h.search("p.cls");//[{tagname: "p", attr: {…}, innerText: "htmlソーステキスト", tree: Array(1), parent: {…}}]呼び出し元のコード
functionHTMLparser(html_str){if(!(thisinstanceofHTMLparser)){returnnewHTMLparser(html_str);}else{this.html=[];this.attr={};this.tags={};varhtml_obj={tree:[]};//str: htmlソース, ary: htmlのツリー, parent: 親要素, self: HTMLparserのthisの参照用functionper(str,ary,parent,self){varobj={tagname:"_Node",attr:null,innerText:"",tree:[],parent:null};obj.parent=parent;//開始タグを見つけるvarmatchTag=str.match(/<([a-zA-Z][^\t\n\r\f\/>\x00]*?)(| [a-zA-Z][^\t\n\r\f>\x00]*?[^\/])>([\s\S]*?)$/);//[0]: 全体, [1]: タグ名, [2]: 属性, [3]: その開始タグ以降のテキストfunctionsameTagBothReg(tagname){returnnewRegExp("(<"+tagname+">|<"+tagname+" [a-zA-Z][^\t\n\r\f>\x00]*?[^\/]>|<\\/"+tagname+">)");}functionsameTagStartReg(tagname){returnnewRegExp("(<"+tagname+">|<"+tagname+" [a-zA-Z][^\t\n\r\f>\x00]*?[^\/]>)");}functionsameTagEndReg(tagname,count){returnnewRegExp("(<\\/"+tagname+">)");}varattrReg=/([a-zA-Z][^\t\n\r\f>\x00]*=\".*?\")/g;varattrReg_g=/([a-zA-Z][^\t\n\r\f>\x00]*)=\"(.*?)\"/g;if(matchTag){varattr_obj={};varattr_node_list=matchTag[2].split(attrReg).filter(function(r){returnr.match(attrReg);})attr_node_list.forEach(function(r){//タグの属性を収集vara=r.split(attrReg_g);if(!self.attr[a[1]]){self.attr[a[1]]={};}varv;//クラスのみリスト化if(a[1]=="class"){v=a[2].split("");v.forEach(function(r){if(!self.attr[a[1]][r]){self.attr[a[1]][r]=[];}self.attr[a[1]][r].push(obj);});}else{v=a[2];if(!self.attr[a[1]][v]){self.attr[a[1]][v]=[];}self.attr[a[1]][v].push(obj);}attr_obj[a[1]]=v;});obj.attr=attr_obj;obj.tagname=matchTag[1];//その開始タグの対となる終了タグを見つける//もし開始タグがspanだった場合、その開始タグ以降のテキストから、//spanの開始タグと終了タグをまとめて探索するvarst_cnt=1;//開始タグの数(開始タグはすでに一個あるので、初期値は1)vared_cnt=0;//終了タグの数varsp_idx=0;//目的の終了タグのインデックスvarsplitted_same_tag=matchTag[3].split(sameTagBothReg(matchTag[1]));splitted_same_tag.forEach(function(v,i){if(sp_idx){return;}//開始タグがマッチしたら+1elseif(v.match(sameTagStartReg(matchTag[1]))){st_cnt++;return;}//終了タグがマッチしたら+1elseif(v.match(sameTagEndReg(matchTag[1]))){ed_cnt++;//開始タグ数と終了タグ数が一致したら、インデックスを記録if(st_cnt==ed_cnt){sp_idx=i;}else{return;}}});//始点からインデックスまでが、そのタグの子要素varchild=splitted_same_tag.slice(0,sp_idx).join("");if(matchTag[1]=="title"){self.title=child;}//scriptタグの中には稀にhtmlのタグが紛れ込んでいるので、//誤マッチ回避のため、scriptタグの中身は切り捨てif(matchTag[1]!=="script"){//子要素をターゲットにして自身(obj)を親とし、perを再帰的に呼び出しper(child,obj.tree,obj,self);}//インデックスから終点までが、そのタグの兄弟要素varbro=splitted_same_tag.slice(sp_idx+1).join("");if(bro!==""){//兄弟要素をターゲットにして自身と同じ親要素(parent)//を親とし、perを再帰的に呼び出しper(bro,ary,parent,self);}}else{obj.tree=[str];}ary.unshift(obj);//もしタグ名が見つからなかったら(_Nodeのままなら)、//自身と親・先祖のinnerTextに中身の文字列を追加if(obj.tagname=="_Node"){Text(obj,obj.tree.join(""));functionText(o,txt){o.innerText+=txt;if(o.parent){Text(o.parent,txt);}}}else{if(!self.tags[obj.tagname]){self.tags[obj.tagname]=[];}self.tags[obj.tagname].push(obj);}}per(html_str.replace(/<!--[\s\S]*?-->/g,""),this.html,null,this);}}//検索機能(最も削り取られた部分)HTMLparser.prototype.search=function(selector){varsel=sel_parse(selector);//終端の1要素のみ読み取る//CSSセレクタは「右から」読み取るのが効率的returncheck_tree(sel[0],this);functioncheck_tree(s,self){varr=[];var_tag=s.tag;if(_tag){if(self.tags[_tag]){for(vartofself.tags[_tag]){var_atr=t.attr;if(s.class.length){if(_atr.class){var_chk=false;for(varclsofs.class){if(_atr.class.indexOf(cls)<0){_chk=true;break;}}if(_chk){continue;}}else{continue;}}if(s.id.length){if(s.id[0]!==_atr.id){continue;}}r.push(t);}}}else{varc_ary=[];vari_ary=[];vara_ary=[];//削り取られた残滓if(s.class.length){var_chk=true;varclsList=Object.keys(self.attr.class);for(varclsofs.class){if(clsList.indexOf(cls)<0){break;}else{if(!c_ary.length){c_ary=self.attr.class[cls];}else{c_ary=c_ary.concat(self.attr.class[cls]).filter(function(x,i,self){returnself.indexOf(x)===i&&i!==self.lastIndexOf(x);});}}}}if(s.id.length){varidList=Object.keys(self.attr.id);if(-1<idList.indexOf(s.id[0])){i_ary=self.attr.id[s.id[0]];}}if(s.attr.length){var_chk=false;for(varaofs.attr){var_k=a.atr;var_m=false;if(_k.match(/(\*|\^|\$)$/)){_m=_k.match(/(\*|\^|\$)$/)[1];_k=_k.replace(/(\*|\^|\$)$/,"");}}}varfull_cnt=0;if(c_ary.length){full_cnt++;r=r.concat(c_ary);}if(i_ary.length){full_cnt++;r=r.concat(i_ary);}if(a_ary.length){full_cnt++;r=r.concat(a_ary);}if(1<full_cnt){r=r.filter(function(x,i,self){returnself.indexOf(x)===i&&i!==self.lastIndexOf(x);});}}returnr;}//CSSセレクタの解析用functionsel_parse(sel){if(sel.match(/ ?[\+\~]?/g)){throwError('You cannot use Adjacent sibling combinator "+/~".');}elseif(sel.match(/\:(nth-child\(|nth-of-type\(|not\(|first-child|first-of-type|last-child|last-of-type)/g)){throwError('You cannot use Pseudo-elements like ":nth-of-type()"');}varsp=sel.split(/(?> ?|(?<=[a-zA-Z0-9\]\_\-])(?=[a-zA-Z\[\.\#\_]))/g);vara=[],nxt=false;sp.forEach(function(s,idx){if(s.match(/^ $/g)){return;}elseif(s.match(/^ ?> ?$/g)){nxt=true;return;}var_o={"tag":null,"class":[],"id":[],"attr":[],"next":false};if(nxt){_o.next=true;nxt=false;}var_s=s.split(/(\[.*?\]|(?<=(?:[a-zA-Z\]]|^))(?:\.|\#)[a-zA-Z\_][a-zA-Z0-9\_\-]*)/g).filter(function(r){returnr;});_s.forEach(function(p){if(p.match(/^\#/)){_o.id.push(p.replace(/\#/,""));}elseif(p.match(/^\./)){_o.class.push(p.replace(/\./,""));}elseif(p.match(/^[a-zA-Z]/)){_o.tag=p;}elseif(p.match(/^\[(.*?)(?:\=(?:\"(.*?)\"|\'(.*?)\')|)\]/)){var_m=p.match(/^\[(.*?)(?:\=(?:\"(.*?)\"|\'(.*?)\')|)\]/);_o.attr.push({"atr":_m[1],"que":_m[2]?_m[2]:""});}});a.unshift(_o);});returna;}}ちなみに、HTMLparserオブジェクトの中身は以下の通り。
treeにはその要素の子要素のリストが、parentにはその要素の親要素への参照が渡されている(図では一番外側のタグのため参照が無い)。下のattrとtagsは検索用の参照のリストが詰まっている。
