Google Search Suggestions Collector

Collect Google search suggestions

  1. // ==UserScript==
  2. // @name Google Search Suggestions Collector
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Collect Google search suggestions
  6. // @author WWW
  7. // @include *://www.google.*/*
  8. // @include *://google.*/*
  9. // @grant GM_setClipboard
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. let MAX_CONCURRENT_REQUESTS = 5; // 最大并发请求数
  14. let REQUEST_DELAY = 100; // 请求间隔(ms)
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // 在全局作用域内添加状态变量
  20. let isCollecting = false;
  21. let shouldStop = false;
  22.  
  23. function addStyles() {
  24. const style = document.createElement('style');
  25. style.textContent = `
  26. .suggest-collector-btn {
  27. position: fixed;
  28. right: 200px;
  29. top: 20px;
  30. width: 50px;
  31. height: 50px;
  32. border-radius: 25px;
  33. background: var(--collector-bg, #ffffff);
  34. border: 2px solid var(--collector-border, #e0e0e0);
  35. box-shadow: 0 2px 12px rgba(0,0,0,0.15);
  36. cursor: move;
  37. z-index: 10000;
  38. display: flex;
  39. align-items: center;
  40. justify-content: center;
  41. user-select: none;
  42. }
  43.  
  44. .suggest-collector-panel {
  45. position: fixed;
  46. width: 300px;
  47. background: var(--collector-bg, #ffffff);
  48. border: 1px solid var(--collector-border, #e0e0e0);
  49. border-radius: 8px;
  50. padding: 15px;
  51. box-shadow: 0 2px 12px rgba(0,0,0,0.15);
  52. z-index: 9999;
  53. display: none;
  54. }
  55.  
  56. .suggest-collector-panel input {
  57. width: 100%;
  58. padding: 8px;
  59. border: 1px solid var(--collector-border, #e0e0e0);
  60. border-radius: 4px;
  61. margin-bottom: 10px;
  62. background: var(--collector-input-bg, #ffffff);
  63. color: var(--collector-text, #333333);
  64. }
  65.  
  66. .suggest-collector-panel button {
  67. padding: 8px 16px;
  68. border: none;
  69. border-radius: 4px;
  70. background: #4CAF50;
  71. color: white;
  72. cursor: pointer;
  73. transition: background 0.3s;
  74. }
  75.  
  76. .suggest-collector-panel button:hover {
  77. background: #45a049;
  78. }
  79.  
  80. .suggest-collector-panel textarea {
  81. background: var(--collector-input-bg, #ffffff);
  82. color: var(--collector-text, #333333);
  83. border: 1px solid var(--collector-border, #e0e0e0);
  84. border-radius: 4px;
  85. }
  86.  
  87. @media (prefers-color-scheme: dark) {
  88. :root {
  89. --collector-bg: #2d2d2d;
  90. --collector-border: #404040;
  91. --collector-text: #e0e0e0;
  92. --collector-input-bg: #3d3d3d;
  93. }
  94. }
  95.  
  96. .input-mode-selector {
  97. display: flex;
  98. gap: 20px;
  99. margin-bottom: 15px;
  100. padding: 0 10px;
  101. }
  102.  
  103. .input-mode-selector label {
  104. display: flex;
  105. align-items: center;
  106. gap: 5px;
  107. cursor: pointer;
  108. color: var(--collector-text, #333333);
  109. min-width: 70px;
  110. }
  111.  
  112. .input-mode-selector input[type="radio"],
  113. .filter-options input[type="checkbox"] {
  114. margin: 0;
  115. cursor: pointer;
  116. width: 16px;
  117. height: 16px;
  118. }
  119.  
  120. .filter-options {
  121. margin-bottom: 15px;
  122. padding: 0 10px;
  123. }
  124.  
  125. .filter-options label {
  126. display: flex;
  127. align-items: center;
  128. gap: 5px;
  129. cursor: pointer;
  130. color: var(--collector-text, #333333);
  131. justify-content: flex-end;
  132. }
  133.  
  134. #singleInput {
  135. padding: 0 10px;
  136. }
  137.  
  138. .depth-selector {
  139. margin-bottom: 15px;
  140. padding: 0 10px;
  141. display: flex;
  142. align-items: center;
  143. gap: 10px;
  144. }
  145.  
  146. .depth-selector label {
  147. color: var(--collector-text, #333333);
  148. }
  149.  
  150. .depth-selector select {
  151. padding: 5px;
  152. border-radius: 4px;
  153. border: 1px solid var(--collector-border, #e0e0e0);
  154. background: var(--collector-input-bg, #ffffff);
  155. color: var(--collector-text, #333333);
  156. cursor: pointer;
  157. }
  158. `;
  159. document.head.appendChild(style);
  160. }
  161.  
  162. function createUI() {
  163. const btn = document.createElement('div');
  164. btn.className = 'suggest-collector-btn';
  165. btn.innerHTML = '🔍';
  166. document.body.appendChild(btn);
  167.  
  168. const panel = document.createElement('div');
  169. panel.className = 'suggest-collector-panel';
  170. panel.innerHTML = `
  171. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
  172. <div class="input-mode-selector">
  173. <label><input type="radio" name="inputMode" value="single" checked> single</label>
  174. <label><input type="radio" name="inputMode" value="batch"> batch</label>
  175. </div>
  176. <div class="filter-options">
  177. <label><input type="checkbox" id="onlyEnglish"> Only English</label>
  178. </div>
  179. </div>
  180. <div class="depth-selector">
  181. <label>Search Depth:</label>
  182. <select id="searchDepth">
  183. <option value="1">1 letter</option>
  184. <option value="2">2 letters</option>
  185. <option value="3">3 letters</option>
  186. <option value="4">4 letters</option>
  187. <option value="5">5 letters</option>
  188. </select>
  189. </div>
  190. <div class="performance-settings" style="display: flex; gap: 10px; margin-bottom: 15px; padding: 0 10px;">
  191. <div style="flex: 1;">
  192. <label style="display: block; margin-bottom: 5px; color: var(--collector-text);">Max Concurrent:</label>
  193. <input type="number" id="maxConcurrent" value="5" min="1" max="20"
  194. style="width: 100%; padding: 5px; border: 1px solid var(--collector-border);
  195. border-radius: 4px; background: var(--collector-input-bg);
  196. color: var(--collector-text);">
  197. </div>
  198. <div style="flex: 1;">
  199. <label style="display: block; margin-bottom: 5px; color: var(--collector-text);">Delay (ms):</label>
  200. <input type="number" id="requestDelay" value="100" min="0" max="1000" step="50"
  201. style="width: 100%; padding: 5px; border: 1px solid var(--collector-border);
  202. border-radius: 4px; background: var(--collector-input-bg);
  203. color: var(--collector-text);">
  204. </div>
  205. </div>
  206. <div id="singleInput">
  207. <input type="text" id="baseKeyword" placeholder="type keyword">
  208. </div>
  209. <div id="batchInput" style="display: none;">
  210. <textarea id="batchKeywords" placeholder="type keyword in each line" style="width: 100%; height: 100px; margin-bottom: 10px;"></textarea>
  211. </div>
  212. <button id="startCollect">start collect</button>
  213. <div id="estimatedTime" style="margin: 10px 0; color: var(--collector-text);"></div>
  214. <div id="progress" style="display: none; margin-top: 10px;">
  215. <div style="margin-bottom: 8px;">
  216. total progress: <span id="totalProgress">0/0</span>
  217. <div style="background: var(--collector-border); height: 20px; border-radius: 10px;">
  218. <div id="totalProgressBar" style="width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s;"></div>
  219. </div>
  220. </div>
  221. <div style="margin-bottom: 8px;">
  222. current keyword progress: <span id="progressText">0/26</span>
  223. <div style="background: var(--collector-border); height: 20px; border-radius: 10px;">
  224. <div id="progressBar" style="width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s;"></div>
  225. </div>
  226. </div>
  227. <div>collected: <span id="collectedCount">0</span> items</div>
  228. </div>
  229. <div id="result" style="max-height: 300px; overflow-y: auto; margin-top: 10px;"></div>
  230. `;
  231. document.body.appendChild(panel);
  232.  
  233. let isDragging = false;
  234. let currentX;
  235. let currentY;
  236. let initialX;
  237. let initialY;
  238. let xOffset = 0;
  239. let yOffset = 0;
  240.  
  241. // 更新面板位置的函数
  242. function updatePanelPosition() {
  243. const btnRect = btn.getBoundingClientRect();
  244. panel.style.right = `${window.innerWidth - (btnRect.right + 20)}px`;
  245. panel.style.top = `${btnRect.bottom + 20}px`;
  246. }
  247.  
  248. btn.addEventListener('mousedown', dragStart);
  249. document.addEventListener('mousemove', drag);
  250. document.addEventListener('mouseup', dragEnd);
  251.  
  252. function dragStart(e) {
  253. initialX = e.clientX - xOffset;
  254. initialY = e.clientY - yOffset;
  255. if (e.target === btn) {
  256. isDragging = true;
  257. }
  258. }
  259.  
  260. function drag(e) {
  261. if (isDragging) {
  262. e.preventDefault();
  263. currentX = e.clientX - initialX;
  264. currentY = e.clientY - initialY;
  265. xOffset = currentX;
  266. yOffset = currentY;
  267. btn.style.transform = `translate(${currentX}px, ${currentY}px)`;
  268. // 拖动时更新面板位置
  269. updatePanelPosition();
  270. }
  271. }
  272.  
  273. function dragEnd() {
  274. isDragging = false;
  275. }
  276.  
  277. btn.addEventListener('click', (e) => {
  278. if (!isDragging) {
  279. panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  280. if (panel.style.display === 'block') {
  281. updatePanelPosition();
  282. }
  283. }
  284. });
  285.  
  286. // 添加事件监听器来实时更新预估时间
  287. function updateEstimatedTime() {
  288. const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value) || 5;
  289. const requestDelay = parseInt(document.getElementById('requestDelay').value) || 100;
  290. const searchDepth = parseInt(document.getElementById('searchDepth').value);
  291. const isBatchMode = document.querySelector('input[name="inputMode"]:checked').value === 'batch';
  292. let keywordCount = 0;
  293. if (isBatchMode) {
  294. const batchText = document.getElementById('batchKeywords').value.trim();
  295. keywordCount = batchText.split('\n').filter(k => k.trim()).length;
  296. } else {
  297. const singleKeyword = document.getElementById('baseKeyword').value.trim();
  298. keywordCount = singleKeyword ? 1 : 0;
  299. }
  300.  
  301. if (keywordCount === 0) {
  302. document.getElementById('estimatedTime').innerHTML =
  303. 'Please enter keyword(s) to see estimated time';
  304. return;
  305. }
  306.  
  307. const { totalRequests, estimatedSeconds } = calculateEstimatedTime(
  308. keywordCount,
  309. searchDepth,
  310. maxConcurrent,
  311. requestDelay
  312. );
  313.  
  314. const minutes = Math.floor(estimatedSeconds / 60);
  315. const seconds = estimatedSeconds % 60;
  316. const timeStr = minutes > 0
  317. ? `${minutes} min ${seconds} sec`
  318. : `${seconds} sec`;
  319. document.getElementById('estimatedTime').innerHTML =
  320. `Estimated time: ${timeStr}<br>Total requests: ${totalRequests}`;
  321. }
  322.  
  323. // 添加事件监听器到所有可能影响预估时间的输入元素
  324. document.getElementById('maxConcurrent').addEventListener('input', updateEstimatedTime);
  325. document.getElementById('requestDelay').addEventListener('input', updateEstimatedTime);
  326. document.getElementById('searchDepth').addEventListener('change', updateEstimatedTime);
  327. document.getElementById('baseKeyword').addEventListener('input', updateEstimatedTime);
  328. document.getElementById('batchKeywords').addEventListener('input', updateEstimatedTime);
  329. const radioButtons = panel.querySelectorAll('input[name="inputMode"]');
  330. radioButtons.forEach(radio => {
  331. radio.addEventListener('change', (e) => {
  332. document.getElementById('singleInput').style.display =
  333. e.target.value === 'single' ? 'block' : 'none';
  334. document.getElementById('batchInput').style.display =
  335. e.target.value === 'batch' ? 'block' : 'none';
  336. updateEstimatedTime(); // 添加这行来更新预估时间
  337. });
  338. });
  339. }
  340.  
  341. async function getSuggestions(keyword, retries = 3) {
  342. for (let i = 0; i < retries; i++) {
  343. try {
  344. const response = await fetch(`https://suggestqueries.google.com/complete/search?client=chrome&q=${encodeURIComponent(keyword)}`);
  345. const data = await response.json();
  346. return data[1];
  347. } catch (error) {
  348. if (i === retries - 1) throw error;
  349. await new Promise(resolve => setTimeout(resolve, 1000)); // 失败后等待1秒再重试
  350. }
  351. }
  352. }
  353.  
  354. function updateProgress(current, total, collectedItems) {
  355. const progressBar = document.getElementById('progressBar');
  356. const progressText = document.getElementById('progressText');
  357. const collectedCount = document.getElementById('collectedCount');
  358. const progress = document.getElementById('progress');
  359.  
  360. progress.style.display = 'block';
  361. const percentage = (current / total) * 100;
  362. progressBar.style.width = percentage + '%';
  363. progressText.textContent = `${current}/${total}`;
  364. collectedCount.textContent = collectedItems.size;
  365. }
  366.  
  367. function generateCombinations(letters, depth) {
  368. if (depth === 1) return letters.map(letter => [letter]);
  369.  
  370. const combinations = [];
  371. for (let i = 0; i < letters.length; i++) {
  372. const subCombinations = generateCombinations(letters.slice(i + 1), depth - 1);
  373. subCombinations.forEach(subComb => {
  374. combinations.push([letters[i], ...subComb]);
  375. });
  376. }
  377. return combinations;
  378. }
  379.  
  380. async function asyncPool(concurrency, iterable, iteratorFn) {
  381. const ret = []; // 存储所有的异步任务
  382. const executing = new Set(); // 存储正在执行的异步任务
  383.  
  384. for (const item of iterable) {
  385. const p = Promise.resolve().then(() => iteratorFn(item, ret)); // 创建异步任务
  386. ret.push(p); // 保存新的异步任务
  387. executing.add(p); // 添加到执行集合
  388.  
  389. const clean = () => executing.delete(p);
  390. p.then(clean).catch(clean);
  391.  
  392. if (executing.size >= concurrency) {
  393. await Promise.race(executing); // 等待某个任务完成
  394. }
  395.  
  396. await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY)); // 添加请求间隔
  397. }
  398.  
  399. return Promise.all(ret);
  400. }
  401.  
  402. async function collectSuggestions(baseKeyword) {
  403. // 获取用户设置的值
  404. MAX_CONCURRENT_REQUESTS = parseInt(document.getElementById('maxConcurrent').value) || 5;
  405. REQUEST_DELAY = parseInt(document.getElementById('requestDelay').value) || 100;
  406.  
  407. const result = new Set();
  408. const letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
  409. const resultDiv = document.getElementById('result');
  410. const onlyEnglish = document.getElementById('onlyEnglish').checked;
  411. const searchDepth = parseInt(document.getElementById('searchDepth').value);
  412.  
  413. const isEnglishOnly = (text) => /^[A-Za-z0-9\s.,!?-]+$/.test(text);
  414.  
  415. if (shouldStop) {
  416. return Array.from(result);
  417. }
  418.  
  419. // 收集基础关键词建议
  420. const baseSuggestions = await getSuggestions(baseKeyword);
  421. baseSuggestions.forEach(s => {
  422. if (!onlyEnglish || isEnglishOnly(s)) {
  423. result.add(s);
  424. }
  425. });
  426.  
  427. // 生成所有可能的字母组合
  428. const allCombinations = [];
  429. for (let depth = 1; depth <= searchDepth; depth++) {
  430. const depthCombinations = generateCombinations(letters, depth);
  431. allCombinations.push(...depthCombinations);
  432. }
  433.  
  434. // 更新进度条的总数
  435. const totalCombinations = allCombinations.length;
  436. let completedCount = 0;
  437.  
  438. // 创建查询任务
  439. const searchTasks = allCombinations.map(combination => {
  440. return async () => {
  441. if (shouldStop) return [];
  442.  
  443. const letterCombination = combination.join('');
  444. const suggestions = await getSuggestions(`${baseKeyword} ${letterCombination}`);
  445.  
  446. completedCount++;
  447. updateProgress(completedCount, totalCombinations, result);
  448.  
  449. return suggestions.filter(s => !onlyEnglish || isEnglishOnly(s));
  450. };
  451. });
  452.  
  453. // 建一个固定的 textarea 元素
  454. resultDiv.innerHTML = `<textarea style="width: 100%; height: 200px;"></textarea>`;
  455. const resultTextarea = resultDiv.querySelector('textarea');
  456.  
  457. // 使用并发池执行查询
  458. const results = await asyncPool(MAX_CONCURRENT_REQUESTS, searchTasks, async (task) => {
  459. const suggestions = await task();
  460. suggestions.forEach(s => result.add(s));
  461.  
  462. // 保存当前滚动位置
  463. const scrollTop = resultTextarea.scrollTop;
  464.  
  465. // 更新内容
  466. resultTextarea.value = Array.from(result).join('\n');
  467.  
  468. // 恢复滚动位置
  469. resultTextarea.scrollTop = scrollTop;
  470.  
  471. return suggestions;
  472. });
  473.  
  474. return Array.from(result);
  475. }
  476.  
  477. function calculateEstimatedTime(keywordCount, searchDepth, maxConcurrent, requestDelay) {
  478. const letters = 'abcdefghijklmnopqrstuvwxyz';
  479. let totalRequests = 0;
  480. // 计算每个关键词的请求数(基础请求 + 字母组合请求)
  481. for (let depth = 1; depth <= searchDepth; depth++) {
  482. // 计算组合数
  483. let combinations = 1;
  484. for (let i = 0; i < depth; i++) {
  485. combinations *= (letters.length - i);
  486. }
  487. for (let i = depth; i > 0; i--) {
  488. combinations = Math.floor(combinations / i);
  489. }
  490. totalRequests += combinations;
  491. }
  492. totalRequests += 1; // 加上基础关键词的请求
  493. totalRequests *= keywordCount; // 乘以关键词数量
  494.  
  495. // 计算总时长(毫秒)
  496. const avgResponseTime = 300; // 假设平均响应时间为300ms
  497. const batchCount = Math.ceil(totalRequests / maxConcurrent);
  498. const totalTime = batchCount * (avgResponseTime + requestDelay);
  499. return {
  500. totalRequests,
  501. estimatedSeconds: Math.ceil(totalTime / 1000)
  502. };
  503. }
  504.  
  505. function init() {
  506. addStyles();
  507. createUI();
  508.  
  509. const startCollectBtn = document.getElementById('startCollect');
  510.  
  511. startCollectBtn.addEventListener('click', async () => {
  512. if (isCollecting) {
  513. // 如果正在收集,点击按钮则停止
  514. shouldStop = true;
  515. startCollectBtn.textContent = 'start collect';
  516. startCollectBtn.style.background = '#4CAF50';
  517. isCollecting = false;
  518. return;
  519. }
  520.  
  521. const isBatchMode = document.querySelector('input[name="inputMode"]:checked').value === 'batch';
  522. let keywords = [];
  523.  
  524. if (isBatchMode) {
  525. const batchText = document.getElementById('batchKeywords').value.trim();
  526. keywords = batchText.split('\n').filter(k => k.trim());
  527. } else {
  528. const singleKeyword = document.getElementById('baseKeyword').value.trim();
  529. if (singleKeyword) {
  530. keywords = [singleKeyword];
  531. }
  532. }
  533.  
  534. if (keywords.length === 0) {
  535. alert('Please enter a keyword');
  536. return;
  537. }
  538.  
  539. // 开始收集
  540. isCollecting = true;
  541. shouldStop = false;
  542. startCollectBtn.textContent = 'stop collect';
  543. startCollectBtn.style.background = '#ff4444';
  544. const resultDiv = document.getElementById('result');
  545. resultDiv.innerHTML = 'Collecting...';
  546. document.getElementById('progress').style.display = 'block';
  547.  
  548. try {
  549. const allSuggestions = new Set();
  550. const totalKeywords = keywords.length;
  551.  
  552. for (let i = 0; i < keywords.length; i++) {
  553. if (shouldStop) {
  554. break;
  555. }
  556.  
  557. const keyword = keywords[i];
  558. document.getElementById('totalProgress').textContent = `${i + 1}/${totalKeywords}`;
  559. document.getElementById('totalProgressBar').style.width = `${((i + 1) / totalKeywords) * 100}%`;
  560.  
  561. const suggestions = await collectSuggestions(keyword);
  562. suggestions.forEach(s => allSuggestions.add(s));
  563. }
  564.  
  565. const resultText = Array.from(allSuggestions).join('\n');
  566. resultDiv.innerHTML = `
  567. <textarea style="width: 100%; height: 200px;">${resultText}</textarea>
  568. <button id="copyBtn">Copy to Clipboard</button>
  569. `;
  570.  
  571. document.getElementById('copyBtn').addEventListener('click', () => {
  572. GM_setClipboard(resultText);
  573. alert('Copied to clipboard!');
  574. });
  575. } catch (error) {
  576. resultDiv.innerHTML = 'Error occurred while collecting: ' + error.message;
  577. } finally {
  578. // 恢复按钮状态
  579. isCollecting = false;
  580. shouldStop = false;
  581. startCollectBtn.textContent = 'start collect';
  582. startCollectBtn.style.background = '#4CAF50';
  583. }
  584. });
  585. }
  586.  
  587. init();
  588. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址