ブログ内検索では文字コードで検索が出来ない
この間、私自身がブログ記事中に使っていた「\u200B」(文字コードはUnicode)という文字を、他のスペース文字に置き換えるツール「Every Page Rewiter 💢」を作りました。 ただ、修復コードは記事を損なわない様に控え目で、書き込まれた周囲の状況に関係して、おそらく取り残しがあると予想していました。
最初に、ブログ内検索でこの文字をチェックしようとしましたが、ブログ内検索で記事中の特殊文字は検索できません。 一般的な文字ならまだしも、こういった特殊な機能文字は、直接に記事ソースをパターンマッチのコードで検索する必要があります。
「Rewiter 💢」は「記事の編集・削除」の1ページずつの書換え処理でしたが、検索は処理が早いので、この機会にブログ全域の記事を連続で自動で開いて調べるツールに発展させる事にしました。
今までのツールは、通しで数百件の記事の処理を行うのは、問題が生じた場合に困ると考えて作りませんでした。 しかし、今回はブログ記事のある限り動作するコードです。 処理部のコードが確実なものなら、1時間や2時間の自動処理が可能と思います。
「Every Page Worker 💢💢」
今回のツールは、上記の様に各記事を開き、文字コードによる記事内検索を行うコードを積んでいます。 しかし「Rewiter 💢」と同様に、コードの差替える事で、色々な作業に使える様にしました。
今回の調査では、「\u200B」を残している記事を発見したら、その記事IDをローカルストレージの配列に記録します。 この配列は、これまでに作って来たツールと同様に「11文字の記事ID」「データ」の2個の要素を単位とした配列です。 今回はデータは「1」か「null」で充分ですが、作業に拠っては、フラグとして「2」「3」… や、もっと複雑な文字列等を登録できます。
以下が「task()」部のコードで、このコードが各記事で実行されます。
記事が読み込まれてから 数msec程度で処理が終了すると思われます。 文字数限界の記事のテストで 50msecで充分動作しましたが、100msec の余裕を持たせました。
1ページの20件は80secで処理しますが、「Every Page Worker 💢💢」は自動的に次のページを開いて処理を続行します。 停止ボタンで停止・作業再開が出来ます。 操作部は「Every Page Snap 💢」を流用しましたが、コードはかなり異なります。 ページを切替えるコードは、各ページの処理の最後で呼ばれるだけで、主導権は各ぺージの処理の側にあります。
「Check1」「Check2」「Check3」の3種のフラグを結果に出来ますが、今回に使用しているのは「\u200B」を見つけた場合の「Check1」だけです。 各ページへの連続的な自動処理は、常に旧→新の方向へ進行しますが、開始場所の選択と途中停止で、必要な範囲だけの処理が可能です。
ローカルデータのファイル化は、一旦停止した状態でも、処理が最近の記事で終了した状態でも可能です。 ファイルは「EPW.json」というファイル名で、ダウンロードフォルダに保存されます。 下は結果の一例で、メモ帳で開く事が出来ます。
最初の「"e0000000000","s"」だけはコントロール項で、他が採取データです。 これらは「\u200B」の発見された記事IDで、編集画面を適当に開き、URL末尾をこれに書換えると、その記事を開く事が出来ます。
「Every Page Rewiter 💢」で処理が出来なかった「\u200B」
以下は自動処理のコードでは取り残した「\u200B」の例です。 処理範囲が固定される「h2」「h3」のものは、全くありません。「Font Awesome」の記入時の、タグの後方に配置したものが殆どです。
基本的な処理コードの失敗
下は本来は削除されるべきものですが、残っていました。
このコードの部分に原因は無く、ひとつ手前の<i>タグの状態が関係していました。
上は例ですが、最初は<i>タグの後ろに「\u200B」があり、2行目は「スペース」があります。 こういうのが普通ですが、たまに記事作成時に3行目の様に「\u200B」を消している場合があります。 この「\u200B」や「スペース」は、自動入力コードの安定した動作に必要なだけで、後で削除しても構わないものです。
以下は「Every Page Rewiter 💢」の自動処理コードですが、不完全で最後の場合を想定しておらず、太字の部分がありませんでした。
簡単に言うと、<i>タグの後ろにノードが無い場合に、太字のコードが無いとスクリプトがエラーとなり、それ以降の<i>タグの処理を放棄していたのです。
特殊なケース
以下は、面白い状態で「\u200B」が残っていました。 これは、自動処理コードでは対応できません。 見出しから文字をコピーして来た時に着いて来たのでしょうか。
「\u200B」はとにかく削除というコードは、記事破壊しかねないので使わなかったのですが、正解といえるかも。 下は、何か意図的に使っていたものらしい。
700件ほどの内で24件がチェック出来たのですが、やはり例外や想定外はあります。
連続動作は優秀
「Every Page Worker 💢💢」は、ページ内の検索以外にも色々と使えます。 処理が少し複雑なら、ページを閉じるまでに余裕を持たせることが出来ます。 今回は、約1時間ほどの自動運転でしたが、暫く離れて戻るとPCの自動スリープが働いていました。 スリープから復帰させると自動処理が止まっているので、何度か「一旦停止・再開」を押すと、再び動作を再開しました。 リロードしてもストレージデータは消えませんが、連続処理はとてもしたたかに動作します。
「Every Page Worker 💢💢」のコード
以下のコードを「Tampermonkey」にコピー&ペーストして登録する事で、このツールを利用する事が出来ます。 もっとも、処理部「task()」のコードは、必要に応じて作成する必要があります。 動作は、Chrome と Firefoxで確認しました。
〔コピー方法〕 軽量シンプルなツール「PreBox Button 」を使うと
コード枠内を「Ctrl+左Click」➔「Copy code 」を「左Click」
の操作で、掲載コードのコピーが可能になります。
〔 Every Page Worker 💢💢 〕 ver. 3.0
// ==UserScript== // @name Every Page Worker 💢💢 // @namespace http://tampermonkey.net/ // @version 3.0 // @description 「記事の編集・削除」でブログ全記事を開いて作業する // @author Ameba Blog User // @match https://blog.ameba.jp/ucs/entry/srventrylist* // @match https://blog.ameba.jp/ucs/entry/srventryupdate* // @run-at document-start // @grant none // ==/UserScript== window.addEventListener('DOMContentLoaded', function(){ // CSSデザインを適用するためのスクリプト let css=[ '.include-ex-linkBtn, .save-browserPush, .save-gsc, .save-hashtag-module,', '.adcrossBanner{ display: none }', 'html { overflow-y: overlay !important }', '#globalHeader, #ucsHeader, #ucsMainLeft h1, .l-ucs-sidemenu-area, #ucsMainRight,', '#entryList dl, #entryList .rightCol input, .actionControl,', '#footerAd, #globalFooter, .checkboxAllControl { display: none }', '#ucsContent{ width: 720px !important; box-shadow: 0 0 0 100vh #c5d8e1 }', '#ucsMain{ background: #fff } #ucsMainLeft { width: 690px !important; float: none }', '#entryList li{ height: 24px; padding: 8px 5px 5px !important }', '#entryList li:hover{ background-color: #e2eef0 }', '#entryList .leftCol, #entryList .titleCol h2{ width: 400px !important; line-height: 24px }', '.leftCol a:hover{ text-decoration: none } .leftCol a:visited{ color: #3970b5 }', '#entryList .rightCol{ display: flex; width: 260px !important }', '#entryList .txtCol .date{ font-size: 13px; color: #000 }', '#entryList .txtCol{ white-space: nowrap; font-size: 13px; margin-top: 4px; line-height: 16px }', '#entryList .deleteCol { margin: -3px 0 0 20px }', '.deleteCol a:nth-child(2), .deleteCol a:nth-child(3), .rightCol input { display: none }', '#entryList .btnDefault { height: 25px }', '#entryList .btnDefault:hover { box-shadow: inset 0 0 0px 2px #1976d2 }', '#entryList ul input[value="1"] + input + li .txtCol span:first-child{ display: inline-block;', 'margin-top: -2px; padding: 2px 13px 0; color: #fff; background: #2196f3 !important }', '#entryList ul input[value="2"] + input + li .txtCol span:first-child{ display: inline-block;', 'width: 3em; text-indent: -1em; overflow: hidden; margin-top: -2px; padding: 2px 13px 0;', 'vertical-align: -4px; color: #fff; background: #009688 !important }', '#entryMonth { font-size: 14px; overflow: visible; margin-top: 3px }', '#entryMonth li, #entryMonth #nowMonthLi { padding: 2px 10px 0 }', '#nowMonth { padding: 1px 6px 0; border: none; border-radius: 1px;', 'box-shadow: 0 0 0 2px #0066cc; color: #000 }', '#entryListEdit form { display: flex; flex-direction: column } #entrySort { order: -2 }', '.pagingArea { order: -1; padding: 4px; margin-bottom: -33px; background: #ddedf3 }', '.pagingArea a { border: 1px solid #888 } .pagingArea .active{ border: 2px solid #0066cc }', '.pagingArea a, .pagingArea .active, .pagingArea .disabled { font-size: 14px; line-height: 23px }', '#sorting { margin: 38px 0 4px; padding: 4px 0; height: 110px; background: #ddedf3 }', '#sorting select, #sorting ul { display: none }', 'input { font-family: meiryo; font-size: 14px }'].join(' '); let style=document.createElement('style'); style.insertAdjacentHTML('afterbegin', css); let head=document.getElementsByTagName('head')[0]; head.appendChild(style); }) window.addEventListener('load', function(){ // このスクリプトは孫ウインドウだけで働く let body_id=document.body.getAttribute('id'); if(body_id=="entryCreate"){ // この項だけ孫ウインドウで働く window.document.body.style.background='#c5d8e1'; window.document.body.style.boxShadow='0 0 0 100vh #c5d8e1'; select_e(close_w); function select_e(close_w){ let error_report=document.querySelector('h1.p-error__head'); if(error_report==null){ // 保存エラー無い場合 grayを送信 編集画面を閉じる if(window.opener){ report('gray'); window.opener.close(); }} else{ // 保存エラーのある場合は redを送信 編集画面を残す if(window.opener){ report('red'); window.opener.location.reload(); }} close_w(); } function close_w(){ window.open('about:blank','_self').close(); } // 孫ウインドウは常に閉じる function report(color){ window.opener.document.querySelector('html').style.color=color; }} }) window.addEventListener('load', function(){ // このスクリプトは親ウインドウで働くメインスクリプト let body_id=document.body.getAttribute('id'); if(body_id=='entryListEdit'){ // 親ウインドウの条件 let drive_mode; // ページ更新時の動作モード let blogDB={}; //「対象記事のID/チェックフラグ または内容」の記録配列 let entry_id_DB; // ID検索用の配列 let pub_1; // flag 1 が記録された記事総数 let pub_2; // flag 2 が記録された記事総数 let pub_3; // flag 3 が記録された記事総数 let entry_id; let publish_f; let entry_target; let list_bar; let editor_flg; let next_target; // ページ内の次の対象記事 let new_win; let link_target; let editor_iframe; let iframe_doc; let ua=0; let agent=window.navigator.userAgent.toLowerCase(); if(agent.indexOf('firefox') > -1){ ua=1; } // Firefoxの場合のフラグ if(agent.indexOf('edge') > -1){ ua=2; } // Edgeの場合のフラグ let read_json=localStorage.getItem('EPW_DB_back'); // ローカルストレージ 保存名 blogDB=JSON.parse(read_json); if(blogDB==null){ blogDB=[['e0000000000', 's']]; } drive_mode=blogDB[0][1]; // 起動時に動作フラグを取得 blogDB[0][1]='s'; // リロード時等のためにリセット let write_json=JSON.stringify(blogDB); localStorage.setItem('EPW_DB_back', write_json); // ローカルストレージ 保存 reg_set(); function reg_set(){ let k; entry_id_DB=[]; // リセット pub_1=0; pub_2=0; pub_3=0; for(k=0; k<blogDB.length; k++){ entry_id_DB[k]=blogDB[k][0]; // ID検索用の配列を作成 if(blogDB[k][1]=='1'){ pub_1 +=1; continue; } // flag 0 が記録された記事総数 if(blogDB[k][1]=='2'){ pub_2 +=1; continue; } // flag 1 が記録された記事総数 if(blogDB[k][1]=='3'){ pub_3 +=1; continue; }}} // flag 2 が記録された記事総数 entry_id=document.querySelectorAll('input[name="entry_id"]'); entry_target=document.querySelectorAll('.deleteCol'); publish_f=document.querySelectorAll('input[name="publish_flg"]'); list_bar=document.querySelectorAll('#entryList li'); control_pannel(drive_mode); function control_pannel(dm){ let sty; let insert_div0; insert_div0=document.createElement('div'); insert_div0.setAttribute('id', 'div0'); insert_div0.style='color: #333; font-size: 14px; margin: 10px -10px 0 15px'; let box=document.querySelector('#sorting'); box.appendChild(insert_div0); let insert_div1; insert_div1=document.createElement('div'); insert_div1.setAttribute('id', 'div1'); insert_div1.style='color: #000; font-size: 14px; margin: 8px 15px; border: 1px solid #888'; box.appendChild(insert_div1); let button1=document.createElement('input'); button1.setAttribute('id', 'list_snap'); button1.setAttribute('type', 'submit'); button1.setAttribute('style', 'padding: 2px 0 0; margin: 7px 20px 7px 0; width: 180px'); insert_div0.appendChild(button1); if(dm=='s'){ button1.setAttribute('value', '全記事へ処理を開始 ▶'); } else if(dm=='c'){ button1.setAttribute('value', ' 処理を一旦停止 ❚❚'); } else if(dm=='e'){ button1.setAttribute('value', '処理が全て終了しました'); } button1.addEventListener('click', function(e){ e.preventDefault(); start_stop(); }, false); function start_stop(){ if(drive_mode=='s'){ // 最初の起動直後 let conf_str=[' 🔴 このページ以降の記事に連続した処理を実行します', '\n\n 停止ボタンのクリックで処理停止/処理再開ができます'].join(' '); let ok=confirm(conf_str); if(ok){ drive_mode='c'; // ページ内の連続処理 button1.setAttribute('value', ' 処理を一旦停止 ❚❚'); next(); }} else if(drive_mode=='c'){ // 連続動作状態の場合 drive_mode='p'; // クリックされたら「p」停止モード button1.setAttribute('value', ' 処理を再開する ▶'); } else if(drive_mode=='p'){ // 動作停止状態の場合 drive_mode='c'; // クリックされたら連続動作を再開 button1.setAttribute('value', ' 処理を一旦停止 ❚❚'); open_win(next_target); }} if(dm=='c'){ // ページを開いた時に「c」は連続動作 setTimeout(next, 200); } // 「c」連続動作はぺージ遷移時 0.2sec で自動実行 ⭕ else if(dm=='e'){ // 「e」は終了 button1.style.pointerEvents='none'; } let button2=document.createElement('input'); button2.setAttribute('id', 'reset'); button2.setAttribute('type', 'submit'); button2.setAttribute('value', '初期化'); button2.setAttribute('style', 'padding: 2px 0 0; margin-right: 20px; width: 60px'); insert_div0.appendChild(button2); button2.onclick=function(e){ e.preventDefault(); blogDB=[['e0000000000', 's']]; let write_json=JSON.stringify(blogDB); localStorage.setItem('EPW_DB_back', write_json); // ローカルストレージ保存 snap_disp(); document.querySelector('#reset').value='〔 〕'; } let button3=document.createElement('input'); button3.setAttribute('type', 'submit'); button3.setAttribute('value', 'ファイル保存'); button3.setAttribute('style', 'padding: 2px 0 0; margin: 7px 10px 7px 0; width: 100px'); insert_div0.appendChild(button3); button3.onclick=function(e){ e.preventDefault(); let write_json=JSON.stringify(blogDB); let blob=new Blob([write_json], {type: 'application/json'}); if(ua==2){ window.navigator.msSaveBlob(blob, 'EPW.json'); } // 保存ファイル名 else{ let a_elem=document.createElement('a'); a_elem.href=URL.createObjectURL(blob); a_elem.download='EPW.json'; // 保存ファイル名 if(ua==1){ a_elem.target='_blank'; document.body.appendChild(a_elem); } a_elem.click(); if(ua==1){ document.body.removeChild(a_elem); } URL.revokeObjectURL(a_elem.href); }} let button4=document.createElement('input'); button4.setAttribute('type', 'file'); button4.setAttribute('style', 'vertical-align: 1px; width: 285px'); if(ua==2){ button4.setAttribute('style', 'vertical-align: 0; width: 270px; direction: rtl'); } insert_div0.appendChild(button4); button4.addEventListener("change", function(){ if(!(button4.value)) return; // ファイルが選択されない場合 let file_list=button4.files; if(!file_list) return; // ファイルリストが選択されない場合 let file=file_list[0]; if(!file) return; // ファイルが無い場合 let file_reader=new FileReader(); file_reader.readAsText(file); file_reader.onload=function(){ if(file_reader.result.slice(0, 15)=='[["e0000000000"'){ // EPW.jsonの確認 let data_in=JSON.parse(file_reader.result); blogDB=data_in; // 読込み上書き処理 let write_json=JSON.stringify(blogDB); localStorage.setItem('EPW_DB_back', write_json); // ローカルストレージ 保存 button2.setAttribute('value', '初期化'); // 初期化後なら読み込んだ事を示す snap_disp(); } else{ alert(" ⛔ 不適合なファイルです EPW.json ファイルを選択してください");}};}); let span5=document.createElement('span'); span5.setAttribute('id', 'snap_result'); span5.style='display: inline-block; margin: 6px 12px 4px'; insert_div1.appendChild(span5); snap_disp(); } function snap_disp(){ reg_set(); let span5=document.querySelector('#snap_result'); span5.textContent=' 記録件数:' + (blogDB.length -1) + ' Check 1:' + pub_1 + ' Check 2:' + pub_2 + ' Check 3:' + pub_3; } function next(){ entry_id=document.querySelectorAll('input[name="entry_id"]'); if(entry_id.length >0){ open_win(0); } // 投稿記事がある場合 open_win を開始 else{ next_call();}} // 投稿記事が無ければ 次ページをcall する function open_win(k){ next_target=k; // 送信完了までは未処理とする new_win=Array(entry_target.length); link_target=Array(entry_target.length); link_target[k]=entry_target[k].getElementsByTagName('a')[0].getAttribute('href'); let top_p=100 + 30*k; if(drive_mode=='c'){ let win_option='top=' + top_p + ', left=100, width=800, height=300'; new_win[k]=window.open(link_target[k], k, win_option); list_bar[k].style.boxShadow='inset 0 0 0 2px #03a9f4'; // リスト欄に青枠表示 new_win[k].addEventListener('load', function(){ // 子ウインドウの処理 🟦 work(); setTimeout( function(){ end_target(); }, 100); // 処理の余裕時間 task内容で増減の必要あり🟥 }); } function work(){ let editor_flg=new_win[k].document.querySelector('input[name="editor_flg"]'); if(editor_flg.value=="5"){ // 最新版エディタの文書の場合のみ処理 let interval=setInterval(find_iframe, 10); // iframe 読込み待機コード 🟥 function find_iframe(){ let editor_iframe=new_win[k].document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ let iframe_doc=editor_iframe.contentWindow.document; if(iframe_doc){ clearInterval(interval); task(); function task(){ let iframe_body=iframe_doc.querySelector('.cke_editable'); let result = iframe_body.textContent.match(/\u200B/); if(result){ let index=entry_id_DB.indexOf(entry_id[k].value); if(index==-1){ // IDがblogDBに記録されていない場合 blogDB.push([entry_id[k].value, 1]); } // 記事ID/フラグを記録 else{ // IDがblogDBに記録されていた場合 blogDB[index]=[entry_id[k].value, 1]; }} // 記事ID/フラグを更新 let write_json=JSON.stringify(blogDB); localStorage.setItem('EPW_DB_back', write_json); // ストレージ保存 snap_disp() }}}}}} // work() function end_target(){ // 終了処理 let editor_flg=new_win[k].document.querySelector('input[name="editor_flg"]'); new_win[k].addEventListener('beforeunload', flag_line , false); publish_do(); function flag_line(){ let send_color; if(new_win[k]){ send_color=new_win[k].document.querySelector('html').style.color; } if(send_color=='gray'){ // 文書保存が正常終了した場合 リスト背景 淡ブルー list_bar[k].style.boxShadow='none'; if(editor_flg.value=='5'){ list_bar[k].style.background='#caedf2'; } else{ list_bar[k].style.background='#eceff1'; } next_do(k); } //⏩ else if(send_color=='red'){ // 文書保存が異常終了の場合は編集画面は閉ず リスト背景白 赤枠表示 list_bar[k].style.boxShadow='inset 0 0 0 2px red'; list_bar[k].style.backgroundColor='#fff'; next_do(k); }} //⏩ function publish_do(){ let publish_b0=new_win[k].document.querySelector('button.js-submitButton[publishflg="0"]'); let publish_b1=new_win[k].document.querySelector('button.js-submitButton[publishflg="1"]'); let publish_b2=new_win[k].document.querySelector('button.js-submitButton[publishflg="2"]'); if(publish_f[k].value==0){ publish_b0.click(); } if(publish_f[k].value==1){ publish_b1.click(); } if(publish_f[k].value==2){ publish_b2.click(); }} function next_do(k){ next_target=k+1; if(next_target<entry_target.length){ open_win(next_target); } else{ next_call(); } // ページの終りまで終了した状態 }}} // open_win() function next_call(){ let win_url; let current; let pageid; let next_url; let pager; let end; blogDB[0][1]='c'; // 連続動作フラグを連続にセット let write_json=JSON.stringify(blogDB); localStorage.setItem('EPW_DB_back', write_json); // ローカルストレージ保存 win_url=window.location.search.substring(1,window.location.search.length); current=win_url.slice(-6); if(win_url.indexOf('pageID') ==-1){ // pageIDが無い 月のトップページの場合 pager=document.querySelector('.pagingArea'); if(pager){ // ページャーが有りその末尾でなければ同月次ページへ next_url=['https://blog.ameba.jp/ucs/entry/srventrylist.do?', 'pageID=2&entry_ym=' + current].join(''); window.open( next_url, '_self'); } else{ // ページャーが無ければ次月トップページへ current=make_next(current); if(current!=0){ // 現在を越えないなら次月へ next_url=['https://blog.ameba.jp/ucs/entry/srventrylist.do?', 'entry_ym=' + current].join(''); window.open( next_url, '_self'); } else{ // 現在を越えたら0が戻り停止 when_edge(); }}} else{ // pageIDを含み 月のトップページでない場合 end=document.querySelector('.pagingArea .disabled.next'); if(!end){ // ページャーの末尾でなければ同月次ページへ pageid=parseInt(win_url.slice(7).slice(0, -16), 10) +1; next_url=['https://blog.ameba.jp/ucs/entry/srventrylist.do?', 'pageID=' + pageid + '&entry_ym=' + current].join(''); window.open( next_url, '_self'); } else{ // ページャーの末尾なら次月トップページへ current=make_next(current); if(current!=0){ // 現在を越えないなら次月へ next_url=['https://blog.ameba.jp/ucs/entry/srventrylist.do?', 'entry_ym=' + current].join(''); window.open( next_url, '_self'); } else{ // 現在を越えたら0が戻り停止 when_edge(); }}} function make_next(curr){ let ym; let y; let m; ym=parseInt(curr, 10); // 10進数値化 y=Math.floor(ym/100); // 年は100で割った商 m=ym % 100; // 月は100で割った余り if(m !=12){ ym=100*y + m +1; } else{ ym=100*y + 101; } let now=new Date(); if(ym > 100*now.getFullYear() + now.getMonth() +1){ return 0; } // 現在の月を越える場合は0を返す else{ return ym; }} // 次年月の数値を返す function when_edge(){ blogDB[0][1]='s'; // 連続動作フラグをリセット let write_json=JSON.stringify(blogDB); localStorage.setItem('EPW_DB_back', write_json); // ローカルストレージ保存 document.querySelector('#div0').remove(); document.querySelector('#div1').remove(); control_pannel('e'); } // 全作業の終了時の表示をさせる } // next_call() } // 親ウインドウの条件 })
「Every Page Worker 💢💢」最新版について
旧いバージョンの JavaScriptツールは、アメーバのページ構成の変更で動作しない場合があり、導入する場合は最新バージョンをお勧めします。
●「Every Page Worker 💢💢」の最新バージョンへのリンクは、以下のページのリンクリストから探せます。