
note記事のマガジンつき統計情報を出力するブックマークレット【無料配布中】
note生活を便利にしたい
推しであるBTSの記事をnoteで書き始めてから、二年半。
意外と長く続いているので、自分の環境を便利に整えるため少しずつChrome拡張機能を作っていましたが、拡張機能管理画面を開くのが面倒くさくなってきました。最近は、簡単なものはブックマークレットに移行しています。
javascriptは昔触ってたので、AIに投げて、直してデバッグして……の壁打ちで作りました。便利な世の中になりましたね!
今回は、作ったものを公開します。ご参考になれば幸いです。
ブックマークレット用コード
javascript:(async()=>{const UNIT='ALL';const Q=`query Dashboard_StatPageQuery($unit: DashboardPeriodUnit!, $date: Datetime!, $endDate: Datetime, $order: DashboardNoteListOrder, $first: Int!, $after: String) { dashboardNoteListConnection(unit: $unit, date: $date, endDate: $endDate, order: $order, first: $first, after: $after) { pageInfo { hasNextPage endCursor } edges { node { note { title status publishedAt link { absoluteUrl } } metrics { pageViewCount impressionCount likeCount commentCount } } } } }`;const q=(v)=>{const s=String(v==null?'':v);return/["\r\n,]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;};const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));if(location.pathname!=='/magazines/my'){alert('「マイマガジン画面」に自動で移動します。\n移動完了後、もう一度このブックマークレットをクリックしてください。');location.href='/magazines/my';return;}let isAborted=false;const statusEl=document.createElement('div');statusEl.style.cssText='position:fixed;top:20px;right:20px;z-index:2147483647;background:#1a1814;color:#fff;padding:16px 20px;font:13px sans-serif;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.4);width:260px;';const msgArea=document.createElement('div');const btnArea=document.createElement('div');btnArea.style.cssText='margin-top:10px;text-align:right;';const abortBtn=document.createElement('button');abortBtn.textContent='中断してCSV出力';abortBtn.style.cssText='background:#c93b2b;color:#fff;border:none;padding:5px 10px;font-size:11px;border-radius:4px;cursor:pointer;';abortBtn.onclick=()=>{if(!isAborted){isAborted=true;abortBtn.disabled=true;abortBtn.style.background='#888';abortBtn.textContent='中断処理中...';}};btnArea.appendChild(abortBtn);statusEl.appendChild(msgArea);statusEl.appendChild(btnArea);document.body.appendChild(statusEl);const setMsg=async(txt,cur,tot)=>{if(tot){const pct=Math.floor((cur/tot)*100);msgArea.innerHTML='<div style="margin-bottom:8px;font-weight:bold;">'+txt+'</div><div style="background:#444;border-radius:4px;width:100%;height:8px;overflow:hidden;"><div style="background:#4caf50;width:'+pct+'%;height:100%;transition:width 0.2s;"></div></div><div style="text-align:right;font-size:11px;color:#bbb;margin-top:6px;">'+cur+' / '+tot+' ('+pct+'%)</div>';}else{msgArea.innerHTML='<div style="font-weight:bold;">'+txt+'</div>';}console.log(txt);await sleep(20);};const rows=[];const noteMagazines={};const noteTags={};const noteEyecatch={};const today=new Date(Date.now()+9*3600e3).toISOString().slice(0,10);try{await setMsg('自動スクロール中...');let prevCount=0;let sameCount=0;for(let i=0;i<30;i++){if(isAborted)break;window.scrollTo(0,document.body.scrollHeight);await sleep(1000);const magKeys=new Set([...document.querySelectorAll('a[href*="/m/m"]')].map(a=>{const m=a.href.match(/\/m\/(m[a-z0-9]+)/);return m?m[1]:null}).filter(Boolean));let currentCount=magKeys.size;await setMsg('マガジンを検索中... ('+currentCount+'件検出)');if(currentCount===prevCount){sameCount++;if(sameCount>=3)break;}else{sameCount=0;}prevCount=currentCount;}const magMap=new Map();document.querySelectorAll('a[href*="/m/m"]').forEach(a=>{const m=a.href.match(/\/m\/(m[a-z0-9]+)/);if(m){const mid=m[1];let n=a.getAttribute('aria-label')||a.getAttribute('title')||a.textContent.replace(/<[^>]+>/g,'').trim();if(!n||n==='マガジン')n=mid;if(!magMap.has(mid)||magMap.get(mid)===mid){magMap.set(mid,n);}}});if(!magMap.has('m11c9b2908314'))magMap.set('m11c9b2908314','BTSの話をしよう');if(!isAborted){await setMsg('認証確認中...');const a=await fetch('/api/v3/graphql/auth',{method:'POST',credentials:'include',headers:{'X-Requested-With':'XMLHttpRequest'}});if(!a.ok)throw new Error('ログインした状態で、note画面から実行してください');const tok=(document.cookie.match(/note_gql_auth_token=([^;]+)/)||[])[1];if(!tok)throw new Error('トークン取得失敗');let urlname='thracia776';try{const me=await fetch('/api/v1/current_user',{credentials:'include'});if(me.ok){const mj=await me.json();urlname=mj.data?.urlname||urlname;}}catch(e){}await setMsg('全記事の統計データを取得中...');let after=null;for(let p=0;p<60;p++){if(isAborted)break;const v={unit:UNIT,date:today+'T00:00:00.000Z',order:'PUBLISHED_DATE_DESC',first:100};if(after)v.after=after;const r=await fetch('https://graphql.note.com/graphql',{method:'POST',headers:{'content-type':'application/json','authorization':'Bearer '+tok},body:JSON.stringify({operationName:'Dashboard_StatPageQuery',variables:v,query:Q})});const j=await r.json();if(j.errors)throw new Error(j.errors[0].message);const c=j.data&&j.data.dashboardNoteListConnection;if(!c)throw new Error('データ取得失敗');for(const e of c.edges){const n=e.node,m=n.metrics,u=(n.note.link||{}).absoluteUrl||'';rows.push({key:(u.match(/\/n\/([A-Za-z0-9]+)/)||['',''])[1],pd:(n.note.publishedAt||'').slice(0,10),t:n.note.title||'',imp:m.impressionCount||0,pv:m.pageViewCount||0,lk:m.likeCount||0,cm:m.commentCount||0,u});}if(!c.pageInfo.hasNextPage)break;after=c.pageInfo.endCursor;await setMsg('全記事の統計データを取得中... ('+rows.length+'件)');await sleep(70);}if(!rows.length&&!isAborted)throw new Error('記事なし');let idx=0;const totalMags=magMap.size;for(let [mid,mname] of magMap.entries()){if(isAborted)break;if(mname===mid){try{const mRes=await fetch('/api/v1/magazines/'+mid);if(mRes.ok){const mJson=await mRes.json();mname=mJson.data?.name||mJson.data?.title||mid;magMap.set(mid,mname);}}catch(e){}}idx++;await setMsg('マガジン解析: '+mname,idx,totalMags);let npg=1;while(npg<=100){if(isAborted)break;let found=false;try{const nr=await fetch('/api/v1/layout/magazine/'+mid+'/section?page='+npg+'&include_details=true',{credentials:'include'});if(!nr.ok)break;const nj=await nr.json();const findNotes=(obj)=>{if(!obj||typeof obj!=='object')return;if(Array.isArray(obj)){obj.forEach(findNotes);return;}const k=obj.key||obj.note_key;if(k&&typeof k==='string'&&k.startsWith('n')&&k.length>=8){found=true;if(!noteMagazines[k])noteMagazines[k]=[];if(!noteMagazines[k].includes(mname))noteMagazines[k].push(mname);}Object.values(obj).forEach(findNotes);};findNotes(nj);if(nj.data?.is_last_page)break;}catch(e){break;}if(!found)break;npg++;await sleep(80);}}const visited=new Set();let contentsCount=0;if(!isAborted){await setMsg('記事詳細(タグ・見出し画像)を一括取得中...');for(let pg=1;pg<=150;pg++){if(isAborted)break;try{const cr=await fetch('/api/v2/creators/'+urlname+'/contents?kind=note&page='+pg+'&per=20',{credentials:'include'});if(!cr.ok)break;const cj=await cr.json();const cs=cj.data?.contents||[];if(!cs.length)break;contentsCount+=cs.length;await setMsg('記事詳細を一括取得中... ('+contentsCount+'件完了)');for(const c of cs){const k=c.key;if(k){visited.add(k);const raw=c.hashtags||c.hash_tags||c.hashtag_notes||c.tags||[];const tags=raw.map(t=>typeof t==='string'?t:(t.hashtag_name||t.name||t.hashtag?.name||'')).filter(Boolean);if(tags.length)noteTags[k]=tags;if(c.eyecatch)noteEyecatch[k]=typeof c.eyecatch==='string'?c.eyecatch:(c.eyecatch.url||'');}}if(cj.data?.isLastPage)break;}catch(e){}await sleep(60);}}const missingKeys=rows.map(r=>r.key).filter(k=>k&&!visited.has(k));if(missingKeys.length>0&&!isAborted){for(let i=0;i<missingKeys.length;i++){if(isAborted)break;const k=missingKeys[i];if(i%2===0)await setMsg('詳細データを回収中(タブを閉じずにお待ちください)',i+1,missingKeys.length);try{const res=await fetch('/api/v3/notes/'+k,{credentials:'include'});if(res.ok){const dj=await res.json();const d=dj.data||{};const raw=d.hashtags||d.hash_tags||d.hashtag_notes||d.tags||[];const tags=raw.map(t=>typeof t==='string'?t:(t.hashtag_name||t.name||t.hashtag?.name||'')).filter(Boolean);if(tags.length)noteTags[k]=tags;if(d.eyecatch)noteEyecatch[k]=typeof d.eyecatch==='string'?d.eyecatch:(d.eyecatch.url||'');}}catch(e){}await sleep(80);}}}if(!rows.length){statusEl.innerHTML='<div style="font-weight:bold;">出力対象データがありません</div>';setTimeout(()=>statusEl.remove(),3000);return;}await setMsg('CSV生成中...');btnArea.style.display='none';rows.sort((x,y)=>y.pv-x.pv);const head=['公開日','記事タイトル','インプレッション','ページビュー','スキ','コメント数','マガジン','ハッシュタグ','見出し画像URL','URL'];const body=rows.map((r)=>[r.pd,r.t,r.imp,r.pv,r.lk,r.cm,(noteMagazines[r.key]||[]).join(' | '),(noteTags[r.key]||[]).map(t=>'#'+t.replace(/^#/,'')).join(' '),noteEyecatch[r.key]||'',r.u]);const csvText='\uFEFF'+[head,...body].map((r)=>r.map(q).join(',')).join('\r\n');const blob=new Blob([csvText],{type:'text/csv;charset=utf-8;'});const link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=(isAborted?'note_stats_partial_':'note_stats_')+today+'.csv';document.body.appendChild(link);link.click();document.body.removeChild(link);const resLabel=isAborted?'中断出力完了':'完了';statusEl.innerHTML='<div style="font-weight:bold;">'+resLabel+'!('+rows.length+'件ダウンロード)</div>';statusEl.style.background=isAborted?'#b8621b':'#3f6b4a';setTimeout(()=>statusEl.remove(),5000);}catch(e){if(statusEl){statusEl.innerHTML='<div style="font-weight:bold;">取得エラー: '+e.message+'</div>';statusEl.style.background='#b33030';}console.error(e);}})();作成に当たっては、こちらを参考にさせていただきました。すごく便利なので使ってみてください!
このブックマークレットで何ができるのか
「マイマガジン」画面で起動すると、マガジン情報と全記事の統計データを取得し、csvで出力します。
公開記事をマガジン分けで整理しているのですが、どれがどれに入っているかわからなくなるので、把握用に使っています。
取得するデータ
公開日
記事タイトル
インプレッション
ページビュー
スキ
コメント数
マガジン
ハッシュタグ
見出し画像URL
URL
マイマガジン画面で起動するのは、そこでスタートしないと、自分のマガジン一覧&マガジン振り分け情報が上手く取れなかったからです。いい感じのAPIが見つからなくて……( ;∀;)
見出し画像URLがあるのは、昔あった旧「つぶやき」記事が通常記事に変換されたものを見分けるためです。
notionに流し込んだら、そのままギャラリー・ボードビューに使えます。ハッシュタグをマルチセレクトにして、タグ代わりにしてもいいのでは。

取得に少し時間がかかるので、中断ボタンをつけています。押すと、取得したところまでを出力します。
(私の記事は、今16マガジン&公開記事1500本ちょっとありますが、それで5分くらいかかります)

使用例

ブックマークレットとは
ウェブブラウザのブックマーク(お気に入り)に、簡易プログラム(JavaScriptのコード)を登録し、クリック一つでそのコードを実行させる仕組みです。
ブックマークとの違い
ブックマーク
特定のページを開く
ブックマークレット
今開いているページに対して、簡単な加工や変更を施す
Chrome拡張機能より手軽
ブックマークレットの使い方
適当なページをブックマークに保存する
ブックマークレットをコピーする
ブックマークの「編集」から、名前とURLを書き換える
保存する


使う時は、指定された画面で保存したブックマークをクリックするだけ!
カスタマイズ方法
このブックマークレットを普段使っている生成AIにコピペして、「こう修正したい」「こうしたい」と相談してください。
あとはAIと試行錯誤してみてね。便利なものができたら、是非公開してください!( *´艸`)
※自分の環境でしかテストしてないので、困ったらAIに相談するか教えてください( ;∀;)