// ==UserScript== // @name 好き嫌い検索 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 好き嫌い検索 // @match https://suki-kira.com/people/result/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; //////////////////////////////////////////// // 1. UI生成 //////////////////////////////////////////// const ui = document.createElement('div'); Object.assign(ui.style, { position: 'fixed', top: '10px', right: '10px', padding: '8px', background: 'rgba(255,255,255,0.95)', border: '1px solid #ccc', zIndex: '9999' }); ui.innerHTML = `


`; document.body.appendChild(ui); const logEl = ui.querySelector('#skiLog'); function log(msg) { console.log('[SkiTool] ' + msg); const d = document.createElement('div'); d.textContent = msg; logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight; } //////////////////////////////////////////// // 2. 検索キーワード解析 (AND/ORグループ生成) //////////////////////////////////////////// function parseSearchGroups(input) { const groups = []; // 1) " " で囲まれた部分を ORグループとして抜き出し const quoteRe = /"([^"]+)"/g; let m; const used = []; while ((m = quoteRe.exec(input))) { const orTerms = m[1] .split(/\s+/) .map(s => s.trim()) .filter(Boolean); if (orTerms.length) { groups.push(orTerms); used.push(m[0]); } } // 2) 残りの文字列から単語を抜き、各々AND用グループ(1ワード OR) let remainder = input; used.forEach(tok => { remainder = remainder.replace(tok, ' '); }); remainder .split(/\s+/) .map(s => s.trim()) .filter(Boolean) .forEach(word => { groups.push([word]); }); return groups; } //////////////////////////////////////////// // 3. START ボタン押したら //////////////////////////////////////////// ui.querySelector('#skiStartBtn').addEventListener('click', () => { const raw = ui.querySelector('#skiKeyword').value.trim(); const startId = parseInt(ui.querySelector('#skiStart').value, 10); const endId = parseInt(ui.querySelector('#skiEnd').value, 10); if (!raw || isNaN(startId) || isNaN(endId) || startId <= endId) { alert('キーワード/開始/終了 を正しく指定してね'); return; } const groups = parseSearchGroups(raw); log(`検索条件: ${JSON.stringify(groups)}`); ui.querySelector('#skiStartBtn').disabled = true; runScan(groups, startId, endId) .catch(err => { console.error(err); log('エラー: ' + err.message); ui.querySelector('#skiStartBtn').disabled = false; }); }); //////////////////////////////////////////// // 4. レスの走査ループ //////////////////////////////////////////// async function runScan(groups, startId, endId) { const baseUrl = location.origin + location.pathname; let currentNxc = startId + 1; const seen = new Set(); const results = []; while (true) { const url = `${baseUrl}?nxc=${currentNxc}`; log(`→ ページ取得 ${url}`); const html = await fetchPage(url); const doc = new DOMParser().parseFromString(html, 'text/html'); const comments = Array.from(doc.querySelectorAll('.comment-container')); if (!comments.length) { log('警告: レス0件'); break; } let minId = Infinity; for (const c of comments) { const id = parseInt(c.id, 10); if (isNaN(id)) continue; minId = Math.min(minId, id); if (id > startId || id < endId || seen.has(id)) continue; // 本文テキスト取得、小文字化 const bodyEl = c.querySelector('.comment_body'); const text = bodyEl ? bodyEl.textContent.replace(/\u00A0/g,' ') : ''; const low = text.toLowerCase(); // AND/ORグループ一致判定 const ok = groups.every(group => { return group.some(term => { return low.includes(term.toLowerCase()); }); }); if (ok) { seen.add(id); results.push({ id, text }); log(` Hit: ${id}`); } } if (minId <= endId) { log(`終了: ${minId} ≤ ${endId}`); break; } currentNxc = minId; await sleep(500); } makeDownload(results); ui.querySelector('#skiStartBtn').disabled = false; } //////////////////////////////////////////// // 5. ページ取得 //////////////////////////////////////////// async function fetchPage(url) { const res = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Referer': location.href } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.text(); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } //////////////////////////////////////////// // 6. テキスト出力 //////////////////////////////////////////// function makeDownload(items) { if (!items.length) { log('該当レスがないよ'); } let txt = ''; items.sort((a,b) => b.id - a.id); for (const {id, text} of items) { txt += `${id}:\n${text}\n\n`; } const blob = new Blob([txt], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `sukikira-${Date.now()}.txt`; a.textContent = `結果をダウンロード (${items.length} 件)`; a.style.display = 'block'; ui.querySelector('#skiDownload').appendChild(a); log('ファイル準備完了'); } })();