(笔记)数据采集基础07

20240420

1.布隆过滤器理论

Bloom Filter 使用位数组表示一个待检测集合,并可以快速地通过概率算法判断一个元素是否
存在于这个集合中。利用这个算法我们可以实现去重效果。

2.布隆过滤器python实现 

import redis
class HashMap(object):
    def __init__(self, m, seed):
        self.m = m
        self.seed = seed

    def hash(self, value):
        """
        Hash Algorithm
        :param value: Value
        :return: Hash Value
        """
        ret = 0
        for i in range(len(value)):
            ret += self.seed * ret + ord(value[i])
        # print(self.m - 1,ret,(self.m - 1) & ret)
        return (self.m - 1) & ret


BLOOMFILTER_HASH_NUMBER = 6
BLOOMFILTER_BIT = 30
class BloomFilter(object):
    def __init__(self, server, key, bit=BLOOMFILTER_BIT, hash_number=BLOOMFILTER_HASH_NUMBER):
        """
        Initialize BloomFilter
        :param server: Redis Server
        :param key: BloomFilter Key
        :param bit: m = 2 ^ bit
        :param hash_number: the number of hash function
        """
        # default to 1 << 30 = 10,7374,1824 = 2^30 = 128MB, max filter 2^30/hash_number = 1,7895,6970 fingerprints
        self.m = 1 << bit
        self.seeds = range(hash_number)
        self.maps = [HashMap(self.m, seed) for seed in self.seeds]
        self.server = server
        self.key = key

# 0000 0001 2^0
# 0000 0010 2^1
# 0000 0100 2^2

    def exists(self, value):
        """
        if value exists
        :param value:
        :return:
        """
        if not value:
            return False
        exist = 1
        for map in self.maps:
            offset = map.hash(value)

            # 3 6 9
            # print(self.server.getbit(self.key, offset))
            exist = exist & self.server.getbit(self.key, offset)
            if not exist:
                return 0
        return exist


    def insert(self, value):
        """
        add value to bloom
        :param value:
        :return:
        """
        for f in self.maps:
            offset = f.hash(value)
            self.server.setbit(self.key, offset, 1)

a = ['hello','nihao','nihao','nihao','nihao','nihao']
conn = redis.StrictRedis(host='localhost', port=6379)

bf = BloomFilter(conn, 'testbf', 5, 4)
for i in a :
    if bf.exists(i):
        pass
    else:
        bf.insert(i)
        print(i)


# print(123)
# for i in range(10000):
#     print(chr(i))

3.Scrapy中用布隆过滤器

htt ps://pypi.python.org/pypi/scrapy-redis-bloomfilter ,可以直接使用
ScrapyRedisBloomFilter
我们可以直接使用 pip 来安装,命令如下:
pip3 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 亿,请务必将此参数对应调高。
源代码附有一个测试项目,放在 tests 文件夹,该项目使用了 ScrapyRedisBloomFilter 来去重,
Spider 的实现如下
start_requests() 方法首先循环 10 次,构造参数为 0 9 URL ,然后重新循环了 100 次,构造了参数为 0 99
URL 。那么这里就会包含 10 个重复的 Request ,我们运行项目测试一下:
最后统计的第一行的结果:
'bloomfilter/filtered' : 10 ,
这就是 Bloom Filter 过滤后的统计结果,它的过滤个数为 10 个,也就是它成功将重复的 10
Reqeust 识别出来了,测试通过。
以上内容便是 Bloom Filter 的原理及对接实现, Bloom Filter 的使用可以大大节省 Redis 内存。
在数据量大的情况下推荐此方案

4.Session登录和jwt登录 

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

确保已经做好了如下准备工作:

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

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

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

链接:Scrape | Movie    基于 Session + Cookies 认证的网站

打开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 方法就好了。

运行效果是完全一样的,结果如下:

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

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

这个案例整体来说比较简单,但是如果碰上复杂一点的网站,如带有验证码,带有加密参数等等,直接用 requests 并不好处理模拟登录,如果登录不了,那岂不是整个页面都没法爬了吗?那么有没有其他的方式来解决这个问题呢?当然是有的,比如说,我们可以使用 Selenium 来通过模拟浏览器的方式实现模拟登录,然后获取模拟登录成功后的 Cookies,再把获取的 Cookies 交由 requests 等来爬取就好了。

这里我们还是以刚才的页面为例,我们可以把模拟登录这块交由 Selenium 来实现,后续的爬取交由 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 的网站,其通常都是采用前后端分离式的,前后端的数据传输依赖于 Ajax,登录验证依赖于 JWT 本身这个 token 的值,如果 JWT 这个 token 是有效的,那么服务器就能返回想要的数据。

链接:Scrape | Book     基于 JWT 认证的网站

先来在浏览器里面操作登录,观察下其网络请求过程,如图所示。 在这里插入图片描述 这里我们发现登录时其请求的 URL 为Scrape | Book,是通过 Ajax 请求的,同时其 Request Body 是 JSON 格式的数据,而不是 Form Data,返回状态码为 200。

然后再看下返回结果,如图所示。 可以看到返回结果是一个 JSON 格式的数据,包含一个 token 字段,其结果为:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3ODc3OTQ2LCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm9yaWdfaWF0IjoxNTg3ODM0NzQ2fQ.ujEXXAZcCDyIfRLs44i_jdfA3LIp5Jc74n-Wq2udCR8
​

这就是我们上一课时所讲的 JWT 的内容,格式是三段式的,通过“.”来分隔。

那么有了这个 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 认证的网站,也可以通过类似的方式来实现模拟登录。当然可能某些页面比较复杂,需要具体情况具体分析。

5.超级鹰使用


import requests
from chaojiying import Chaojiying_Client

headers = {
    "authority": "www.chaojiying.com",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "cache-control": "no-cache",
    "content-type": "application/x-www-form-urlencoded",
    "origin": "https://www.chaojiying.com",
    "pragma": "no-cache",
    "referer": "https://www.chaojiying.com/user/login/",
    "sec-ch-ua": "^\\^Not_A",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "^\\^Windows^^",
    "sec-fetch-dest": "document",
    "sec-fetch-mode": "navigate",
    "sec-fetch-site": "same-origin",
    "sec-fetch-user": "?1",
    "upgrade-insecure-requests": "1",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"
}
cookies = {
    "PHPSESSID": "u4nifua4ut73bhr30fv1l0gjd2",
    "__51cke__": "",
    "__tins__16851773": "^%^7B^%^22sid^%^22^%^3A^%^201713603731561^%^2C^%^20^%^22vd^%^22^%^3A^%^207^%^2C^%^20^%^22expires^%^22^%^3A^%^201713606434912^%^7D",
    "__51laig__": "7"
}
url = "https://www.chaojiying.com/user/login/"
data = {
    "user": "18514203544",
    "pass": "a706486a",
    "imgtxt": "dhrg",
    "act": "1"
}
img = 'https://www.chaojiying.com/include/code/code.php?u=1'
s = requests.Session()
response = s.get(img).content
op = open('yan.jpg','wb')
op.write(response)
op.close()
chaojiying = Chaojiying_Client('18514203544', 'a706486a', '912162')  # 用户中心>>软件ID 生成一个替换 96001
im = open('yan.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
print(111111111111, chaojiying.PostPic(im, 1902))
# response = requests.post(url, headers=headers, cookies=cookies, data=data)
#
# print(response.text)
# print(response)
# 'https://www.chaojiying.com/user/'

6.pypteer反屏蔽

Pyppeteer 就是依赖于 Chromium 这个浏览器来运行的。那么有了 Pyppeteer 之后,我们就可以免去那些烦琐的环境配置等问题。如果第一次运行的时候,Chromium 浏览器没有安装,那么程序会帮我们自动安装和配置,就免去了烦琐的环境配置等工作。另外 Pyppeteer 是基于 Python 的新特性 async 实现的,所以它的一些执行也支持异步操作,效率相对于 Selenium 来说也提高了。

 安装:pip3 install pyppeteer           测试:import pyppeteer

举例:https://dynamic2.scrape.cuiqingcai.com/

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

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())

反屏蔽

https://antispider1.scrape.cuiqingcai.com/,这个网站就是使用了上述原理实现了 WebDriver 的检测,如果使用 Selenium 直接爬取的话,那就会返回如下页面: 在这里插入图片描述 这时候我们可能想到直接使用 JavaScript 直接把这个 webdriver 属性置空,比如通过调用 execute_script 方法来执行如下代码:

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

这行 JavaScript 的确是可以把 webdriver 属性置空,但是 execute_script 调用这行 JavaScript 语句实际上是在页面加载完毕之后才执行的,执行太晚了,网站早在最初页面渲染之前就已经对 webdriver 属性进行了检测,所以用上述方法并不能达到效果。

在 Selenium 中,我们可以使用 CDP(即 Chrome Devtools-Protocol,Chrome 开发工具协议)来解决这个问题,通过 CDP 我们可以实现在每个页面刚加载的时候执行 JavaScript 代码,执行的 CDP 方法叫作 Page.addScriptToEvaluateOnNewDocument,然后传入上文的 JavaScript 代码即可,这样我们就可以在每次页面加载之前将 webdriver 属性置空了。另外我们还可以加入几个选项来隐藏 WebDriver 提示条和自动化扩展信息,代码实现如下:

from selenium import webdriver
from selenium.webdriver import ChromeOptions
​
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
option.add_experimental_option('useAutomationExtension', False)
browser = webdriver.Chrome(options=option)
browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
   'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
})
browser.get('https://antispider1.scrape.cuiqingcai.com/')
​

这样整个页面就能被加载出来了: 在这里插入图片描述 对于大多数的情况,以上的方法均可以实现 Selenium 反屏蔽。但对于一些特殊的网站,如果其有更多的 WebDriver 特征检测,可能需要具体排查。 上面的案例在运行的时候,我们可以观察到其总会弹出一个浏览器窗口,虽然有助于观察页面爬取状况,但在有些时候窗口弹来弹去也会形成一些干扰。

Chrome 浏览器从 60 版本已经支持了无头模式,即 Headless。无头模式在运行的时候不会再弹出浏览器窗口,减少了干扰,而且它减少了一些资源的加载,如图片等资源,所以也在一定程度上节省了资源加载时间和网络带宽。

我们可以借助于 ChromeOptions 来开启 Chrome Headless 模式,代码实现如下:

from selenium import webdriver
from selenium.webdriver import ChromeOptions
​
option = ChromeOptions()
option.add_argument('--headless')
browser = webdriver.Chrome(options=option)
browser.set_window_size(1366, 768)
browser.get('https://www.baidu.com')
browser.get_screenshot_as_file('preview.png')
​

这里我们通过 ChromeOptions 的 add_argument 方法添加了一个参数 --headless,开启了无头模式。在无头模式下,我们最好需要设置下窗口的大小,接着打开页面,最后我们调用 get_screenshot_as_file 方法输出了页面的截图。

运行代码之后,我们发现 Chrome 窗口就不会再弹出来了,代码依然正常运行,最后输出了页面截图如图所示。 在这里插入图片描述 这样我们就在无头模式下完成了页面的抓取和截图操作。

现在,我们基本对 Selenium 的常规用法有了大体的了解。使用 Selenium,处理 JavaScript 渲染的页面不再是难事。 但是把实际上还是很容易暴毙,如何真的想比较完美的消除指纹需要使用一些黑科技 stealth.min.js 它的本质是一段js脚本,可以帮我们消除指纹

option.add_argument(
    'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36')
​
option.add_argument('--disable-blink-features=AutomationControlled')
driver_path = r'C:\\Python39\\chromedriver.exe'  # 定义好路径
driver = webdriver.Chrome(executable_path=driver_path, options=option)  # 初始化路径+规避检测
with open('stealth.min.js') as f:
    js = f.read()
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": js
})

这里还有一个检测网站

'https://bot.sannysoft.com/'

可以自行对比浏览器环境,注意js软件包需要随浏览器的版本更迭调整

7.node.js环境安装及使用

https://nodejs.org/en/

import zlib
# pip install PyExecJS
import execjs
import requests
import time
import hashlib
import base64
import os
import json
def get_js():
    f = open("wyy.js", 'r',encoding='utf8')
    line = f.readline()
    htmlstr = ''
    while line:
        htmlstr = htmlstr+line
        line = f.readline()
    return htmlstr


def get_des_psswd(e):
    js_str = get_js()
    ctx = execjs.compile(js_str)
    #这里hello为js文件里的函数,e为向hello这个函数里传递的参数
    #这里我们的e这个形参主要用来传递歌曲的id,这样我们只需要向该函数传递不同的歌曲Id,即可返回不同的下载链接
    return (ctx.call('hello',e))

token = get_des_psswd('404459664')

  • 47
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值