pokemonlei

陈磊的博客 | pokemonlei

爬虫基础

前面http和https的介绍,可直接跳过看下面的案例

HTTP与HTTPS

为什么要先简要复习一下http和https?因为要发送请求,模拟浏览器,获取和浏览器一模一样的响应。

HTTP:超文本传输协议,默认端口:80,是一种建立在TCP上的无状态连接,说白了就是个协议,规定了一些列的规则,整个基本的工作流程是客户端发送一个HTTP请求,说明客户端想要访问的资源和

请求的动作,服务端收到请求之后,服务端开始处理请求,并根据请求做出相应的动作访问服务器资源,最后通过发送HTTP响应把结果返回给客户端。

HTTPS:即HTTP+SSL(安全套接字层),默认端口号:443,即在HTTP上多了个安全套接字层SSL,SSL是一个提供数据安全和完整性的协议,负责网络连接的加密。    

题外话:HTTPS通信中的几个概念:加密分为对称和非对称
对称加密:信息的加密和解密都是通过同一个密钥进行的,实际通信中,一个服务器可以同时对应好几个客户端,也就是A客户端获得的加解密算法同样可以加解密B客户端
的消息,这跟没加密一样,为了防止这种情况,就只能不同的客户端使用不同密钥,这样的话密钥将会有很多,并且在通信刚开始,服务器和A客户端就要协商好用什么密钥,而这个协商过程是不能加密
的,不然A客户端就读不懂服务器消息了,因此还是存在风险。

非对称加密:应用最广的加密机制“非对称加密”,特点是私钥加密后的密文,只要是公钥,都可以解密,但是反过来公钥加密后的密文,只有私钥可以解密。私钥只有一个
人有,而公钥可以发给所有的人。所以公钥不需要加密,而私钥只存在于服务器。这样只需要一套公钥和私钥就可以了。那么现在的问题是,如何让客户端安全的获取公钥?如果服务器直接明文发送给客户
端,那么可能发生被劫持的情况,例如客户端A和服务器S通信,S给A的公钥被B劫持后修改了,那么之后A将会用假的公钥进行加密,将消息发给服务器时B继续劫持,查看内容或者修改之后用真公钥加密然
后发给服务器,这就是中间人攻击。这时候风险依然存在,而为了解决这个问题,采用了一种SSL 证书(需要购买)和CA机构的方法,涉及防伪和证书链的概念,大概流程是:
在客户端第一次请求服务器时,服务器发送回一个SSL证书给客户端,SSL 证书中包含的具体内容有证书的颁发机构的证书、有效期、公钥、证书持有者、签名。防伪的步骤:浏览器
拿到这个证书后读取证书中的证书所有者、有效期等信息进行一一校验,开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁
发,如果没找到,则报错,如果找到了,浏览器会从操作系统中取出颁发者CA的公钥,然后对服务器发来的证书里面的签名进行解密,如果能够解密则一定是证书颁发机构颁发的证书如果解密后信息
与Server信息一致则确实是颁发给该Server的,综上校验通过,这就是防伪的流程,而证书链的作用可以保证正在通信的Server确实是证书颁发机构指定的Server,流程是:通过和Server证书验证同
的过程通过内嵌在浏览器或者JDK中的根证书验证下中级证书的合法性就好了。因为根证书是内嵌的具有绝对的合法性,如果根证书信任该中级机构,则该中级证书颁发的证书也是可信的这就是证书链了。
这样通过第三方的校验保证了身份的合法,解决了公钥获取的安全性。什么你问我怎么申请CA证书?国内的阿里云和腾讯云上找去

举个栗子:爬取某贴吧前1000页内容

先来一个简单的例子熟悉一下api和爬虫基本流程

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
import requests

class TiebaSpider:
def __init__(self, tieba_name):
self.tieba_name = tieba_name
self.url_temp = "https://tieba.baidu.com/f?kw=" + tieba_name + "&ie=utf-8&pn={}"
self.headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36"}

def get_url_list(self): #构造url列表
# url_list = []
# for i in range(1000):
# url_list.append(self.url_temp.format(i*50))
# return url_list
return [self.url_temp.format(i*50) for i in range(1000)]

def parse_url(self, url): #发送请求获取相应
print(url)
response = requests.get(url, headers=self.headers)
return response.content.decode()

def save_html(self, html_str, page_num):
file_path = "{}第{}页.html".format(self.tieba_name, page_num)
with open(file_path, "w",encoding="utf-8") as f:
f.write(html_str)

def run(self):
# 构造url列表
url_list = self.get_url_list()
# 遍历,发送请求,获取响应
for url in url_list:
html_str = self.parse_url(url)
page_num = url_list.index(url)+1
self.save_html(html_str, page_num)
# 保存


if __name__ == '__main__':
tieba_spider = TiebaSpider("李毅")
tieba_spider.run()

这里爬取下来的内容是没问题的,但保存下来的html直接打开是有问题的,因为万恶的百度在其中把大量有用内容添加了注释,后续是通过js来去掉注释的。这里暂时不处理,只展示基本api的使用。

发送POST请求

什么时候需要发送post请求?

  • 登录注册等有敏感信息的时候,POST比GET更安全
  • 需要传输大文本的时候(POST请求对数据长度没有要求)
    python
    1
    response = requests.post("",data=data,headers=headers)

使用代理

为什么爬虫需要代理?

  • 让服务器以为不是同一个客户端在不断请求
  • 防止我们真实地址倍泄露,防止被追究
python
1
response = requests.get("url",proxies=proxies)

proxies是一个字典,

1
2
3
4
proxies={
"http":"http://xx.xx.xx.xx:xxx",
"https":"https://xx.xx.xx.xx:xxxx",
}

request模拟登陆

cookie和session区别:

  • cookie数据存放在客户的浏览器上,session数据存放在服务器上
  • cookie不安全,别人可以分析存放在本地的cookie并进行cookie欺骗
  • session会在一定时间内保存在服务器上,当访问增多,会比较占用服务器性能
  • 单个cookie保存的数据不能超过4k,很多浏览器会限制一个站点最多保存20个cookie

1)request提供了一个session类,来实现客户端和服务器的会话保持,先实例化一个session,用session发送post请求登陆网站,把cookie保存在session中,再使用session请求登陆之后才能访问的网站,session能够自动的携带登陆成功时保存在其中的cookie。
使用方法:

1
2
session = requests.session()
response = session.get(url,headers)

2)如果请求页面时不发送post请求的情况下,可以在headers中添加cookie键值对,要注意cookie的有效时间

3)也可以把cookie作为request的参数。此时cookie参数时一个字典。

request小技巧

  • request.utils.dict_from_cookiejar(response.cookies) 把cookie转换为字典,request.utils.cookiejar_from_dict ,将字典转化为cookie
  • request.utils.unquote(“编码后的url”) 将url地址解码,反之quote为编码
  • 忽视证书错误 request.get(“url”,verify=false)
  • 设置超时 request.get(“url”,timeout)
  • 刷新网页 使用第三方模块retrying,还可以定义最大重新请求次数

爬虫的基本套路

  • 准备url

    • 准备start_url
      • url地址规律不明显,总数不确定的情况下
      • 通过代码提取下一页的url地址
        • 当下一页的地址在网页的响应中时,可以通过xpath
        • 通过其他方式寻找url地址,比如通过js生成,这种情况下部分参数是在当前响应中
    • 准备url_list
      • 页码总数明显的时候
      • url地址规律很明显
  • 发送请求,获取响应

    • 添加随机的User-Agent,防反爬虫
    • 添加随机的代理ip,防反爬虫
    • 在对方判断出是爬虫之后,应该添加更多的header字段,包括cookies
    • cookie可以用session来解决
    • 如果不登录的话
      • 准备能成功请求对方网站的cookie,即接收对方网站设置在response的cookie
      • 下次请求的时候,使用之前的列表中的cookie来请求
    • 如果登录的话
      • 准备多个账号
      • 获取多个账号cookie
      • 之后随机选择cookie来请求登录之后才能访问的网站
  • 提取数据

    • 确定数据位置,确定数据是否在当前的url地址响应中
      • 如果在当前url地址响应中
        • 提取的是列表页的数据
          • 直接请求列表页的url地址,不需要进入详情页
        • 提取的是详情页的数据
          • 1.确定详情页url地址
          • 2.发送请求
          • 3.提取数据
          • 4.返回
      • 如果不在当前url响应中
        • 在其他响应中,寻找数据位置
    • 数据的提取
      • xpath,从html中提取整块的数据,先分组,然后针对每一组进行提取
      • json
      • re,提取html中的json字符串或者某些容易用正则区分出的属性等
  • 保存数据

一个小案例,爬去豆瓣上最近热播的英美剧,国产剧,动漫以及综艺的节目名称和评分

网页截图以及爬去后的效果图如下:

代码如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# coding=utf-8
import requests
import json

class DoubanSpider:
def __init__(self):
self.headers = {
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
"Referer": "https://m.douban.com/tv/american"}
self.url_temp_list = [
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_american/items?start{}&count=18&loc_id=108288",
"type": "英美剧",
},
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_domestic/items?start{}&count=18&loc_id=108288",
"type": "国产剧",
},
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_animation/items?start{}&count=18&loc_id=108288",
"type": "动漫",
},
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_variety_show/items?start{}&count=18&loc_id=108288",
"type": "综艺",
}
]
self.curIndex=1

def parse_url(self, url):
print(url)
response = requests.get(url, headers=self.headers)
return response.content.decode()

def get_content_list(self, json_str):
dict_ret = json.loads(json_str)
content_list = dict_ret["subject_collection_items"]
total = dict_ret["total"]
return content_list, total

def save_content_list(self, content_list):
with open("douban.txt", "a", encoding='utf-8') as f:
for content in content_list:
#f.write(json.dumps(content, ensure_ascii=False))
f.write(self.curIndex.__str__()+" :" + content["title"]+" 评分:"+content["rating"]["value"].__str__())
f.write("\n")
self.curIndex += 1

def run(self): # 主逻辑
for url_temp in self.url_temp_list:
with open("douban.txt", "a", encoding='utf-8') as f:
f.write(url_temp["type"] + ":================================================================") # 分割线,方便数据查看
f.write("\n")
self.curIndex = 1
num = 0
total = 1 # 假设刚开始的条件成立
while num < total + 18:
# 1.构造一个start_rul
url = url_temp["url_temp"].format(num)
# 2.发送请求,获取响应
json_str = self.parse_url(url)
# 3.提取数据,保存
content_list, total = self.get_content_list(json_str)
self.save_content_list(content_list)
# if len(content_list < 18):
# break
# 4,构造下一页的url地址,进入2,3循环
num += 18


if __name__ == '__main__':
douban = DoubanSpider()
douban.run()
print("complete")

2019-02-25 更新

通用案例之糗事百科段子

作为一名段子手- -不应该只会刷段子,还要学会爬取。。
其中用到了 lxml模块和xpath相关内容,爬取到的信息不止有文字内容,还有图片以及作者名字和性别,但此处只在txt文档里放了段子内容。
效果图如下:

代码:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# coding=utf-8
import requests
from lxml import html


class QuibaiSpdier:
def __init__(self):
self.url_temp = "https://www.qiushibaike.com/hot/page/{}/"
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36"}

def get_url_list(self):
return [self.url_temp.format(i) for i in range(1, 14)]

def parse_url(self, url):
response = requests.get(url, headers=self.headers)
return response.content.decode()

def get_content_list(self, html_str):
html_elements = html.etree.HTML(html_str)
div_list = html_elements.xpath("//div[@id='content-left']/div")
content_list = []
for div in div_list:
item = {}
item["content"] = div.xpath(".//div[@class='content']/span/text()")
item["author_gender"] = div.xpath(".//div[contains(@class,'articleGender')]/@class")
item["author_gender"] = item["author_gender"][0].split(" ")[-1].replace("Icon", "") if len(
item["author_gender"]) > 0 else None
item["content_img"] = div.xpath(".//div[@class='thumb']/a/img/@src")
item["content_img"] = "https:" + item["content_img"][0] if len(item["content_img"]) > 0 else None
item["author_img"] = div.xpath(".//div[@class='author clearfix']//img/@src")
item["author_img"] = "https:" + item["author_img"][0] if len(item["author_img"]) > 0 else None
item["stats_vote"] = div.xpath(".//span[@class='stats-vote']/i/text()")
item["stats_vote"] = item["stats_vote"][0] if len(item["stats_vote"]) > 0 else None
content_list.append(item)
return content_list

def save_content_list(self, content_list):
with open("qiubai.txt", "a", encoding='utf-8') as f:
for content in content_list:
# f.write(json.dumps(content, ensure_ascii=False))
f.write("\n".join(content["content"]) + "点赞数:" + content["stats_vote"])
f.write("\n")

def run(self): # 实现主要逻辑
# 1.构造url_list
url_list = self.get_url_list()
# 2.遍历list,发送请求获取相应
for url in url_list:
html_str = self.parse_url(url)
# 3.提取数据
content_list = self.get_content_list(html_str)
# 4.保存数据
self.save_content_list(content_list)


if __name__ == '__main__':
qiubai = QuibaiSpdier()
qiubai.run()