Python网络爬虫 第三章 requests进阶

我们在之前的爬⾍中其实已经使⽤过headers了。 header为HTTP协议中的请求头. ⼀般存放⼀些和请求内容⽆关的数据,有时也会存放⼀些安全验证信息.⽐如常⻅的User-Agent, token, cookie等。
通过requests发送的请求, 我们可以把请求头信息放在headers中, 也可以单独进⾏存放, 最终由requests⾃动帮我们拼接成完整的http请求头。

一、处理cookie模拟登录

这里以17k小说网为例https://www.17k.com/

我们的目的是先去登录,得到用户信息的cookie,带着cookie 去请求到书架url,获得书架上的内容。这个过程中使用了一个新的概念session会话

可以直接理解成session是一连串的请求. 在这个过程中的cookie不会丢失。

1、登录

利用F12获取到登录需要的用户名密码来源。

Request URL: https://passport.17k.com/ck/user/login

post方法

2、拿书架上的数据

Request URL:
https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919

3、代码

import requests

# 会话
session = requests.session()
data = {
    "loginName": "18614075987",
    "password": "q6035945"
}

# 1. 登录
url = "https://passport.17k.com/ck/user/login"
session.post(url, data=data)
# resp = session.post(url, data=data)
# print(resp.text)
# print(resp.cookies)  # 看cookie
#
# # 2. 拿书架上的数据
# # 刚才的那个session中是有cookie的
resp = session.get('https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919')

print(resp.json())

二、防盗链的处理

 我们日常访问网页,如果从一个网页跳转到另一个网页,http 头字段里面会带个 Referer的参数。那么图片服务器通过检测 Referer 是否来自指定域名,来进行防盗链。
简单点说,服务器会判断你是否从规定的域名来访问图片或者视频,如果是那么就正常显示,不是的话,会跳转到别的地方,那么我们用爬虫所采集的图片就会出现问题!

这里我们以梨视频网站为例讲解一下,其中一个视频的URL链接为https://www.pearvideo.com/video_1728542

利用F12可以找到视频的播放源,但是直接查看网页源代码却找不到,可以推断视频资源是通过后期JS生成出来的。

 

 查看一下XHR,预览后发现这样一个链接https://video.pearvideo.com/mp4/adshort/20210503/1620027759929-15669083_adpkg-ad_hd.mp4

 但是却发现打不开这个链接?

对比一下,发现只有cont那部分内容不同,猜测应该进行了替换,所以我们只需要获取cont部分拼接一下,就能获得播放视频的链接了。cont应该就是梨视频的视频编号,我们一开始进去的链接上就含有这个编号video_1728542

 到现在可以梳理一下整个过程

  • 1. 拿到contId
  • 2. 拿到videoStatus返回的json. ->  srcURL
  • 3. srcURL里面的内容进行修整
  • 4. 下载视频
import requests

# 拉取视频的网址
url = "https://www.pearvideo.com/video_1728542"
contId = url.split("_")[1]

videoStatusUrl = f"https://www.pearvideo.com/videoStatus.jsp?contId={contId}"

headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36",
    # 防盗链: 溯源, 当前本次请求的上一级是谁
    "Referer": url
}

resp = requests.get(videoStatusUrl, headers=headers)
dic = resp.json()

srcUrl = dic['videoInfo']['videos']['srcUrl']
systemTime = dic['systemTime']
srcUrl = srcUrl.replace(systemTime, f"cont-{contId}")

# 下载视频
with open("a.mp4", mode="wb") as f:
    f.write(requests.get(srcUrl).content)

三、代理

当我们反复抓取⼀个⽹站时, 由于请求过于频繁, 服务器很可能会将你的IP进⾏封锁来反爬. 应对⽅案就是通过⽹络代理的形式进⾏伪装.
代理的原理:


从图上可以得知. 对于⽬标⽹站来说. 是通过代理服务器发送的请求.也就可以避免你的IP被封锁了.
爬⾍如何使⽤代理

# 原理. 通过第三方的一个机器去发送请求
import requests


# 218.60.8.83:3129
proxies = {
    "https": "https://218.60.8.83:3129"
}

resp = requests.get("https://www.baidu.com", proxies=proxies)
resp.encoding = 'utf-8'
print(resp.text)

四、综合训练 抓取⽹易云⾳乐评论信息

以我最喜欢的歌手许嵩的《雅俗共赏》为例

https://music.163.com/#/song?id=411214279

先准备好

pip install pycryptodome

但是请求的参数是一堆完全看不懂的信息,似乎是被加密过?

至此,我们的需求是

  • 1. 找到未加密的参数                    
  • 2. 想办法把参数进行加密(必须参考网易的逻辑),params  encSecKey
  • 3. 请求到网易. 拿到评论信息

 这里先通过调用栈,查看一下有哪些js脚本执行过

 

 网易云音乐评论的URL https://music.163.com/weapi/comment/resource/comments/get?csrf_token=

 注意到这里参数依旧是被加密的,我们需要向前找找到底是在哪里参数开始被加密了。调用Call Stack查看一下。

 基本可以断定是在这里被加密了

 一步步调试,参数和加密过程基本可以确定

 

 请求参数:

data = {
    "csrf_token": "",
    "cursor": "-1",
    "offset": "0",
    "orderType": "1",
    "pageNo": "1",
    "pageSize": "20",
    "rid": "R_SO_4_411214279",
    "threadId": "R_SO_4_411214279"
}

加密过程 直接搜索window.asrsea,入口是d

 加密过程

!function() {
    function a(a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length,
            e = Math.floor(e),
            c += b.charAt(e);
        return c
    }
    function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b)
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,
            mode: CryptoJS.mode.CBC
        });
        return f.toString()
    }
    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    function d(d, e, f, g) {
        var h = {}
          , i = a(16);
        return h.encText = b(d, g),
        h.encText = b(h.encText, i),
        h.encSecKey = c(i, e, f),
        h
    }
    function e(a, b, d, e) {
        var f = {};
        return f.encText = c(a + e, b, d),
        f
    }
    window.asrsea = d,
    window.ecnonasr = e
}();

解析一下

  function a(a = 16) {  # 随机的16位字符串
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)  # 循环16次
            e = Math.random() * b.length,  # 随机数 1.2345
            e = Math.floor(e),  # 取整  1 
            c += b.charAt(e);  # 去字符串中的xxx位置 b
        return c
    }
    function b(a, b) {  # a是要加密的内容, 
        var c = CryptoJS.enc.Utf8.parse(b) #  # b是秘钥
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)  # e是数据
          , f = CryptoJS.AES.encrypt(e, c, {  # c 加密的秘钥
            iv: d,  # 偏移量
            mode: CryptoJS.mode.CBC  # 模式: cbc
        });
        return f.toString()
    }
    function c(a, b, c) {   # c里面不产生随机数
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    function d(d, e, f, g) {  d: 数据,   e: 010001, f: 很长, g: 0CoJUm6Qyw8W8jud
        var h = {}  # 空对象
          , i = a(16);  # i就是一个16位的随机值, 把i设置成定值 
        h.encText = b(d, g)  # g秘钥
        h.encText = b(h.encText, i)  # 返回的就是params  i也是秘钥
        h.encSecKey = c(i, e, f)  # 得到的就是encSecKey, e和f是定死的 ,如果此时我把i固定, 得到的key一定是固定的
        return h
    }
    
    两次加密: 
    数据+g => b => 第一次加密+i => b = params

 最后的代码

# 1. 找到未加密的参数                       # window.arsea(参数, xxxx,xxx,xxx)
# 2. 想办法把参数进行加密(必须参考网易的逻辑), params  => encText, encSecKey => encSecKey
# 3. 请求到网易. 拿到评论信息

# 需要安装pycrypto:
from Crypto.Cipher import AES
from base64 import b64encode
import requests
import json


url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="

# 请求方式是POST
data = {
    "csrf_token": "",
    "cursor": "-1",
    "offset": "0",
    "orderType": "1",
    "pageNo": "1",
    "pageSize": "20",
    "rid": "R_SO_4_411214279",
    "threadId": "R_SO_4_411214279"
}

# 服务于d的
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
e = "010001"
i = "d5bpgMn9byrHNtAh"  # 手动固定的. -> 人家函数中是随机的

def get_encSecKey():  # 由于i是固定的. 那么encSecText就是固定的.  c()函数的结果就是固定的
    return "1b5c4ad466aabcfb713940efed0c99a1030bce2456462c73d8383c60e751b069c24f82e60386186d4413e9d7f7a9c7cf89fb06e40e52f28b84b8786b476738a12b81ac60a3ff70e00b085c886a6600c012b61dbf418af84eb0be5b735988addafbd7221903c44d027b2696f1cd50c49917e515398bcc6080233c71142d226ebb"


# 把参数进行加密
def get_params(data):  # 默认这里接收到的是字符串
    first = enc_params(data, g)
    second = enc_params(first, i)
    return second  # 返回的就是params


# 转化成16的倍数, 位下方的加密算法服务
def to_16(data):
    pad = 16 - len(data) % 16
    data += chr(pad) * pad
    return data


# 加密过程
def enc_params(data, key):
    iv = "0102030405060708"
    data = to_16(data)
    aes = AES.new(key=key.encode("utf-8"), IV=iv.encode('utf-8'), mode=AES.MODE_CBC)  # 创建加密器
    bs = aes.encrypt(data.encode("utf-8"))  # 加密, 加密的内容的长度必须是16的倍数
    return str(b64encode(bs), "utf-8")  # 转化成字符串返回,


# 处理加密过程
"""
    function a(a = 16) {  # 随机的16位字符串
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)  # 循环16次
            e = Math.random() * b.length,  # 随机数 1.2345
            e = Math.floor(e),  # 取整  1 
            c += b.charAt(e);  # 去字符串中的xxx位置 b
        return c
    }
    function b(a, b) {  # a是要加密的内容, 
        var c = CryptoJS.enc.Utf8.parse(b) #  # b是秘钥
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)  # e是数据
          , f = CryptoJS.AES.encrypt(e, c, {  # c 加密的秘钥
            iv: d,  # 偏移量
            mode: CryptoJS.mode.CBC  # 模式: cbc
        });
        return f.toString()
    }
    function c(a, b, c) {   # c里面不产生随机数
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    function d(d, e, f, g) {  d: 数据,   e: 010001, f: 很长, g: 0CoJUm6Qyw8W8jud
        var h = {}  # 空对象
          , i = a(16);  # i就是一个16位的随机值, 把i设置成定值 
        h.encText = b(d, g)  # g秘钥
        h.encText = b(h.encText, i)  # 返回的就是params  i也是秘钥
        h.encSecKey = c(i, e, f)  # 得到的就是encSecKey, e和f是定死的 ,如果此时我把i固定, 得到的key一定是固定的
        return h
    }
    
    两次加密: 
    数据+g => b => 第一次加密+i => b = params
"""

# 发送请求. 得到评论结果
resp = requests.post(url, data={
    "params": get_params(json.dumps(data)),
    "encSecKey": get_encSecKey()
})

print(resp.text)

代码倒还好,最难的是整个推导过程,涉及到了前端和密码学的知识,有点坑。。。

posted @ 2021-05-03 15:22  王陸  阅读(453)  评论(2编辑  收藏  举报