➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Spring 項目中訂閱 RabbitMQSupport 指引

寫 Chrome extension 排序 Google Lens 搜尋結果

Continued from previous post:
寫第一個 Chrome Extension

Table of contents

1 Google 以圖搜尋

相信大家都知道 Google 搜尋既其中一個叫做 Google Images 既功能可以幫我地上載圖片去搜尋更多圖片,呢個功能已經存在多年。
佢最有用既一個用途係,我地可以上載一張低清圖片,然後 Google Images 會顯示所有相同或相似既圖片結果,然後我地可以喺結果頁面選擇最高清既解像度,Google Images 會以相反次序將結果顯示,一目了然,非常方便。
不過,Google 近年就將 Google Images 既以圖搜尋功能轉左用 Google Lens,亦都冇左重要既功能——解像度既選項、排序。

2 需求

根據而家 Google Lens 搜尋結果頁面既設計,我地係冇任何選項,只能夠撳「More exact matches」既掣去顯示更多搜尋結果。
所以我地需要:
  • 透過自動撳「More exact matches」既掣去顯示所有搜尋結果。
  • 以解像度既相反次序去排序搜尋結果。
  • 避免因為頁面自動刷新而失去我地對頁面既改動。

3 動手寫

3.1 顯示所有搜尋結果

3.1.1 Locate 「More exact matches」掣

Locate 「More exact matches」既掣:
const els = [...document.querySelectorAll("div")] .filter((e) => e.children.length === 0) .filter((e) => e.innerText === "More exact matches"); const el = els[0];
解釋:
  • 因為 Google Lens 既頁面既 DOM elements 都冇 id,而佢地既 class names 似係 front-end library 生成既隨機值,所以我地只能用個掣既顯示文字去搵返佢個 div element 出黎。

3.1.2 自動撳掣機制

1const clickMore = () => { 2 const els = [...document.querySelectorAll("div")] 3 .filter((e) => e.children.length === 0) 4 .filter((e) => e.innerText === "More exact matches"); 5 6 if (els.length > 0) { 7 els[0].click(); 8 setTimeout(clickMore, 500); 9 } 10}; 11 12clickMore();
解釋:
  • 考慮到撳掣之後可能會需要一啲時間,所以我地會用 setTimeout 去每隔一小段時間去一直 check 下個掣存唔存在,然後撳一次。
  • 如果個掣唔再存在,我地就唔需要再繼續 check。

3.2 排序搜尋結果

1const extractWidth = (node) => Number.parseInt(node.innerText.replace(/.*?(\d+)x(\d+)/, '$1')); 2const extractHeight = (node) => Number.parseInt(node.innerText.replace(/.*?(\d+)x(\d+)/, '$2')); 3 4const sortAllResults = () => { 5 const newListEl = document.createElement("ul"); 6 7 const listEl = document.querySelector('ul[aria-label="All results list"]'); 8 9 [...listEl.children] 10 .sort((a, b) => { 11 const aEl = [...a.querySelectorAll('div')].filter(e => e.children.length === 0 || [...e.children].every(o => o.tagName.toLowerCase() === 'span')).filter(e => /.*?(\d+)x(\d+)/.test(e.innerText))[0]; 12 const bEl = [...b.querySelectorAll('div')].filter(e => e.children.length === 0 || [...e.children].every(o => o.tagName.toLowerCase() === 'span')).filter(e => /.*?(\d+)x(\d+)/.test(e.innerText))[0]; 13 14 const aArea = aEl ? (extractWidth(aEl) * extractHeight(aEl)) : 0; 15 const bArea = bEl ? (extractWidth(bEl) * extractHeight(bEl)) : 0; 16 17 return bArea - aArea; 18 }) 19 .forEach(e => { 20 newListEl.appendChild(e); 21 }); 22 23 document.body.replaceChildren(newListEl); 24};
解釋:
  • 我地會用每張圖片既解像度數字去計出圖片既面積,然後以面積既相反次序去排序搜尋結果。
  • 我地會將搜尋結果既 li DOM elements 加到一個新既 ul list,再將 body 既內容換成呢個新既 ul list,從而避免因為 parent DOM elements 自動刷新而失去我地對 ulli DOM elements 排序既改動。

4 包裝成 Chrome extension

我地可以寫一個 Chrome extension 去自動執行以上既 script。

4.1 檔案

只需要 2 個檔案:
檔案用途
manifest.json關於呢個 Chrome extension 既基本資訊,例如呢個 Chrome extension 適用於邊啲 URL patterns。
content.js真正要執行既 script。
我地要將佢地放埋喺同一個 folder 裡面。

4.1.1 manifest.json

格式如下:
1{ 2 "name": "Google-Lens-Sort", 3 "version": "1.0", 4 "description": "Sort Google Lens image search results", 5 "content_scripts": [ 6 { 7 "matches": [ 8 "*://lens.google.com/search*" 9 ], 10 "js": ["content.js"], 11 "run_at": "document_idle" 12 } 13 ], 14 "manifest_version": 3 15}
註:
  • 經過測試後,發現只有 run_atdocument_idle 既情況下,我地既 content.js script 先會喺頁面由 Google Images redirect 去 Google Lens 結果頁之後執行。
    • 如果 run_atdocument_start,咁就要先手動 refresh Google Lens 搜尋結果頁,咁個 script 先會執行。

4.1.2 content.js

經過優化後,以下係最終既 script。
以下既格式其實可以用作 Chrome extensions 既 template。
1const MAX_RUN = Infinity; 2const DELAY_INCREMENT = 1000; 3const DELAY_MAX = 1000; 4var delay = 0; 5var done = false; 6 7const clickMore = () => { 8 const els = [...document.querySelectorAll('div')] 9 .filter(e => e.children.length === 0) 10 .filter(e => e.innerText === 'More exact matches'); 11 12 if (els.length > 0) { 13 els[0].click(); 14 console.log("Clicked button 'More exact matches'"); 15 setTimeout(clickMore, 500); 16 } else if (sortAllResults()) { 17 done = true; 18 } 19}; 20 21const extractWidth = (node) => Number.parseInt(node.innerText.replace(/.*?(\d+)x(\d+)/, '$1')); 22const extractHeight = (node) => Number.parseInt(node.innerText.replace(/.*?(\d+)x(\d+)/, '$2')); 23 24/* For troubleshooting, run below in browser console 25 26const extractWidth = node => Number.parseInt(node.innerText.replace(/.*?(\d+)x(\d+)/, '$1')); 27[...document.querySelectorAll('ul[aria-label="All results list"] li div')] 28 .filter(e => e.children.length === 0 || [...e.children].every(o => o.tagName.toLowerCase() === 'span')) 29 .filter(e => /.*?(\d+)x(\d+)/.test(e.innerText)) 30 .toSorted((a, b) => extractWidth(b)-extractWidth(a)) 31 .map(extractWidth) 32*/ 33 34const sortAllResults = () => { 35 const newListEl = document.createElement("ul"); 36 newListEl.setAttribute('aria-label', 'All results list'); 37 newListEl.style.width = '70%'; 38 39 const listEl = document.querySelector('ul[aria-label="All results list"]'); 40 41 if (!listEl) { 42 return; 43 } 44 45 [...listEl.children] 46 .sort((a, b) => { 47 const aEl = [...a.querySelectorAll('div')].filter(e => e.children.length === 0 || [...e.children].every(o => o.tagName.toLowerCase() === 'span')).filter(e => /.*?(\d+)x(\d+)/.test(e.innerText))[0]; 48 const bEl = [...b.querySelectorAll('div')].filter(e => e.children.length === 0 || [...e.children].every(o => o.tagName.toLowerCase() === 'span')).filter(e => /.*?(\d+)x(\d+)/.test(e.innerText))[0]; 49 50 const aArea = aEl ? (extractWidth(aEl) * extractHeight(aEl)) : 0; 51 const bArea = bEl ? (extractWidth(bEl) * extractHeight(bEl)) : 0; 52 53 return bArea - aArea; 54 }) 55 .forEach(e => { 56 newListEl.appendChild(e); 57 }); 58 59 document.body.replaceChildren(newListEl); 60 61 return true; 62}; 63 64const run = () => { 65 let i = 0; 66 67 const _run = () => { 68 if (i === MAX_RUN) { 69 return; 70 } 71 72 i++; 73 console.log('Running iteration ' + i); 74 75 if (done) { 76 return; 77 } else { 78 clickMore(); 79 } 80 81 setTimeout(_run, delay); 82 delay = Math.min(delay + DELAY_INCREMENT, DELAY_MAX); 83 }; 84 85 _run(); 86}; 87 88run();

4.2 安裝 Chrome extension

  1. 喺 Chrome 打開 chrome://extensions 頁面。
  2. 開啟 Developer mode。
  3. Load unpacked ➜ 打開裝住 manifest.json 既 folder。

5 測試

5.1 手動執行

我地唔一定要用 Chrome extension 去自動執行我地段 script。
喺冇或者禁用左 Chrome extension 既情況下:
  1. 打開 Google Images 頁面
  2. 將測試圖片 drag 到圖片上載框內。
  3. 撳圖片上既「Find image source」掣。
  4. 打開 Chrome developer tools 既 Console tab。
  5. 將段 script paste 落去,然後按 Enter 執行。
  6. 等頁面上既搜尋結果改變。

5.2 以 Chrome extension 自動執行

喺 Chrome extension 生效既情況下:
  1. 打開 Google Images 頁面
  2. 將測試圖片 drag 到圖片上載框內。
  3. 撳圖片上既「Find image source」掣。
  4. 等頁面上既搜尋結果改變。