Greasy Fork

必应净化

去除多余的搜索建议和低质量搜索结果,2024/1/23 21:51:58

// ==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()
  }

})()