0%

python爬虫实例之爬取贴吧页面

爬什么

把百度贴吧【李毅吧】的内容爬取到本地并保存,需要提取用户名称、发表时间、评论数这几个字段,并且把用户头像和发表的图片保存到本地。

怎么做

  • 获取url
  • 发送请求,获取响应
  • 提取数据
  • 保存数据

获取url

观察可知,目标数据所在的url。
image.png
页码数和url的规律是这样:

1
2
3
4
5
第0页:https://tieba.baidu.com/mo/q/m?word=%E6%9D%8E%E6%AF%85&page_from_search=index&tn6=bdISP&tn4=bdKSW&tn7=bdPSB&lm=16842752&lp=6093&sub4=%E8%BF%9B%E5%90%A7&pn=0&
第1页:https://tieba.baidu.com/mo/q/m?word=%E6%9D%8E%E6%AF%85&page_from_search=index&tn6=bdISP&tn4=bdKSW&tn7=bdPSB&lm=16842752&lp=6093&sub4=%E8%BF%9B%E5%90%A7&pn=30&
第2页:https://tieba.baidu.com/mo/q/m?word=%E6%9D%8E%E6%AF%85&page_from_search=index&tn6=bdISP&tn4=bdKSW&tn7=bdPSB&lm=16842752&lp=6093&sub4=%E8%BF%9B%E5%90%A7&pn=60&
……
第i页:https://tieba.baidu.com/mo/q/m?word=%E6%9D%8E%E6%AF%85&page_from_search=index&tn6=bdISP&tn4=bdKSW&tn7=bdPSB&lm=16842752&lp=6093&sub4=%E8%BF%9B%E5%90%A7&pn={30i}&

看到总共是510699页。页码范围(1~510699)
image.png

发送请求,获取响应

遍历url列表,使用requests库发送请求。

提取数据

方法:观察可知,每个用户发表的信息都在相同属性的li标签。这种情况,提取数据最简单的方式就是xpath。

1
2
3
1. 获取分组的element对象(这里就是对提取的li标签内容做etree)
2. 对element对象做xpath,分别提取各个字段的值
3.(用户名称、发表时间、评论数这三个值可以直接用xpath提取出来,要获取到用户头像和发表的图片,必须先提取到图片对应的url地址之后,再发送请求,获取图片内容)

image.png
image.png

  • 用户名称
  • 发表时间
  • 评论数
  • 用户头像
  • 发表的图片

用户名称

image.png

发表时间

image.png

评论数

image.png

用户头像

image.png

发表的图片

image.png

保存数据

f.write保存到本地。注意,保存写入图片的时候,参数要加上b,只能以二进制方式写入,写入内容是bytes类型的response.content

代码

太不容易了———这个代码写了至少3个小时—–获取段子的内容并不难,主要是对获取到的url地址再次发起请求,获取响应。在写入图片的时候没有使用bytes类型,花费了很多时间排查这个错误——

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import requests
from lxml import etree
import json
from pprint import pprint

class tieba:
def __init__(self):
self.url_temp = "https://tieba.baidu.com/mo/q/m?word=%E6%9D%8E%E6%AF%85&page_from_search=index&tn6=bdISP&tn4=bdKSW&tn7=bdPSB&lm=16842752&lp=6093&sub4=%E8%BF%9B%E5%90%A7&pn={}&"
self.headers = {"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Mobile Safari/537.36"}

def get_url_list(self): #获取url
url_list = [self.url_temp.format(i*30) for i in range(2)]
# pprint(url_list)
return url_list

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

def get_content_list(self,html_str):
html_str = etree.HTML(html_str) #<li>标签分组
# pprint(html_str)
grp_str_list = html_str.xpath("//li[@class='tl_shadow tl_shadow_new ']")
# pprint(grp_str_list)
content_list = []
for grp_str in grp_str_list:
item = {}
item["user_name"] = grp_str.xpath(".//span[contains(@class,'ti_author')]/text()")[0] #contains用法,定位sapn属性值包含ti_author的span标签
item["time"] = grp_str.xpath(".//span[@class='ti_time']/text()")[0]
item["comments"] = grp_str.xpath(".//span[@class='btn_icon']/text()")[0]
item["content"] = grp_str.xpath(".//div[@class='ti_title']/span/text()")
item["icon_url"] = grp_str.xpath(".//div[@class='ti_avatar']/img/@src")[0]
item["photo_url"] = grp_str.xpath(".//div[@class='medias_wrap ordinary_thread clearfix']//img/@data-url")[0] if len(grp_str.xpath(".//div[@class='medias_wrap ordinary_thread clearfix']//img/@data-url"))>0 else None #重点:判断xpath是否提取到数据,如果没有提取到,就将值设置为None
# pprint(item["photo_url"])
content_list.append(item)
# pprint(content_list)
return content_list

def save_content_list(self,content_list,page_num): #保存段子内容
file_path = "第{}页.json".format(page_num)
for content in content_list:
with open(file_path,"a",encoding="utf8") as f:
f.write(json.dumps(content,ensure_ascii=False,indent=2))
# print(file_path)

def save_icon(self,icon_byt,page_num): #保存icon图片
file_path = "icon第{}张.jpg".format(page_num)
with open(file_path,"wb") as f:
f.write(icon_byt)

def save_photo(self,photo_byt,page_num): #保存发表图片
file_path = "photo第{}张.jpg".format(page_num)
with open(file_path,"wb") as f:
f.write(photo_byt)

def parse_photo_url(self,url): #获取图片url的响应,返回bytes类型的结果
photo_str = requests.get(url,headers=self.headers)
photo_str = photo_str.content
return photo_str


def run(self): #实现主逻辑
#1. 获取url
url_list = self.get_url_list()
icon_url_list = []
photo_url_list = []
#2. 遍历url列表
for url in url_list:
#3. 发送请求,获取响应
html_str = self.parse_url(url)
#3. 提取数据
content_list = self.get_content_list(html_str)
page_num = url_list.index(url)+1 #页码数和url列表元素的下标之间的关系
#4. 保存内容数据
self.save_content_list(content_list,page_num)
#5. 发送图片请求,获取响应
for content in content_list:
i = 1 #用于计数,表示这个for循环的循环次数
icon_url_list = [] #初始化icon图片url列表
photo_url_list = [] #初始化发表图片url列表
icon_num = content_list.index(content)
icon_url_list.append(content_list[icon_num]["icon_url"]) #获取头像图片
# pprint(icon_url_list)
icon_byt = self.parse_photo_url(icon_url_list[0]) #发送请求,获取数据
photo_num = page_num*i+icon_num #用于表示图片的序号,这个值和for循环的循环次数有数学关系
self.save_icon(icon_byt,photo_num) #保存icon图片
while content_list[icon_num]["photo_url"] is not None: #当图片url地址不为None时,才对url地址发送请求
photo_url_list.append(content_list[icon_num]["photo_url"]) #获取发表图片
photo_byt = self.parse_photo_url(photo_url_list[0]) #发送请求,获取数据
# photo_num = page_num*icon_num+icon_num
self.save_photo(photo_byt,photo_num) #保存发表图片
# pprint(photo_url_list[0])
break #这里必须加上break,否则程序要卡死
i = i +1

if __name__ == '__main__':
my_tieba = tieba()
my_tieba.run()

总结

主要还是对数据类型不熟悉。这个练习,涉及了对列表和字典的操作。
怎么构造字典,怎么构造列表。这里都用了append的方式将新增的数据添加到空列表。在使用之前,必须记得初始化列表。其中有个错误,就是因为没有初始化列表photo_url_list,也是花了很多时间才找到原因。目标数据的字段多于两个,就可以把数据构造成字典,append到空列表中存储。
写入图片,直接使用response.content这种bytes类型的数据,不能再decode。
第一次使用while。while就是永远循环,直到条件不成立,就结束循环,这个代码要搭配break,否则程序卡死动不了。
用xpath提取数据,不要忘记考虑这个点,有些分组可能提取不到某些字段,这个时候,就要把相应字段的值设置为None,保持字典的完整性。
item["photo_url"] = grp_str.xpath(".//div[@class='medias_wrap ordinary_thread clearfix']//img/@data-url")[0] if len(grp_str.xpath(".//div[@class='medias_wrap ordinary_thread clearfix']//img/@data-url"))>0 else None这个叫做三元表达式。和列表推导式很类似,就是先看条件,条件满足,就赋值给左边,否则赋值到右边。
然后就是对于代码里面设置的循环次数这类数学问题,很生疏。虽然日常生活中的加减乘除都没有大问题,代码里面的数学问题,就算只有小学数学的难度,也要把人搞昏……真的挺不喜欢i这个变量。在学校里面学c语言的时候,就是从i的各种加减乘除开始的,什么i++,++i,整得头痛,编程的兴趣就是这么耗尽的,用代码解决纯数学问题太太太枯燥了。
还有一个点需要特别注意,在浏览器使用xpath helper的时候,要注意检查响应的内容和页面的element内容有没有差别。今天就被坑了……element里面的代码和url的响应内容有些些差别,这一点细微的差别,导致的结果就是xpath提取不到数据。所以,用xpath helper工具虽然方便,还是要多留心,最好是以响应数据为准,做xpath的时候优先看响应数据。

更正

代码的计数方式不对。改为:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import requests
from lxml import etree
import json
from pprint import pprint

class tieba:
def __init__(self):
self.url_temp = "https://tieba.baidu.com/mo/q/m?word=%E6%9D%8E%E6%AF%85&page_from_search=index&tn6=bdISP&tn4=bdKSW&tn7=bdPSB&lm=16842752&lp=6093&sub4=%E8%BF%9B%E5%90%A7&pn={}&"
self.headers = {"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Mobile Safari/537.36"}

def get_url_list(self): #获取url
url_list = [self.url_temp.format(i*30) for i in range(2)]
# pprint(url_list)
return url_list

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

def get_content_list(self,html_str):
html_str = etree.HTML(html_str) #<li>标签分组
# pprint(html_str)
grp_str_list = html_str.xpath("//li[@class='tl_shadow tl_shadow_new ']")
# pprint(grp_str_list)
content_list = []
for grp_str in grp_str_list:
item = {}
item["user_name"] = grp_str.xpath(".//span[contains(@class,'ti_author')]/text()")[0] #contains用法,定位sapn属性值包含ti_author的span标签
item["time"] = grp_str.xpath(".//span[@class='ti_time']/text()")[0]
item["comments"] = grp_str.xpath(".//span[@class='btn_icon']/text()")[0]
item["content"] = grp_str.xpath(".//div[@class='ti_title']/span/text()")
item["icon_url"] = grp_str.xpath(".//div[@class='ti_avatar']/img/@src")[0]
item["photo_url"] = grp_str.xpath(".//div[@class='medias_wrap ordinary_thread clearfix']//img/@data-url")[0] if len(grp_str.xpath(".//div[@class='medias_wrap ordinary_thread clearfix']//img/@data-url"))>0 else None
# pprint(item["photo_url"])
content_list.append(item)
# pprint(content_list)
return content_list

def save_content_list(self,content_list,page_num):
file_path = "第{}页.json".format(page_num)
for content in content_list:
with open(file_path,"a",encoding="utf8") as f:
f.write(json.dumps(content,ensure_ascii=False,indent=2))
# print(file_path)

def save_icon(self,icon_byt,page_num): #保存icon图片
file_path = "icon第{}张.jpg".format(page_num)
with open(file_path,"wb") as f:
f.write(icon_byt)

def save_photo(self,photo_byt,page_num): #保存发表图片
file_path = "photo第{}张.jpg".format(page_num)
with open(file_path,"wb") as f:
f.write(photo_byt)

def parse_photo_url(self,url): #获取图片url的响应,返回bytes类型的结果
photo_str = requests.get(url,headers=self.headers)
photo_str = photo_str.content
return photo_str


def run(self): #实现主逻辑
#1. 获取url
url_list = self.get_url_list()
icon_url_list = []
photo_url_list = []
#2. 遍历url列表
for url in url_list:
#3. 发送请求,获取响应
html_str = self.parse_url(url)
#3. 提取数据
content_list = self.get_content_list(html_str)
page_num = url_list.index(url)+1 #页码数和url列表元素的下标之间的关系
#4. 保存内容数据
self.save_content_list(content_list,page_num)
#5. 发送图片请求,获取响应
for content in content_list:
content_num = content_list.index(content)
icon_url_list.append(content_list[content_num]["icon_url"]) #获取头像图片
# pprint(icon_url_list)
icon_num = len(icon_url_list)
icon_byt = self.parse_photo_url(icon_url_list[icon_num-1]) #发送请求,获取数据
self.save_icon(icon_byt,icon_num) #保存icon图片
while content_list[content_num]["photo_url"] is not None: #当图片url地址不为None时,才对url地址发送请求
photo_url_list.append(content_list[content_num]["photo_url"]) #获取发表图片
photo_num = len(photo_url_list)
photo_byt = self.parse_photo_url(photo_url_list[photo_num-1]) #发送请求,获取数据
self.save_photo(photo_byt,photo_num) #保存发表图片
break #这里必须加上break,否则程序要卡死

if __name__ == '__main__':
my_tieba = tieba()
my_tieba.run()

灵感来源

好像每一次的疑问,都一定能找到答案。真的很巧。

-------------本文结束感谢您的阅读-------------