// ==UserScript==
// @name 必应净化
// @namespace [email protected]
// @match https://www.bing.com/search*
// @match https://www2.bing.com/search*
// @match https://cn.bing.com/search?*
// @run-at document-start
// @version 1.2.3
// @author 魂祈梦
// @description 去除多余的搜索建议和低质量搜索结果,2024/1/23 21:51:58
// @icon https://s11.ax1x.com/2024/01/24/pFetIiR.png
// @require https://registry.npmmirror.com/jquery/3.7.0/files/dist/jquery.min.js
// @require https://registry.npmmirror.com/sweetalert2/10.16.6/files/dist/sweetalert2.all.min.js
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @noframes
// @license GPL-3.0-or-later
// ==/UserScript==
(function () {
'use strict';
let count = 0
let regex = []
// 提供一个默认配置,用户设置覆盖该默认值
/** @type {BingSearchConfig} */
let config = {
// 用户如果修改,直接复制一份存起来
filterList: [
{ name: 'csdn', domain: 'csdn.net', enabled: true },
{ name: '抖音', domain: 'www.douyin.com', enabled: true }
],
blockList: [
{ name: 'baidu', regexList: ["/shouji.baidu/", "/lightapp.baidu/", "/author.baidu/", "/ubs.baidu/", "/m2.baidu/", "/baijiahao.baidu/"], enabled: true },
],
userBlockList: []
}
// main,主进程现在开始
// 只有首次执行使用默认配置来初始化用户配置,之后全部使用用户配置
init()
// 使用bing搜索引擎的内置规则,过滤一些站点
filterWithEngine(config.filterList)
// 使用js代码,在搜索结果生成后,移除部分搜索结果
blockSearchResult()
// 构建按钮等GUI界面
initGUI();
// 后面都是方法定义等代码
/**
* 脚本安装后首次运行时,初始化
* */
function init() {
var userConfig = GM_getValue('dreamqi-bingSearch-config')
if (userConfig) {
config = userConfig;
} else {
config.blockList.forEach(e => {
if (e.enabled) config.userBlockList.push(...e.regexList)
})
GM_setValue('dreamqi-bingSearch-config', config)
}
regex = config.userBlockList
// 添加菜单选项
GM_registerMenuCommand('屏蔽域名列表设置', function () {
settingGUI()
console.log('菜单选项被点击')
});
GM_registerMenuCommand('列表合并', function () {
// window.open('https://listmerge.dreamsoul.cn/', '_blank');
function openLinkInNewTab(url) {
// 创建 <a> 元素
const link = document.createElement('a');
// 设置 <a> 元素的属性
link.href = url;
link.target = '_blank';
link.style.display = 'none';
// 将 <a> 元素添加到文档中
document.body.appendChild(link);
// 模拟点击事件
link.click();
// 点击后立即删除 <a> 元素
document.body.removeChild(link);
}
// 示例调用
openLinkInNewTab('https://listmerge.dreamsoul.cn/');
});
}
/**
* 通过搜索引擎的高级搜索功能,过滤某些站点。好处是:过滤掉的站点不占搜索结果的条数。而目前谷歌google和必应bing都是一页10条(如果把csdn屏蔽了可能一页都看不到3条记录)
* 用法:传入域名列表,效果:使用-site:xxx来屏蔽域名,将会重新跳转(因此该脚本生效时,一般会先通过-site来屏蔽域名,而后直接使用dom操作移除某些不太常见的网站)
* @param {FilterItem[]} list - The list of filter items to apply
*/
function filterWithEngine(list) {
let currentUrl = window.location.href;
// 解析URL
let url = new URL(currentUrl);
// 大概是下面这样子 -site:csdn.net -site:baidu.com
let strBlockList = ''
list.forEach(item => {
if (!item.enabled) return;
strBlockList += ` -site:${item.domain}`
})
// 修改查询参数
if (!url.searchParams.has("dreamsoulmodified")) {
// 修改查询参数
let keyword = url.searchParams.get("q");
url.searchParams.set('q', keyword + strBlockList)
url.searchParams.set("dreamsoulmodified", "true");
console.log(keyword)
// 使用history.replaceState修改URL而不刷新页面
history.replaceState({}, '', url.toString());
// 重新加载页面以显示修改后的URL
window.location.reload();
} else {
let keyword = url.searchParams.get("q");
console.log(keyword)
// 如果URL已经包含了修改的查询参数,则不再进行修改
console.log("URL已经包含了修改的查询参数,不再进行修改。");
}
}
function blockSearchResult() {
const currentUrl = window.location.href;
// cn版本bing
const cnBingRegex = /^https:\/\/cn.bing.com/;
const isCn = currentUrl.match(cnBingRegex);
let isSelfModified = false;
document.addEventListener("DOMContentLoaded", () => {
console.log('DOMContentLoaded')
purify()
isSelfModified = true;
createObserver()
})
function createObserver() {
console.log('开始监听dom变化')
// 监听整个文档的 DOM 变化
const TARGET_SELECTOR = "#b_results .b_algo"; // 目标元素选择器
// const PARENT_SELECTOR = document.querySelector('#b_results'); // 监听的父容器(可缩小范围,提升性能)
const PARENT_SELECTOR = document.body; // 监听的父容器(可缩小范围,提升性能),至少得是body,因为我们不清楚到底哪里变了
const DEBOUNCE_TIME = 300; // 防抖时间(单位:ms,根据实际渲染速度调整)
// 防抖计时器
let renderTimer = null;
// 创建监听器
const observer = new MutationObserver((mutations) => {
// 每次 DOM 变化时,重置防抖计时器
clearTimeout(renderTimer);
renderTimer = setTimeout(() => {
// 检查目标元素是否存在
let element = document.querySelectorAll(TARGET_SELECTOR)
if (element.length > 0) {
purify()
isSelfModified = true;
}
}, DEBOUNCE_TIME);
});
// 配置监听选项(监听子节点变化)
observer.observe(PARENT_SELECTOR, {
childList: true,
subtree: true, // 递归监听所有子节点
});
}
function purify() {
if (isCn) {
removeList(regex)
} else {
removeGlobalList(regex)
}
removeOthers()
}
function removeGlobalList(regex) {
regex = regex.map(el => {
// 去除首尾的斜杠
el = el.slice(1, el.length - 1)
// 将双斜杠换成单斜杠,注意在控制台还是显示两个斜杠(如果是真正的两个斜杠,在控制台会打印出四个斜杠)
// el = el.replace(/\\\\/g,"\\");
el = new RegExp(el)
return el
})
let resultList = document.querySelectorAll('cite');
console.log(resultList)
for (let item of resultList) {
for (let el of regex) {
if (el.test(item.textContent)) {
item.closest('.b_algo').remove();
count++
console.log('已去除:' + item.textContent)
}
}
}
}
function removeList(regex) {
regex = regex.map(el => {
// 去除首尾的斜杠
el = el.slice(1, el.length - 1)
// 将双斜杠换成单斜杠,注意在控制台还是显示两个斜杠(如果是真正的两个斜杠,在控制台会打印出四个斜杠)
// el = el.replace(/\\\\/g,"\\");
el = new RegExp(el)
return el
})
let originList = $('#b_results .b_algo')
// 去除没有链接的
// console.log('originList', originList)
for (let item of originList) {
// 6月28日改,与正常搜索结果不同的(没有b_tpcn这个类的)是广告。
if (item.querySelector('.b_tpcn') === null) {
item.remove();
count++;
continue;
}
// 正则匹配href,如果匹配到了,则移除该节点。
let url
if (item.querySelector('.tilk')) {
url = item.querySelector('.tilk').href
} else {
url = ''
}
// item.querySelector('.tilk').href
for (let el of regex) {
if (el.test(url)) {
item.remove()
count++
console.log('已去除:' + url)
}
}
}
}
function removeOthers() {
// 去除搜索建议
let removeList = document.querySelectorAll('#b_results .b_ans')
removeList.forEach((item) => {
item.remove()
count++
})
// 去除展开,比如知乎的问答,一个链接下面出现了很多莫名其妙的小链接,去除了这些小链接。
let deepLink = document.querySelector('.b_deeplinks_block_container')
if (deepLink) {
console.log('去除deeplinks(常见于知乎)');
deepLink.remove()
count++
}
let deepDesk = document.querySelector('.b_deepdesk')
if (deepDesk) {
deepDesk.remove()
console.log('去除deepDesk(搜索bing时出现词典地图之类的同类产品)');
count++
}
// 去除各种乱七八糟的搜索结果(推荐搜索、广告、推荐回答等)
let recommandList = document.querySelectorAll('#b_pole,.b_algoRCAggreFC,.rpr_light,.b_algospacing,.b_ad,.b_ans,#inline_rs')
recommandList.forEach(item => {
item.remove()
count++
})
// 去除带有data-favicon-t属性的父级元素(也就是搜索结果)
// document.querySelectorAll('[data-favicon-t]').forEach(item =>{
// console.log(item)
// item.parentNode.remove();
// count++;
// })
// 去除豆包广告
let doubaoList = document.querySelectorAll('.b_overflow2')
if (doubaoList.length) {
console.log('去除豆包广告');
count++;
}
doubaoList.forEach(item => {
// 去除包含广告类的li
item.closest('.b_algo').remove()
count++
})
}
}
// bing净化按钮被点击时触发的方法,bing搜索页面的按钮可触发,油猴脚本中的选项按钮也能触发。
function settingGUI() {
console.log('设置被点击。')
// 注意,下面这个函数是异步的
Swal.fire({
title: '添加正则',
input: "text",
inputLabel: "此处输入正则(左右不需要加/)",
showCloseButton: true,
showDenyButton: true,
showCancelButton: true,
confirmButtonText: `
确定
`,
confirmButtonAriaLabel: "确定",
// preConfirm: input=>{
// input = '/'+input+'/'
// console.log("preConfirm")
// regex.push(input)
// Swal.fire({
// title: "添加成功",
// icon: "success"
// })
// console.log(regex)
// },
denyButtonText: `
修改正则列表
`,
cancelButtonText: `
取消
`,
cancelButtonAriaLabel: "Thumbs down",
focusConfirm: true
})
.then(result => {
console.log(result)
if (result.isConfirmed) {
console.log('确认')
let input = result.value
if (input == '') return
input = '/' + input + '/'
regex.push(input)
config.userBlockList = regex
GM_setValue('dreamqi-bingSearch-config', config)
Swal.fire({
title: "添加成功",
icon: "success"
})
// Swal.fire({
// title: '获取到正则',
// text: test
// })
} else if (result.isDenied) {
const textarea = document.createElement('textarea')
textarea.innerHTML = JSON.stringify(GM_getValue("dreamqi-bingSearch-config").userBlockList)
textarea.id = 'modText'
textarea.style.width = '100%'
textarea.style.height = '75vh'
Swal.fire({
title: "修改正则",
html: textarea
// `
// <textarea id="modText" style="width: 100%;height:75vh;">${GM_getValue("dreamqi-bingSearch-regex")}</textarea>
// `
,
showCloseButton: true,
showCancelButton: true,
focusConfirm: false,
})
.then(result => {
// 如果点了确定以外的按钮就退出
if (!result.isConfirmed) return
let input = textarea.value
input = JSON.parse(input)
config.userBlockList = input
GM_setValue('dreamqi-bingSearch-config', config)
Swal.fire({
title: "修改成功",
icon: "success"
})
})
// 由于异步函数的特性,下面这条语句能在滑动条出现时(前)将其设置在底部,不知道为什么提前设置没有反应(可能是在该元素出现在页面上时会被重新赋值scrollTop)。
textarea.scrollTop = textarea.scrollHeight
} else if (result.isDismissed) {
console.log('关闭')
}
});
}
// 依赖于lit的页面构建以及修改,应当写在该函数中
// 去除广告等功能和UI构建并无关系,因此不应该阻塞运行
async function initGUI() {
// 外站不能引用博客园的文件,因此还是用jsdelivr的链接,后续看情况可以换CloudFlare。(又拍云还不如jsdelivr)
let { LitElement, html, css } = await import('https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js')
class SettingButton extends LitElement {
open() {
class FilterSwitcher extends LitElement {
static properties = {
/** @type {{ type: { value: FilterItem[] } }} */
filterList: { type: Array }
};
constructor() {
super();
this.filterList = config.filterList;
console.log('油猴配置读取', this.filterList);
}
render() {
return html`
<div class="filter-inner">
<div class="header">
<h2 style="margin-top:0;">域名过滤设置</h2>
<button class="close-btn" @click=${() => this.remove()}>×</button>
</div>
${this.filterList.map((item, index) => html`
<div class="filter-item">
<label>
<input type="checkbox"
?checked=${item.enabled}
@change=${(e) => this.handleChange(e, item)}>
${item.name} (${item.domain})
</label>
<button @click=${() => this.handleDeleteFilter(index)} class="delete-btn">删除</button>
</div>
`)}
<div class="button-group">
<button @click=${this.handleAddFilter} class="add-btn">添加过滤</button>
<button @click=${this.handleSave} class="save-btn">保存</button>
</div>
</div>
`;
}
handleChange(e, item) {
item.enabled = e.target.checked;
}
handleSave() {
config.filterList = this.filterList
// Save to GM storage
GM_setValue('dreamqi-bingSearch-config', config);
// Remove the component
this.remove();
// Reload page to apply new filters
// window.location.reload();
}
async handleAddFilter() {
const { value: formValues } = await Swal.fire({
title: '添加新过滤',
html: `
<input id="swal-input1" class="swal2-input" placeholder="名称">
<input id="swal-input2" class="swal2-input" placeholder="域名">
`,
focusConfirm: false,
preConfirm: () => {
return {
name: document.getElementById('swal-input1').value,
domain: document.getElementById('swal-input2').value
}
}
});
if (formValues) {
this.filterList = [...this.filterList, {
name: formValues.name,
domain: formValues.domain,
enabled: true
}];
this.requestUpdate();
}
}
handleDeleteFilter(index) {
this.filterList.splice(index, 1); // 从filterList中删除指定索引的元素
this.requestUpdate(); // 更新组件以反映更改
}
static styles = css`
:host{
display: flex;
position: fixed;
z-index: 10;
inset: 0;
background: rgba(0,0,0,.4);
}
.filter-inner{
margin: auto;
padding: 1.5em;
width: 32em;
border-radius: 5px;
background-color: #fff;
animation: scaleAnimation 0.3s ease-out forwards;
}
@keyframes scaleAnimation {
0% {
transform: scale(0.7);
}
45% {
transform: scale(1.05);
}
80% {
transform: scale(0.95);
}
100% {
transform: scale(1);
}
}
.filter-item {
margin: 10px 0;
}
.save-btn {
padding: 8px 16px;
background: #0078d4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.save-btn:hover {
background: #006cbd;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.add-btn {
padding: 8px 16px;
background: #107c10;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-btn:hover {
background: #0b5a0b;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0 8px;
color: #666;
line-height: 1;
}
.close-btn:hover {
color: #000;
}
`
}
if (!customElements.get('filter-switcher')) {
customElements.define('filter-switcher', FilterSwitcher);
}
const filterSwitcher = document.createElement('filter-switcher');
document.body.appendChild(filterSwitcher);
}
static styles = css`
:host {
float: left;
}
`
render() {
return html`<div style="text-align:center;">
<button @click=${this.open}>-site配置</button>
</div>`;
}
}
const addElement = () => {
customElements.define('setting-button', SettingButton);
// 创建元素
const button = document.createElement('setting-button');
const $result = $('#b_tween,.b_scopebar');
// 添加到#b_tween或者b_scopebar中(开启代理后,只有b_scopebar)
$result.first().append(button);
if (/Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
$('#b_header').append(button);
}
// 添加block按钮
{
let $pNode = $('#b_tween,.b_scopebar').first()
// 添加设置按钮
let setting = document.createElement('button')
setting.style = 'border-width: revert;border-style: revert; padding: revert;'
setting.innerHTML = 'bing净化设置'
setting.addEventListener('click', () => {
settingGUI()
})
$pNode.append(setting)
var ResultText = document.createElement('div')
ResultText.id = 'resultText'
ResultText.innerHTML = `
<h2>已去除${count}个广告或者多余链接</h2>
`
// 移动端UA,去除IPad,webOS
if (/Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
$('#b_header').append(setting);
$('#b_header').append(ResultText);
return;
}
$pNode.append(ResultText)
}
}
addElement()
}
})()