為一個 React、Vue 雙修選手,在 Vue 3 逐漸穩定下來之后,是時候摸摸 Vue 3 了。Vue3 的變化不可謂不大,所以,本系列主要通過對 Vue3 中的一些 Big Changes 做詳細的介紹,然后封裝一個比較通用的業務腳手架,里面會增加很多非常有用的小技巧,讓你在 Vue 3 的世界里縱享絲滑 ~
第一篇將著重介紹一些 Vue3 中重要的變化和概念 ~
找個可以開機的電腦,在終端輸入:
打開鏈接 http://127.0.0.1:5173,你的眼睛就會看到:
代表項目初始化成功,你真棒!
你的目錄會長這個樣子:
注意:記筆記,小技巧學起來,在 vscode 終端中使用 tree 指令打印當前項目的文件樹:
tree -I "node_modules|.vscode|cypress" > 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 也可以啟動項目!
Vue3項目打包的主要目:
打包主要是為了優化性能、安全性、擴展性、部署便利性等等,實際項目部署中也是必要的步驟。
Vue 3 提供 Option API 和 Composition API 兩種寫法,Option API 是延續自 Vue 2 的寫法,Composition API 則是跟著 Vue 3 一起推出的新寫法。
Option API 和 Composition API 主要差別有兩個:
這里引用 FU 大神在 Vueconf 上的一個解釋:
所謂的 Option (選項),就是 Vue 將代碼根據特性不同,分類成 data、method、計算屬性(computed)、生命周期......不同選項,讓開發者通過選項 (Option) 的方式,來定義組件內的邏輯。
代碼一般會根據選項式 API 被分成幾個區塊,比如這樣:
在 Composition API 搭配 <script setup> 的情況下,可以將所有邏輯代碼直接寫在<script>內,就跟寫原生 Javascript 的感覺很像,幾乎沒有什么特殊規則,交由開發者自行管理。
使用 Composition API 時,通常會根據「功能」來管理邏輯代碼,一個功能使用到的 data、method,聲明位置可以比較靠近,對于大的、復雜的組件來說,會提升可讀性。
Composition API 的代碼一般是這樣:
兩種方式都有人喜歡,沒有絕對的好壞。 其實 「自由/ 彈性」 在程式碼管理上是把雙刃劍,更考驗開發者管理代碼的能力;也有開發者偏好在訂好的規則下進行開發,兩者并沒有優劣,而是風格差異。
但整個兩者背后的響應式機制是一樣的,核心概念不變,而是根據Vue 定義的寫法不同,有不同的使用方式,所以 Vue 官網將教學指南分成兩種 API 形式。
復雜組件的情況下,Composition API 可讀性更好
官網也給了這兩者的直觀上的區別:
建議是都能熟悉使用,不然你怎么看懂別人的 shishan 代碼呢?
在 Vue 3 中,可以使用 Composition API 來封裝可重用的邏輯,類似于 React 中的 Hooks。
通過自定義 Hooks,我們可以將可重用的邏輯封裝起來,并在多個組件中共享使用。這樣可以提高代碼的可維護性、重用性和可讀性,避免了代碼重復,并且使組件更加關注自身的業務邏輯。并且 hook 具有良好的命名空間和類型推導,易于測試等特性。
下面封裝一個 useWindowSize 的自定義 Hooks,用于獲取當前窗口的寬度和高度:
將窗口大小的邏輯封裝成一個可重用的函數,并且與具體的組件解耦。其他組件只需要導入并調用 useWindowSize,就可以獲取窗口大小,而不需要重復編寫監聽器和邏輯。
比如,你就可以在組件中這樣使用 useWindowSize:
下面再來封裝兩個非常常用的自定義 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 參數,表示分頁請求的接口。
最后,返回了一個對象,包含了 paginatedEndpoint、nextPage 和 prevPage。這樣,在多個組件中可以方便地使用分頁功能,而不需要重復編寫邏輯。通過修改 currentPage 的值,可以輕松地切換頁碼,并且 paginatedEndpoint 會自動更新,以便發起正確的分頁請求。
2. useFetch:
這個自定義 Hook 用于發送異步請求并處理響應數據。它接收一個 endpoint 參數,表示請求的接口。
最后,返回一個對象,包含了 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>
在最新的 Composition API 中引入 <script setup>,其設計理念是去除不必要的包裝器和其他舊的組件選項,這樣就能更簡單、更集中地編寫組件。你可以認為 <script setup> 是 setup() 的語法糖,比如:
使用 <script setup> 語法糖,我們可以更簡潔地編寫組件邏輯。不再需要寫多余的 return 語句:
對比一下:
要聲明 props 和 emits 等選項,我們必須使用 <script setup> 中自動提供的所謂編譯器宏:
這兩個函數不需要導入,在預處理 <script setup> 時會自動編譯。
如果你使用了 TypeScript,那么最好也聲明一下 props 和 emits 的類型:
在某些情況下,我們有可能希望在組件中聲明一些自定義選項,比如自定義組件名稱,<script setup> 實現不了的,可以用另外一種傳統的寫法,在編譯 *.vue 文件時,這兩個區塊會自動合并。
在 Vue 3 的 Composition API 中,defineExpose 用于將組件的一部分內容公開給父組件。你可以指定哪些屬性、方法或其他內容可以被父組件訪問和使用。比如:
定義了一些。
使用 defineExpose 函數將內部的屬性 exposedValue 和一個方法 exposedMethod 暴露出去。在模板中,我們可以像使用普通的屬性和方法一樣使用這些暴露的內容。在點擊按鈕時,調用了 exposedMethod 方法。
父組件可以像下面這樣訪問和使用子組件暴露的內容:
在父組件中,需要使用 ref 創建了一個對子組件的引用 childComponent。然后,我們可以通過 childComponent.value 來訪問子組件實例,并調用其公開的方法 exposedMethod。
在組件中,我們通常需要異步請求數據,比如在 <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 也可能會發生變化。
在 Vue 3 中,SFC(Single File Component,單文件組件)是一種將模板、腳本和樣式封裝在一個單獨的文件中的開發模式。SFC 提供了一種組織和編寫 Vue 組件的便捷方式,并提供了更好的可讀性和可維護性。
一個典型的 Vue 3 SFC 包含三個部分:
以下是一個示例 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命令行 打印文件系統樹結構有兩種方式:
這里以 wordpress 博客的源代碼做測試;
? 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
...
? 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
...
awk -F'/' '{
if(NF==2) {
print "|-- "
}else if(NF==3) {
print "| |--"
}
}'
當分隔字段的數目為2,表示樹結構的根目錄,則打印“|-”;
當分隔字段的書目為3,則表示樹結構的葉節點,打印“| |–”;
好的,您可以打印一個兩級樹形目錄結構。
如果我們需要打印多級樹目錄結構呢?難道要寫多個if/else嗎?
當然不。
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里面提取的。如果僅僅是一個網站的新聞網頁,提取這三個內容很簡單,寫三個正則表達式就可以完美提取了。然而,我們的爬蟲抓來的是成百上千的網站的網頁。對這么多不同格式的網頁寫正則表達式會累死人的,而且網頁一旦稍微改版,表達式可能就失效,維護這群表達式也是會累死人的。
累死人的做法當然想不通,我們就要探索一下好的算法來實現。
標題基本上都會出現在html的<title>標簽里面,但是又被附加了諸如頻道名稱、網站名稱等信息;
標題還會出現在網頁的“標題區域”。
那么這兩個地方,從哪里提取標題比較容易呢?
網頁的“標題區域”沒有明顯的標識,不同網站的“標題區域”的html代碼部分千差萬別。所以這個區域并不容易提取出來。
那么就只剩下<title>標簽了,這個標簽很容易提取,無論是正則表達式,還是lxml解析都很容易,不容易的是如何去除頻道名稱、網站名稱等信息。
先來看看,<title>標簽里面都是設么樣子的附加信息:
上海用“智慧”激活城市交通脈搏,讓道路更安全更有序更通暢_浦江頭條_澎湃新聞-The Paper
“滬港大學聯盟”今天在復旦大學成立_教育_新民網
三亞老人腳踹司機致公交車失控撞墻 被判刑3年_社會
外交部:中美外交安全對話9日在美舉行
進博會:中國行動全球矚目,中國擔當世界點贊_南方觀瀾_南方網
資本市場迎來重大改革 設立科創板有何深意?-新華網
觀察這些title不難發現,新聞標題和頻道名、網站名之間都是有一些連接符號的。那么我就可以通過這些連接符吧title分割,找出最長的部分就是新聞標題了。
這個思路也很容易實現,這里就不再上代碼了,留給小猿們作為思考練習題自己實現一下。
發布時間,指的是這個網頁在該網站上線的時間,一般它會出現在正文標題的下方——meta數據區域。從html代碼看,這個區域沒有什么特殊特征讓我們定位,尤其是在非常多的網站版面面前,定位這個區域幾乎是不可能的。這需要我們另辟蹊徑。
這些寫在網頁上的發布時間,都有一個共同的特點,那就是一個表示時間的字符串,年月日時分秒,無外乎這幾個要素。通過正則表達式,我們列舉一些不同時間表達方式(也就那么幾種)的正則表達式,就可以從網頁文本中進行匹配提取發布時間了。
這也是一個很容易實現的思路,但是細節比較多,表達方式要涵蓋的盡可能多,寫好這么一個提取發布時間的函數也不是那么容易的哦。小猿們盡情發揮動手能力,看看自己能寫出怎樣的函數實現。這也是留給小猿們的一道練習題。
正文(包括新聞配圖)是一個新聞網頁的主體部分,它在視覺上占據中間位置,是新聞的內容主要的文字區域。正文的提取有很多種方法,實現上有復雜也有簡單。本文介紹的方法,是結合老猿多年的實踐經驗和思考得出來的一個簡單快速的方法,姑且稱之為“節點文本密度法”。
我們知道,網頁的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
跟新聞爬蟲一樣,我們把整個算法實現為一個類: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的單元格之間加入空格。這樣處理后,得到的文本格式比較符合原始網頁的效果。
爬蟲知識點
用于快速判斷文本編碼的模塊
結構化html代碼的模塊,通過xpath解析網頁的工具,高效易用,是寫爬蟲的居家必備的模塊。
我們這里實現的正文提取的算法,基本上可以正確處理90%以上的新聞網頁。
但是,世界上沒有千篇一律的網頁一樣,也沒有一勞永逸的提取算法。大規模使用本文算法的過程中,你會碰到奇葩的網頁,這個時候,你就要針對這些網頁,來完善這個算法類。非常歡迎小猿們把自己的改善代碼提交到github,群策群力,讓這個算法越來越棒!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。