使用JavaScript爬取網站數據

前言

主流的爬蟲一般都是使用 Python 的 Scrapy,但對於前端工程師來說 Python 可能會有一點不習慣,那麼我們熟悉的 JavaScript 能否完成這個任務呢,答案是可以的,而且可能比 Python 更適合用於爬蟲。

原因如下:

JavaScript 異步 IO 機制適用於爬蟲這種 IO 密集型任務。JavaScript 中的回調非常自然,使用異步網絡請求能夠充分利用 CPU。
JavaScript 中的 jQuery 毫無疑問是最強悍的 HTML 解析工具,使用 JavaScript 寫爬蟲能夠減少學習負擔和記憶負擔。雖然 Python 中有 PyQuery,但終究還是比不上 jQuery 自然。
爬取結果多為 JSON,JavaScript 是最適合處理 JSON 的語言。
來源: https://zhuanlan.zhihu.com/p/53763115

目標網站

GitHub 的Trending 頁

想要的數據:

  1. 項目名(repositoryName)
  2. 作者(autor)
  3. stars
  4. 描述(description)
  5. 語言(language)

使用到的庫

cheerio

Fast, flexible & lean implementation of core jQuery designed specifically for the server.
GITHUB: https://github.com/cheeriojs/cheerio

cheerio 是用來解析 HTML,他的接口跟 Jquery 基本一樣,能夠直接上手使用。用這個代替 Jquery 的原因,是因為 Cheerio 只實現了 Jquery DOM 的部分,要比 Jquery 要輕便一點。

isomorphic-fetch

> Isomorphic WHATWG Fetch API, for Node & Browserify

GITHUB: https://github.com/matthew-andrews/isomorphic-fetch

我比較習慣用 fetch 的接口,而這個 isomorphic-fetch 是可以在前、後端都可以使用的,讀者也可以選擇自己習慣的就行。

正式開始

  1. 獲得 HTML
1
2
const response = await fetch(targetUrl);
const html = await response.text();
  1. 解析 HTML
1
const $ = cheerio.load(html);
  1. 提取所需數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let data = [];

$(".Box-row").each((index, element) => {
const repositoryNameAndAuthor = $(element)
.find("h1 a")
.text()
.trim();

// 作者
const author = repositoryNameAndAuthor.split("/")[0].trim();

// 項目名
const repositoryName = repositoryNameAndAuthor.split("/")[1].trim();

// 項目描述
const description = $(element)
.find("p")
.text()
.trim();

// 語言
const language = $(element)
.find('span[itemprop="programmingLanguage"]')
.text()
.trim();

// stars
const stars = $(element)
.find(".muted-link")
.first()
.text()
.trim();

data.push({
repositoryName,
author,
description,
language,
stars
});

完整代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const cheerio = require("cheerio");
const fetch = require("isomorphic-fetch");

const targetUrl = "https://github.com/trending";

const scrape = async () => {
const response = await fetch(targetUrl);
const html = await response.text();

const $ = cheerio.load(html);

let data = [];

$(".Box-row").each((index, element) => {
const repositoryNameAndAuthor = $(element)
.find("h1 a")
.text()
.trim();
const author = repositoryNameAndAuthor.split("/")[0].trim();
const repositoryName = repositoryNameAndAuthor.split("/")[1].trim();
const description = $(element)
.find("p")
.text()
.trim();

const language = $(element)
.find('span[itemprop="programmingLanguage"]')
.text()
.trim();

const stars = $(element)
.find(".muted-link")
.first()
.text()
.trim();

data.push({
repositoryName,
author,
description,
language,
stars
});
});

return data;
};

scrape().then(data => console.log(data));

結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
[
{
"repositoryName": "Kotlin-Pokedex",
"author": "mrcsxsiq",
"description": "🌀 A Pokedex app using ViewModel, LiveData, Room and Navigation",
"language": "Kotlin",
"stars": "343"
},
{
"repositoryName": "java-design-patterns",
"author": "iluwatar",
"description": "Design patterns implemented in Java",
"language": "Java",
"stars": "54,397"
},
{
"repositoryName": "chaos-mesh",
"author": "pingcap",
"description": "A Chaos Engineering Platform for Kubernetes",
"language": "Go",
"stars": "801"
},
{
"repositoryName": "deeplearning-models",
"author": "rasbt",
"description": "A collection of various deep learning architectures, models, and tips",
"language": "Jupyter Notebook",
"stars": "10,903"
},
{
"repositoryName": "AndroidKnowledgeSystem",
"author": "feelschaotic",
"description": "The most complete Android advanced route knowledge map ⭐️你想要的最全 Android 进阶路线知识图谱+干货资料收集🚀",
"language": "JavaScript",
"stars": "1,234"
},
{
"repositoryName": "fxxkmakeding",
"author": "xyjoey",
"description": "",
"language": "",
"stars": "487"
},
{
"repositoryName": "pulse-sms-android",
"author": "klinker-apps",
"description": "The ultimate SMS app for Android, available across all of your devices.",
"language": "Kotlin",
"stars": "298"
},
{
"repositoryName": "nodetube",
"author": "mayeaux",
"description": "Open-source YouTube alternative that also supports image and audio uploads. Powered by NodeJS",
"language": "JavaScript",
"stars": "939"
},
{
"repositoryName": "coding_challenge-25",
"author": "zero-to-mastery",
"description": "",
"language": "",
"stars": "51"
},
{
"repositoryName": "computer-science",
"author": "ossu",
"description": "🎓 Path to a free self-taught education in Computer Science!",
"language": "",
"stars": "53,446"
},
{
"repositoryName": "GitHubDaily",
"author": "GitHubDaily",
"description": "GitHubDaily 分享内容定期整理与分类。欢迎推荐、自荐项目,让更多人知道你的项目。",
"language": "",
"stars": "5,411"
},
{
"repositoryName": "ALBERT",
"author": "google-research",
"description": "ALBERT: A Lite BERT for Self-supervised Learning of Language Representations",
"language": "Python",
"stars": "728"
},
{
"repositoryName": "architect-awesome",
"author": "xingshaocheng",
"description": "后端架构师技术图谱",
"language": "",
"stars": "41,832"
},
{
"repositoryName": "isocity",
"author": "victorqribeiro",
"description": "A isometric city builder in JavaScript",
"language": "JavaScript",
"stars": "1,644"
},
{
"repositoryName": "dockerlabs",
"author": "collabnix",
"description": "Docker - Beginners | Intermediate | Advanced",
"language": "PHP",
"stars": "736"
},
{
"repositoryName": "rhasspy",
"author": "synesthesiam",
"description": "Rhasspy voice assistant for Home Assistant and Hass.IO",
"language": "HTML",
"stars": "485"
},
{
"repositoryName": "flutter",
"author": "flutter",
"description": "Flutter makes it easy and fast to build beautiful mobile apps.",
"language": "Dart",
"stars": "83,214"
},
{
"repositoryName": "coding-interview-university",
"author": "jwasham",
"description": "A complete computer science study plan to become a software engineer.",
"language": "",
"stars": "95,699"
},
{
"repositoryName": "tailwind-starter-kit",
"author": "creativetimofficial",
"description": "Tailwind Starter Kit a beautiful extension for TailwindCSS, Free and Open Source",
"language": "CSS",
"stars": "318"
},
{
"repositoryName": "flowy",
"author": "alyssaxuu",
"description": "The minimal javascript library to create flowcharts ✨",
"language": "JavaScript",
"stars": "5,350"
},
{
"repositoryName": "awesome-python",
"author": "vinta",
"description": "A curated list of awesome Python frameworks, libraries, software and resources",
"language": "Python",
"stars": "77,646"
},
{
"repositoryName": "overreacted.io",
"author": "gaearon",
"description": "Personal blog by Dan Abramov.",
"language": "JavaScript",
"stars": "4,903"
},
{
"repositoryName": "spring-boot-demo",
"author": "xkcoding",
"description": "spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 63 个集成demo,已经完成 52 个。 该项目已成功集成 actuator(监控)、admin(可视化监控)、logback(日志)、aopLog(通过AOP记录web请求日志)、统一异常处理(json级别和页面级别)、freemarker(模板引擎)、thymeleaf(模板引擎)、Beetl(模板引擎)、Enjoy(模板引擎)、JdbcTemplate(通用JDBC操作数据库)、JPA(强大的ORM框架)、mybatis(强大的ORM框架)、通用Mapper(快速操作Mybatis)、PageHelper(通用的Mybatis分页插件)、mybatis-plus(快速操作M…",
"language": "Java",
"stars": "9,139"
},
{
"repositoryName": "hasskit",
"author": "tuanha2000vn",
"description": "HassKit is a Touch-Friendly - Zero Config App to help users instantly start using Home Assistant",
"language": "Dart",
"stars": "196"
},
{
"repositoryName": "linux",
"author": "torvalds",
"description": "Linux kernel source tree",
"language": "C",
"stars": "84,872"
}
]

奇怪的Array.prototype.length

先說結論

根據使用場景,length不一定代表數組的長度。實際上,length是指大於數組中最後一個元素的索引的32位無符號整數。

稠密數組(Dense Array) vs 稀疏數組(Sparse Array)

稠密數組是指一個數組的元素由0開始連續索引。

1
2
const denseArray = ["A", "B", "C"];
// A, B, C連續索引於0, 1, 2

顯然而見,稀疏數組就是一個不連續索引的數組

1
2
const sparseArray = ["A", , "C", "D"];
// A, C, D 分散地索引於0, 2, 3

甚麼時候length是指元素個數

當數組是稠密數組時,length是指元素個數。

1
2
3
4
5
6
7
8
const fruits = ['orange', 'apple', 'banana']; // fruits 是稠密數組
fruits.length // 3,正確的元素個數

fruits.push('mango');
fruits.length // 4,增加了一個元素

var empty = [];
empty.length // 0,空數組

甚麼時候是最後一個索引+1

當數組是稀疏數組,length等於最後一個索引加一,但不代表元素個數。

1
2
3
4
5
6
const animals = ['cat', 'dog', , 'monkey']; // animals 是稀疏數組
animals.length // 4,最後一個索引加一,但元素只有三個

var words = ['hello'];
words[6] = 'welcome'; // 這時最後的索引變成了6
words.length // 7

修改length

JavaScript允許修改length屬性,可以刪除元素或令數組變成稀疏。

1
2
3
4
const numbers = [1, 3, 5, 7, 8];

numbers.length = 3; // 修改length
console.log(numbers) // [1, 3, 5] 7和8都被刪除了

當把length設置為大於元素個數時,會令數組變成稀疏。

1
2
3
4
5
var osTypes = ['OS X', 'Linux', 'Windows'];

osTypes.length = 5; // index 4和5的元素為undefined, osTypes是稀疏

console.log(osTypes) // ['OS X', 'Linux', 'Windows', , , ]

JavaScript中的類型及其判斷方法

內置類型

JavaScript有七種的內置類型:

  1. 空值(null)
  2. 未定義(undefined)
  3. 布爾值(boolean)
  4. 數學(number)
  5. 字符串(string)
  6. 對象(object)
  7. 符號(symbol)

其中除了對象之外,其它六種統稱為基本類型

如何進行類型判斷

使用typeof

使用typeof進行類型判斷的返回值是該類型的字符串值,返回值只能為"object""number""undefined""string""boolean""symbol""function"其中一個。可以注意到的是,這些返回值跟JavaScript的內置類型不是一一進應的。
缺少了null,而多了function

與內置類型一一對應的就不詳細說了。重點關注nullfunction

對於null比較特殊,這算一個JavaScript歷史悠久的一個Bug,是因為object的底層實現中頭三位都是0,而null的底層實現直接是全0,因此typeof在判斷null才會誤判為object

1
console.log(typeof null); // object

function屬於object的子類型,屬於一個特例吧。

1
2
3
function a() {}

console.log(typeof a); // function

對於其它object的子類型,例如數組,都會直接返回object

1
2
3
const a = [1, 2, 3];

console.log(typeof a); // object

其它的判斷方法還有instance ofconstructorObject.prototype.toString.cal等方法,之後再詳細再說。

值和類型

JavaScript中的變量是沒有類型的。只有值才有類型,變量可以隨時持有任何類型的值。

1
2
3
4
5
let a = 42;
console.log(typeof a); // "number"

a = true;
console.log(typeof a); // "boolean"

淺談JavaScript的垃圾回收(V8為例)

前言

JavaSciptJava一樣都是由垃圾回收機制進行自動內存管理,與C/C++不同,JavaScript不需要時刻關注內存的分配和釋放問題

由於Node的出現,JavaScript已不僅僅是一門瀏覽器的腳本語言,Node極大地拓寬了JavaScript的應用場景,從客戶端延申到服務器端之後,這使得JavaScript使用者需要更加注意,在服務器端,內存管理的好壞將嚴重影響服務器的性能。

目前主流的JavaScript引擎是由Google開發的V8引擎V8性能優秀,使Google贏得了瀏覽器大戰。因此Node亦選用了V8。

V8的垃圾回收機制

主要的垃圾回收算法

V8的垃圾回數策略主要基於分代式垃圾回收機制。分代式垃圾回收機制是指將對象按照存活時間劃歸不同的世代,不同世代之後採用不同的回收算法。之所以這樣劃分是因為在本質上沒有任何一種回收算法能勝任所有場景

V8的內存分代主要分為新生代老生代兩代。其中新生代是指存活時間較短的對象,老生代中的對象則存活時間較長或常駐內存的對象

新生代

在分代的基础上,新生代中的對象主要通過Scavenge算法進行垃圾回數。在Scavenge的具體採用了Cherry算法。

Cherry算法採用複制&移動的方式實現垃圾回收。它將堆內存分為兩等份,每一部份的空間稱為semispace,兩個semispace只有一個處於使用中,另一個處理閑置狀態,使用中的叫做from空間,閑置的叫做to空間

當開始進行垃圾回收時,先檢查From空間中的存活對象,這些存活對象則會複制到To空間,而非存活的對象則會被直接釋放。然後From空間To空間的⻆色就會調換

對象晉升

當一個對象經歷過Scavenge回收,又或者To空間的內存占用比超過限制,則該對象將會由新生代晉升至老生代

老生代

對於老生代中的對象,V8主要採用Mark-SweepMark-Compact相結合的方式進行垃圾回收。Mark-Sweep是標記清除的意思,分為標記和清除兩個步驟,和Scavenge不同,Mark-Sweep不需要把內存劃分兩等份,在標記階段,Mark-Sweep會遍歷堆中的所有對象,并標記活著的對象,在清除階段,則只會清除沒有標記的對象。

Mark-Sweep會導致內存出現碎片化的現象,當需要分配一個大對象,會觸發一次不必要的垃圾回收。這時就有Mark-Compact,它跟Mark-Sweep差不多,區別只在對象在標記為死亡後,在整理的過程中,將活動的對象往一端移動,移動完成後,直接清理掉邊界外的內存。

V8主要使用Mark-Sweep,在空間不足以對從新生代中晉升過來的對象進行分配時才使用Mark-Compact

3種垃圾回收算法的簡單對比

回數算法 Mark-Sweep Mark-Compact Scavenge
速度 中等 最慢 最快
空間開銷 少(有碎片) 少(無碎片) 雙倍空間(無碎片)
是否移動對象

Javascript的This機制

先簡單說兩句

相信很多朋友在剛學習Javascript都被Javascriptthis弄得很混亂,由於我一開始就是學習Java先的,很自然就Java中的this代入了Javascript中,其實這是一個重大的錯誤,有人()說「如果上帝用七天打造這個世界,那麼Javascript就是在最後0.01秒才匆匆設計的」,現在就讓我跟大家詳細說說Javascriptthis吧!

this的意義

Java中,this是一個引用對象,引用的就是這個對象自身,在Javascript中也會有這種場景,如以下代碼的this便指向new出來的那個對象自身

1
2
3
4
5
6
7
8
9
10
11
12
13
export class People{
constructor(name, age){
this.name = name;
this.age = age;
}

sayHi(){
return `Hi, My name is ${this.name} and I am ${this.age} old`;
}
}

let patrick = new People("Patrick", 21);
console.log(patrick.sayHi()); // Hi, My name is Patrick and I am 21 old

又或者在使用Javascript的對象時,this也是指向car對象本身,但這種情況上面跟通過new關鍵字又有一些不一樣。

1
2
3
4
5
6
const car = {
speed: "100km/h",
introduce: function(){
console.log(`I can run ${this.speed}`);
}
}

在我現在的工作中,經過需要使用到React,一般在Reactconstructor中需要做以下這件事,就是需要bind綁定,為甚麼需要呢,不綁定會怎樣?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class Input extends React.Component{
constructor(props){
super(props);
// here
this.handleChange = this.handleChange.bind(this);

this.state = {value: ""};
}

handleChange(value){
this.setState({value: value});
}

render(){
return (
<input value={this.state.value} onChange={this.onChange}/>
)
}
}

還有一種更簡單的情況,在瀏覽器打開chrome devTools,在console面版中輸入this,看看會出現甚麼東西。為甚麼會輸出window呢?

接著我跟大家解釋釋,究竟this的意義是甚麼,其實this是指運行期間的上下文,根據不同的情況有不同的綁定規則。

this的綁定規則

默認綁定

默認綁定就是最簡單的情況,在全域或者不在函數內部使this的時候,this指向的便全局變量,在瀏覽器中是window,在nodejs中便是global,默認綁定會在嚴格下失效,即嚴格模式,默認綁定的this會指向undefined。上面第四個例子便是默認綁定。

隱式綁定

當函數被調用時,若函數引用具有上下文對象,則隱式綁定會將 this 綁定到這個上下文對象。第二個例子說的就是隱式綁定,在this的綁定規則裡,最容易出錯的就是隱式綁定,來看一段代碼,最後的那兩個函數會分別返回asurprise,這種情況叫做隱式丟失,丟失的就是thisbarobj.foo的一個函數別名,在調用bar的時候會丟失對objthis引用,而直接是綁定到全局的對象中。

1
2
3
4
5
6
7
8
9
10
11
12
// 非嚴格模式
this.a = "surprise"
const obj = {
a: "a",
foo: function(){
return this.a;
}
}

const bar = obj.foo;
obj.foo();
bar();
顯式綁定

顯式綁定就要為了避免隱式丟失的出現,通過applycallbind三個方法來顯式的綁定要調用函數內部的this,看看例子最直接,在例子就直接指定this的值,這樣可以避免出現丟失的問題,所以現在為甚麼第三個例子中React constructor需要用bind綁定一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj1 = {
content: "i am object 1"
};
const obj2 = {
content: "i am object 2"
};
const obj3 = {
content: "i am object 3"
}
function foo(x, y ,z){
return `${this.content} arg ${x} ${y} ${z}`;
}

// use apply
foo.apply(obj1, ["a", "a", "a"]); // i am object 1 arg a a a

// use call
foo.call(obj2, "b", "b", "b"); // i am object 2 arg b b b

// use bind
const bindedFoo = foo.bind(obj3); // i am object 3 arg c c c
bindedFoo("c", "c", "c");

new綁定

第一個例子中,我們通過一個new關鍵字「實例化」了一個對象,實際上的new過程只做了下面幾件事:

  1. 創建一個新的對象
  2. 將新的對象的__prototype__屬性指向函數的prototype
  3. 將函數的this綁定向新的對象
  4. 調用函數

注意第三步,這裡就是實際上new綁定出現的原因,因為在new關鍵字的執行過程已經把新創建的對象綁定到函數的this裡,所以在new出來的對象裡面的this在運行時是會指向這個對象的。

優先級

依次為new綁定顯式綁定隱式綁定默認綁定

Javascript中的this機制其實還有一層,就是thisprototype原型鏈上的查找,可以理解this是一個二維平面,水平方向是綁定規則,垂直方向就是prototype原型鏈和JS的各種作用域。

下期將為大家介紹Javascript的原型鏈和作用域。

本人獻醜了,大家加油~!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×