python学习笔记大纲

目录

Scrapy去重效率优化

Scrapy-Redis去重机制

安装

配置

修改 Scheduler

修改 Redis 连接信息

修改去重类 

配置持久化 

获取源码

解析

scrapyd

了解Bloom Filter

Bloom Filter的算法

对接Scrapy-Redis

基本的散列算法

Bloom Filter实现

exists()方法和insert()方法

实例

模拟登录基础

准备工作

案例

session登录

jwt登录

超级鹰的使用

反屏蔽

Pyppeteer 介绍

安装

基本的页面渲染操作

详细用法

防止检测

消除指纹

Pyppeteer模拟登录

node.js安装

js逆向

网易云案例

js基础

var let const 区别

区别

var声明的作用域

var声明的变量提升

let声明

var 与let(作用域)

var 声明

let 声明

const 声明

条件语句

if else结构

switch 结构

 三元运算符

 循环语句

while循环

 for 循环

 do...while 循环

 break 语句和 continue 语句

数据类型

字符串与数组

length 属性

Base64 转码 (该知识点比较重要)

 对象

对象的读值与赋值

 属性的查看

 属性是否存在:in 运算符

属性的遍历:for...in 循环

函数

js三种声明函数的方式

第一等公民

函数名的提升

函数作用域

定义

参数

arguments 对象

闭包

语句与表达式的区别

语句(Statement)

表达式(Expression)

立即调用的函数表达式(IIFE)

eval 命令

基本用法

数组的本质

length 属性

in 运算符

数组遍历

数据类型转换

强制转换

Number()

类似函数parseInt函数的使用

String()

Boolean()

自动转换

标准库

Object实例对象的方法

object对象函数

Object.setPrototypeOf

Object.getOwnPropertyDescriptor


Scrapy去重效率优化

        Scrapy有自动去重功能,它的去重使用了 Python 中的集合。这个集合记录了 Scrapy 中每个Request 的指纹,这个指纹实际上就是 Request 的散列值。(散列值就是转化成一一对应的东西,比如天上的太阳对应“太阳”这两个字,“太阳”这两个字就是)

        request_fingerprint 就是计算 Request 指纹的方法,其方法内部使用的是 hashlib 的 sha1 方法(sha1加密,对请求的方法、链接、请求体加密)。计算的字段包括 Request的 Method、URL、Body、Headers 这几部分内容,这里只要有一点不同,那么计算的结果就不同。计算得到的结果是加密后的字符串,也就是指纹。

我们可以看一下散列:

        不管运行几次hello world出来都是这一串,可以把一个字符串转成另一个字符串,我们在抓一个新的小说时,把标题之类的拼起来,再用sha1加密,就可以把长文章变成40位字符串,节省空间,利用这个可以做一个去重操作,方便去重

Scrapy-Redis去重机制

         Redis相当于一个调度器,多台电脑可以共享Redis数据库,比如Redis数据库在远程服务器上,知道远程服务器ip地址,知道端口号(Redis一般是6379),就可以连到数据库,这样几个电脑就可以连同一个数据库,实现共享,Redis里存的是任务,这样一个电脑抓取一个任务,由于任务池共享,任务从Redis里拿出来,其他电脑就不能抓这个任务,Redis不光可以装任务,还可以存数据的去重记录,可以用来做增量爬虫。

安装

pip install scrapy-redis

配置

修改 Scheduler

        在 settings.py 里面添加如下代码,将 Scheduler 的类修改为 Scrapy-Redis 提供的 Scheduler 类,这样在我们运行爬虫时,Request 队列就会出现在 Redis 中

SCHEDULER = "scrapy_redis.scheduler.Scheduler"
修改 Redis 连接信息
REDIS_URL = 'redis://[user:pass]@hostname:9001'
#本地运行不用填写用户名密码,如下
REDIS_URL = 'redis://localhost:6379'
修改去重类 
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
配置持久化 

        开启了 Redis 分布式队列之后,我们不希望爬虫在关闭时将整个队列和去重信息全部删除,因 为很有可能在某个情况下我们会手动关闭爬虫或者爬虫遭遇意外终止,为了解决这个问题,我们可以配 置 Redis 队列的持久化

SCHEDULER_PERSIST = True

        上面我们完成的实际上并不是真正意义的分布式爬虫,因为 Redis 队列我们使用的是本地的Redis,所 以多个爬虫需要运行在本地才可以,如果想实现真正意义的分布式爬虫,可以使用远程 Redis,这样我 们就能在多台主机运行爬虫连接此 Redis 从而实现真正意义上的分布式爬虫了。

        Scrapy-Redis 库已经为我们提供了 Scrapy 分布式的队列、调度器、去重等功能,其 GitHub
地址为: https://github.com/rmax/scrapy-redis

获取源码

可以把源码克隆下来,执行如下命令:

git clone https://github.com/rmax/scrapy-redis.git

核心源码在 scrapy-redis/src/scrapy_redis 目录下。

解析

        源码文件为 queue.py ,它包含了三个队列的实现,首先它实现了一个父类 Base ,提供一些基本方法和属性,如下所示:
        _encode_request 和 _decode_request 方法,因为我们需要把一个 Request 对象存储到数据库中,但数据库无法直接存储对象,所以需要将 Request 序列转化成字符串再存储。而这两个方法分别是序列化和反序列化的操作,利用 pickle 库来实现,一般在调用 push 将Request 存入数据库时会调用 _encode_request 方法进行序列化,在调用 pop 取出 Request的时候会调用_decode_request 进行反序列化。
        在父类中 len push pop 方法都是未实现的,会直接抛出 NotImplementedError ,因此是
不能直接使用这个类的,必须实现一个子类来重写这三个方法。那在源码中就有三个子类的实现,它们分别是 FifoQueue、PriorityQueue、LifoQueue
        这个类继承了 Base 类,并重写了 len push pop 这三个方法,在这三个方法中都是对 server 对象的操作,而 server 对象就是一个 Redis 连接对象,我们可以直接调用其操作Redis 的方法对数据库进行操作,可以看到这里的操作方法有 llen lpush rpop 等,这就代表此爬取队列是使用的 Redis 的列表。
        序列化后的 Request 会被存入列表中,就是列表的其中一个元素, len 方法是获取列表的长
度, push 方法中调用了 lpush 操作,这代表从列表左侧存入数据, pop 方法中调用了 rpop 操作,这代表从列表右侧取出数据。所以 Request 在列表中的存取顺序是左侧进、右侧出,所以这是有序的进出,即先进先出,英文叫作 First Input First Output ,也被简称为 FIFO ,而此类的名称就叫作 FifoQueue。
        与 FifoQueue 不同的就是它的 pop 方法,在这里使用的是 lpop 操作,也就是从左侧出,而
push 方法依然是使用的 lpush 操作,是从左侧入。那么这样达到的效果就是先进后出、后进先出,英文叫作 Last In First Out ,简称为 LIFO ,而此类名称就叫作 LifoQueue 。同时这个存取方式类似栈的操作,所以其实也可以称作StackQueue。
        len、 push pop 方法中使用了 server 对象的 zcard zadd zrange 操作,可以知道这里使用的存储结果是有序集合 Sorted Set ,在这个集合中每个元素都可以设置一个分数,那么这个分数就代表优先级。
        len 方法里调用了 zcard 操作,返回的就是有序集合的大小,也就是爬取队列的长度,在push 方法中调用了 zadd 操作,就是向集合中添加元素,这里的分数指定成 Request 的优先级的相反数,因为分数低的会排在集合的前面,所以这里高优先级的 Request 就会存在集合的最前面。               pop 方法是首先调用了 zrange 操作取出了集合的第一个元素,因为最高优先级的 Request 会 存在集合最前面,所以第一个元素就是最高优先级的 Request ,然后再调用 zremrangebyrank
操作将这个元素删除,这样就完成了取出并删除的操作。
        此队列是默认使用的队列,也就是爬取队列默认是使用有序集合来存储的。
        
        Scrapy 的去重是利用集合来实现的,而在 Scrapy 分布式中的去重就需要利用共享的集合,那么这里使用的就是 Redis 中的集合数据结构。去重类源码文件是 dupefilter.py ,其内实现了一个 RFPDupeFilter
class DemoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()
    pass

class RFPDupeFilter(BaseDupeFilter):
    """Redis-based request duplicates filter.
    This class can also be used with default Scrapy's scheduler.
    """
    logger = logger
    def __init__(self, server, key, debug=False):
        """Initialize the duplicates filter.
        Parameters
        ----------
        server : redis.StrictRedis
        The redis server instance.
        key : str
        Redis key Where to store fingerprints.
        debug : bool, optional
        Whether to log filtered requests.
        """
        self.server = server
        self.key = key
        self.debug = debug
        self.logdupes = True
        @classmethod
    def from_settings(cls, settings):
        """Returns an instance from given settings.
        This uses by default the key ``dupefilter:<timestamp>``. When using the
        ``scrapy_redis.scheduler.Scheduler`` class, this method is not used as
        it needs to pass the spider name in the key.
        Parameters
        ----------
        settings : scrapy.settings.Settings
        Returns
        -------
        RFPDupeFilter
        A RFPDupeFilter instance.
        """
        server = get_redis_from_settings(settings)
        key = defaults.DUPEFILTER_KEY % {'timestamp': int(time.time())}
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(server, key=key, debug=debug)
        @classmethod
    def from_crawler(cls, crawler):
        """Returns instance from crawler.
        Parameters
        ----------
        crawler : scrapy.crawler.Crawler
        Returns
        -------
        RFPDupeFilter
        Instance of RFPDupeFilter.
        """
        return cls.from_settings(crawler.settings)
    def request_seen(self, request):
        """Returns True if request was already seen.
        Parameters
        ----------
        request : scrapy.http.Request
        Returns
        -------
        bool
        """
        fp = self.request_fingerprint(request)
        added = self.server.sadd(self.key, fp)
        return added == 0
    def request_fingerprint(self, request):
        """Returns a fingerprint for a given request.
        Parameters
        ----------
        request : scrapy.http.Request
        Returns
        -------
        str
        """
        return request_fingerprint(request)
    def close(self, reason=''):
        """Delete data on close. Called by Scrapy's scheduler.
        Parameters
        ----------
        reason : str, optional
        """
        self.clear()
    def clear(self):
        """Clears fingerprints data."""
        self.server.delete(self.key)
    def log(self, request, spider):
        """Logs given request.
        Parameters
        ----------
        request : scrapy.http.Request
        spider : scrapy.spiders.Spider
        """
        if self.debug:
            msg = "Filtered duplicate request: %(request) s"
            self.logger.debug(msg, {'request': request}, extra={'spider': spider
        elif self.logdupes:
            msg = ("Filtered duplicate request %(request) s"
                    "- no more duplicates will be shown"
                    "(see DUPEFILTER_DEBUG to show all duplicates)")
            self.logger.debug(msg, {'request': request}, extra={'spider': spider
            self.logdupes = False
        实现了一个 request_seen 方法,和 Scrapy 中的 request_seen 方法实现极其类似。不过这里集合使用的是 server 对象的 sadd 操作,也就是集合不再是一个简单数据结构了,而是直接换成了数据库的存储方式。
        鉴别重复的方式还是使用指纹,指纹同样是依靠 request_fingerprint 方法来获取的。获取指纹
之后就直接向集合添加指纹,如果添加成功,说明这个指纹原本不存在于集合中,返回值 1 。代码中最后的返回结果是判定添加结果是否为 0 ,如果刚才的返回值为 1 ,那这个判定结果就是False ,也就是不重复,否则判定为重复。这样我们就成功利用 Redis 的集合完成了指纹的记录和重复的验证。
        Scrapy-Redis 还帮我们实现了配合 Queue DupeFilter 使用的调度器 Scheduler ,源文件名称是 scheduler.py 。我们可以指定一些配置,如 SCHEDULER_FLUSH_ON_START 即是否在爬取开始的时候清空爬取队列,SCHEDULER_PERSIST 即是否在爬取结束后保持爬取队列不清除。我们可以在 settings.py 里自由配置,而此调度器很好地实现了对接。 ​​​​​​​
        enqueue_request 可以向队列中添加 Request ,核心操作就是调用 Queue push 操作,还有一些统计和日志操作。next_request 就是从队列中取 Request ,核心操作就是调用 Queue 的 pop 操作,此时如果队列中还有 Request ,则 Request 会直接取出来,爬取继续,否则如果队列为空,爬取则会重新开始。

scrapyd

后期补充说明

了解Bloom Filter

        当爬取达到亿级别规模时,Scrapy-Redis提供的集合去重已经不能满足我们的要求。所以我们需要使用一个更加节省内存的去重算法Bloom Filter。

         Bloom Filter,中文名称叫作布隆过滤器,可以被用来检测一个元素是否在一个集合中。Bloom Filter的空间利用效率很高,使用它可以大大节省存储空间。Bloom Filter使用位数组表示一个待检测集合,并可以快速地通过概率算法判断一个元素是否 存在于这个集合中。利用这个算法我们可以实现去重效果。

Bloom Filter的算法

       ​​​​​​​在Bloom Filter中使用位数组来辅助实现检测判断。在初始状态下,我们声明一个包含m位的

位数组,它的所有位都是 0
        现在我们有了一个待检测集合,其表示为S={x1, x2, …, xn} 。接下来需要做的就是检测一个 x 是否已经存在于集合S 中。在 Bloom Filter 算法中,首先使用 k 个相互独立、随机的散列函数来将集合S 中的每个元素 x1, x2, …, xn 映射到长度为 m 的位数组上,散列函数得到的结果记作位置索引,然后将位数组该位置索引的位置1 。例如,我们取 k 3 ,表示有三个散列函数, x1 经过三个散列函数映射得到的结果分别为1 4 8 x2 经过三个散列函数映射得到的结果分别为 4 、6、 10 ,那么位数组的 1 4 6 8 10 这五位就会置为 1 ,如下图所示。
        如果有一个新的元素x ,我们要判断 x 是否属于 S 集合,我们仍然用 k 个散列函数对 x 求映射结果。如果所有结果对应的位数组位置均为1,那么x属于S这个集合;如果有一个不为1,则x不 属于S 集合。例如,新元素x 经过三个散列函数映射的结果为 4 6 8 ,对应的位置均为 1 ,则 x 属于 S 集合。如果结果为4 6 7 ,而 7 对应的位置为 0 ,则 x 不属于 S 集合。注意,这里m n k 满足的关系是 m>nk ,也就是说位数组的长度 m 要比集合元素 n 和散列函数k的乘积还要大。
        这样的判定方法很高效,但是也是有代价的,它可能把不属于这个集合的元素误认为属于这个集合。我们来估计一下这种方法的错误率。当集合S={x1, x2,…, xn} 的所有元素都被 k 个散列函数映射到m 位的位数组中时,这个位数组中某一位还是 0 的概率是:
        散列函数是随机的,则任意一个散列函数选中这一位的概率为1/m ,那么 1-1/m 就代表散列函数从未没有选中这一位的概率,要把S 完全映射到 m 位数组中,需要做 kn 次散列运算,最后的概率就是1-1/m kn 次方。一个不属于S 的元素 x 如果误判定为在 S 中,那么这个概率就是 k 次散列运算得到的结果对应的位数组位置都为1 ,则误判概率为:
在给定 m n 时,可以求出使得 f 最小化的 k 值为:
这里将误判概率归纳如下:
        表中第一列为m/n 的值,第二列为最优 k 值,其后列为不同 k 值的误判概率。当 k 值确定时,随着m/n 的增大,误判概率逐渐变小。当 m/n 的值确定时,当 k 越靠近最优 K 值,误判概率越小。误判概率总体来看都是极小的,在容忍此误判概率的情况下,大幅减小存储空间和判定速度是完全值得的。

对接Scrapy-Redis

基本的散列算法

        实现Bloom Filter时,首先要保证不能破坏 Scrapy-Redis 分布式爬取的运行架构。我们需要修改Scrapy-Redis的源码,将它的去重类替换掉。同时, Bloom Filter 的实现需要借助于一个位数组,既然当前架构还是依赖于Redis ,那么位数组的维护直接使用 Redis 就好了。首先实现一个基本的散列算法,将一个值经过散列运算后映射到一个m 位数组的某一位上,代码如下:
        这里新建了一个HashMap 类。构造函数传入两个值,一个是 m 位数组的位数,另一个是种子值seed。不同的散列函数需要有不同的 seed ,这样可以保证不同的散列函数的结果不会碰撞。在hash() 方法的实现中, value 是要被处理的内容。这里遍历了 value 的每一位,并利用 ord() 方法取到每一位的ASCII 码值,然后混淆 seed 进行迭代求和运算,最终得到一个数值。这个数值的结果就由value seed 唯一确定。我们再将这个数值和 m 进行按位与运算,即可获取到 m 位数组的映射结果,这样就实现了一个由字符串和seed 来确定的散列函数。当 m 固定时,只要 seed值相同,散列函数就是相同的,相同的value 必然会映射到相同的位置。所以如果想要构造几个不同的散列函数,只需要改变其seed 就好了。以上内容便是一个简易的散列函数的实现。

Bloom Filter实现

        接下来我们再实现Bloom Filter Bloom Filter 里面需要用到 k 个散列函数,这里要对这几个散列函数指定相同的m 值和不同的 seed 值,构造如下:
        由于我们需要亿级别的数据的去重,即前文介绍的算法中的n 1 亿以上,散列函数的个数 k 大约取10 左右的量级。而 m>kn ,这里 m 值大约保底在 10 亿,由于这个数值比较大,所以这里用移位操作来实现,传入位数bit ,将其定义为 30 ,然后做一个移位操作 1<<30 ,相当于 2 30 次方,等于1073741824 ,量级也是恰好在 10 亿左右,由于是位数组,所以这个位数组占用的大小就是2^30b=128 MB 。开头我们计算过 Scrapy-Redis 集合去重的占用空间大约在 2 GB 左右,可见Bloom Filter 的空间利用效率极高。
        随后我们再传入散列函数的个数,用它来生成几个不同的seed 。用不同的 seed 来定义不同的散列函数,这样我们就可以构造一个散列函数列表。遍历seed ,构造带有不同 seed 值的HashMap对象,然后将 HashMap 对象保存成变量 maps 供后续使用。另外,server 就是 Redis 连接对象, key 就是这个 m 位数组的名称。

exists()方法和insert()方法

        接下来,我们要实现比较关键的两个方法:一个是判定元素是否重复的方法exists() ,另一个是添加元素到集合中的方法insert() ,实现如下:
        首先看下insert() 方法。 Bloom Filter 算法会逐个调用散列函数对放入集合中的元素进行运算,得到在m 位位数组中的映射位置,然后将位数组对应的位置置 1 。这里代码中我们遍历了初始化好的散列函数,然后调用其hash() 方法算出映射位置 offset ,再利用 Redis setbit() 方法将该位置1
        在exists() 方法中,我们要实现判定是否重复的逻辑,方法参数 value 为待判断的元素。我们首先定义一个变量exist ,遍历所有散列函数对 value 进行散列运算,得到映射位置,用 getbit() 方法取得该映射位置的结果,循环进行与运算。这样只有每次getbit() 得到的结果都为 1 时,最后的exist 才为 True ,即代表 value 属于这个集合。如果其中只要有一次 getbit() 得到的结果为 0 ,即m位数组中有对应的 0 位,那么最终的结果 exist 就为 False ,即代表 value 不属于这个集合。

实例

conn = StrictRedis(host='localhost', port=6379, password='foobared')
bf = BloomFilter(conn, 'testbf', 5, 6)
bf.insert('Hello')
bf.insert('World')
result = bf.exists('Hello')
print(bool(result))
result = bf.exists('Python')
print(bool(result))

        首先定义了一个Redis连接对象,然后传递给Bloom Filter。为了避免内存占用过大,这里传的位数bit比较小,设置为5,散列函数的个数设置为6。调用insert()方法插入HelloWorld两个字符串,随后判断HelloPython这两个字符串是否存在,最后输出它的结果,运行结果如下:

        接下来继续修改Scrapy-Redis 的源码,将它的 dupefilter 逻辑替换为 Bloom Filter 的逻辑。这里主要是修改RFPDupeFilter 类的 request_seen() 方法,实现如下:
def request_seen(self, request):
    fp = self.request_fingerprint(request)
    if self.bf.exists(fp):
        self.bf.insert(fp)
        return False
        利用request_fingerprint() 方法获取 Request 的指纹,调用 Bloom Filter exists() 方法判定该指纹是否存在。如果存在,则说明该Request 是重复的,返回 True ,否则调用 Bloom Filter insert()方法将该指纹添加并返回False 。这样就成功利用 Bloom Filter 替换了 Scrapy-Redis 的集合去重。
        对于Bloom Filter的初始化定义,我们可以将__init__()方法修改为如下内容: 
def __init__(self, server, key, debug, bit, hash_number):
    self.server = server
    self.key = key
    self.debug = debug
    self.bit = bit
    self.hash_number = hash_number
    self.logdupes = True
    self.bf = BloomFilter(server, self.key, bit, hash_number)
        其中bit hash_number 需要使用 from_setti ngs() 方法传递,修改如下:
其中,常量 DUPEFILTER_DEBUG BLOOMFILTER_BIT 统一定义在 defaults.py 中,默认如下:
BLOOMFILTER_HASH_NUMBER = 6BLOOMFILTER_BIT = 30
我们可以直接使用 pip 来安装,命令如下:
        pip install scrapy - redis - bloomfilter
使用的方法和 Scrapy-Redis 基本相似,在这里说明几个关键配置。
DUPEFILTER_CLASS 是去重类,如果要使用 Bloom Filter ,则 DUPEFILTER_CLASS 需要修改为该包的去重类。
BLOOMFILTER_HASH_NUMBER Bloom Filter 使用的散列函数的个数,默认为 6 ,可以根据去重量级自行修改。
BLOOMFILTER_BIT 即前文所介绍的 BloomFilter 类的 bit 参数,它决定了位数组的位数。如果BLOOMFILTER_BIT 30 ,那么位数组位数为 2 30 次方,这将占用 Redis 128 MB的存储空间,去重量级在1 亿左右,即对应爬取量级 1 亿左右。如果爬取量级在 10 亿、 20亿甚至100 亿,请务必将此参数对应调高。

模拟登录基础

网站登录验证主要有两种实现,一种是基于 Session + Cookies 的登录验证,另一种是基于 JWT 的登录验证

准备工作

在本课时开始之前,请你确保已经做好了如下准备工作:

  • 安装好了 Python (最好 3.6 及以上版本)并能成功运行 Python 程序;

  • 安装好了 requests 请求库并学会了其基本用法;

  • 安装好了 Selenium 库并学会了其基本用法。

案例

这里有两个需要登录才能抓取的网站,链接为Scrape | MovieScrape | Book,前者是基于 Session + Cookies 认证的网站,后者是基于 JWT 认证的网站。

首先看下第一个网站,打开后会看到如图所示的页面。

在这里插入图片描述

它直接跳转到了登录页面,这里用户名和密码都是 admin,我们输入之后登录。

登录成功之后,我们便看到了电影网站的展示页面,如图所示。

在这里插入图片描述

这个网站是基于传统的 MVC 模式开发的,因此也比较适合 Session + Cookies 的认证。

第二个网站打开后同样会跳到登录页面,如图所示。

在这里插入图片描述

用户名和密码是一样的,都输入 admin 即可登录。

登录之后会跳转到首页,展示了一些书籍信息,如图所示。

​​​​​​​

在这里插入图片描述

这个页面是前后端分离式的页面,数据的加载都是通过 Ajax 请求后端 API 接口获取,登录的校验是基于 JWT 的,同时后端每个 API 都会校验 JWT 是否是有效的,如果无效则不会返回数据。

session登录

我们如果要模拟登录,就需要先分析下登录过程究竟发生了什么,首先我们打开Scrape | Movie,然后执行登录操作,查看其登录过程中发生的请求,如图所示

https://img-blog.csdnimg.cn/20200810121246831.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODgxOTg4OQ==,size_16,color_FFFFFF,t_70        这里我们可以看到其登录的瞬间是发起了一个 POST 请求,目标 URL 为 https://login2.scrape.cuiqingcai.com/login,通过表单提交的方式提交了登录数据,包括 username 和 password 两个字段,返回的状态码是 302,Response Headers 的 location 字段是根页面,同时 Response Headers 还包含了 set-cookie 信息,设置了 Session ID。

        由此我们可以发现,要实现模拟登录,我们只需要模拟这个请求就好了,登录完成之后获取 Response 设置的 Cookies,将 Cookies 保存好,以后后续的请求带上 Cookies 就可以正常访问了。

        requests 默认情况下每次请求都是独立互不干扰的,比如我们第一次先调用了 post 方法模拟登录,然后紧接着再调用 get 方法请求下主页面,其实这是两个完全独立的请求,第一次请求获取的 Cookies 并不能传给第二次请求,因此说,常规的顺序调用是不能起到模拟登录的效果的。

我们先来看一个无效的代码:

import requests
from urllib.parse import urljoin

BASE_URL = 'https://login2.scrape.cuiqingcai.com/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'

response_login = requests.post(LOGIN_URL, data={
   'username': USERNAME,
   'password': PASSWORD
})

response_index = requests.get(INDEX_URL)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)

        这里我们先定义了几个基本的 URL 和用户名、密码,接下来分别用 requests 请求了登录的 URL 进行模拟登录,然后紧接着请求了首页来获取页面内容,但是能正常获取数据吗?

        由于 requests 可以自动处理重定向,我们最后把 Response 的 URL 打印出来,如果它的结果是 INDEX_URL,那么就证明模拟登录成功并成功爬取到了首页的内容。如果它跳回到了登录页面,那就说明模拟登录失败。

        我们通过结果来验证一下,运行结果如下:

Response Status 200
Response URL https://login2.scrape.cuiqingcai.com/login?next=/page/1

        这里可以看到,其最终的页面 URL 是登录页面的 URL,另外这里也可以通过 response 的 text 属性来验证页面源码,其源码内容就是登录页面的源码内容,由于内容较多,这里就不再输出比对了。

        总之,这个现象说明我们并没有成功完成模拟登录,这是因为 requests 直接调用 post、get 等方法,每次请求都是一个独立的请求,都相当于是新开了一个浏览器打开这些链接,这两次请求对应的 Session 并不是同一个,因此这里我们模拟了第一个 Session 登录,而这并不能影响第二个 Session 的状态,因此模拟登录也就无效了。 那么怎样才能实现正确的模拟登录呢?

        我们知道 Cookies 里面是保存了 Session ID 信息的,刚才也观察到了登录成功后 Response Headers 里面是有 set-cookie 字段,实际上这就是让浏览器生成了 Cookies。

        Cookies 里面包含了 Session ID 的信息,所以只要后续的请求携带这些 Cookies,服务器便能通过 Cookies 里的 Session ID 信息找到对应的 Session,因此服务端对于这两次请求就会使用同一个 Session 了。而因为第一次我们已经完成了模拟登录,所以第一次模拟登录成功后,Session 里面就记录了用户的登录信息,第二次访问的时候,由于是同一个 Session,服务器就能知道用户当前是登录状态,就可以返回正确的结果而不再是跳转到登录页面了。

        所以,这里的关键就在于两次请求的 Cookies 的传递。所以这里我们可以把第一次模拟登录后的 Cookies 保存下来,在第二次请求的时候加上这个 Cookies 就好了,所以代码可以改写如下:

​​​​​​​

import requests
from urllib.parse import urljoin

BASE_URL = 'https://login2.scrape.cuiqingcai.com/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'

response_login = requests.post(LOGIN_URL, data={
   'username': USERNAME,
   'password': PASSWORD
}, allow_redirects=False)

cookies = response_login.cookies
print('Cookies', cookies)

response_index = requests.get(INDEX_URL, cookies=cookies)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)

        由于 requests 可以自动处理重定向,所以模拟登录的过程我们要加上 allow_redirects 参数并设置为 False,使其不自动处理重定向,这里登录之后返回的 Response 我们赋值为 response_login,这样通过调用 response_login 的 cookies 就可以获取到网站的 Cookies 信息了,这里 requests 自动帮我们解析了 Response Headers 的 set-cookie 字段并设置了 Cookies,所以我们不需要手动解析 Response Headers 的内容了,直接使用 response_login 对象的 cookies 属性即可获取 Cookies。

        接下来我们再次用 requests 的 get 方法来请求网站的 INDEX_URL,不过这里和之前不同,get 方法多加了一个参数 cookies,这就是第一次模拟登录完之后获取的 Cookies,这样第二次请求就能携带第一次模拟登录获取的 Cookies 信息了,此时网站会根据 Cookies 里面的 Session ID 信息查找到同一个 Session,校验其已经是登录状态,然后返回正确的结果。

        这里我们还是输出了最终的 URL,如果其是 INDEX_URL,那就代表模拟登录成功并获取到了有效数据,否则就代表模拟登录失败。运行结果如下:

Cookies <RequestsCookieJar[<Cookie sessionid=psnu8ij69f0ltecd5wasccyzc6ud41tc for login2.scrape.cuiqingcai.com/>]>
Response Status 200
Response URL https://login2.scrape.cuiqingcai.com/page/1

        这下就没有问题了,这次我们发现其 URL 就是 INDEX_URL,模拟登录成功了!同时还可以进一步输出 response_index 的 text 属性看下是否获取成功。接下来后续的爬取用同样的方式爬取即可。

        但是我们发现其实这种实现方式比较烦琐,每次还需要处理 Cookies 并进行一次传递,我们可以直接借助于 requests 内置的 Session 对象来帮我们自动处理 Cookies,使用了 Session 对象之后,requests 会将每次请求后需要设置的 Cookies 自动保存好,并在下次请求时自动携带上去,就相当于帮我们维持了一个 Session 对象,这样就更方便了。

所以,刚才的代码可以简化如下:

import requests
from urllib.parse import urljoin

BASE_URL = 'https://login2.scrape.cuiqingcai.com/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'

session = requests.Session()

response_login = session.post(LOGIN_URL, data={
   'username': USERNAME,
   'password': PASSWORD
})

cookies = session.cookies
print('Cookies', cookies)

response_index = session.get(INDEX_URL)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)

        可以看到,这里我们无需再关心 Cookies 的处理和传递问题,我们声明了一个 Session 对象,然后每次调用请求的时候都直接使用 Session 对象的 post 或 get 方法就好了。

        因此,为了简化写法,这里建议直接使用 Session 对象来进行请求,这样我们就无需关心 Cookies 的操作了,实现起来会更加方便。

        如果碰上复杂一点的网站,如带有验证码,带有加密参数等等,直接用 requests 并不好处理模拟登录我们可以使用 Selenium 来通过模拟浏览器的方式实现模拟登录,然后获取模拟登录成功后的 Cookies,再把获取的 Cookies 交由 requests 等来爬取就好了。

from urllib.parse import urljoin
from selenium import webdriver
import requests
import time

BASE_URL = 'https://login2.scrape.cuiqingcai.com/'
LOGIN_URL = urljoin(BASE_URL, '/login')
INDEX_URL = urljoin(BASE_URL, '/page/1')
USERNAME = 'admin'
PASSWORD = 'admin'

browser = webdriver.Chrome()
browser.get(BASE_URL)
browser.find_element_by_css_selector('input[name="username"]').send_keys(USERNAME)
browser.find_element_by_css_selector('input[name="password"]').send_keys(PASSWORD)
browser.find_element_by_css_selector('input[type="submit"]').click()
time.sleep(10)

# get cookies from selenium
cookies = browser.get_cookies()
print('Cookies', cookies)
browser.close()

# set cookies to requests
session = requests.Session()
for cookie in cookies:
   session.cookies.set(cookie['name'], cookie['value'])

response_index = session.get(INDEX_URL)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)

        这里我们使用 Selenium 先打开了 Chrome 浏览器,然后跳转到了登录页面,随后模拟输入了用户名和密码,接着点击了登录按钮,这时候我们可以发现浏览器里面就提示登录成功,然后成功跳转到了主页面。

        这时候,我们通过调用 get_cookies 方法便能获取到当前浏览器所有的 Cookies,这就是模拟登录成功之后的 Cookies,用这些 Cookies 我们就能访问其他的数据了。

        接下来,我们声明了 requests 的 Session 对象,然后遍历了刚才的 Cookies 并设置到 Session 对象的 cookies 上面去,接着再拿着这个 Session 对象去请求 INDEX_URL,也就能够获取到对应的信息而不会跳转到登录页面了。运行结果如下:

Cookies [{'domain': 'login2.scrape.cuiqingcai.com', 'expiry': 1589043753.553155, 'httpOnly': True, 'name': 'sessionid', 'path': '/', 'sameSite': 'Lax', 'secure': False, 'value': 'rdag7ttjqhvazavpxjz31y0tmze81zur'}]

Response Status 200

Response URL https://login2.scrape.cuiqingcai.com/page/1

        可以看到这里的模拟登录和后续的爬取也成功了。所以说,如果碰到难以模拟登录的过程,我们也可以使用 Selenium 或 Pyppeteer 等模拟浏览器操作的方式来实现,其目的就是取到登录后的 Cookies,有了 Cookies 之后,我们再用这些 Cookies 爬取其他页面就好了。

        所以这里我们也可以发现,对于基于 Session + Cookies 验证的网站,模拟登录的核心要点就是获取 Cookies,这个 Cookies 可以被保存下来或传递给其他的程序继续使用。甚至说可以将 Cookies 持久化存储或传输给其他终端来使用。另外,为了提高 Cookies 利用率或降低封号几率,可以搭建一个 Cookies 池实现 Cookies 的随机取用。

​​​​​​​

jwt登录

        基于 JWT 的网站,其通常都是采用前后端分离式的,前后端的数据传输依赖于 Ajax,登录验证依赖于 JWT 本身这个 token 的值,如果 JWT 这个 token 是有效的,那么服务器就能返回想要的数据。下面我们先来在浏览器里面操作登录,观察下其网络请求过程,如图所示。

在这里插入图片描述

        这里我们发现登录时其请求的 URL 为Scrape | Book,是通过 Ajax 请求的,同时其 Request Body 是 JSON 格式的数据,而不是 Form Data,返回状态码为 200。然后再看下返回结果,如图所示。 可以看到返回结果是一个 JSON 格式的数据,包含一个 token 字段,其结果为:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3ODc3OTQ2LCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm9yaWdfaWF0IjoxNTg3ODM0NzQ2fQ.ujEXXAZcCDyIfRLs44i_jdfA3LIp5Jc74n-Wq2udCR8

        那么有了这个 JWT 之后,后续的数据怎么获取呢?下面我们再来观察下后续的请求内容,如图所示。

在这里插入图片描述

        这里我们可以发现,后续获取数据的 Ajax 请求中的 Request Headers 里面就多了一个 Authorization 字段,其结果为 jwt 然后加上刚才的 JWT 的内容,返回结果就是 JSON 格式的数据。 在这里插入图片描述         没有问题,那模拟登录的整个思路就简单了: 模拟请求登录结果,带上必要的登录信息,获取 JWT 的结果。

        后续的请求在 Request Headers 里面加上 Authorization 字段,值就是 JWT 对应的内容。 好,接下来我们用代码实现如下:

import requests
from urllib.parse import urljoin

BASE_URL = 'https://login3.scrape.cuiqingcai.com/'
LOGIN_URL = urljoin(BASE_URL, '/api/login')
INDEX_URL = urljoin(BASE_URL, '/api/book')
USERNAME = 'admin'
PASSWORD = 'admin'

response_login = requests.post(LOGIN_URL, json={
   'username': USERNAME,
   'password': PASSWORD
})
data = response_login.json()
print('Response JSON', data)
jwt = data.get('token')
print('JWT', jwt)

headers = {
   'Authorization': f'jwt {jwt}'
}
response_index = requests.get(INDEX_URL, params={
   'limit': 18,
   'offset': 0
}, headers=headers)
print('Response Status', response_index.status_code)
print('Response URL', response_index.url)
print('Response Data', response_index.json())

这里我们同样是定义了登录接口和获取数据的接口,分别为 LOGIN_URL 和 INDEX_URL,接着通过 post 请求进行了模拟登录,这里提交的数据由于是 JSON 格式,所以这里使用 json 参数来传递。接着获取了返回结果中包含的 JWT 的结果。第二步就可以构造 Request Headers,然后设置 Authorization 字段并传入 JWT 即可,这样就能成功获取数据了。

运行结果如下:

​​​​​​

Response JSON {'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3ODc4NzkxLCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm9yaWdfaWF0IjoxNTg3ODM1NTkxfQ.iUnu3Yhdi_a-Bupb2BLgCTUd5yHL6jgPhkBPorCPvm4'}

JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3ODc4NzkxLCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm9yaWdfaWF0IjoxNTg3ODM1NTkxfQ.iUnu3Yhdi_a-Bupb2BLgCTUd5yHL6jgPhkBPorCPvm4

Response Status 200
Response URL https://login3.scrape.cuiqingcai.com/api/book/?limit=18&offset=0
Response Data {'count': 9200, 'results': [{'id': '27135877', 'name': '校园市场:布局未来消费群,决战年轻人市场', 'authors': ['单兴华', '李烨'], 'cover': 'https://img9.doubanio.com/view/subject/l/public/s29539805.jpg', 'score': '5.5'},
...
{'id': '30289316', 'name': '就算這樣,還是喜歡你,笠原先生', 'authors': ['おまる'], 'cover': 'https://img3.doubanio.com/view/subject/l/public/s29875002.jpg', 'score': '7.5'}]}

        可以看到,这里成功输出了 JWT 的内容,同时最终也获取到了对应的数据,模拟登录成功!

        类似的思路,如果我们遇到 JWT 认证的网站,也可以通过类似的方式来实现模拟登录。当然可能某些页面比较复杂,需要具体情况具体分析。

​​​​​​​

超级鹰的使用

一个打码平台,可以解决验证码的问题https://www.chaojiying.com

后期补

反屏蔽

注意一下以下所有练习网站可能有已经失效的参考Python爬虫案例 | Scrape Center即可

现在很多网站都加上了对 Selenium 的检测,来防止一些爬虫的恶意爬取。即如果检测到有人在使用 Selenium 打开浏览器,那就直接屏蔽。

其大多数情况下,检测基本原理是检测当前浏览器窗口下的 window.navigator 对象是否包含 webdriver 这个属性。因为在正常使用浏览器的情况下,这个属性是 undefined,然而一旦我们使用了 Selenium,Selenium 会给 window.navigator 设置 webdriver 属性。很多网站就通过 JavaScript 判断如果 webdriver 属性存在,那就直接屏蔽。

这边有一个典型的案例网站:https://antispider1.scrape.cuiqingcai.com/,这个网站就是使用了上述原理实现了 WebDriver 的检测,如果使用 Selenium 直接爬取的话,那就会返回如下页面:

在这里插入图片描述

这时候我们可能想到直接使用 JavaScript 直接把这个 webdriver 属性置空,比如通过调用 execute_script 方法来执行如下代码:

Object.defineProperty(navigator, "webdriver", {get: () => undefined})

Pyppeteer 介绍

在前面我们学习了 Selenium 的基本用法,它功能的确非常强大,但很多时候我们会发现 Selenium 有一些不太方便的地方,比如环境的配置,得安装好相关浏览器,比如 Chrome、Firefox 等等,然后还要到官方网站去下载对应的驱动,最重要的还需要安装对应的 Python Selenium 库,而且版本也得好好看看是否对应,确实不是很方便,另外如果要做大规模部署的话,环境配置的一些问题也是个头疼的事情。

那么本课时我们就介绍另一个类似的替代品,叫作 Pyppeteer。注意,是叫作 Pyppeteer,而不是 Puppeteer。

Puppeteer 是 Google 基于 Node.js 开发的一个工具,有了它我们可以通过 JavaScript 来控制 Chrome 浏览器的一些操作,当然也可以用作网络爬虫上,其 API 极其完善,功能非常强大,Selenium 当然同样可以做到。

而 Pyppeteer 又是什么呢?它实际上是 Puppeteer 的 Python 版本的实现,但它不是 Google 开发的,是一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。

在 Pyppetter 中,实际上它背后也是有一个类似 Chrome 浏览器的 Chromium 浏览器在执行一些动作进行网页渲染,首先说下 Chrome 浏览器和 Chromium 浏览器的渊源。

安装

首先就是安装问题了,由于 Pyppeteer 采用了 Python 的 async 机制,所以其运行要求的 Python 版本为 3.5 及以上。

pip3 install pyppeteer

基本的页面渲染操作

  接下来我们测试基本的页面渲染操作,这里我们选用的网址为:https://dynamic2.scrape.cuiqingcai.com/,如图所示。

在这里插入图片描述

整个页面是用 JavaScript 渲染出来的,同时一些 Ajax 接口还带有加密参数,所以这个网站的页面我们无法直接使用 requests 来抓取看到的数据,同时我们也不太好直接模拟 Ajax 来获取数据。

所以前面一课时我们介绍了使用 Selenium 爬取的方式,其原理就是模拟浏览器的操作,直接用浏览器把页面渲染出来,然后再直接获取渲染后的结果。同样的原理,用 Pyppeteer 也可以做到。

下面我们用 Pyppeteer 来试试,代码就可以写为如下形式:

import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
async def main():
   browser = await launch()
   page = await browser.newPage()
   await page.goto('https://dynamic2.scrape.cuiqingcai.com/')
   await page.waitForSelector('.item .name')
   doc = pq(await page.content())
   names = [item.text() for item in doc('.item .name').items()]
   print('Names:', names)
   await browser.close()
asyncio.get_event_loop().run_until_complete(main())

 接下来我们再看看另外一个例子,这个例子设定了浏览器窗口大小,然后模拟了网页截图,另外还可以执行自定义的 JavaScript 获得特定的内容,代码如下:

import asyncio
from pyppeteer import launch
width, height = 1366, 768
async def main():
   browser = await launch()
   page = await browser.newPage()
   await page.setViewport({'width': width, 'height': height})
   await page.goto('https://dynamic2.scrape.cuiqingcai.com/')
   await page.waitForSelector('.item .name')
   await asyncio.sleep(2)
   await page.screenshot(path='example.png')
   dimensions = await page.evaluate('''() => {
       return {
           width: document.documentElement.clientWidth,
           height: document.documentElement.clientHeight,
           deviceScaleFactor: window.devicePixelRatio,
       }
   }''')

   print(dimensions)
   await browser.close()
asyncio.get_event_loop().run_until_complete(main())

这里我们又用到了几个新的 API,完成了页面窗口大小设置、网页截图保存、执行 JavaScript 并返回对应数据。

首先 screenshot 方法可以传入保存的图片路径,另外还可以指定保存格式 type、清晰度 quality、是否全屏 fullPage、裁切 clip 等各个参数实现截图。

​​​​​​​

在这里插入图片描述

可以看到它返回的就是 JavaScript 渲染后的页面,和我们在浏览器中看到的结果是一模一样的。

最后我们又调用了 evaluate 方法执行了一些 JavaScript,JavaScript 传入的是一个函数,使用 return 方法返回了网页的宽高、像素大小比率三个值,最后得到的是一个 JSON 格式的对象,内容如下:​​​​​​​

{'width': 1366, 'height': 768, 'deviceScaleFactor': 1}

详细用法

防止检测

​​​​​​​如果你只是把提示关闭了,有些网站还是会检测到是 WebDriver 吧,比如拿之前的检测 WebDriver 的案例 https://antispider1.scrape.cuiqingcai.com/ 来验证下

import asyncio
from pyppeteer import launch
 
async def main():
   browser = await launch(headless=False, args=['--disable-infobars'])
   page = await browser.newPage()
   await page.goto('https://antispider1.scrape.cuiqingcai.com/')
   await asyncio.sleep(100)
 
asyncio.get_event_loop().run_until_complete(main())

 果然还是被检测到了,页面如下:

在这里插入图片描述

这说明 Pyppeteer 开启 Chromium 照样还是能被检测到 WebDriver 的存在。

那么此时如何规避呢?Pyppeteer 的 Page 对象有一个方法叫作 evaluateOnNewDocument,意思就是在每次加载网页的时候执行某个语句,所以这里我们可以执行一下将 WebDriver 隐藏的命令,改写如下:

​​​​​​​

import asyncio
from pyppeteer import launch
 
async def main():
   browser = await launch(headless=False, args=['--disable-infobars'])
   page = await browser.newPage()
   await page.evaluateOnNewDocument('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')
   await page.goto('https://antispider1.scrape.cuiqingcai.com/')
   await asyncio.sleep(100)
 
asyncio.get_event_loop().run_until_complete(main())
消除指纹

加载一个js脚本,把不一样的地方全改过来

pip install pyppeteer-stealth

from pyppeteer_stealth import stealth

#震坤行案例
import asyncio
from pyppeteer import launch
from pyppeteer_stealth import stealth
import os
from openpyxl import load_workbook
import random
width, height = 1920, 1080
import re
import openpyxl
import pymysql
db = pymysql.connect(user='root',password='123456',db='test')
cursor = db.cursor()
async def run(url):
    d = re.compile('https://www\.zkh\.com/list/c-(\d+)\.html')
    num = d.findall(url)[0]
    current_page = 1
    # dicts = {}
    # dicts[num]=[]
    browser = await launch(headless=False, args=['--disable-infobars'])
    # 开启一个页面对象
    page = await browser.newPage()
    # 消除指纹
    await stealth(page)  # <-- Here
    # 设置浏览器宽高
    await page.setViewport({'width': width, 'height': height})
    await page.goto(url)
    # await asyncio.sleep(1000)
    while True:
        # await asyncio.sleep(2)
        # 等待id=key的这个元素出现,等9秒,超过不出现报超时错误
        await page.waitForXPath('//*[@class="goods-item-wrap-new clearfix common-item-wrap"]', {'timeout': 9000})
        await page.evaluate('window.scrollBy(10000, document.body.scrollHeight)')
        await asyncio.sleep(1)
        await page.evaluate('window.scrollBy(10000, document.body.scrollHeight)')
        await asyncio.sleep(1)
        await page.evaluate('window.scrollBy(10000, document.body.scrollHeight)')
        await asyncio.sleep(1)
        li_list = await page.xpath('//div[@class="goods-item-wrap-new clearfix common-item-wrap"]/a[1]')

        for content_url in li_list:
            the_url = await (await content_url.getProperty("href")).jsonValue()
            # dicts[num].append(the_url)
            sql = 'insert into task(code,urls,status) values ("{}","{}","{}")'.format(num,the_url,1)
            cursor.execute(sql)
            db.commit()

        # await (await a[0].getProperty("textContent")).jsonValue()
        a = await page.xpath('//b[@class="pagination-page-total"]')
        if len(a) == 0:
            break
        all_page = await (await a[0].getProperty("textContent")).jsonValue()
        print('当前页数为', current_page)
        if current_page == int(all_page):
            break
        else:
            current_page += 1
            await page.click('.nextbtn')

  

async def main():

    # 设置启动时是否开启浏览器可视,消除控制条信息

    
    # 访问某个页面
    task_url = ['https://www.zkh.com/list/c-10290173.html','https://www.zkh.com/list/c-10290175.html']


    await asyncio.gather(*[run(_) for _ in task_url])


asyncio.get_event_loop().run_until_complete(main())
Pyppeteer模拟登录

后边补

node.js安装

js逆向

网易云案例

js基础

var let const 区别

​​​​​​​区别

1 var 声明的变量属于函数作用域,而 let const 声明的变量属于块级作用域;( js 作用域在上篇文章)
2 var 声明的变量存在变量提升,而 let const 没有
3 var 声明的变量可以重复声明,而在同一块级作用域, let 变量不能重新声明, const 常量不能修改(对象的属性和方法,数组的内容可以修改)

var声明的作用域

使用 var 声明的变量,这个变量属于当前的函数作用域,如果变量的声明在任何函数外,那么这个变量就属于全局作用域
如果在声明变量时,省略 var 的话,该变量就会变成全局变量,如全局作用域中存在该变量,就会更新其值

var声明的变量提升

var 的声明会在 js 预解析时把 var 的声明提升到当前作用域的最前面,意思是是指无论 var 出现在一个作用域的哪个位置,这个声明都属于当前的整个作用域,在其中到处都可以访问到。只有变量声明才会提升,对变量赋值并不会提升
console . log ( a ); //undefined
var a = 1 ;
相当于执行以下代码
var a ;
console . log ( a ); //undefined
a = 1 ;

let声明

let 声明的变量具有块作用域的特征。
在同一个块级作用域,不能重复声明变量
let 声明的变量不存在变量提升

var let(作用域)

var 声明

1. 此时的 var 声明的变量 i 属于函数作用域,声明又不在函数里,所以 i 属于全局变量
2. 此时的定时器函数属于异步函数,隔 100 毫秒才会执行,而这 100 毫秒的时间内, for 循环已经循环结束,全局变量i 已经为 10,最后代码的执行后,会在控制台打印出10 10 )主要的原因是 var 声明的变量的没有块级作用域
let 声明

const 声明

const 声明方式,除了具有 let 的上述特点外,其还具备一个特点,即 const 定义的变量,一旦定义后,就不能修改,即 const 声明的为常量。
但是,并不是说 const 声明的变量其内部内容不可变,如:
所以准确的说,是 const 声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。

条件语句

if else结构

if结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。所谓布尔值,指的是 JavaScript 的两个特殊值,true表示“真”,false表示“伪”。

if (布尔值)
  语句;

// 或者
if (布尔值) 语句;

如果想执行多个语句,必须在if的条件判断之后,加上大括号,表示代码块(多个语句合并成一个语句)。

if (m !== 1) {
  if (n === 2) {
    console.log('hello');
  }
} else {
  console.log('world');
}
// world

switch 结构

switch (x) {
  case 1:
    console.log('x 等于1');
    break;
  case 2:
    console.log('x 等于2');
    break;
  default:
    console.log('x 等于其他值');
}

 三元运算符

JavaScript 还有一个三元运算符(即该运算符需要三个运算子)?:,也可以用于逻辑判断。

(条件) ? 表达式1 : 表达式2

上面代码中,如果“条件”为true,则返回“表达式1”的值,否则返回“表达式2”的值。

var even = (n % 2 === 0) ? true : false;
//例子一
var myVar;
console.log(
  myVar ?
  'myVar has a value' :
  'myVar does not have a value'
)
// myVar does not have a value
//例子二
var msg = '数字' + n + '是' + (n % 2 === 0 ? '偶数' : '奇数');

 循环语句

while循环
var i = 0;

while (i < 100) {
  console.log('i 当前为:' + i);
  i = i + 1;
}

 下面的例子是一个无限循环,因为循环条件总是为真。

while (true) {
  console.log('Hello, world');
}
 for 循环
var x = 3;
for (var i = 0; i < x; i++) {
  console.log(i);
}
// 0
// 1
// 2
var x = 3;
var i = 0;

while (i < x) {
  console.log(i);
  i++;
}
for ( ; ; ){
  console.log('Hello World');
}
 do...while 循环

do
  语句
while (条件);

// 或者
do {
  语句
} while (条件);

不管条件是否为真,do...while循环至少运行一次,这是这种结构最大的特点。另外,while语句后面的分号注意不要省略。

var x = 3;
var i = 0;

do {
  console.log(i);
  i++;
} while(i < x);
 break 语句和 continue 语句

break语句和continue语句都具有跳转作用,可以让代码不按既有的顺序执行。break语句用于跳出代码块或循环。

var i = 0;

while(i < 100) {
  console.log('i 当前为:' + i);
  i++;
  if (i === 10) break;
}

 上面代码只会执行10次循环,一旦i等于10,就会跳出循环。

for循环也可以使用break语句跳出循环。

var i = 0;

while (i < 100){
  i++;
  if (i % 2 === 0) continue;
  console.log('i 当前为:' + i);
}

上面代码只有在i为奇数时,才会输出i的值。如果i为偶数,则直接进入下一轮循环。

数据类型

字符串与数组

var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"

// 直接对字符串使用方括号运算符
'hello'[1] // "e"

 如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回`undefined`。

'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined

 但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。

var s = 'hello';

delete s[0];
s // "hello"

s[1] = 'a';
s // "hello"

s[5] = '!';
s // "hello"

 上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会默默地失败。

length 属性

length属性返回字符串的长度,该属性也是无法改变的

var s = 'hello';
s.length // 5

s.length = 3;
s.length // 5

s.length = 7;
s.length // 5

上面代码表示字符串的length属性无法改变,但是不会报错。

Base64 转码 (该知识点比较重要)

        有时,文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另一个场景是,有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码。

        所谓 Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+/这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。

JavaScript 原生提供两个 Base64 相关的方法。

  • btoa():任意值转为 Base64 编码

  • atob():Base64 编码转为原来的值

var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"

 注意,这两个方法不适合非 ASCII 码的字符,会报错。

btoa('你好') // 报错

 要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。

function b64Encode(str) {
  return btoa(encodeURIComponent(str));
}

function b64Decode(str) {
  return decodeURIComponent(atob(str));
}

b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"

 对象

你其实完全可以理解为python里的字典

var obj = {
  foo: 'Hello',
  bar: 'World'
};

 如果键名是数值,会被自动转为字符串。如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错,记不住就一律加引号得了。

// 报错
var obj = {
  1p: 'Hello World'
};

// 不报错
var obj = {
  '1p': 'Hello World',
  'h w': 'Hello World',
  'p+q': 'Hello World'
};

 对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。

var obj = {
  p: function (x) {
    return 2 * x;
  }
};

obj.p(1) // 2

对象的读值与赋值

var obj = {};
obj.foo = 'Hello';
obj['bar'] = 'World';

 属性的查看

var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj);
// ['key1', 'key2']

​​​​​​​属性的删除:delete 命令

var obj = { p: 1 };
Object.keys(obj) // ["p"]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

delete命令用于删除对象的属性,删除成功后返回true

注意,删除一个不存在的属性,delete不报错,而且返回true

var obj = {};
delete obj.p // true

上面代码中,对象obj并没有p属性,但是delete命令照样返回true。因此,不能根据delete命令的结果,认定某个属性是存在的。

只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除。

​​​​​​​

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

 属性是否存在:in 运算符

in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false。它的左边是一个字符串,表示属性名,右边是一个对象。

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj本身并没有toString属性,但是in运算符会返回true,因为这个属性是继承的。

这时,可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性。

var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}

属性的遍历:for...in 循环

var obj = {a: 1, b: 2, c: 3};
​
for (var i in obj) {
  console.log('键名:', i);
  console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3

for...in循环有两个使用注意点。

  • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。

  • 它不仅遍历对象自身的属性,还遍历继承的属性。

举例来说,对象都继承了toString属性,但是for...in循环不会遍历到这个属性。

var obj = {};
​
// toString 属性是存在的
obj.toString // toString() { [native code] }
​
for (var p in obj) {
  console.log(p);
} // 没有任何输出

上面代码中,对象obj继承了toString属性,该属性不会被for...in循环遍历到,因为它默认是“不可遍历”的。

如果继承的属性是可遍历的,那么就会被for...in循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for...in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。

var person = { name: '老张' };
​
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name

函数

js三种声明函数的方式

方法一

function print(s) {
  console.log(s);
}

方法二

var print = function(s) {
  console.log(s);
};

方法三 (这种没人用)

var add = new Function(
  'x',
  'y',
  'return x + y'
);
​
// 等同于
function add(x, y) {
  return x + y;
}

第一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。

由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。

function add(x, y) {
  return x + y;
}
​
// 将函数赋值给一个变量
var operator = add;
​
// 将函数作为参数和返回值
function a(op){
  return op;
}
a(add)(1, 1)
// 2

函数名的提升

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。

f();
​
function f() {}

表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。

f();
var f = function (){};
// TypeError: undefined is not a function

上面的代码等同于下面的形式。

var f;
f();
f = function () {};

上面代码第二行,调用f的时候,f只是被声明了,还没有被赋值,等于undefined,所以会报错。

注意,如果像下面例子那样,采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。

var f = function () {
  console.log('1');
}
​
function f() {
  console.log('2');
}
​
f() // 1

上面例子中,表面上后面声明的函数f,应该覆盖前面的var赋值语句,但是由于存在函数提升,实际上正好反过来。

函数作用域

定义

作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。

函数内部定义的变量,会在该作用域内覆盖同名全局变量。

var v = 1;
​
function f(){
  var v = 2;
  console.log(v);
}
​
f() // 2
v // 1

上面代码中,变量v同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量v覆盖了全局变量v

注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。

if (true) {
  var x = 5;
}
console.log(x);  // 5

上面代码中,变量x在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

var a = 1;
var x = function () {
  console.log(a);
};
​
function f() {
  var a = 2;
  x();
}
​
f() // 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2

总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

参数

js里的函数的参数,不用跟实际传过来的值一一对应

arguments 对象

由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用

var f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}
​
f(1, 2, 3)

需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如sliceforEach),不能在arguments对象上直接使用。

如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。

var args = Array.prototype.slice.call(arguments);
​
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}

关于slice函数的语法

let fruits = ['apple', 'banana', 'cherry', 'date', 'fig'];
​
// 提取从索引1开始到索引3的元素
let slicedFruits = fruits.slice(1, 3); // 返回 ["banana", "cherry"]
​
// 如果不指定end参数,则提取从start到数组末尾的元素
let slicedFruitsToEnd = fruits.slice(1); // 返回 ["banana", "cherry", "date", "fig"]
​
// 如果start参数是负数,则表示从数组末尾开始计算的位置
let slicedFruitsFromEnd = fruits.slice(-3); // 返回 ["date", "fig"]
​
// 如果省略start和end参数,则返回数组的一个完整副本
let fruitsCopy = fruits.slice(); // 返回 ["apple", "banana", "cherry", "date", "fig"]

闭包

闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。

var n = 999;
​
function f1() {
  console.log(n);
}
f1() // 999

上面代码中,函数f1可以读取全局变量n

但是,正常情况下,函数外部无法读取函数内部声明的变量。

function f1() {
  var n = 999;
}
​
console.log(n)
// Uncaught ReferenceError: n is not defined(

上面代码中,函数f1内部声明的变量n,函数外是无法读取的。

如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

function f1() {
  var n = 999;
  function f2() {
  console.log(n); // 999
  }
}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
​
var result = f1();
result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}
​
var inc = createIncrementor(5);
​
inc() // 5
inc() // 6
inc() // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

闭包的另一个用处,是封装对象的私有属性和私有方法。

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }
​
  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}
​
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

上面代码中,函数Person的内部变量_age,通过闭包getAgesetAge,变成了返回对象p1的私有变量。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

语句与表达式的区别

语句(Statement)

语句是执行动作的指令。它告诉JavaScript执行某些操作,通常会有副作用(即改变程序的状态或环境)。语句的结束通常需要一个分号(;),但这不是强制性的,因为在JavaScript中,分号是语句的可选结束符。例如:

// 赋值语句
let x = 10;
​
// 函数调用语句
doSomething();
​
// 控制流语句
if (condition) {
  // ...
}
​
// 循环语句
for (let i = 0; i < 10; i++) {
  // ...
}
表达式(Expression)

表达式是一个计算并返回值的代码片段。它可以是一个变量、一个函数调用、一个算术运算或者任何能够产生值的组合。表达式可以是语句的一部分,但单独的表达式不能作为语句存在(除非它是返回值的函数或语句末尾有分号)。 例如:

// 变量名是一个表达式
let y = 20;
​
// 算术运算是一个表达式
let sum = 10 + 5; // 这里的 10 + 5 是一个表达式
​
// 函数调用也是一个表达式,特别是当它返回一个值时
let result = doSomething(); // doSomething() 是一个返回值的表达式

立即调用的函数表达式(IIFE)

根据 JavaScript 的语法,圆括号()跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。

有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。

function(){ /* code */ }();
// SyntaxError: Unexpected token (

产生这个错误的原因是,function这个关键字既可以当作语句,也可以当作表达式。

// 语句
function f() {}
​
// 表达式
var f = function f() {}
当作表达式时,函数可以定义后直接加圆括号调用。

var f = function f(){ return 1}();
f // 1

上面的代码中,函数定义后直接加圆括号调用,没有报错。原因就是function作为表达式,引擎就把函数定义当作一个值。这种情况下,就不会报错。

为了避免解析的歧义,JavaScript 规定,如果function关键字出现在行首,一律解释成语句。因此,引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

函数定义后立即调用的解决方法,就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表达式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。

注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。

// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())

上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。

推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

甚至像下面这样写,也是可以的。

!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

eval 命令

基本用法

eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。

eval('var a = 1;');
a // 1

数组的本质

本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object

typeof [1, 2, 3] // "object"

但是,对于数值的键名,不能使用点结构

var arr = [1, 2, 3];
arr.0 // SyntaxError

上面代码中,arr.0的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]表示(方括号是运算符,可以接受数值)。

length 属性

数组的length属性,返回数组的成员数量。

['a', 'b', 'c'].length // 3

JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232 - 1)个,也就是说length属性的最大值就是 4294967295。

只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1

var arr = ['a', 'b'];
arr.length // 2
​
arr[2] = 'c';
arr.length // 3
​
arr[9] = 'd';
arr.length // 10
​
arr[1000] = 'e';
arr.length // 1001

上面代码表示,数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。

length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length设置的值。

in 运算符

检查某个键名是否存在的运算符in,适用于对象,也适用于数组。

var arr = [ 'a', 'b', 'c' ];
2 in arr  // true
'2' in arr // true
4 in arr // false

上面代码表明,数组存在键名为2的键。由于键名都是字符串,所以数值2会自动转成字符串。

注意,如果数组的某个位置是空位,in运算符返回false

var arr = [];
arr[100] = 'a';
​
100 in arr // true
1 in arr // false

上面代码中,数组arr只有一个成员arr[100],其他位置的键名都会返回false

数组遍历

var a = [1, 2, 3];
​
// for循环
for(var i = 0; i < a.length; i++) {
  console.log(a[i]);
}
​
// while循环
var i = 0;
while (i < a.length) {
  console.log(a[i]);
  i++;
}
​
var l = a.length;
while (l--) {
  console.log(a[l]);
}

上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。

数组的forEach方法,也可以用来遍历数组

var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
  console.log(color);
});
// red
// green
// blue

数据类型转换

强制转换

强制转换主要指使用Number()String()Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。

Number()
// 数值:转换后还是原来的值
Number(324) // 324
​
// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324
​
// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN
​
// 空字符串转为0
Number('') // 0
​
// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0
​
// undefined:转成 NaN
Number(undefined) // NaN
​
// null:转成0
Number(null) // 0
类似函数parseInt函数的使用
parseInt('42 cats') // 42
Number('42 cats') // NaN
String()
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null"
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"
Boolean()

Boolean()函数可以将任意类型的值转为布尔值。

它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true

  • undefined

  • null

  • 0(包含-0+0

  • NaN

  • ''(空字符串)

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean(true) // true
Boolean(false) // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

自动转换

遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。

第一种情况,不同类型的数据互相运算。

123 + 'abc' // "123abc"

第二种情况,对非布尔值类型的数据求布尔值。

if ('abc') {
  console.log('hello')
}  // "hello"

第三种情况,对非数值类型的值使用一元运算符(即+-)。

+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String()函数进行转换。如果该位置既可以是字符串,也可能是数值,那么默认转为数值。

由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用Boolean()Number()String()函数进行显式转换。

标准库

Object实例对象的方法

Object实例对象的方法,主要有以下六个。

  • Object.prototype.valueOf():返回当前对象对应的值。

  • Object.prototype.toString():返回当前对象对应的字符串形式。

  • Object.prototype.toLocaleString():返回当前对象对应的本地字符串形式。

  • Object.prototype.hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。

  • Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型。

  • Object.prototype.propertyIsEnumerable():判断某个属性是否可枚举。

//这里用来toString来判断数据类型
var o1 = new Object();
o1.toString() // "[object Object]"
​
var o2 = {a:1};
o2.toString() // "[object Object]"
​
​
​
var obj = {
  p: 123
};
​
//Object.prototype.hasOwnProperty方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性。
obj.hasOwnProperty('p') // true
obj.hasOwnProperty('toString') // false

object对象函数

Object.setPrototypeOf

在 JavaScript 中用于设置一个对象的原型(即内部的 [[Prototype]] 属性)。这个方法通常用于改变对象的继承链,或者将一个对象的原型设置为 null 来防止进一步的原型继承。

下面是一个使用 Object.setPrototypeOf 方法的例子:

// 构造函数
function Animal(name) {
  this.name = name;
}
​
// 添加一个方法到 Animal 原型上
Animal.prototype.speak = function() {
  console.log(this.name + ' makes a sound.');
};
​
// 使用构造函数创建一个实例
const animal = new Animal('Generic animal');
​
// 创建另一个构造函数
function Vehicle(make, model) {
  this.make = make;
  this.model = model;
}
​
// 添加一个方法到 Vehicle 原型上
Vehicle.prototype.describe = function() {
  console.log('This is a ' + this.make + ' ' + this.model + ' vehicle.');
};
​
// 使用构造函数创建一个 Vehicle 实例
const car = new Vehicle('Toyota', 'Corolla');
​
// 现在,我们将 car 实例的原型设置为 animal 实例
// 这意味着 car 将继承 animal 实例的所有属性和方法
Object.setPrototypeOf(car, animal);
​
// 验证原型链是否已改变
console.log(Object.getPrototypeOf(car) === animal); // 输出: true
​
// 由于 car 的原型现在是 animal,它继承了 speak 方法
car.speak(); // 输出: Generic animal makes a sound.
​
// 为了演示,我们也可以将 car 的原型设置为 null,这样它就不再有原型
Object.setPrototypeOf(car, null);
​
// 再次验证原型链
console.log(Object.getPrototypeOf(car) === null); // 输出: true
​
// 现在 car 没有原型,因此不能访问 speak 方法
car.speak(); // 输出: TypeError: car.speak is not a function

在这个例子中,我们首先定义了两个构造函数 AnimalVehicle,并分别为它们的原型添加了方法 speakdescribe。然后,我们分别使用这两个构造函数创建了 animalcar 实例。

接下来,我们使用 Object.setPrototypeOf 方法将 car 实例的原型设置为 animal 实例。这意味着 car 现在继承了 animal 实例的所有属性和方法。我们通过调用 car.speak() 来验证这一点,它输出了 animal 实例的名字和声音。

最后,我们再次使用 Object.setPrototypeOf 方法将 car 的原型设置为 null,这意味着 car 将不再继承任何属性和方法。当我们尝试调用 car.speak() 时,由于 car 没有 speak 方法,因此抛出了一个类型错误。

这个例子展示了 Object.setPrototypeOf 方法如何用于修改对象的原型链,以及如何通过改变原型来控制对象继承的属性和方法

Object.getOwnPropertyDescriptor

`Object.getOwnPropertyDescriptor` 方法在 JavaScript 中用于获取指定对象上给定属性的描述信息。描述信息是一个包含属性的各种元数据的对象,例如属性是否可枚举、是否可写、默认值以及 getter 和 setter 函数等。

这个方法对于深入了解对象属性的具体行为和特征非常有用,尤其是在调试或者需要精确控制对象属性时。

下面是一个使用 `Object.getOwnPropertyDescriptor` 方法的例子:

```javascript
// 创建一个对象,并定义一个属性
const myObject = {
  myProperty: 'Hello, World!'
};

// 定义一个 getter 和一个 setter 函数
const getter = function () {
  return this.myProperty + ' (accessed via getter)';
};

const setter = function (newValue) {
  this.myProperty = newValue + ' (updated via setter)';
};

// 使用 Object.defineProperty 为对象添加一个带有 getter 和 setter 的属性
Object.defineProperty(myObject, 'accessorProperty', {
  get: getter,
  set: setter,
  enumerable: true,
  configurable: true
});

// 使用 Object.getOwnPropertyDescriptor 获取属性描述信息
const descriptor = Object.getOwnPropertyDescriptor(myObject, 'accessorProperty');

// 打印属性描述信息
console.log(descriptor);
// 输出:
// {
//   value: 'Hello, World! (accessed via getter)',
//   writable: false,
//   enumerable: true,
//   configurable: true,
//   get: [Function: getter],
//   set: [Function: setter]
// }

// 直接访问属性值
console.log(myObject.accessorProperty); // 输出: 'Hello, World! (accessed via getter)'

// 修改属性值
myObject.accessorProperty = 'Goodbye, World!';
console.log(myObject.accessorProperty); // 输出: 'Goodbye, World! (updated via setter)'


```

在这个例子中,我们首先创建了一个对象 `myObject` 并给它定义了一个普通的属性 `myProperty`。接着,我们定义了一个 getter 函数和一个 setter 函数,并将它们与 `Object.defineProperty` 方法一起使用,为 `myObject` 添加了一个带有访问器(accessor)的属性 `accessorProperty`。

使用 `Object.getOwnPropertyDescriptor` 方法,我们获取了 `accessorProperty` 的描述信息,并打印出来。描述信息对象包含了属性的值、是否可写、是否可枚举、是否可配置以及 getter 和 setter 函数的引用。

然后,我们通过直接访问 `accessorProperty` 来触发 getter 函数,并打印出它的返回值。之后,我们尝试修改 `accessorProperty` 的值,这将触发 setter 函数,并将修改后的值打印出来。

通过这个例子,我们可以看到 `Object.getOwnPropertyDescriptor` 方法如何帮助我们获取和理解对象属性的详细信息。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值