<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          副業(yè)1000元,文末有代碼,制作一個谷歌瀏覽器插件,實現(xiàn)網(wǎng)頁數(shù)據(jù)爬蟲

          共 27945字,需瀏覽 56分鐘

           ·

          2022-11-29 15:26

          一、什么是瀏覽器插件

          瀏覽器插件,基于瀏覽器的原有功能,另外增加新功能的工具,是可定制瀏覽體驗的小型軟件程序,讓用戶可以根據(jù)個人需要或偏好來定制瀏覽器。

          如攔截網(wǎng)頁中的廣告、劃詞翻譯、倍速視頻等等。

          Chrome、edge等瀏覽器中都有專門的插件下載商店。 受某些原因限制,Chrome服務(wù)并不能正常訪問

          這里提供幾個常用的瀏覽器插件下載地址:

          Chrome插件,谷歌瀏覽器插件下載,chrome谷歌商店插件crx應(yīng)用推薦與下載-擴展迷

          Chrome插件,Chrome商店,谷歌瀏覽器插件下載,谷歌商店 - Chrome插件網(wǎng)

          極簡插件_Chrome擴展插件商店_優(yōu)質(zhì)crx應(yīng)用下載

          有興趣的小伙伴可以進入網(wǎng)站看看有沒有感興趣的、滿足自己定制化需求的插件。

          如何開發(fā)瀏覽器插件

          假如小伙伴找了半天,發(fā)現(xiàn)網(wǎng)上并沒有滿足自身需求的插件,那么你是否會考慮自己做一個呢?

          其實這并不是什么高大上的事,網(wǎng)上那么多好用的瀏覽器插件都是開發(fā)者基于Chrome開放出的瀏覽器插件api完成的。

          Chrome已經(jīng)把可能用到的各種“磚”封裝好開放出來了,那我們只需要按照一定的規(guī)則“搬”就行了啊。

          開發(fā)插件之前,我們首先要了解的是插件都有哪些能力,什么能實現(xiàn),什么不能實現(xiàn)。

          如果你想點一點就把15寸的筆記本屏放大到30寸,用完讓屏幕再縮小回去,那這插件肯定是實現(xiàn)不了的。

          相關(guān)文檔:

          chrome谷歌瀏覽器開發(fā)文檔

          360瀏覽器綜述--擴展開發(fā)文檔

          瀏覽器擴展 - Mozilla | MDN

          以上都是通篇文檔,可能對新手并不友好,讀起來枯燥無味。

          下面推薦一個能快速上手的博客,同時感謝該作者的技術(shù)輸出。Chrome插件(擴展)開發(fā)全攻略(干貨)

          常見爬蟲方法的對比

          后面我們會實現(xiàn)一個爬蟲功能的插件。 在開始實戰(zhàn)之前,我們可以先聊一聊常見爬蟲能力的優(yōu)缺點。

          1. api接口 該方法速度快,容易上手,會任意編程語言都可以實現(xiàn),且操作用戶對此無感知。

          但同時也有很大的缺點,這種方法很難同時發(fā)起用戶行為收集請求,有些產(chǎn)品會通過這些行為收集接口分析用戶的操作,如果邏輯變化,需要手動更新代碼到客戶處。

          如果只有數(shù)據(jù)接口請求,沒有統(tǒng)計接口請求,很容易被判定為爬蟲,從而產(chǎn)生一系列負面影響。

          有些產(chǎn)品還會有加密代碼,需要一些逆向工作,這就更進一步提高這種方法爬取數(shù)據(jù)的難度了。

          1. Selenium 該方法是通過運行測試的開源工具實現(xiàn)的,常見編程語言都有對應(yīng)的工具,相較于第一種方法有著更廣范圍的應(yīng)用場景。

          該方法通過啟動相關(guān)驅(qū)動支持的真實的瀏覽器,盡可能的模擬用戶操作,相關(guān)行為分析會自動請求,幾乎不需要逆向,一定程度上填補了第一種方法的弊端。

          但同時該方法也有弊端,需要給客戶機安裝運行環(huán)境和客戶的Chrome瀏覽器升級等問題。

          瀏覽器升級可能導(dǎo)致Selenium驅(qū)動版本和瀏覽器版本不匹配,程序就會運行失敗。邏輯變化需要手動更新到客戶處。

          該方式也會被產(chǎn)品方識別出是程序啟動而不是真實用戶啟動的瀏覽器,從而產(chǎn)生負面影響。

          1. 瀏覽器插件 該方法是通過瀏覽器的開放能力實現(xiàn)的,是用戶啟動的真實瀏覽器,進一步填充了前兩種方法的弊端,通過各種形式的腳本實現(xiàn)復(fù)雜的操作。

          可以發(fā)布到像app發(fā)布到應(yīng)用商店一樣發(fā)布到瀏覽器應(yīng)用商店,且提供線上更新功能。

          該方法必須得會JavaScript腳本語言,同時熟知瀏覽器的開放能力,增加了學(xué)習(xí)難度。

          該方法仍可以被產(chǎn)品識別出,如使用 MutationObserver 方法檢測出dom變化等。

          沒有完美的方法,只有更適合的方法,不同場景使用不同的技術(shù)應(yīng)對不同的困難即可。

          實現(xiàn)一個瀏覽器爬蟲插件

          介紹完常見爬蟲的區(qū)別后,接下來,我們就開始實現(xiàn)一個瀏覽器爬蟲插件。

          此處假設(shè)小伙伴已經(jīng)閱讀上述推薦的博客并基本熟悉瀏覽器插件的能力。

          需求:爬取10頁boss直聘網(wǎng)站上全國范圍內(nèi)Python崗位的招聘信息。

          拆解需求:

          1. 目標(biāo)網(wǎng)站:

          boss直聘網(wǎng)站

          1. 篩選條件:

          城市:全國 關(guān)鍵詞:Python

          1. 數(shù)量:

          1-10頁內(nèi)的全部數(shù)據(jù)

          url地址:https://www.zhipin.com/web/geek/job?query=Python&city=100010000&page=1

          難點分析

          1. 使用什么腳本類型

          插件有injected、content、popup、background、devtools 5種類型的腳本,不同類型擁有不同的能力,相互之間的通信方式也不盡相同。

          所以首先需要根據(jù)需求結(jié)合具體類型腳本的能力來確定使用什么腳本。

          popup肯定是需要的,給文件指定名稱和下達開始爬取的命令時要用到該類型腳本。

          此處已確定popup腳本,其他類型待定。

          1. 攔截網(wǎng)絡(luò)請求

          經(jīng)分析,從dom結(jié)構(gòu)中獲取數(shù)據(jù)不靠譜。 如,跳轉(zhuǎn)鏈接,某些產(chǎn)品的鏈接并不放在dom中,而是通過點擊事件句柄判斷按鈕的index、id等唯一標(biāo)識,從js作用域中找到對應(yīng)的鏈接進行跳轉(zhuǎn)。

          那么就需要考慮怎么能攔截到網(wǎng)絡(luò)請求了。插件的核心是不同類型js腳本,不同類型的腳本能力不同,需結(jié)合實際考慮。

          在5種腳本類型對比可知,只有injected、devtools、background可以攔截到網(wǎng)絡(luò)請求。但background拿不到響應(yīng)體,故拋棄。

          devtools功能很強大,它可以模擬出一個和開發(fā)者工具(F12)-網(wǎng)絡(luò)(network)功能幾乎一樣的面板,但實現(xiàn)起來會相對復(fù)雜。

          前端同學(xué)常用的React Developer Tools、vue-tools調(diào)試面板就是使用該技術(shù)開發(fā)的。

          經(jīng)過權(quán)衡對比,使用更加簡單的injected來做網(wǎng)絡(luò)攔截。

          此處已確定popup和injected兩種腳本。

          1. 通信

          爬取過程很簡單,通信是一件復(fù)雜的事,詳情通信可參考上述文檔。

          現(xiàn)在的流程是 injected攔截網(wǎng)絡(luò)請求 -> popup下達開始爬取的指令 -> injected開始執(zhí)行腳本收集數(shù)據(jù) -> injected清洗并導(dǎo)出數(shù)據(jù)。

          現(xiàn)在確定的popup和injected兩種類型能滿足嗎?很遺憾,不能,通過上述博客中總結(jié)的通信方式可知,這兩種類型的腳本不能直接通信,也就是popup不能告訴injected可以開始收集數(shù)據(jù)了。

          怎么實現(xiàn)呢?需要引入一個“中介”——content,作為popup和injected中間通信的橋梁。

          現(xiàn)在的過程就變成了,injected攔截網(wǎng)絡(luò)請求 -> popup下達開始爬取的指令 -> content轉(zhuǎn)發(fā)指令-> injected開始執(zhí)行腳本收集數(shù)據(jù) -> injected清洗并導(dǎo)出數(shù)據(jù)

          這里可以留一個小問題,最后一步可以使用content實現(xiàn)嗎,為什么不使用這種方式?

          此處已確定popup和injected、content三種腳本。

          代碼部分

          確定腳本選型后就可以創(chuàng)建工程了,新建一個文件夾,創(chuàng)建如下的目錄結(jié)構(gòu):

          boss-plugin
          ├─ html
          │  └─ popup
          │     ├─ popup.html // 點擊瀏覽器右上角插件,彈出popup,傳遞用戶指令
          │     └─ popup.js
          ├─ js
          │  ├─ content // content腳本通過manifest.json配置文件可以直接添加到頁面中
          │  │  ├─ install.js // injected腳本并不能直接通過配置添加到頁面中,需要通過content執(zhí)行js代碼動態(tài)插入到dom中
          │  │  └─ page.js // “中介角色”,轉(zhuǎn)發(fā)指令
          │  └─ inject
          │     ├─ network.js // 攔截網(wǎng)絡(luò)請求
          │     ├─ page.js // 具體執(zhí)行收集、清洗、導(dǎo)出數(shù)據(jù)的邏輯
          │     └─ pikazExcel.js  // 導(dǎo)出數(shù)據(jù)為Excel的js類庫
          └─ manifest.json // 瀏覽器識別插件配置的文件,必須

          manifest.json

          {
            "name""爬取boss數(shù)據(jù)",
            "version""1.0",
            "manifest_version"2,
            "browser_action": {
              "default_popup""/html/popup/popup.html"
            },
            "content_scripts": [
              {
                "matches": ["*://www.zhipin.com/*"],
                "js": ["/js/content/page.js""/js/content/install.js"],
                "run_at""document_start"
              }
            ],
            "web_accessible_resources": [
              "/js/inject/pikazExcel.js",
              "/js/inject/page.js",
              "/js/inject/network.js"
            ]
          }

          html/popup/popup.html

          <!DOCTYPE html>
          <html lang="en">
            <head>
              <meta charset="UTF-8" />
              <style>
                #box {
                  align-items: center;
                }
                #input-box {
                  display: flex;
                  align-items: center;
                }
                .label {
                  white-space: nowrap;
                }
                #btn-box {
                  padding-left50px;
                  padding-top10px;
                }
              
          </style>
            </head>
            <body>
              <div id="box">
                <div id="input-box">
                  <span class="label">文件名:</span>
                  <input id="filename" type="text" placeholder="可選,默認為當(dāng)前時間" />
                </div>
                <div id="btn-box">
                  <button id="export-btn">導(dǎo)出數(shù)據(jù)</button>
                </div>
              </div>
              <script src="/html/popup/popup.js"></script>
            </body>
          </html>

          html/popup/popup.js

          function onClickExport({
              document.getElementById('export-btn').disabled = true
              const filename = document.getElementById('filename').value
              const cb = (tab) => {
                  chrome.tabs.sendMessage(tab.id, { action"CHANGE_POPUP_ALLOW_DOWNLOAD", filename });
              }
              chrome.tabs.getSelected(null, cb);
          }
          document.getElementById('export-btn').onclick = onClickExport

          js/content/install.js

          setTimeout(() => {
              const pageScript = document.createElement('script');
              pageScript.setAttribute('type''text/javascript');
              pageScript.setAttribute('src', chrome.extension.getURL("/js/inject/page.js"));
              document.head.appendChild(pageScript);

              const networkScript = document.createElement('script');
              networkScript.setAttribute('type''text/javascript');
              networkScript.setAttribute('src', chrome.extension.getURL('/js/inject/network.js'));
              document.head.appendChild(networkScript);

              const excelScript = document.createElement('script');
              excelScript.setAttribute('type''text/javascript');
              excelScript.setAttribute('src', chrome.extension.getURL("/js/inject/pikazExcel.js"));
              document.head.appendChild(excelScript);
          });

          js/content/page.js

          // 轉(zhuǎn)發(fā)popup指令  popup => content script => inject script
          chrome.extension.onMessage.addListener(
              function (request{
                  if (request.action == "CHANGE_POPUP_ALLOW_DOWNLOAD") {
                      // popup 告訴頁面可以開始收集并下載數(shù)據(jù)了
                      window.postMessage({ action'CHANGE_POPUP_ALLOW_DOWNLOAD'popupAllowDownloadtruefilename: request.filename }, '*');
                  }
              }
          );

          js/inject/network.js

          此處需要注意瀏覽器發(fā)起請求的兩種方式:xhr和fetch,前者使用較多,后者也在開發(fā)過程中見到過。

          const _requestTools = {
              formatQueryString(queryString = '') {
                  const result = {};
                  if (queryString.length > 0) {
                      queryString = queryString.split('?')[1].split('&');
                      for (let kv of queryString) {
                          kv = kv.split('=');
                          if (kv[0]) result[kv[0]] = decodeURIComponent(kv[1]);
                      }
                  }
                  return result
              }
          }

          function _initXMLHttpRequest({
              // 攔截網(wǎng)絡(luò)請求方法1
              const open = XMLHttpRequest.prototype.open;
              const _targetApiList = [
                  'wapi/zpgeek/search/joblist.json'
              ]
              XMLHttpRequest.prototype.open = function (...args{
                  this.addEventListener('load'function ({
                      // 如果當(dāng)前url并不包含_targetApiList中任意一個地址,則阻止后續(xù)操作
                      if (!_targetApiList.find(item => this.responseURL.includes(item))) return

                      const result = {
                          responseHeaders: {},
                          responseData: {},
                          requestthis,
                          statusthis.status,
                          params: _requestTools.formatQueryString(this.responseURL)
                      }
                      // 格式化響應(yīng)頭
                      this.getAllResponseHeaders().split("\r\n").forEach((item) => {
                          const [key, value] = item.split(": ");
                          if (key) result.responseHeaders[key] = value;
                      });
                      if (result.responseHeaders["content-type"].includes("application/json")) {
                          // 如果響應(yīng)頭是content-type是json,則格式化響應(yīng)體
                          if (this.response?.length) result.responseData = JSON.parse(this.response);
                      }

                      _crawler.collectData(result)
                  })

                  return open.apply(this, args);
              };

              // 攔截網(wǎng)絡(luò)請求方法2
              // 此處的方法攔截在目標(biāo)網(wǎng)站中并沒有遇到,在其他項目中遇到過,故添加在此做補充知識點。
              const { fetch: originalFetch } = window;
              window.fetch = async (...args) => {
                  let [resource, config] = args;

                  let response = await originalFetch(resource, config);
                  if (response.status === 200) {
                      response
                          .clone()
                          .json()
                          .then((data) => {
                              console.log('響應(yīng)數(shù)據(jù):', data)
                          });
                  }

                  return response;
              };

          }
          _initXMLHttpRequest();

          js/inject/page.js

          // 因為inject js和頁面共享js作用域,為防止污染全局變量,故插件中變量名都以_開頭
          class _Crawler {
              constructor() {
                  this.downloadPageNum = 10  // 允許下載多少頁
                  this.filename = ''  // 從popup傳進來的輸入的文件名
                  this.allowDownload = false // popup給出指令允許下載
                  this.collectionList = [] // 收集每頁請求得到的數(shù)據(jù)
              }

              /**
               * 獲取當(dāng)前年-月-日 時:分:秒
               * @returns string
               */

              getTime() {
                  const time = new Date();
                  const timeInfo =
                      (time.getFullYear() + '-' + (time.getMonth() + 1) + '-' + time.getDate() + ' ' + time.getHours() + ':' + time.getMinutes() + ':' + time.getSeconds())
                  return timeInfo
              }

              // 生成隨機延遲秒數(shù), 默認3-4秒
              getRandomTimeOut(x = 3000, y = 4000) {
                  return Math.round(Math.random() * (y - x) + x)
              }

              collectData(result) {
                  // 首次進來或搜索條件變化,清空收集結(jié)果
                  const currentPage = result.params.page * 1;
                  if (currentPage * 1 === 1this.collectionList = []
                  if (!this.collectionList.find(el => result.request.responseURL.includes(el.responseURL))) {
                      const item = {
                          responseURL: result.request.responseURL,
                          responseData: result.responseData
                      }
                      this.collectionList.push(item)
                  }

                  // 如果沒有點擊導(dǎo)出按鈕,則阻止后續(xù)操作
                  if (!this.allowDownload) return

                  // 結(jié)束收集行為的條件,然后進行數(shù)據(jù)清洗和導(dǎo)出excel
                  if (currentPage >= this.downloadPageNum) {
                      const sheet = this.clearData()
                      this.download(sheet)
                  } else {
                      // 隨機3-4秒后進行點擊下一頁
                      // 這是寫爬蟲最基本的道德了,盡量在學(xué)習(xí)技術(shù)的同時,不要對目標(biāo)服務(wù)器產(chǎn)生壓力和影響其正常運行
                      const randomTimeout = this.getRandomTimeOut()
                      setTimeout(() => {
                          this.handleClickNext()
                      }, randomTimeout);
                  }
              }

              clearData() {
                  const headerAndKeyList = [
                      {
                          header'崗位名稱',
                          key'jobName'
                      },
                      {
                          header'地址',
                          key'jobAddress'
                      },
                      {
                          header'薪資',
                          key'salaryDesc'
                      },
                      {
                          header'經(jīng)驗',
                          key'jobExperience'
                      },
                      {
                          header'學(xué)歷',
                          key'jobDegree'
                      },
                      {
                          header'技術(shù)棧',
                          key'skills'
                      },
                      {
                          header'公司名稱',
                          key'brandName'
                      },
                      {
                          header'公司行業(yè)',
                          key'brandIndustry'
                      },
                      {
                          header'公司融資階段',
                          key'brandStageName'
                      },
                      {
                          header'公司規(guī)模',
                          key'brandScaleName'
                      },
                      {
                          header'福利待遇',
                          key'welfareList'
                      },
                  ]
                  const itemTableConfig = {
                      tHeader: headerAndKeyList.map(el => el.header),
                      keys: headerAndKeyList.map(el => el.key),
                      table: []
                  }
                  this.collectionList.forEach(el1 => {
                      el1.responseData.zpData.jobList.forEach(el2 => {
                          const { jobName, cityName, areaDistrict, businessDistrict, salaryDesc, jobExperience, jobDegree, skills, brandName, brandIndustry, brandStageName, brandScaleName, welfareList } = el2
                          const item = {
                              jobName,
                              jobAddress`${cityName}·${areaDistrict}·${businessDistrict}`,
                              salaryDesc, jobExperience, jobDegree, skills, brandName, brandIndustry, brandStageName, brandScaleName, welfareList
                          }
                          itemTableConfig.table.push(item)
                      })
                  })
                  return [itemTableConfig]
              }

              download(sheet) {
                  const filename = this.filename || this.getTime()
                  window.pikazExcelJs.default.excelExport({
                      sheet,
                      filename,
                      beforeStart(bookType, filename, sheet) => {
                          console.log("開始導(dǎo)出", bookType, sheet, filename);
                      },
                  }).then(() => {
                      this.filename = ''
                      this.allowDownload = false
                      this.collectionList = []
                  });
              }

              handleClickNext() {
                  const nextSelector = '.pagination-area .options-pages a:last-child'
                  const nextDom = document.querySelector(nextSelector)
                  nextDom.click()
                  // 如果目標(biāo)網(wǎng)站有收集用戶行為的接口,此處可添加模擬用戶操作,如滾動頁面、點擊某些元素
              }
          }
          const _crawler = new _Crawler();

          // 監(jiān)聽從popup發(fā)送的指令 popup => content script => inject script
          window.addEventListener("message"function (e{
              if (e.data.action === 'CHANGE_POPUP_ALLOW_DOWNLOAD') {
                  _crawler.filename = e.data.filename
                  _crawler.allowDownload = true
                  _crawler.handleClickNext()
              }
          }, false);

          js/inject/pikazExcel.js

          文檔和下載地址: 
          https://www.npmjs.com/package/pikaz-excel-js

          最后在Chrome瀏覽器中打開這個地址 chrome://extensions/

          開啟開發(fā)者模式 -> 加載已解壓的擴展程序 -> 選擇剛才新建的文件夾 -> 確認導(dǎo)入

          這時候就已經(jīng)把剛才編寫的導(dǎo)入到瀏覽器中了,打開目標(biāo)頁面

          然后點擊紅框區(qū)域,輸入文件名(可選),點擊導(dǎo)出數(shù)據(jù),就可以開始爬取內(nèi)容了

          最終效果:

          參考鏈接,感謝以下鏈接提供相關(guān)技術(shù)的解決思路:

          XMLHttpRequest 攔截處理

          Chrome插件(擴展)開發(fā)全攻略(干貨)

          pikaz-excel-js

          代碼下載地址

          鏈接:https://pan.baidu.com/s/1RHYE-CuZqmBJm7Wj9G4fYQ

          提取碼:u5cp

          如果想接副業(yè)單子

          如果想跟著螞蟻老師做副業(yè)兼職,可以每晚22點來螞蟻老師抖音直播間:
          抖音賬號:Python導(dǎo)師-螞蟻


          瀏覽 128
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本乱伦福利 | 九九伊人大香蕉 | 四虎影院永久在线 | 色女人大香蕉 | 久久亚洲AV成人无码电影麻豆 |