为一个 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数据区域(发布时间等)
配图区域(如果想把配图也提取)
而导航栏区域、相关链接区域的文字就不属于该新闻的要素。
新闻的标题、发布时间、正文内容一般都是从我们抓取的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小时内与您取得联系。