整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          「Vue3」保姆級毫無廢話的進階到實戰教程 - 01

          「Vue3」保姆級毫無廢話的進階到實戰教程 - 01

          為一個 React、Vue 雙修選手,在 Vue 3 逐漸穩定下來之后,是時候摸摸 Vue 3 了。Vue3 的變化不可謂不大,所以,本系列主要通過對 Vue3 中的一些 Big Changes 做詳細的介紹,然后封裝一個比較通用的業務腳手架,里面會增加很多非常有用的小技巧,讓你在 Vue 3 的世界里縱享絲滑 ~

          第一篇將著重介紹一些 Vue3 中重要的變化和概念 ~

          1、新建項目

          找個可以開機的電腦,在終端輸入:

          打開鏈接 http://127.0.0.1:5173,你的眼睛就會看到:

          代表項目初始化成功,你真棒!

          你的目錄會長這個樣子:

          注意:記筆記,小技巧學起來,在 vscode 終端中使用 tree 指令打印當前項目的文件樹:

          tree -I "node_modules|.vscode|cypress" > tree.txt

          這個命令會做兩件事:

          1. 使用 tree 命令輸出目錄樹結構,同時排除 node_modules、.vscode 和 cypress 文件夾
          2. tree 命令可以輸出目錄的樹形結構
          3. -I 參數用于排除某些文件夾不顯示
          4. 在排除參數中用 | 分隔多個要排除的文件夾名
          5. 使用 '>' 符號將標準輸出重定向到 tree.txt 文件(沒有就會自動生成一個)

          這樣 tree 命令的輸出就會被寫入到 tree.txt 文件中,而不是打印到終端屏幕上。在項目中執行,然后你就會在項目根目錄中看到一個 tree.txt 文件:

          .
          ├── README.md
          ├── cypress.config.ts
          ├── dist
          │   ├── assets
          │   │   ├── AboutView-4d995ba2.css
          │   │   ├── AboutView-675ecf4b.js
          │   │   ├── index-9f680dd7.css
          │   │   ├── index-d5df9149.js
          │   │   └── logo-277e0e97.svg
          │   ├── favicon.ico
          │   └── index.html
          ├── env.d.ts
          ├── index.html
          ├── package.json
          ├── pnpm-lock.yaml
          ├── public
          │   └── favicon.ico
          ├── src
          │   ├── App.vue
          │   ├── assets
          │   │   ├── base.css
          │   │   ├── logo.svg
          │   │   └── main.css
          │   ├── components
          │   │   ├── HelloWorld.vue
          │   │   ├── TheWelcome.vue
          │   │   ├── WelcomeItem.vue
          │   │   ├── __tests__
          │   │   │   └── HelloWorld.spec.ts
          │   │   └── icons
          │   │       ├── IconCommunity.vue
          │   │       ├── IconDocumentation.vue
          │   │       ├── IconEcosystem.vue
          │   │       ├── IconSupport.vue
          │   │       └── IconTooling.vue
          │   ├── main.ts
          │   ├── router
          │   │   └── index.ts
          │   ├── stores
          │   │   └── counter.ts
          │   └── views
          │       ├── AboutView.vue
          │       └── HomeView.vue
          ├── tree.txt
          ├── tsconfig.app.json
          ├── tsconfig.json
          ├── tsconfig.node.json
          ├── tsconfig.vitest.json
          ├── vite.config.ts
          └── vitest.config.ts

          簡單介紹一下重要的文件/文件夾:

          public

          里面的東西不會經過編譯和打包,所以放在 public 內的東西 ”不要引用“;

          通常會放置( 不被JavaScript ) 引用的靜態資源,例如:網頁標題欄 icon,即 favicon.ico;

          打包的時候,里面的東西會直接被復制一份,放在根目錄內,所以要引用 public 內的資源時,要使用根目錄絕對路徑,比如:要取 public/favicon.ico ,你應寫成 /favicon.ico;

          assets:放靜態資源,如:圖片、CSS

          components:放公用組件

          stores:與 Pinia 狀態管理器有關

          router:與 Vue Router 路由管理有關

          views:放路由組件

          2、深入認識 npm xxx

          在 package.json 的 scripts 中常用的有三個指令:

          npm run dev|npx vite:啟動項目本地服務,并提供 Hot Module Replacement (HMR),可以在更新代碼后即時更新頁面;

          是的,在項目根目錄下 npx vite 也可以啟動項目!

          • npm run build|npx vite build:項目打包

          • npm run preview|npx vite preview --port 4173:啟動一個本地服務,預覽打包后的 dist 內容,可以在正式布署前檢查打包后的內容是否有問題

          3、為什么要打包?

          Vue3項目打包的主要目:

          1. 必要的語法編譯
          2. 瀏覽器不認識 vue 文件,需要編譯成瀏覽器能讀懂的 js 和 css
          3. 瀏覽器不認識的新 ES 語法,需要 polyfill。
          4. Minify 代碼:去除無用代碼,節省瀏覽器解析的時間
          5. 在不影響程序運行的前提下,去除不必要的空白字元、注解,將變量名稱、函數名稱、參數名稱縮短等等,減少部署后的代碼體積。
          6. 整合第三方模塊:打包可以將引入的各種第三方模塊打包合并到一起,作為一個整體進行部署。通過 npm 或yarn 安裝所需的第三方庫,實際都放在 node_modules 內,卻可以透過 import from "xxx" 直接使用,不需要寫出該庫的完整路徑,這是打包工具的功勞。
          7. 性能優化:通過打包合并文件,減少頁面 HTTP 請求數量,加快加載速度。
          8. 代碼保護:源碼經過打包后會進行壓縮和混淆,可以起到一定的代碼保護作用。
          9. 兼容性優化:打包可以將代碼轉換為主流瀏覽器都能識別的語法,優化不同瀏覽器的兼容性。
          10. 部署方便:打包后生成的靜態資源文件可以很方便地部署到各種環境。
          11. 開啟gzip壓縮:打包過程可以進行gzip壓縮,優化靜態資源文件的體積。
          12. 分析代碼:通過分析打包后的文件,可以清晰看到項目中不同文件的組成和大小。

          打包主要是為了優化性能、安全性、擴展性、部署便利性等等,實際項目部署中也是必要的步驟。

          4、Option API 和Composition API 區別

          Vue 3 提供 Option API 和 Composition API 兩種寫法,Option API 是延續自 Vue 2 的寫法,Composition API 則是跟著 Vue 3 一起推出的新寫法。

          Option API 和 Composition API 主要差別有兩個:

          1. 管理邏輯代碼的方式,也就是<script>的部分,模板 template 和 style 的用法是一樣的。
          2. Composables (Vue 官網翻譯成 組合式函數)

          這里引用 FU 大神在 Vueconf 上的一個解釋:

          4.1 Option API

          所謂的 Option (選項),就是 Vue 將代碼根據特性不同,分類成 data、method、計算屬性(computed)、生命周期......不同選項,讓開發者通過選項 (Option) 的方式,來定義組件內的邏輯。

          代碼一般會根據選項式 API 被分成幾個區塊,比如這樣:

          4.2 Composition API

          在 Composition API 搭配 <script setup> 的情況下,可以將所有邏輯代碼直接寫在<script>內,就跟寫原生 Javascript 的感覺很像,幾乎沒有什么特殊規則,交由開發者自行管理。

          使用 Composition API 時,通常會根據「功能」來管理邏輯代碼,一個功能使用到的 data、method,聲明位置可以比較靠近,對于大的、復雜的組件來說,會提升可讀性。

          Composition API 的代碼一般是這樣:

          4.3 兩者區別

          • Option API:有既定規則,代碼按照選項分區管理
          • Composition API:較為彈性、自由,代碼通常按照功能邏輯分區管理,(或隨開發者偏好)

          兩種方式都有人喜歡,沒有絕對的好壞。 其實 「自由/ 彈性」 在程式碼管理上是把雙刃劍,更考驗開發者管理代碼的能力;也有開發者偏好在訂好的規則下進行開發,兩者并沒有優劣,而是風格差異。

          • Composition API 形式下,需要由開發者自己利用 reactive() 和 ref() 定義 data 是否有響應性,在其他地方取 data 時,也就要根據 reactive() 和 ref() 的規則進行取值和操作。
          • Option API 形式下,Vue 已經幫開發者做好了響應式,自動為 data 中的數據加上響應性,其他選項要取用 data 內的數據時,用 this.變量名 取得即可。

          但整個兩者背后的響應式機制是一樣的,核心概念不變,而是根據Vue 定義的寫法不同,有不同的使用方式,所以 Vue 官網將教學指南分成兩種 API 形式。

          復雜組件的情況下,Composition API 可讀性更好

          • 在開發比較小的組件時,兩者的可讀性其實沒有太大的差異。
          • 但隨著組件越大、越復雜,功能用到幾個不同的 Option,邏輯就會被分散在不同 Option 的區塊,隨著組件長大,可讀性會變差,要管理一個功能時,需要在不同的 Option 區塊之間切換。

          官網也給了這兩者的直觀上的區別:

          建議是都能熟悉使用,不然你怎么看懂別人的 shishan 代碼呢?

          5、使用 Composition API 封裝自定義 Hooks

          在 Vue 3 中,可以使用 Composition API 來封裝可重用的邏輯,類似于 React 中的 Hooks。

          通過自定義 Hooks,我們可以將可重用的邏輯封裝起來,并在多個組件中共享使用。這樣可以提高代碼的可維護性、重用性和可讀性,避免了代碼重復,并且使組件更加關注自身的業務邏輯。并且 hook 具有良好的命名空間和類型推導,易于測試等特性。

          5.1 useWindowSize

          下面封裝一個 useWindowSize 的自定義 Hooks,用于獲取當前窗口的寬度和高度:

          將窗口大小的邏輯封裝成一個可重用的函數,并且與具體的組件解耦。其他組件只需要導入并調用 useWindowSize,就可以獲取窗口大小,而不需要重復編寫監聽器和邏輯。

          比如,你就可以在組件中這樣使用 useWindowSize:

          5.2 useFetch 和 usePagination

          下面再來封裝兩個非常常用的自定義 Hooks:usePagination 和 useFetch。

          import { ref, computed, onMounted, isRef, watch } from "vue";
          
          export function usePagination(endpoint) {
            const currentPage=ref(1);
            const paginatedEndpoint=computed(()=> {
              return `${endpoint}?page=${currentPage.value}`;
            });
          
            function nextPage() {
              currentPage.value++;
            }
          
            function prevPage() {
              if (currentPage.value <=1) {
                return;
              }
          
              currentPage.value--;
            }
          
            return {
              endpoint: paginatedEndpoint,
              nextPage,
              prevPage
            };
          }
          
          export function useFetch(endpoint) {
            const data=ref(null);
            const loading=ref(true);
            const error=ref(null);
          
            function fetchData() {
              loading.value=true;
              // 也可以使用 axios
              return fetch(isRef(endpoint) ? endpoint.value : endpoint, {
                method: "get",
                headers: {
                  "content-type": "application/json"
                }
              })
                .then(res=> {
                  // 非 200 響應碼
                  if (!res.ok) {
                    const error=new Error(res.statusText);
                    error.json=res.json();
                    throw error;
                  }
          
                  return res.json();
                })
                .then(json=> {
                  data.value=json;
                })
                .catch(err=> {
                  error.value=err;
                  if (err.json) {
                    return err.json.then(json=> {
                      error.value.message=json.message;
                    });
                  }
                })
                .finally(()=> {
                  loading.value=false;
                });
            }
          
            onMounted(()=> {
              fetchData();
            });
          
            if (isRef(endpoint)) {
              watch(endpoint, ()=> {
                // 重新請求數據
                fetchData();
              });
            }
          
            return {
              data,
              loading,
              error
            };
          }

          下面逐個分析這兩個 Hooks 的封裝思路:

          1. usePagination:

          這個自定義 Hook 用于處理分頁邏輯。它接收一個 endpoint 參數,表示分頁請求的接口。

          • currentPage 是一個響應式引用(ref),用于存儲當前頁碼,默認為 1。
          • paginatedEndpoint 是一個計算屬性(computed),根據當前頁碼和 endpoint 拼接出分頁請求的完整接口。
          • nextPage 函數用于增加當前頁碼,即跳轉到下一頁。
          • prevPage 函數用于減少當前頁碼,即回到上一頁。

          最后,返回了一個對象,包含了 paginatedEndpoint、nextPage 和 prevPage。這樣,在多個組件中可以方便地使用分頁功能,而不需要重復編寫邏輯。通過修改 currentPage 的值,可以輕松地切換頁碼,并且 paginatedEndpoint 會自動更新,以便發起正確的分頁請求。

          2. useFetch:

          這個自定義 Hook 用于發送異步請求并處理響應數據。它接收一個 endpoint 參數,表示請求的接口。

          • data、loading 和 error 都是響應式引用(ref),分別用于存儲請求的響應數據、加載狀態和錯誤信息。
          • fetchData 函數用于發送請求。在函數內部,首先將 loading 置為 true,表示請求正在進行中。然后使用 Fetch API 發起請求,并根據請求的狀態進行相應的處理:
            • 如果響應狀態碼不是 200,拋出一個帶有狀態文本的錯誤。
            • 如果響應狀態碼為 200,將響應數據解析為 JSON,并將其賦值給 data。
            • 如果發生錯誤,將錯誤信息存儲在 error 中,并嘗試提取自定義的 JSON 錯誤響應。
          • onMounted 鉤子在組件掛載后調用 fetchData 函數,即在組件加載時發送請求。
          • 如果 endpoint 是響應式引用,則使用 watch 監聽 endpoint 的變化,并在變化時重新調用 fetchData,以重新獲取數據。

          最后,返回一個對象,包含了 data、loading 和 error。這樣,在組件中發送請求變得更加簡單。通過調用 fetchData 函數,可以發起請求并獲取響應數據,同時也可以監控請求的加載狀態和錯誤信息。這樣,組件可以更專注于業務邏輯,而不需要過多關注請求的細節。

          怎么使用呢?

          <script setup>
          import { usePagination, useFetch } from './hooks'
          
          const endpoint='https://example.com/api' 
          
          const { 
            endpoint: paginatedEndpoint,
            nextPage,
            prevPage
          }=usePagination(endpoint)
          
          const {
            data,
            loading,
            error
          }=useFetch(paginatedEndpoint)
          
          function handleNextPage() {
            nextPage()
          }
          
          function handlePrevPage() {
            prevPage()
          }
          </script>
          
          <template>
            <div>
              <button @click="handlePrevPage">Prev</button> 
              <button @click="handleNextPage">Next</button>
          
              <div v-if="loading">Loading...</div>
          
              <div v-if="data">
                <!-- display data -->
              </div>
          
              <div v-if="error">
                Error: {{ error }}
              </div>
            </div>
          </template>

          6、幾個重要的東西

          6.1 setup() 和 <script setup>

          在最新的 Composition API 中引入 <script setup>,其設計理念是去除不必要的包裝器和其他舊的組件選項,這樣就能更簡單、更集中地編寫組件。你可以認為 <script setup> 是 setup() 的語法糖,比如:

          使用 <script setup> 語法糖,我們可以更簡潔地編寫組件邏輯。不再需要寫多余的 return 語句:

          對比一下:

          6.2 Props 和 Events

          要聲明 props 和 emits 等選項,我們必須使用 <script setup> 中自動提供的所謂編譯器宏:

          這兩個函數不需要導入,在預處理 <script setup> 時會自動編譯。

          如果你使用了 TypeScript,那么最好也聲明一下 props 和 emits 的類型:

          6.3 多個 script

          在某些情況下,我們有可能希望在組件中聲明一些自定義選項,比如自定義組件名稱,<script setup> 實現不了的,可以用另外一種傳統的寫法,在編譯 *.vue 文件時,這兩個區塊會自動合并。

          6.4 defineExpose

          在 Vue 3 的 Composition API 中,defineExpose 用于將組件的一部分內容公開給父組件。你可以指定哪些屬性、方法或其他內容可以被父組件訪問和使用。比如:

          定義了一些。

          使用 defineExpose 函數將內部的屬性 exposedValue 和一個方法 exposedMethod 暴露出去。在模板中,我們可以像使用普通的屬性和方法一樣使用這些暴露的內容。在點擊按鈕時,調用了 exposedMethod 方法。

          父組件可以像下面這樣訪問和使用子組件暴露的內容:

          在父組件中,需要使用 ref 創建了一個對子組件的引用 childComponent。然后,我們可以通過 childComponent.value 來訪問子組件實例,并調用其公開的方法 exposedMethod。

          6.5 Suspense

          在組件中,我們通常需要異步請求數據,比如在 <script setup> 的頂層使用了 await。以這種方式定義的組件必須與 Suspense 組件一起使用(React 有的我也要有?),這樣 Vue 才能解決異步問題并正確加載組件。

          <Suspense> 是一個內置組件,用來在組件樹中協調對異步依賴的處理。它讓我們可以在組件樹上層等待下層的多個嵌套異步依賴項解析完成,并可以在等待時渲染一個加載狀態。

          <script setup>
          const user=await fetch(`/users/1`).then((d)=> d.json())
          </script>
          <template>
            <Suspense>
              <AsyncComponent />
            </Suspense>
          </template>

          不過需要注意:<Suspense> 還是一項實驗性功能。它不一定會最終成為穩定功能,并且在穩定之前相關 API 也可能會發生變化。

          7、SFC

          在 Vue 3 中,SFC(Single File Component,單文件組件)是一種將模板、腳本和樣式封裝在一個單獨的文件中的開發模式。SFC 提供了一種組織和編寫 Vue 組件的便捷方式,并提供了更好的可讀性和可維護性。

          一個典型的 Vue 3 SFC 包含三個部分:

          • 模板(template):每個 .vue 文件最多只能有一個<template>,內容會被 @vue/compiler-dom 預編譯成 JavaScript 的渲染函數。
          • 腳本(script):每個.vue最多只能有一個<script>(不包含<script setup>)
          • 樣式(style):每個.vue可以有多個<style>,樣式可以是普通的 CSS,也可以使用預處理器(如 Sass、Less)或 CSS-in-JS 庫(如 CSS Modules)來編寫。

          以下是一個示例 SFC 文件的結構:

          <template>
            <!-- 模板內容 -->
          </template>
          
          <script>
          // 腳本內容
          </script>
          
          <style>
          /* 樣式內容 */
          </style>

          因為瀏覽器并不認識.vue檔,所以在開發的時候,我們不能像以前一樣直接通過 live server 在瀏覽器上預覽項目,這也是為什么我們需要用到 Vite 或 Vue Cli 等建構工具,建構工具會根據 Vue 提供的 loader -@vue/compiler-sfc 來編譯 .vue 文件。

          Vue 3 的編譯器會解析 SFC 的模板、腳本和樣式部分,并將它們轉換為可執行的渲染函數、JavaScript 代碼和樣式。這樣,開發人員可以通過單個文件來組織組件的結構和邏輯,提高代碼的可維護性和可讀性,并且在構建過程中,Vue 3 的編譯器會將 SFC 轉換為可執行的 Vue 組件,使其能夠在瀏覽器中運行。

          打開瀏覽器的控制臺,你就會發現,雖然它還有.vue,但你再仔細看看它,它已經是js了:


          原文鏈接:https://juejin.cn/post/7277089907973603388

          日常的開發過程中,有時需要打印出代碼的樹狀目錄結構。

          讓我們看看如何使用Linux命令來打印目錄的樹形結構

          Linux命令行 打印文件系統樹結構有兩種方式:

          • 使用Linux tree命令
          • 使用Linux find、awk和sed命令的組合命令。

          使用linux tree命令打印目錄樹形結構

          這里以 wordpress 博客的源代碼做測試;

          1. 顯示當前目錄深度為1 的樹形目錄結構:

          ? tree -dL 1

          .

          ├── wp-admin

          ├── wp-content

          └── wp-includes

          ...

          -d

          僅列出目錄

          -L level

          目錄樹的最大顯示深度

          顯示當前目錄深度為2的樹形目錄結構:

          ? tree -dL 2

          .

          ├── wp-admin

          │ ├── css

          │ ├── images

          │ ├── includes

          │ ...

          ├── wp-content

          │ ├── ew_backup

          │ ├── languages

          │ ├── plugins

          │ ...

          └── wp-includes

          ├── ID3

          ├── IXR

          ├── Requests

          ...

          使用linux find 和 awk命令打印目錄樹形結構

          ? find . -type d | awk -F'/' '{

          if(NF==2){print "|-- " }else if(NF==3) {print "| |--" }}'

          這個命令和tree命令一樣,是一個打印深度為2的目錄樹結構。

          |-- wp-admin

          | |--css

          | |--images

          | |--js

          | ...

          |-- wp-includes

          | |--blocks

          | |--ID3

          | |--SimplePie

          | ...

          |-- wp-content

          | |--upgrade

          | |--wflogs

          | |--plugins

          | ...

          命令解釋:

          • 首先,列出當前目錄下的所有目錄結構;

          ? find . -type d

          .

          ./wp-admin

          ./wp-admin/css

          ./wp-admin/css/colors

          ...

          ./wp-includes

          ./wp-includes/blocks

          ./wp-includes/ID3

          ...

          • 然后,使用linux awk命令根據 “/” 分割行數據;

          awk -F'/' '{

          if(NF==2) {

          print "|-- "

          }else if(NF==3) {

          print "| |--"

          }

          }'

          當分隔字段的數目為2,表示樹結構的根目錄,則打印“|-”;

          當分隔字段的書目為3,則表示樹結構的葉節點,打印“| |–”;

          好的,您可以打印一個兩級樹形目錄結構。

          如果我們需要打印多級樹目錄結構呢?難道要寫多個if/else嗎?

          當然不。

          如何使用linux find和awk組合命令打印多級樹目錄結構


          find . -type d | awk -F'/' '{

          depth=3;

          offset=2;

          str="| ";

          path="";

          if(NF >=2 && NF < depth + offset) {

          while(offset < NF) {

          path=path "| ";

          offset ++;

          }

          print path "|-- "$NF;

          }

          }'

          depth:要打印的層數

          可以通過修改 depth來實現要打印的樹形目錄層級。

          面我們實現的爬蟲,運行起來后很快就可以抓取大量網頁,存到數據庫里面的都是網頁的html代碼,并不是我們想要的最終結果。最終結果應該是結構化的數據,包含的信息至少有url,標題、發布時間、正文內容、來源網站等。

          網頁正文抽取的方法

          所以,爬蟲不僅要干下載的活兒,清理、提取數據的活兒也得干。所以說嘛,寫爬蟲是綜合能力的體現。

          一個典型的新聞網頁包括幾個不同區域:

          新聞網頁區域

          我們要提取的新聞要素包含在:

          標題區域

          meta數據區域(發布時間等)

          配圖區域(如果想把配圖也提?。?/p>

          正文區域

          而導航欄區域、相關鏈接區域的文字就不屬于該新聞的要素。

          新聞的標題、發布時間、正文內容一般都是從我們抓取的html里面提取的。如果僅僅是一個網站的新聞網頁,提取這三個內容很簡單,寫三個正則表達式就可以完美提取了。然而,我們的爬蟲抓來的是成百上千的網站的網頁。對這么多不同格式的網頁寫正則表達式會累死人的,而且網頁一旦稍微改版,表達式可能就失效,維護這群表達式也是會累死人的。

          累死人的做法當然想不通,我們就要探索一下好的算法來實現。

          1. 標題的提取

          標題基本上都會出現在html的<title>標簽里面,但是又被附加了諸如頻道名稱、網站名稱等信息;

          標題還會出現在網頁的“標題區域”。

          那么這兩個地方,從哪里提取標題比較容易呢?

          網頁的“標題區域”沒有明顯的標識,不同網站的“標題區域”的html代碼部分千差萬別。所以這個區域并不容易提取出來。

          那么就只剩下<title>標簽了,這個標簽很容易提取,無論是正則表達式,還是lxml解析都很容易,不容易的是如何去除頻道名稱、網站名稱等信息。

          先來看看,<title>標簽里面都是設么樣子的附加信息:

          上海用“智慧”激活城市交通脈搏,讓道路更安全更有序更通暢_浦江頭條_澎湃新聞-The Paper

          “滬港大學聯盟”今天在復旦大學成立_教育_新民網

          三亞老人腳踹司機致公交車失控撞墻 被判刑3年_社會

          外交部:中美外交安全對話9日在美舉行

          進博會:中國行動全球矚目,中國擔當世界點贊_南方觀瀾_南方網

          資本市場迎來重大改革 設立科創板有何深意?-新華網

          觀察這些title不難發現,新聞標題和頻道名、網站名之間都是有一些連接符號的。那么我就可以通過這些連接符吧title分割,找出最長的部分就是新聞標題了。

          這個思路也很容易實現,這里就不再上代碼了,留給小猿們作為思考練習題自己實現一下。

          2. 發布時間提取

          發布時間,指的是這個網頁在該網站上線的時間,一般它會出現在正文標題的下方——meta數據區域。從html代碼看,這個區域沒有什么特殊特征讓我們定位,尤其是在非常多的網站版面面前,定位這個區域幾乎是不可能的。這需要我們另辟蹊徑。

          這些寫在網頁上的發布時間,都有一個共同的特點,那就是一個表示時間的字符串,年月日時分秒,無外乎這幾個要素。通過正則表達式,我們列舉一些不同時間表達方式(也就那么幾種)的正則表達式,就可以從網頁文本中進行匹配提取發布時間了。

          這也是一個很容易實現的思路,但是細節比較多,表達方式要涵蓋的盡可能多,寫好這么一個提取發布時間的函數也不是那么容易的哦。小猿們盡情發揮動手能力,看看自己能寫出怎樣的函數實現。這也是留給小猿們的一道練習題。

          3. 正文的提取

          正文(包括新聞配圖)是一個新聞網頁的主體部分,它在視覺上占據中間位置,是新聞的內容主要的文字區域。正文的提取有很多種方法,實現上有復雜也有簡單。本文介紹的方法,是結合老猿多年的實踐經驗和思考得出來的一個簡單快速的方法,姑且稱之為“節點文本密度法”。

          我們知道,網頁的html代碼是由不同的標簽(tag)組成了一個樹狀結構樹,每個標簽是樹的一個節點。通過遍歷這個樹狀結構的每個節點,找到文本最多的節點,它就是正文所在的節點。根據這個思路,我們來實現一下代碼。

          3.1 實現源碼

          #!/usr/bin/env python3
          #File: maincontent.py
          #Author: veelion
          import re
          import time
          import traceback
          import cchardet
          import lxml
          import lxml.html
          from lxml.html import HtmlComment
          REGEXES={
           'okMaybeItsACandidateRe': re.compile(
           'and|article|artical|body|column|main|shadow', re.I),
           'positiveRe': re.compile(
           ('article|arti|body|content|entry|hentry|main|page|'
           'artical|zoom|arti|context|message|editor|'
           'pagination|post|txt|text|blog|story'), re.I),
           'negativeRe': re.compile(
           ('copyright|combx|comment|com-|contact|foot|footer|footnote|decl|copy|'
           'notice|'
           'masthead|media|meta|outbrain|promo|related|scroll|link|pagebottom|bottom|'
           'other|shoutbox|sidebar|sponsor|shopping|tags|tool|widget'), re.I),
          }
          class MainContent:
           def __init__(self,):
           self.non_content_tag=set([
           'head',
           'meta',
           'script',
           'style',
           'object', 'embed',
           'iframe',
           'marquee',
           'select',
           ])
           self.title=''
           self.p_space=re.compile(r'\s')
           self.p_html=re.compile(r'<html|</html>', re.IGNORECASE|re.DOTALL)
           self.p_content_stop=re.compile(r'正文.*結束|正文下|相關閱讀|聲明')
           self.p_clean_tree=re.compile(r'author|post-add|copyright')
           def get_title(self, doc):
           title=''
           title_el=doc.xpath('//title')
           if title_el:
           title=title_el[0].text_content().strip()
           if len(title) < 7:
           tt=doc.xpath('//meta[@name="title"]')
           if tt:
           title=tt[0].get('content', '')
           if len(title) < 7:
           tt=doc.xpath('//*[contains(@id, "title") or contains(@class, "title")]')
           if not tt:
           tt=doc.xpath('//*[contains(@id, "font01") or contains(@class, "font01")]')
           for t in tt:
           ti=t.text_content().strip()
           if ti in title and len(ti)*2 > len(title):
           title=ti
           break
           if len(ti) > 20: continue
           if len(ti) > len(title) or len(ti) > 7:
           title=ti
           return title
           def shorten_title(self, title):
           spliters=[' - ', '–', '—', '-', '|', '::']
           for s in spliters:
           if s not in title:
           continue
           tts=title.split(s)
           if len(tts) < 2:
           continue
           title=tts[0]
           break
           return title
           def calc_node_weight(self, node):
           weight=1
           attr='%s %s %s' % (
           node.get('class', ''),
           node.get('id', ''),
           node.get('style', '')
           )
           if attr:
           mm=REGEXES['negativeRe'].findall(attr)
           weight -=2 * len(mm)
           mm=REGEXES['positiveRe'].findall(attr)
           weight +=4 * len(mm)
           if node.tag in ['div', 'p', 'table']:
           weight +=2
           return weight
           def get_main_block(self, url, html, short_title=True):
           ''' return (title, etree_of_main_content_block)
           '''
           if isinstance(html, bytes):
           encoding=cchardet.detect(html)['encoding']
           if encoding is None:
           return None, None
           html=html.decode(encoding, 'ignore')
           try:
           doc=lxml.html.fromstring(html)
           doc.make_links_absolute(base_url=url)
           except :
           traceback.print_exc()
           return None, None
           self.title=self.get_title(doc)
           if short_title:
           self.title=self.shorten_title(self.title)
           body=doc.xpath('//body')
           if not body:
           return self.title, None
           candidates=[]
           nodes=body[0].getchildren()
           while nodes:
           node=nodes.pop(0)
           children=node.getchildren()
           tlen=0
           for child in children:
           if isinstance(child, HtmlComment):
           continue
           if child.tag in self.non_content_tag:
           continue
           if child.tag=='a':
           continue
           if child.tag=='textarea':
           # FIXME: this tag is only part of content?
           continue
           attr='%s%s%s' % (child.get('class', ''),
           child.get('id', ''),
           child.get('style'))
           if 'display' in attr and 'none' in attr:
           continue
           nodes.append(child)
           if child.tag=='p':
           weight=3
           else:
           weight=1
           text='' if not child.text else child.text.strip()
           tail='' if not child.tail else child.tail.strip()
           tlen +=(len(text) + len(tail)) * weight
           if tlen < 10:
           continue
           weight=self.calc_node_weight(node)
           candidates.append((node, tlen*weight))
           if not candidates:
           return self.title, None
           candidates.sort(key=lambda a: a[1], reverse=True)
           good=candidates[0][0]
           if good.tag in ['p', 'pre', 'code', 'blockquote']:
           for i in range(5):
           good=good.getparent()
           if good.tag=='div':
           break
           good=self.clean_etree(good, url)
           return self.title, good
           def clean_etree(self, tree, url=''):
           to_drop=[]
           drop_left=False
           for node in tree.iterdescendants():
           if drop_left:
           to_drop.append(node)
           continue
           if isinstance(node, HtmlComment):
           to_drop.append(node)
           if self.p_content_stop.search(node.text):
           drop_left=True
           continue
           if node.tag in self.non_content_tag:
           to_drop.append(node)
           continue
           attr='%s %s' % (
           node.get('class', ''),
           node.get('id', '')
           )
           if self.p_clean_tree.search(attr):
           to_drop.append(node)
           continue
           aa=node.xpath('.//a')
           if aa:
           text_node=len(self.p_space.sub('', node.text_content()))
           text_aa=0
           for a in aa:
           alen=len(self.p_space.sub('', a.text_content()))
           if alen > 5:
           text_aa +=alen
           if text_aa > text_node * 0.4:
           to_drop.append(node)
           for node in to_drop:
           try:
           node.drop_tree()
           except:
           pass
           return tree
           def get_text(self, doc):
           lxml.etree.strip_elements(doc, 'script')
           lxml.etree.strip_elements(doc, 'style')
           for ch in doc.iterdescendants():
           if not isinstance(ch.tag, str):
           continue
           if ch.tag in ['div', 'h1', 'h2', 'h3', 'p', 'br', 'table', 'tr', 'dl']:
           if not ch.tail:
           ch.tail='\n'
           else:
           ch.tail='\n' + ch.tail.strip() + '\n'
           if ch.tag in ['th', 'td']:
           if not ch.text:
           ch.text=' '
           else:
           ch.text +=' '
           # if ch.tail:
           # ch.tail=ch.tail.strip()
           lines=doc.text_content().split('\n')
           content=[]
           for l in lines:
           l=l.strip()
           if not l:
           continue
           content.append(l)
           return '\n'.join(content)
           def extract(self, url, html):
           '''return (title, content)
           '''
           title, node=self.get_main_block(url, html)
           if node is None:
           print('\tno main block got !!!!!', url)
           return title, '', ''
           content=self.get_text(node)
           return title, content
          

          3.2 代碼解析

          跟新聞爬蟲一樣,我們把整個算法實現為一個類:MainContent。

          首先,定義了一個全局變量: REGEXES。它收集了一些經常出現在標簽的class和id中的關鍵詞,這些詞標識著該標簽可能是正文或者不是。我們用這些詞來給標簽節點計算權重,也就是方法calc_node_weight()的作用。

          MainContent類的初始化,先定義了一些不會包含正文的標簽 self.non_content_tag,遇到這些標簽節點,直接忽略掉即可。

          本算法提取標題實現在get_title()這個函數里面。首先,它先獲得<title>標簽的內容,然后試著從<meta>里面找title,再嘗試從<body>里面找id和class包含title的節點,最后把從不同地方獲得的可能是標題的文本進行對比,最終獲得標題。對比的原則是:

          <meta>, <body>里面找到的疑似標題如果包含在<title>標簽里面,則它是一個干凈(沒有頻道名、網站名)的標題;

          如果疑似標題太長就忽略

          主要把<title>標簽作為標題

          從<title>標簽里面獲得標題,就要解決標題清洗的問題。這里實現了一個簡單的方法: clean_title()。

          在這個實現中,我們使用了lxml.html把網頁的html轉化成一棵樹,從body節點開始遍歷每一個節點,看它直接包含(不含子節點)的文本的長度,從中找出含有最長文本的節點。這個過程實現在方法:get_main_block()中。其中一些細節,小猿們可以仔細體會一下。

          其中一個細節就是,clean_node()這個函數。通過get_main_block()得到的節點,有可能包含相關新聞的鏈接,這些鏈接包含大量新聞標題,如果不去除,就會給新聞內容帶來雜質(相關新聞的標題、概述等)。

          還有一個細節,get_text()函數。我們從main block中提取文本內容,不是直接使用text_content(),而是做了一些格式方面的處理,比如在一些標簽后面加入換行符合\n,在table的單元格之間加入空格。這樣處理后,得到的文本格式比較符合原始網頁的效果。

          爬蟲知識點

          1. cchardet模塊

          用于快速判斷文本編碼的模塊

          2. lxml.html模塊

          結構化html代碼的模塊,通過xpath解析網頁的工具,高效易用,是寫爬蟲的居家必備的模塊。

          3. 內容提取的復雜性

          我們這里實現的正文提取的算法,基本上可以正確處理90%以上的新聞網頁。

          但是,世界上沒有千篇一律的網頁一樣,也沒有一勞永逸的提取算法。大規模使用本文算法的過程中,你會碰到奇葩的網頁,這個時候,你就要針對這些網頁,來完善這個算法類。非常歡迎小猿們把自己的改善代碼提交到github,群策群力,讓這個算法越來越棒!


          主站蜘蛛池模板: 97se色综合一区二区二区| 日韩精品一区二区三区老鸦窝| 国产在线乱子伦一区二区| 日美欧韩一区二去三区| 无码中文人妻在线一区二区三区 | 精品三级AV无码一区| 精品国产一区二区三区麻豆| 亚洲一区在线视频| 国产人妖视频一区二区破除| 久久亚洲中文字幕精品一区四| 中文字幕人妻丝袜乱一区三区| 天堂国产一区二区三区| 国产精品第一区揄拍无码| 天天爽夜夜爽人人爽一区二区| 精品视频一区二区三区四区五区| 亚洲日本一区二区三区在线| 国产精品综合AV一区二区国产馆| 韩国一区二区视频| 无码AV动漫精品一区二区免费| 亚洲国产精品一区二区久久hs | 污污内射在线观看一区二区少妇| 国产伦精品一区二区三区女| 国产在线观看精品一区二区三区91| 国产韩国精品一区二区三区| 亚洲av无码一区二区三区天堂| 久久青草国产精品一区| 无码日韩AV一区二区三区| 久久亚洲国产精品一区二区| 四虎在线观看一区二区| 一区二区三区国产精品| 波多野结衣高清一区二区三区| 国产精品久久一区二区三区| 久夜色精品国产一区二区三区| 国产SUV精品一区二区四| 人妻夜夜爽天天爽爽一区| 中文字幕日韩人妻不卡一区| 无码中文字幕乱码一区| 亚洲一区二区三区在线观看蜜桃| chinese国产一区二区| 内射一区二区精品视频在线观看| 鲁丝丝国产一区二区|