深入理解JavaScript原型链

JavaScript 是一种基于原型的语言。理解原型和原型链对于掌握 JavaScript 的继承机制和对象模型至关重要。在本文中,我们将深入探讨 JavaScript 原型链的概念及其在实际编程中的应用。

1.什么是原型

在 JavaScript 中,每个对象都有一个与之关联的原型对象。这个原型对象也是一个对象,它可以拥有自己的原型,以此类推,形成一个链条,这就是所谓的原型链。

2.构造函数和原型
1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};

let person1 = new Person('Alice');
let person2 = new Person('Bob');

person1.sayHello(); // Hello, my name is Alice
person2.sayHello(); // Hello, my name is Bob

在上述的实例中Person是一个构造函数,Person.prototype是Person构造函数的原型对象,sayHello方法被添加到Person.prototype上,这意味着所有 Person 的实例都可以访问这个方法。

3.property和__proto__属性

在JavaScript中,对象属性分为两类:自身属性和原型链属性。
自身属性是直接定义在对象自身上的属性,原型属性是通过原型链继承的属性。

1
2
3
4
5
6
7
8
const person = {
name: "Alice",
age: 30
};

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age')); // true
console.log(person.hasOwnProperty('toString')); // false,因为 toString 是从 Object.prototype 继承来的

__proto__属性是每一个对象都有的一个内部属性,它指向对象的原型(即构造函数的 prototype 属性)。

4.原型链

当我们访问对象的属性或方法时,JavaScript 引擎会首先在对象自身的属性中查找。如果找不到,则会沿着原型链向上查找,直到找到属性或到达原型链的顶端(Object.prototype),其 proto 为 null。

1
2
3
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

原型链检测

在我们扣代码时通常会遇到一些环境检测,环境检测通过才会返回正确的结果,但我们在补环境时通常不会按照实际浏览器的原型链去补,下面是常用的补环境方式:

1
2
navightor = {}
navightor.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'

当js代码中存在原型链检测,这种补环境的方式是无法通过的,比如:

1
2
3
4
5
6
7
8
navightor = {}
navightor.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
if (Object.hasOwnProperty(navigator, 'userAgent')) {
return '错误的结果'
} else {
return '正确的结果'
}
// hasOwnProperty方法是JavaScript中所有对象从 Object.prototype 继承而来的一个方法。它用于检查一个对象是否拥有指定的属性(该属性必须是对象自身的属性,而不是从原型链继承来的属性)。

上面这种常规补环境的方式如果遇到原型链的检测那无法得到正确的结果,我们必须符合浏览器的环境,所以我们要将对应的属性补在原型链上,如下是补环境的方式符合浏览器的真实环境:

1
2
3
4
5
6
7
Navigator = function () {
}
Navigator.prototype = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
}
navigator = new Navigator()
// 以上将userAgent属性补在了原型对象上面,符合真实的浏览器环境

描述符检测

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

1
2
3
4
5
6
7
8
9
// 浏览器里面执行
Object.getOwnPropertyDescriptor(navigator,'userAgent') // ---> undefined

// node环境执行
var navigator = {
userAgent:"aaaaa"
}
Object.getOwnPropertyDescriptor(navigator,'userAgent')
// 输出{value: 'aaaaa',writable: true,enumerable: true,configurable: true}

value:与属性关联的值(仅限数据描述符)。
writable:当且仅当与属性关联的值可以更改时,为 true(仅限数据描述符)
configurable:当且仅当此属性描述符的类型可以更改且该属性可以从相应对象中删除时,为 true
enumerable:当且仅当此属性在相应对象的属性枚举中出现时,为 true

以下是检测描述符检测的正确补法

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
// 第1种方式,直接通过navigator来补他的隐式原型
navigator = {}
navigator.__proto__.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'

console.log(navigator.userAgent);
console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'));

// 第2种方式, 通过navigator创建的类来补显式原型
var Navigator = function() {};
Navigator.prototype = {"userAgent": "123123123"};
navigator = new Navigator();
console.log(navigator.userAgent)
console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'));

// 补方法
Document = function Document(){}
// Object.defineProperty 直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
Object.defineProperty(Document.prototype,'createElement',{
configurable: true,
enumerable: true,
value: function createElement(){},
writable: true,
})
HTMLDocument = function HTMLDocument(){}
//可以将一个指定对象的原型(即内部的隐式原型属性)设置为另一个对象
Object.setPrototypeOf(HTMLDocument.prototype,Document.prototype)
document = new HTMLDocument()

console.log(Object.getOwnPropertyDescriptor(document.__proto__.__proto__, 'createElement'));

toString检测

在 JavaScript 中,toString() 是一个内置函数,用于将一个值转换为字符串。
toString() 方法可以应用于大部分 JavaScript 值类型,包括数字、布尔值、对象、数组等。它的返回值是表示该值的字符串。
如果js代码中检测toSting方法,那么即使我们按照原型链的方式去补环境也无法通过检测

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
Document = function Document(){}
Object.defineProperty(Document.prototype,'createElement',{
configurable: true,
enumerable: true,
value: function createElement(){},
writable: true,
})
HTMLDocument = function HTMLDocument(){}
Object.setPrototypeOf(HTMLDocument.prototype,Document.prototype)
document = new HTMLDocument()

console.log(document.toString()); --> 输出function createElement(){}

// 浏览器控制台输出 --> 'function createElement() { [native code] }'
// 这个时候如果检测了toString即使补原型链也无法通过
// 我们可以重写toString方法来达到目的
Document = function Document(){}
Object.defineProperty(Document.prototype,'createElement',{
configurable: true,
enumerable: true,
value: function createElement(){},
writable: true,
})

HTMLDocument = function HTMLDocument(){}
Object.setPrototypeOf(HTMLDocument.prototype,Document.prototype)
document = new HTMLDocument()
document.createElement.toString = function(){return "function createElement() { [native code] }"}
console.log(document.createElement.toString());

什么是jsdom

jsdom是一个纯粹由 javascript 实现的一系列 web标准,特别是 WHATWG 组织制定的DOMHTML 标准,用于在nodejs中使用。大体上来说,该项目的目标是模拟足够的Web浏览器子集,以便用于测试和挖掘真实世界的Web应用程序

jsdom的基本使用

1
2
3
4
5
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<div>Hello Wrold!</div>`);
title = dom.window.document.querySelector("div").textContent
console.log(title)

很多人都喜欢使用jsdom来补环境,但是jsdom设置userAgent时会有jsdom的一些标识,如果生成的数据中检测了userAgent就会导致数据错误从而请求失败

以下是常见的userAgent的设置方式

1
2
3
4
5
6
7
8
9
10
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
userAgent: userAgent,
});
window = dom.window
console.log(window.navigator.userAgent)

以上代码输出的结果为: Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0
在userAgent中有很明显的jsdom特征

再次尝试使用如下代码:

1
2
3
4
5
6
7
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM();
window = dom.window
navigator = window.navigator
navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
console.log(window.navigator.userAgent)

以上代码输出的结果仍然为: Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0

jsdom正确设置userAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const resourceLoader = new jsdom.ResourceLoader({
userAgent: userAgent
});
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
resources: resourceLoader,
});
window = dom.window
console.log(window.navigator.userAgent)

以上代码输出的结果为: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
通过以上方式则可以正常设置userAgent

有些网站一打开控制台页面就会发生跳转会关闭当前页面,防止我们对网页进行抓包分析

常见的检测用户打开开发者工具的js代码

1.通过profiles记录里面有没有数据来判断用户是否打开了控制台

1
2
3
4
5
6
7
8
9
10
function ck() { 
console.profile();
console.profileEnd(); //我们判断一下profiles里面有没有东西,如果有,肯定有人按F12了,没错!!
if(console.clear) {
console.clear()
};
if (typeof console.profiles =="object"){
return console.profiles.length > 0;
}
}

2.通过判断窗口的高度来检测用户是否打开了控制台

1
2
3
4
5
6
7
8
9
function fuckyou(){ 
window.close(); //关闭当前窗口(防抽)
window.location="about:blank"; //将当前窗口跳转置空白页
}

window.onresize = function(){
if((window.outerHeight-window.innerHeight)>200) //判断当前窗口内页高度和窗口高度,如果差值大于200,则关闭当前窗口跳转至空白页
fuckyou();
}

如何进行调试

1.分析打开网站后网站的表现(如:跳转至指定页面、关闭当前窗口、控制输出”已清除控制台”等)

2.提前打开控制台,在源代码页面中找到事件侦听器断点,勾选脚本选项,再次进入指定网站

3.页面处于断点时搜索文件中相关的js代码进行定位,找到对应的位置

如搜索window.close/console.clear/window.location.href/window.history.back

4.将对应的js代码进行注释,并替换文件

1.在要扣取的webpack模块内打上断点,刷新页面在断点处停留

2.进入到加载器,在加载器第一行的位置打上断点并点击下一步进入到加载器,将加载器置空,并重新命名一个对象

3.在加载器第一行的断点取消并右键添加条件断点,输入aaa[t] = e[t],0

4.回到最开始打上断点的webpack模块,在下方位置再次添加一处断点,防止模块加载过多

5.点击跳到下一个断点位置

6.控制台输入aaa就可以看见webpack所加载的模块了

7.使用以下js代码将加载的webpack模块导出

1
2
3
4
5
result = '{';
for (let x of Object.keys(aaa)) {
result = result + '"' + x + '"' + ":" + aaa[x] + ','
}
result = result + '}'

1、爬虫子模块的使用

1.1.通过parse函数返回字典数据

通过(scrapy genspider 爬虫名称 目标网址)命令可以创建我们的爬虫子模块,爬虫子模块的作用主要是负责定义白名单、起始请求的url地址、以及对response对象的数据清洗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def parse(self, response: HtmlResponse, **kwargs):
a_list = response.xpath("//div[@class='rank-list']/a")
for a_temp in a_list:
rank_number = a_temp.xpath('./div[@class="badge"]/text()').extract_first() # 排名
img_url = a_temp.xpath('./img/@src').extract_first() # 图片地址
title = a_temp.xpath('./div[@class="content"]/div[@class="title"]/text()').extract_first() # 标题
desc = a_temp.xpath('./div[@class="content"]/div[@class="desc"]/text()').extract_first() # 描述
play_number = a_temp.xpath('.//div[@class="info-item"][1]/span/text()').extract_first()

# 需要使用yield关键字将解析好的数据交给管道模块
yield {
'type': 'info',
'rank_number': rank_number,
'img_url': img_url,
'title': title,
'desc': desc,
'play_number': play_number
}

注意: yield能够传递的对象只有: BaseItem、Request、dict、None

1.2.通过parse函数返回request对象

在实际爬虫过程中我们可能需要先提取到一个url,再通过url再次发起请求去获取对应的数据

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
def parse(self, response: HtmlResponse, **kwargs):
a_list = response.xpath("//div[@class='rank-list']/a")
for a_temp in a_list:
rank_number = a_temp.xpath('./div[@class="badge"]/text()').extract_first() # 排名
img_url = a_temp.xpath('./img/@src').extract_first() # 图片地址
title = a_temp.xpath('./div[@class="content"]/div[@class="title"]/text()').extract_first() # 标题
desc = a_temp.xpath('./div[@class="content"]/div[@class="desc"]/text()').extract_first() # 描述
play_number = a_temp.xpath('.//div[@class="info-item"][1]/span/text()').extract_first()

# 需要使用yield关键字将解析好的数据交给管道模块
yield {
'rank_number': rank_number,
'img_url': img_url,
'title': title,
'desc': desc,
'play_number': play_number
}

# 自行构造新的request请求并交给下载器下载
"""
callback: 指定解析函数
cb_kwargs: 如果解析方法中存在形参,则可以通过cb_kwargs传递,传递参数的类型必须是字典,字典中的key必须与解析方法中的形参名称保持一致
"""
yield scrapy.Request(img_url, callback=self.parse_imgage, cb_kwargs={'image_name': title})

def parse_imgage(self, response, image_name):
yield {
'image_name': image_name.replace('|', '') + '.png',
'image_content': response.body
}

在上一个例子中我们通过yield返回了一个request对象,通过callback定义了一个回调函数,这个回调函数主要负责去对该request请求的响应结果进行解析,通过cb_kwargs将回调函数需要的参数值进行传递。
注意: cb_kwargs接收的是一个字典,并且字典中的key名称必须与回调函数的形参名称保持一致。

1.3.start_request方法的使用

假设我们需要访问多页的数据或者访问的是一个api接口,如果将所有的请求都放在start_urls列表中就会显得很臃肿,并且可扩展性不高,那么我们可以在爬虫子类中重写start_requests方法来定义我们请求的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Top250Spider(scrapy.Spider):
"""
如果使用spider类默认的请求方式,则不会对重复请求进行过滤
会重复请求相同的url
"""
name = "top250"
allowed_domains = ["movie.douban.com"]
start_urls = ["http://movie.douban.com/top250", "http://movie.douban.com/top250"]

# 需要实现对同一个url去重操作,必须重写start_requests方法
# 如果请求的地址是api接口,则需要通过start_request方法构造链接
def start_requests(self):
url = 'https://movie.douban.com/top250?start={}&filter='
for page in range(10):
yield scrapy.Request(url.format(page * 25), dont_filter=False)

当我们重写了start_requests方法时,爬虫启动后则不会去遍历start_urls列表构造request请求,而是直接进入start_requests方法中构造我们自己定义的请求规则。scrapy.Request方法中有一个参数dont_filter,这个参数主要的作用是在请求url时不会重复请求相同的url

1.4.发送post请求

1.4.1.发送表单数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class JcInfoSpider(scrapy.Spider):
name = "jc_info"

def start_requests(self):
url = 'http://www.xxx.com.cn/new/disclosure'
for page in range(1, 11):
data = {
"column": "szse_latest",
"pageNum": str(page),
"pageSize": "30",
"sortName": "",
"sortType": "",
"clusterFlag": "true"
}
yield scrapy.FormRequest(url=url, formdata=data)
1.4.2.发送json数据
1
2
3
4
5
6
7
8
9
10
11
12
13
from scrapy.http import JsonRequest

class NetEaseInfoSpider(scrapy.Spider):
name = "netease_info"

def start_requests(self):
url = 'https://xxx.com/api/hr163/position/queryPage'
for page in range(1, 10):
payload = {
"currentPage": page,
"pageSize": 10
}
yield JsonRequest(url, data=payload)

发送json数据的时候需要导入JsonRequest方法

2、下载中间件的使用

下载中间件位于middlewares.py文件中,当引擎将request对象交给下载器时会先经过下载中间件,下载中间件可以给request添加请求头或者代理等,自定义的下载中间件需要在setting文件中激活。

2.1.下载中间件给request对象添加请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UserAgentDownloaderMiddleware:
USER_AGENTS_LIST = [
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
]

def process_request(self, request, spider):
print('下载中间件的process_request方法被调用...')
user_agent = random.choice(self.USER_AGENTS_LIST)
request.headers['User-Agent'] = user_agent

"""
如果返回None, 表示当前的request提交下一个权重低的process_request。
如果传递到最后一个process_request,则传递给下载器进行下载。

如果返回的是response对象则不再请求,把response对象直接交给引擎
"""

2.2.下载中间件给request对象添加代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ProxyDownloaderMiddleware:
def process_request(self, request, spider):
print('免费代理中间件 - 代理设置...')

# 元数据
request.meta['proxy'] = 'http://117.0.0.1:7890'
print(request.meta['proxy'])

def process_response(self, request, response, spider):
print('免费中间件 - 代理检测...')
if response.status != 200:
request.dont_filter = True # 关闭过滤
return request # 把请求失败的请求对象重新提交给调度器
return response

3、管道的使用

管道用于接收spider解析好的数据,通过管道可以将解析好的数据进行保存,使用管道前需要将管在setting文件中进行激活
管道中的方法:
process_item(self, item, spider): 管道中必须要有的方法,实现对item数据的处理,一般情况下都会return item,如果没有return item,那么会将None传递给权重较低的process item
open_spider(self, spider): 在爬虫开启的时候仅执行一次,可以在该方法中链接数据库、打开文件等
close_spider(self, spider): 在爬虫关闭的时候仅执行一次,可以在该方法中关闭数据库连接、关闭文件对象等

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
class TxWorkFilePipeline:
# 在爬虫启动时创建一个文件对象
def open_spider(self, spider): # spider: 接收的是spider类的一个实例
if spider.name == 'tx_work_info':
self.file_obj = open('tx_work_info.txt', 'a', encoding='utf-8')

def process_item(self, item, spider):
"""
在当前方法中可以对item进行数据判断,如果不符合数据要求,一般有两种方式来处理:
1. 扔掉
抛出一个异常中断当前管道,阻止item通过return传递给下一个管道
2. 修复
在当前方法中编辑修复代码逻辑并使用return将修复的数据传递给下一个item

注意:
当前方法如果存在return item则将item数据传递给下一个item
如果return不存在则将None传递给下一个item
"""
# raise DropItem # 丢掉不符合要求的数据
if spider.name == 'tx_work_info':
self.file_obj.write(json.dumps(item, ensure_ascii=False, indent=4) + '\n')
print('数据写入成功:', item)
return item

def close_spider(self, spider):
if spider.name == 'tx_work_info':
self.file_obj.close()


class TxWorkMongoPipeline:
# 在爬虫启动时创建一个数据库链接对象对象
def open_spider(self, spider): # spider: 接收的是spider类的一个实例
if spider.name == 'tx_work_info':
self.mongo_client = pymongo.MongoClient(host='192.168.198.130')
self.collection = self.mongo_client['py_spider']['tx_work_info']

def process_item(self, item, spider):
if spider.name == 'tx_work_info':
self.collection.insert_one(item)
print('数据写入mongodb成功:', item)
return item

def close_spider(self, spider):
if spider.name == 'tx_work_info':
self.mongo_client.close()

上述例子中自定义了2个管道类,这两个管道类分别将数据存储在不同的地方,需要注意的是如果某一个管道类需要先执行完再去执行下一个管道,则需要在settings文件中将需要先执行的管道类权重低于后执行管道类的权重

4.settings文件的参数详解

1
ROBOTSTXT_OBEY = True

是否开启robots.txt验证

1
DOWNLOAD_DELAY = 3

请求延迟

1
2
3
4
DEFAULT_REQUEST_HEADERS = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en",
}

设置默认的请求头信息

1
2
3
SPIDER_MIDDLEWARES = {
"TxWork.middlewares.SeleniumDownloaderMiddleware": 543,
}

爬虫中间件的配置信息

1
2
3
DOWNLOADER_MIDDLEWARES = {
"TxWork.middlewares.SeleniumDownloaderMiddleware": 543,
}

下载中间件的配置信息,若要使用下载中间件就需要在settings文件中将下载中间件的注释放开

1
2
3
4
ITEM_PIPELINES = {
"TxWork.pipelines.TxWorkFilePipeline": 300,
"TxWork.pipelines.TxWorkMongoPipeline": 301
}

管道配置信息,若要使用管道则需要在settings文件中手动开启

1
2
3
EXTENSIONS = {
"scrapy.extensions.telnet.TelnetConsole": None,
}

拓展类的配置信息,如果自定义了一些工具类想要使用,则需要在settings文件中启用

5、items的使用

items类主要用于进行字段的校验,如果将数据保存进数据库,数据库表字段名称和我们最终解析返回字典数据中的key不匹配时会导致数据插入失败,我们可以通过在items中定义字段名用于校验解析数据字典的key是否准确。
先将期望的数据库表字段在items中进行声明

1
2
3
4
5
6
7
8
9
10
class BookItem(scrapy.Item):
"""
items文件主要用于字段校验
"""
title = scrapy.Field()
price = scrapy.Field()
author = scrapy.Field()
date_data = scrapy.Field()
detail = scrapy.Field()
producer = scrapy.Field()

在爬虫文件中导入定义的items类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BookInfoSpider(scrapy.Spider):
name = "book_info"
start_urls = ["http://xxx.com/"]

def parse(self, response: HtmlResponse, **kwargs):
li_list = response.xpath('//ul[@class="bigimg"]/li')
for li in li_list:
item = BookItem()
item['title'] = li.xpath('./a/@title').extract_first()
item['price'] = li.xpath('./p[@class="price"]/span[1]/text()').extract_first()
item['author'] = li.xpath('./p[@class="search_book_author"]/span[1]/a[1]/@title').extract_first()
item['date_data'] = li.xpath('./p[@class="search_book_author"]/span[last()-1]/text()').extract_first()
item['detail'] = li.xpath('./p[@class="detail"]/text()').extract_first() if li.xpath(
'./p[@class="detail"]/text()') else '空'
item['producer'] = li.xpath(
'./p[@class="search_book_author"]/span[last()]/a/text()').extract_first() if li.xpath(
'./p[@class="search_book_author"]/span[last()]/a/text()') else '空'

yield item

当我们定义字典的key时,没有按照items声明的key进行定义则会抛出异常

scrapy框架内置模块


注意: 爬虫中间件和下载中间件只是运行逻辑的位置不同,作用是重复的: 如替换user-Agent、设置代理ip等。

scrapy框架的执行流程


scrapy框架执行流程说明:
1.scrapy从spider子类中提取start_urls,然后构造为request请求对象
2.将request对象传递给爬虫中间件
3.再将request对象传递给scrap引擎
4.将request对象传递给调度器(调度器负责多个request调度,好比交通管理负责交通的指挥员)
5.将request对象传递给scrapy引擎
6.scrapy引擎将request请求对象传递给下载中间件(下载中间件可以更换代理IP、更换cookie、更换user-agent以及自动重试等)
7.request对象传递给下载中间件经过处理后会给到下载器(下载器通过异步的方式发送http(s)请求),得到响应后封装为response对象
8.将response对象传递给下载中间件
9.下载中间件将response对象传递给scrapy引擎
10.scrapy引擎将response对象传递给爬虫中间件(这里可以处理异常情况等)
11.爬虫对象中的parse函数被调用(这里主要负责对接收到的response对象进行处理。如通过xpath提取数据等)
12.将提取的数据传递给scrapy引擎,它将数据再传递给管道(在管道中可以定义数据存储的方式,如MySQL、Mongodb、csv文件等)

什么是装饰器

在Python中,装饰器是一种高级功能,它允许你在不修改已有代码的情况下,动态地修改或增强函数或类的行为。装饰器本质上是一个函数,它接受另一个函数作为参数,并返回一个新的函数,通常也会在内部定义一个函数来实现这个功能。

为什么需要装饰器

我们假设你的程序实现了say_hello()say_goodbye()两个函数

1
2
3
4
5
6
7
8
9
10
11
def say_hello():
print("hello!")


def say_goodbye():
print("hello!") # 此处应打印goodbye


if __name__ == '__main__':
say_hello()
say_goodbye()

假设上述代码中的say_goodbye函数出现了bug,为了之后能更好的维护,现在需要调用方法前记录函数调用名称,以定位出错位置。
在不使用装饰器的情况下我们可以用以下方法实现

1
2
3
4
5
6
7
8
9
10
11
def say_hello():
print("[DEBUG]: enter say_hello()")
print("hello!")

def say_goodbye():
print("[DEBUG]: enter say_goodbye()")
print("hello!")

if __name__ == '__main__':
say_hello()
say_goodbye()

我们在上述的两个函数内部都加上了对应的打印信息,这样的话就修改了函数内容的代码。如果有多个函数的话,那么每个函数的内部都需要去增加对应的打印信息,当不需要时又要逐个去除,这里就显得非常难受。装饰器能很好的解决这个问题。

装饰器的简单实现

在早些时候 (Python Version < 2.4,2004年以前),为一个函数添加额外功能是用纯闭包的方式实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def debug(func):
def wrapper():
print("[DEBUG]: enter {}()".format(func.__name__))
return func()
return wrapper


def say_hello():
print("hello!")


say_hello = debug(say_hello)

say_hello()

上面的debug函数其实已经是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。因为这样写实在不太优雅,在后面版本的Python中支持了@语法糖,下面代码等同于早期的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
def debug(func):
def wrapper():
print("[DEBUG]: enter {}()".format(func.__name__))
return func()

return wrapper

@debug
def say_hello():
print("hello!")


say_hello()

但是上述的例子中,被装饰器装饰的函数无法携带参数,所以可以在内部函数wrapper中声明参数被装饰函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
def debug(func):
def wrapper(something):
print("[DEBUG]: enter {}()".format(func.__name__))
return func(something)

return wrapper

@debug
def say_hello(something):
print("hello {}!".format(something))


say_hello('theone')

上述例子中虽然解决了被装饰函数的参数问题,但是我们的装饰器所装饰的函数可能拥有多个参数,这个时候上面这种方法就显然行不通,这个时候我们需要将内部函数wrapper的参数列表改成不定长参数,这样就可以轻松应对不同参数个数的函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
def debug(func):
def wrapper(*args, **kwargs):
print("[DEBUG]: enter {}()".format(func.__name__))
return func(*args, **kwargs)
return wrapper


@debug
def say(something):
print("hello {}!".format(something))


say('theone')

实现一个带参数的装饰器

假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么我们的装饰器就要有支持传递参数的功能。

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
def logging(level):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
print("[{level}]: enter function {func}()".format(
level=level,
func=func.__name__))
return func(*args, **kwargs)
return inner_wrapper
return wrapper


@logging(level='INFO')
def say(something):
print("say {}!".format(something))


# 如果没有使用@语法,等同于
# say = logging(level='INFO')(say)

@logging(level='DEBUG')
def do(something):
print("do {}...".format(something))


if __name__ == '__main__':
say('hello')
do("my work")

基于类的装饰器

我们通过类的方式实现装饰器我们需要借助python中的魔术方法__call__,该魔术方法可以使类的实例对象像函数一一样被调用。

1
2
3
4
5
6
class Demo:
def __call__(*args, **kwargs):
print('实例方法被执行')

demo = Demo()
demo() # 实例方法被执行

接下来我们通过类的方式来实现装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Logging:
def __init__(self, func):
self.func = func

def __call__(self, *args, **kwargs):
print("[DEBUG]: enter function {func}()".format(
func=self.func.__name__))
return self.func(*args, **kwargs)


@Logging
def say(something):
print("say {}!".format(something))


say('木木')

基于类的方式实现一个带参数的装饰器

如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接收的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载__call__方法是就需要接收一个函数并返回一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Logging:
def __init__(self, level='INFO'):
self.level = level

def __call__(self, func): # 接收函数
def wrapper(*args, **kwargs):
print("[{level}]: enter function {func}()".format(
level=self.level,
func=func.__name__))
func(*args, **kwargs)

return wrapper # 返回函数


@Logging(level='INFO')
def say(something):
print("say {}!".format(something))


say('木木')

什么是闭包

在Python中,闭包(Closure)是指一个函数(称为内部函数)捕获了其外部作用域中的变量,并且可以在其内部函数范围之外被调用。换句话说,闭包可以访问其定义时所处环境中的变量,即使在其定义之后这些变量的作用域已经消失了。

闭包的代码结构

1
2
3
4
5
6
7
8
9
10
11
def send_message(message):
def print_message():
print(message) # 获取的是外部函数的形式参数

# 在外部函数中返回内部函数的地址
return print_message


# 创建一个变量去接收内部函数地址
func_obj = send_message('python天下第一...')
func_obj()

上面这段代码是闭包的基本结构,在send_message函数的内部定义了一个print_message函数,send_message函数返回的是内部函数print_message的地址,并且print_message函数可以访问到外部函数send_message的形式参数。func_obj = send_message(‘python天下第一…’),这一段代码是将send_message函数中的内部函数print_message的函数地址值赋值给了func_obj,所以func_obj()实际上就是执行了内部函数print_message。

利用闭包的特性完成代码的复用性

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
def create_calculator(operation):
def add(x, y):
return x + y

def subtract(x, y):
return x - y

def multiply(x, y):
return x * y

def divide(x, y):
if y != 0:
return x / y
else:
return "Error: Division by zero!"

if operation == "add":
return add
elif operation == "subtract":
return subtract
elif operation == "multiply":
return multiply
elif operation == "divide":
return divide
else:
return None

# 创建加法器
add_calculator = create_calculator("add")
result = add_calculator(5, 3)
print(result) # 输出 8

# 创建减法器
subtract_calculator = create_calculator("subtract")
result = subtract_calculator(10, 4)
print(result) # 输出 6

# 创建乘法器
multiply_calculator = create_calculator("multiply")
result = multiply_calculator(7, 2)
print(result) # 输出 14

# 创建除法器
divide_calculator = create_calculator("divide")
result = divide_calculator(8, 2)
print(result) # 输出 4.0

在上面的代码中,create_calculator函数返回了不同的内部函数(add、subtract、multiply、divide),这些内部函数捕获了外部函数中的变量operation。通过调用create_calculator并传递不同的操作类型,我们可以创建不同功能的计算器,实现了代码的复用。

修改闭包中的形式参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def counter(start=0):
def add():
nonlocal start # 针对函数参数
start += 1
return start

return add


obj = counter(10)
print(obj())

"""
global: 作用于不可变对象且这个对象是一个全局的
nonlocal: 作用于不可变对象且这个对象具有作用域的
"""

在上面这段代码中,我们定义了一个函数counter,在函数counter的内部定义了一个函数add,内部函数可以访问到外部函数的形式参数,但是如果内部函数想要修改外部函数的形式参数那就必须使用nonlocal关键字。

多个闭包是内存隔离的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def wrapper(name):
def message(content):
print(f'{name}: {content}')

print(f'内部函数引用的地址为:', id(message))
return message


func_1 = wrapper('安娜')
func_2 = wrapper('双双')

# 闭包多次运行返回的内部函数的地址是不一样的,所以闭包具有内存隔离的特性,与面向对象中的实例对象类似
print(id(func_1)) # 2610767972528
print(id(func_2)) # 2610767970944

什么是生成器

在Python中,生成器(generator)是一种特殊的迭代器,它可以动态地生成值,而不是一次性将所有值存储在内存中。生成器使用yield关键字来产生值,每次调用生成器的__next__()方法(或者直接使用next(generator)函数)时,生成器会执行,直到遇到一个yield语句,然后返回yield语句后面的值,并暂停执行。下次调用时,生成器会从上次暂停的地方继续执行。

生成器的一个常见用法是在处理大量数据或者无限序列时,节省内存开销。因为它们是惰性计算的,只有在需要时才会生成值。

声明一个生成器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_num(number):  # 生成器函数
index = 0
while index < number:
yield index # yield可以返回一个结果并且不会终止一个函数的运行
index += 1


# 生成器函数不能直接使用小括号的方式获取结果
# 生成器函数调用后返回的是一个生成器对象
print(get_num(5).__next__())

# 生成器对象中的结果可以使用next函数进行获取,和迭代器类似
obj = get_num(5)
print(next(obj))
print(next(obj))
print(next(obj))

在python中使用yield关键字来定义一个生成器函数,在上面的例子中get_num函数声明了yield关键字,如果我们要打印get_num函数返回的值不能直接使用get_num(5),get_num函数是一个生成器函数,它返回的是一个生成器对象,生成器对象是一种特殊的迭代器,所以需要调用迭代器对象的next()方法来获取生成器函数的返回值。

生成器的运行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def get_num(number):
print('开始迭代...')
i = 0
while i < number:
print('迭代中...')
yield i
print('迭代结束')
i += 1


obj = get_num(5)
print(next(obj))
print(next(obj))

"""
打印结果为:
开始迭代...
迭代中...
0
迭代结束
迭代中...
1
"""

在上一段代码中定义了一个生成器函数,在执行生成器函数的时候也是和普通函数一样从上往下执行,第一次调用函数的时候满足while循环条件进入到循环体内,生成器函数第一次遇到yield将结果返回出去,并保存当前的运行状态[保存了代码运行的位置],第二次运行则根据上一次运行的位置继续向下执行。

生成器表达式

1
nums = (i for i in range(1, 11))

上面这段代码就是生成器表达式,那么为什么要使用生成器表达式呢?
假设现在我们需要生成100万条数据,我们使用列表推导式生成的话会很慢,因为列表推导式生成100万条数据的时候会一次性把所有数据全部生成,数据没有生成完程序是不会退出的,而生成器表达式并不会直接给你生成数据,而是给你返回一个生成器对象,这个对象占用的内存并不大,生成器具有懒加载机制,调用next方法才可以获取到结果。

生成器的send和close方法

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
def get_num(number):
i = 0
while i < number:
data = yield i
if data == '这是测试时传递的一个值':
print(123)
print('data变量的值:', data)
i += 1


obj = get_num(5)
# print(next(obj))
print(obj.send(None)) # 第一次执行生成器时不能使用send方法去传递任何对象,除了None对象
print(obj.send('这是测试时传递的一个值')) # 可以在运行生成器对象的过程中传递一个值

obj.close() # 关闭生成器对象,后续代码无法运行生成器对象
print(next(obj))

"""
运行结果为:
0
123
data变量的值: 这是测试时传递的一个值
1
"""

生成器的send方法可以给生成器发送一个信号,在上面的实例中我们让data = yield i,在第一次执行生成器函数时执行到yield i程序会停止并返回一个生成器对象,此时还没有声明data变量,所以此时我们的内存中并没有加载data这个变量,如果此时调用send方法传递一个值会出现报错,但是传递None对象除外,我们调用send方法传递一个值之前会调用一次next方法,随后就会将send方法中的参数值传递给我们生成器对象。close方法用来关闭生成器对象,生成器对象一旦关闭,后续代码无法运行生成器对象。

什么是可迭代对象

可迭代对象:在Python中,可迭代对象是指可以被迭代的对象,即可以使用 for 循环遍历其元素的对象。

1
2
3
4
5
6
7
8
from collections.abc import Iterable

nums = [1, 2, 3]

for num in nums:
print(num)

print(isinstance(nums, Iterable)) # True

在上述代码中我们创建了一个列表,这个列表中的元素可以被我们的for循环所遍历,那么nums就是一个可迭代对象。最下边我们通过一个python内置方法instance来检查nums是否是Iterable类的实例,打印了True说明nums是Iterable类的一个实例,要是Iterable的子类一定是一个可迭代对象。

instance内置方法

isinstance是python的内置函数,判断一个类的实例是否是指定类的实例,支持继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A:
pass


class B(A):
pass


a = A()
b = B()

print(isinstance(a, A)) # True
print(isinstance(b, A)) # True
print(isinstance(a, B)) # False

什么是迭代器对象

迭代器是一种对象,它实现了迭代器协议,即包含 iter() 和 next() 方法。迭代器对象用于在迭代过程中逐个访问元素,每次调用 next() 方法都会返回序列中的下一个元素,直到序列中没有元素可返回,此时会引发 StopIteration 异常。
iter函数是python中的一个内置函数,传递一个可迭代对象将这个对象转为一个迭代器对象

1
2
3
4
5
6
7
8
9
10
11
12
nums = [1, 2, 3]
print(next(nums)) # 这里会报错,因为只有迭代器对象才拥有next方法
"""
在python中iter()和__iter__的作用是一样的,因为__iter__是比较底层的方法,所以推荐使用iter()
下面这段代码将nums从一个可迭代对象转为一个迭代器对象
"""
obj = iter(nums) # 此时obj对象将拥有next方法

print(next(nums)) # 1
print(next(nums)) # 2
print(next(nums)) # 3
print(next(nums)) # 如果元素取完则引发报错:StopIteration

通过instance方法来判断对象是否为迭代器对象

1
2
3
4
5
from collections.abc import Iterator

nums = [1, 2, 3]
print(isinstance(nums, Iterator)) # False,列表对象不是迭代器对象
print(isinstance(iter(nums), Iterator)) # True

自定义类实例化返回迭代器对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ListNode:
def __init__(self):
self.person_list = list()

# 如果一个类内部定义了__iter__方法,那么创建出来的实例一定是一个可迭代对象
def __iter__(self):
pass

def __next__(self):
pass


list_node = ListNode()
print(isinstance(list_node, Iterable)) # True
print(isinstance(list_node, Iterator)) # True

在上面这段代码中我们自定义了一个类,该类拥有__iter__和__next__方法,那么该类的实例对象就是一个迭代器对象,如果将该类的__next__方法注释,那么该类的实例对象就是一个可迭代对象。可迭代对象不一定是迭代器对象,迭代器对象一定是一个可迭代对象。

__iter__和__next__具体的使用

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
class ListNode:
def __init__(self):
self.index = 0
self.stu_list = list()

def __iter__(self):
return self

def __next__(self):
if self.index < len(self.stu_list):
item = self.stu_list[self.index]
self.index += 1
return item
else:
print('元素被取完了')
raise StopIteration

def add(self, name):
self.stu_list.append(name)


list_node = ListNode()
list_node.add('安娜')
list_node.add('双双')

for stu in list_node:
print(stu)

在上面这段代码中我们定义了一个ListNode类,该类中声明了__iter__和__next__方法,先实例化了ListNode类对象,随后通过add方法添加了两个学员,那么我们使用for循环直接去遍历ListNode类的实例对象是可以正常将类中列表的内容打印出来,因为for循环实质上是先将调用iter方法将需要遍历的对象转为迭代器对象,然后迭代器对象去执行next方法,从而将元素遍历出来,next方法的作用就是定义如何在一个迭代器对象中获取元素

0%