爬虫公式

总结爬虫的几种姿势

前言

这是我爬虫的通用思路,每一个网页都可以这样套公式分析,无非就三步

  1. 理思路
  2. 抓包分析
  3. 复现代码

具体分析

理思路

打个比方,以我们学校的电费登陆作为例子

例子

有两个登陆选项

有两个登陆选项

这里选择统一身份认证点击由此登入

跳转到网页登陆

跳转登录

输入账号密码

跳转回业务网页

跳转登录

结束

理一下逻辑图

跳转登录

抓包分析

我使用的抓包工具是charles

关于charles的配置可以看另外一篇文章

首先判断登陆以后的状态

因为我抓得多了,很快就能得出结论。

很明显登陆以后是由cookie来保持登陆状态的,那么我们只要直接分析跳转后登陆的逻辑,然后再获取跳转回来的链接,访问这个链接即可获得cookie实现逻辑

如果是完全不知道什么逻辑,那首先可以从点击按钮跳转的那一刻开始抓包

点击按钮以后按时间顺序排列的抓包的请求如下

序列

首先是访问了

http://cwsf.whut.edu.cn/casLogin

这个http返回状态为302

302

重定向位置为

Location: http://zhlgd.whut.edu.cn/tpass/login?service=http%3A%2F%2Fcwsf.whut.edu.cn%2FcasLogin

再从时间顺序可以看到下一个正好对得上就是302的链接

序列

那这里跟进去,随便翻翻

登陆页面

很明显是登陆的页面

流程图

再看刚才的流程图,下一步就是输入账号密码登陆,那么我们就抓包这个登陆的请求

流程图

随便输然后观察产生的http请求记录

http记录

这里产生了两个请求

第一个请求暂且不管

第二个是post请求,可以推断他就是将账号密码发送到后端的请求

我们再想想思路,前端我们输入了账号密码,按下了登陆按钮

我们登陆需要的数据就是

  1. 账号
  2. 密码

仔细观察一下第二个post请求

登陆请求

表单中有5个值,如果解决了这5个值,将请求POST到后端,我们是不是就可以实现登陆了呢,那么我们这里分析一下这5个值

ps:为什么是表单呢?因为http请求头中的Content-type已经规定了发送数据的格式为application/x-www-form-urlencoded,有兴趣可以自己看一下http协议

Content-type

接着说这5个值,我第一次看的时候也看不出来这些是什么,那么我们回到浏览器中去看看

按F12进入浏览器的调试模式

点击这个选择器

调试模式选择器

把鼠标放到账号密码上看看

调试模式选择器

按左键

这里调试器跳转到html对应的html语句 调试

换位思考一下如果你是前端,你要实现登陆,是不是在登录按钮写js函数,填入账号密码应该是没有任何的js逻辑的,所以我们这里用选择器点击登陆按钮找到登陆的js源代码

登陆

跟进这个event

跟进这个event

跟进这个event

这里跟到了js代码,稍微上下看一下,就能发现了

跟进这个event

这里有个我们感兴趣的post请求,他出现了ulpl,上面还定义了lt,这里就是我们要找的参数,对应我们刚才找到的post请求的参数

登陆请求

上面这个lt是使用id选择器查找的html元素,那么我们回到html中找到这个元素

lt

现在找到了其中一个元素lt,我们还要考虑他是否是变化的,我们刷新一下网页,看他是否是变化的,如果是,那么我们直接引用;如果不是,那么我们每次都取他的lt值作为参数

刷新后lt

很明显他是会变化的,实际上不刷新也很好判断他是变化的,如果判断不出来,后面跳转他就是靠这个返回到原来的页面实现cookie登陆的,就可以知道只能是变化的

然后接下来ul和pl

我们在这里下断点,然后再点击登陆,看看他是如何运作的

下断点

点击登陆,调试器会在这里中断

记住要断那个encrypt函数,点encrypt函数下断

下断点

分析这段js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	$.ajax({
		 url : "rsa",
		 dataType : "json",
		 type : "POST",
		 success:function(data){
			var encrypt = new JSEncrypt();
			encrypt.setPublicKey(data.publicKey);
			$("#ul").val(encrypt.encrypt(u));
			$("#pl").val(encrypt.encrypt(p));
			$("#loginForm")[0].submit();
		 }
	});

这里是向/rsa接口发送了一个请求,在返回数据后将data.publicKey传入到encrypt.setPublicKey()中,再调用了encrypt.encrypt(u),使用id选择器将这个值存入了页面的ul中

rsa接口很明显是刚才的第一个http请求

请求

请求返回结果

1
2
3
{
	"publicKey": "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJdvbyudr+Od9CoAuh46D6DjLgZ5DL9iVNTZK4cAVgaQjmvvC0ASGA/URgnSfyswgdI1/9LsNDPmYi2Xdrxrn7UCAwEAAQ=="
}

然后再看看这个u

u的值

u的值来源

123是从哪来的呢,这不就是用户名吗

总结一下,得出结论ul就是u经过rsa加密得来的加密username,u即明文用户名,下面的pl就是同理的密码

既然公钥有了,那登陆部分就可以直接实现了

至于接下来的部分,只要分析正确登陆后的跳转就可以知道

他就是一个302跳转到底就直接能得到cookie

最终跳转

最终跳转

复现代码

代码我就不写了,请看我的项目

位于第30行的whut_login函数

武理电费登录

 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
    def whut_login(self, service, username, password): # service是跳转过来时候的params
        self.sessions.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35",
        }) # 更换请求头的User-Agent,隐藏爬虫身份
        html = self.sessions.get("http://zhlgd.whut.edu.cn/tpass/login", params={
            "service": service
        })  # 获取html,以便接下来获取tpass
        etree.HTMLParser(encoding="utf-8")
        # tree = etree.parse(local_file_path)
        tree = etree.HTML(html._content.decode("utf-8"))
        tpass = dict(tree.xpath('//*[@id="lt"]')[0].attrib)["value"]  # 获取tpass
        # des = strEnc(username + password + tpass, "1", "2", "3") # 这个是之前的加密逻辑,最近刚更新的加密
        self.sessions.headers.update({})
        self.sessions.cookies.set(domain="whut.edu.cn", path="/", name="cas_hash", value=""),设置cookie
        # print(tpass)
        result = self.sessions.post(
            url="http://zhlgd.whut.edu.cn/tpass/login",
            params={
                "service": service
            },
            #  下面对应参数
            data={
                "rsa": "",
                "ul": encrypt(username), # 请看下面的encrypt函数定义
                "pl": encrypt(password),
                "lt": tpass,
                "execution": "e1s1",
                "_eventId": "submit",
            }, verify=False, allow_redirects=False) # allow_redirects设为False,无论登陆成功与否先不跳转
        if result.headers.get("location") is None:  # 判断是否有location,来看登陆是否成功,如果失败返回False
            return False
        return result.headers["location"] # 成功返回登陆成功后的跳转链接

利用公钥加密

1
2
3
4
5
6
7
8
9
def encrypt(plain):  # 你不会以为加个js我就不会写了吧 这个注释是我github里面写的,在我发布这个以后登录加密逻辑就改了,不知道是不是巧合,我就在项目这写下这个注释
    rsakey = RSA.importKey('''-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJdvbyudr+Od9CoAuh46D6DjLgZ5DL9i
VNTZK4cAVgaQjmvvC0ASGA/URgnSfyswgdI1/9LsNDPmYi2Xdrxrn7UCAwEAAQ==
-----END PUBLIC KEY-----''')
    cipher = Cipher_pkcs1_v1_5.new(rsakey)
    cipher_text = b64encode(cipher.encrypt(plain.encode('utf-8')))
    # print(cipher_text.decode('utf-8'))
    return cipher_text.decode('utf-8')