1. 思路分析
打开浏览器,将上述主页地址base64解码(可以使用base64在线编解码工具)后输入并请求。
主页地址:aHR0cHM6Ly9zei5saWFuamlhLmNvbS9lcnNob3VmYW5nL3JzLw==
接口地址: aHR0cHM6Ly9zei5saWFuamlhLmNvbS9lcnNob3VmYW5nL3JzLw==
可以看到,此时页面上有非常多的房屋数据信息,这便是我们今天要爬取的数据。首先打开抓包工具(F12):
此时抓包工具当中并没有任何的数据包,为什么呢,因为主页已经彻底渲染结束了,所以就没有再发包了,抓包工具也不会凭空出现数据包,那怎么样才能让页面重新加载呢,非常简单,只需要保持抓包工具打开状态下刷新页面即可(这里提一下,大家可以看到我的抓包工具上面有几个框我都是选中的,建议大家也进行勾选后再抓包)。
刷新页面之后就会出现非常多的数据包,当前是选中的All,因此页面加载所需要的数据包都会显示出来,如果只想要某一个类型的数据包的话,可以选中同一行的其他选项,如js就是只显示js文件,Img就是图像文件,Doc就是html文件,这里就不再过多叙述了,大家可以自行尝试。
既然有这么多数据包,那么我们怎么知道我们想要的数据到底在哪一个数据文件里面呢,大家都知道,数据有静态加载与动态加载的,如果不知道的同学,简单理解来说就是静态数据就是直接放在页面源码数据响应里面的,对页面文件url直接发请求就能够获取到;而动态加载的数据就是浏览器通过其他数据包将该数据渲染加载到页面中的,这时候如果直接对页面html文件的url发起请求是获取不到数据的,只能获取到一个html骨架。想要查看数据是不是动态加载的数据其实非常的简单,只需要将鼠标选中到任何一个数据包下然后按下键盘ctrl + F即可弹出一个搜索框:
里面直接搜索数据就好了,比如我们这里就搜索第一个房屋信息数据的标题,再回车或者点击旁边的圆圈箭头即可:
然后我们再点击下面的搜索结果即可跳转到该数据包,跳转后发现该数据包就是一个html文件,我们搜索的数据也是在该页面的响应数据当中的,那么就可以确定该页面是静态页面(直接对页面url发起请求即可),来到Headers选项:
第一个红色框是我们待会需要用代码请求的url,第二个红色框是请求方式,当前请求是get请求,往下看看有无特殊的请求头参数,也就是
下面的参数,看起来除了cookie长了一点,其他并无特殊请求头参数,如此一来,请求头也能构造成功了,一般先加三个参数,UA、Cookie和Referer,该页面无需Referer,因此就只添加UA与Cookie即可。
尝试发送一个请求并打印响应数据:
在pycharm输出台局部搜索的案件也是ctrl+F, 搜索发现,数据已经被请求到了,那我们现在是直接进行数据解析吗,虽然没有什么问题,但是不太好,为什么呢,这里需要提到非常重要的一点,很多同学认为在抓包工具中Elements选项里面的html结构就是我们所请求到的响应数据,其实并不是的。
Elements选项里面的html结构是整个页面渲染加载结束后生成的html结构,和响应数据结构不一定相同的,至今我还没有遇到过该选项里面和响应数据结构完全相同的网页,所以Elements选项只能给我们提供一个参考,因为里面可以进行一些选中以及copy xpath路径的操作,所以copy的xpath路径也只能是作为一个参考,需要比对响应数据结构后才能指定为该路径,如果不是相同结构,就需要修改路径。
所以为了更好的解析数据,一般是先将html文件响应数据保存为本地html文件,便于编写解析规则:
...
response = requests.get(url, headers=headers)
with open('szfj.html', 'wb') as f1:
f1.write(response.content)
with open('szfj.html', 'r', encoding='utf8') as f2:
html_obj = f2.read()
因为是html文件,我们可以使用'wb'模式写入数据流,当然也可以直接用'w'模式解编码后写入,那就需要改写为:
with open('szfj.html', 'w', encoding='utf8') as f1:
f1.write(response.content.decode())
运行结束后,就能看到目录级多了一个html文件,我们进入文件,同样使用ctrl + F, 进行搜索,可以发现的确是有这个数据的,那就说明我们写入本地是写入成功了,接下来的调试解析就可以脱机调试了,这里为什么要脱机调试呢,一来是可以减少我们对对方服务器发送请求的次数,防止对方服务器封禁我们的ip,ip被封禁后就需要使用代理ip了,就比较的麻烦;二是可以更加便捷的去进行数据解析,可以一边看着网页中的elements选项中的html结构一边看着真实响应结构,便于编写数据解析规则。
对于普通html结构的话,我们这里以下提供re正则和xpath两种方式进行解析,具体使用哪种方法,各位自行选择,我个人是比较喜欢使用正则,因为在正则面前是有众生平等的感觉的,而xpath的话,是不能解析script标签里面的js代码的,也就是说如果我们需要的数据是在js代码里面的话,用xpath是解析不到的。
2. 数据解析
2.1. 正则re解析
任何数据不能一拿到就直接开始写解析规则的,否则你的规则就会写得非常的杂乱。我们先到网页中进行分析:
这些数据都是我们所需要提取的,先到Elements里面查看数据,虽然我们说过,该数据的结构是渲染加载后的结构,但并不是说不能完全不使用它,用好Elements也能大幅度提高你的数据解析的速度的:
首先点击这个小箭头,确保它是被选中的状态,被选中的状态应该蓝色的,选中后把鼠标移到数据大标签上:
选中后点击,elements项下的标签就会定位到该标签的位置,可以看到,现在是跳转到了li标签的位置:
而且下面都是一个又一个的li标签,鼠标往下滑动一个标签就会跳到第二个数据行,到此,我们就可以确定数据所在的位置了吗,还不行,为什么不行,因为我们还没有确定在我们的响应数据里面的结构是否也是如此,那怎么办呢,我们就在本地html文件里面搜索一下这个ul标签的类名就好了:
可以发现,在本地文件中也是有类名为sellListContent的ul标签的,仔细对比,确定数据就是在这个位置后,我们这再通过上面所说的定位方法,可以确定具体数据都是在这个div标签中的:
这里我就直接将我自己的经验告诉大家了,一般这种数据的话,我们最好是先使用正则匹配到最外层的ul父级标签内的数据,也就是这所有的li标签,怎么去匹配呢,正则匹配特征原则:找唯一,那我们就看这个类名是不是唯一的:
如此便能确定该类名是唯一的,我们就可以使用该特征却进行匹配:
import re
with open('szfj.html', 'r', encoding='utf8') as f2:
html_obj = f2.read()result_1 = re.compile(r'<ul class="sellListContent".*?>(.*?)</ul>').findall(html_obj)
打印result_1可以发现,的确将ul标签当中的所有数据都拿到了,如果正则不熟悉的同学,建议先去练习正则,简单来说在爬虫当中,需要跳过的数据就用.*?跳过, 我们所需要的数据就用(.*?)获取,findall返回的是一个列表,所以需要进行取值,直接取下标为0的元素即可,因为该列表当中只有一个元素。
然后我们再通过result_1的值进行具体数据匹配,先来看标题的位置:
result_2 = re.compile(r'<div class="info clear">.*?<a .*?>(.*?)</a>').findall(result_1)
如此,我们便将所有的标题数据获取到了,获取其他数据也是如此:
需要注意的是,这个地址是分成了两个a标签的,所以需要匹配两个a标签,中间那个连接符我选择自己构造,也可以匹配上,补充上房屋图片的url数据,整个正则提取规则如下:
result_2 = re.compile(r'data-original="(.*?)".*?info clear.*?<a .*?>(.*?)</a>.*?positionInfo.*?<a .*?>(.*?)</a>.*?<a .*?>(.*?)</a>.*?houseIcon.*?/span>(.*?)</div>.*?totalPrice.*?<span class="">(\d+)</span>.*?<div.*?unitPrice.*?span>(.*?)</span>', re.S).findall(result_1)
现在所有的数据都在这个列表当中,想要存入数据库的就可以循环写入数据库了,因为咱这是基础爬虫,我就先写入为json文件了:
lis = []
for i in result_2:
dic = {
'pic_url': i[0],
'title': i[1],
'address': i[2].strip() + '-' + i[3],
'info': i[4],
'price': i[5],
'UnitPrice': i[6]
}
lis.append(dic)
json_str = json.dumps(lis)
with open('szfj.json', 'w', encoding='utf8') as f3:
f3.write(json_str)
可以发现,如此写入的话会将中文转为编码而且全在一行里面看起来很难看,那也不用慌张,只需要添加两个参数即可:
json_str = json.dumps(lis, ensure_ascii=False, indent=4)
再次运行就发现,没有看不懂的编码了,结构也清晰了很多了。
到此,本页的数据就获取完成了,但是大家可以发现,数据是有多页的,那么怎么办呢,我们就看第二页第三页和第一页的区别。先将过滤器设置到doc,只抓取html文件:
可以发现,服务器发了两个包过来,第一个pg2的状态码是301,也就是永久重定向,第二个pg2/状态码是200,点击进入查看response后观察发现这个才是真正的响应数据的位置,现在就可以对比第一页和第二页的区别了,该页面没有设置请求参数,那么我们就对比url:
可以发现只有最后的值不一样,第三页的是pg3/,那我们就尝试将rs/改成pg1/之后请求,能不能请求到第一页,发现的确可以请求到第一页的数据,那就好办了,直接循环修个pg后的那个值即可控制页数了。我们使用同样的规则去解析第二页的数据,能够解析出来,那就可以实现批量解析了, 这时候就不在需要写到本地html文件的操作了。
整体修改后代码:
import json
import re
import requests
class Fj(object):
def __init__(self):
self.url = ''
self.headers={
'User_Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
}
self.lis = []def Urequests(self):
response = requests.get(self.url, headers=self.headers)
return responsedef sjjx(self, i):
self.url = f'(自行拼接)/pg{i}/'
response = self.Urequests()
result_1 = re.compile(r'<ul class="sellListContent".*?>(.*?)</ul>').findall(response.content.decode())[0]
result_2 = re.compile(r'data-original="(.*?)".*?info clear.*?<a .*?>(.*?)</a>.*?positionInfo.*?<a .*?>(.*?)</a>.*?<a .*?>(.*?)</a>.*?houseIcon.*?/span>(.*?)</div>.*?totalPrice.*?<span class="">(\d+)</span>.*?<div.*?unitPrice.*?span>(.*?)</span>', re.S).findall(result_1)
for i in result_2:
dic = {
'pic_url': i[0],
'title': i[1],
'address': i[2].strip() + '-' + i[3],
'info': i[4],
'price': i[5],
'UnitPrice': i[6]
}
self.lis.append(dic)def save_sj(self):
json_str = json.dumps(self.lis, ensure_ascii=False, indent=4)
with open('szfj.json', 'w', encoding='utf8') as f3:
f3.write(json_str)
def main():
f = Fj()
for i in range(1, 10):
f.sjjx(i)
f.save_sj()
if __name__ == '__main__':
main()
2.2. xpath解析(先看xpath基础文档)
想要使用xpath解析的话需要先安装一个库:
pip install lxml
上述代码我们先简单修改为xpath格式:
import json
import requests
from lxml import etree
class Fj(object):
def __init__(self):
self.url = ''
self.headers={
'User_Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
}
self.dic = {
"list": []
}def Urequests(self):
response = requests.get(self.url, headers=self.headers)
return responsedef sjjx(self, i):
self.url = f'...(自己到网页上获取拼接)/pg{i}/'
response = self.Urequests()
tree = etree.HTML(response)
...
# xpath解析规则
...
for i in result_2:
dic = {
'pic_url': i[0],
'title':i[1],
'address': i[2],
'price': i[3],
'UnitPrice': i[4]
}
self.dic["list"].append(dic)def save_sj(self):
with open('szfj.json', 'w', encoding='utf8') as f3:
json.dump(self.dic, f3, ensure_ascii=False, indent=4)
def main():
f = Fj()
for i in range(1, 想要爬取的页数):
f.sjjx(i)
f.save_sj()
if __name__ == '__main__':
main()
xpath解析也是同样的,需要先找到总的父级标签,而且我们在正则解析的时候已经确定elements结构和响应数据的结构大体是相同的,所以就可直接在elements里面右键标签copy xpath了
匹配ul xpath规则
lis = tree.xpath('//*[@id="content"]/div[1]/ul')
因为我们需要的是所有url标签下的li标签,所以需要在下一级增加上li标签:
匹配数据li xpath规则
lis = tree.xpath('//*[@id="content"]/div[1]/ul/li')
获取具体数据的操作也是如此:
dic = {
'pic_url': i.xpath('./a/img[2]/@data-original')[0],
'title': i.xpath('./div[1]/div[1]/a/text()')[0],
'address': ''.join(i.xpath('./div[1]/div[2]/div//text()')).replace(' ', ''),
'info': i.xpath('./div[1]/div[3]/div//text()')[0],
'price': i.xpath('./div[1]/div[6]/div[1]/span/text()')[0],
'UnitPrice': i.xpath('./div[1]/div[6]/div[2]/span/text()')[0]
}
整体代码:
import json
import requests
from lxml import etree
class Fj(object):
def __init__(self):
self.url = ''
self.headers={
'User_Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
'Cookie': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'
}
self.dic = {
"list": []
}def Urequests(self):
response = requests.get(self.url, headers=self.headers)
return responsedef sjjx(self, i):
self.url = f'...(自己到网页上获取拼接)/pg{i}/'
response = self.Urequests()
tree = etree.HTML(response.content.decode())
lis = tree.xpath('//*[@id="content"]/div[1]/ul/li')
for i in lis:
dic = {
'pic_url': i.xpath('./a/img[2]/@data-original')[0],
'title': i.xpath('./div[1]/div[1]/a/text()')[0],
'address': ''.join(i.xpath('./div[1]/div[2]/div//text()')).replace(' ', ''),
'info': i.xpath('./div[1]/div[3]/div//text()')[0],
'price': i.xpath('./div[1]/div[6]/div[1]/span/text()')[0],
'UnitPrice': i.xpath('./div[1]/div[6]/div[2]/span/text()')[0]
}
self.lis.append(dic)def save_sj(self):
with open('szfj.json', 'w', encoding='utf8') as f3:
json.dump(self.dic, f3, ensure_ascii=False, indent=4)
def main():
f = Fj()
for i in range(1, 2):
f.sjjx(i)
f.save_sj()
if __name__ == '__main__':
main()