一、简介
爬虫,英文为Spider,译为“网络蜘蛛”。爬虫是一种按照一定规则,自动抓取互联网信息的程序或脚本。在我们身边其实就有很多爬虫,例如百度、谷歌、搜狗等搜索引擎都是大型网络爬虫。
二、分类
按照爬虫对象,可以分为通用网络爬虫、聚焦网络爬虫、增量式网络爬虫和深层网络爬虫等。
通用网络爬虫又称为全网爬虫,因其能爬的范围和数量巨大,导致其存在速度较低和所需存储空间较大的缺陷,其主要的应用场景为大型搜索引擎。通用网络爬虫主要由初始URL集合、URL队列、页面爬行模块、页面分析模块、页面数据库和链接过滤模块等构成。
聚焦网络爬虫又称主体爬虫,与通用网络爬虫不同的是,聚焦网络爬虫依据主体,爬取特定网页,具有爬取速度快和占用网络资源少等特点。
增量式网络爬虫是爬取新产生的页面或发生变化的页面,对未发生变化的页面不进行爬取。这种方式虽然有效减少了数据下载量,但其实现算法的难度也随之增加。
深层网络爬虫指不能通过静态URL链接进行爬取信息,需要提交一定的关键字,才能访问更深层次的页面。深层网络爬虫主要由六个基本功能模块(爬行控制器、解析器、表单分析器、表单处理器、响应分析器、LVS控制器)和两个爬虫内部数据结构(URL列表、LVS表)等部分构成。其中 LVS(LabelValue Set)表示标签/数值集合,用来表示填充表单的数据源。
三、爬虫技术
1、请求库
爬虫常见的请求库主要由requests和urllib,下面只对requests库进行阐述,因requests是基于urllib所实现的HTML库。
1.1 requests库简介
Requests是Python中的第三方库,是基于urllib,采用Apache2 Licensed开源协议的 HTTP 库。requests用于发送HTTP请求(以下简称URL),并获取响应数据。因urllib的代码臃肿,实现功能逻辑较为繁杂,所以衍生出requests模块。常常应用于爬虫中对网页内容的下载。
Requests访问HTTP会创建request对象(请求)和response对象(响应)。
1.2 安装环境
因Requests是Python的第三方库,所以需要手动安装Requests环境。当使用VsCode作为编译工具时,可以在VsCode的“终端”窗口输入安装命令即可(如果遇到安装失败的情况,可以重试几次,具体原因不详)。
pip install requests
1.3 请求方法
1.3.1 requests.request()
Request()是所有请求方法的基本方法。其代码格式为:
import requests
"""request()"""
url = "http://www.baidu.com/"
req = requests.request('get',url)
request()方法主要的参数有:
Method:可选参数,字符串类型,用于指定请求所使用的方法,包括GET、POST、PUT、DELETE等。
url:可选参数,字符串类型,用于设定请求资源的接口(API),或者说是请求网页的网址。
params:可选参数,字典类型,用于GET请求的查询参数。例如:
'''params参数'''
# 例如通过百度搜索“伊朗”
url = "http://www.baidu.com/s"
params = {'wd':'伊朗'}
req = requests.get(url,params=params)
data:可选参数,字典类型,常用于POST、PUT、DELETE等方法提交的表单参数。
json:可选参数,字典类型,用于上传json数据的参数,被封装到body(请求体)中。请求头的Content-Type默认设置为application/json。
files:可选参数,字典类型,是指定files用于上传文件,一般使用POST方法,默认请求头content-type为‘multipat/from-data类型。’
headers:可选参数,字典类型,用于设定user-agent、refer等请求头。能够模拟浏览器向服务器发起访问。
Auth:可选参数,元组类型,用于授权的用户名和口令,格式为(‘username’,’password’)
timeout:可选参数,用于设置超时时间,单位为“s”
verify:True/False,默认为True,判断是否需要进行HTTPS证书验证,如果为True,则需要自己设置证书地址
allow_redirects:True/False是否让requests做重定向处理,默认True
cookies:附带本地的cookies数据
cert:本地SSL证书路径
1.3.2 其他请求方法
其他请求方法与request()方法用法类似,不做过多说明。
方法 | 内容 |
delete(url, args) | 向网页提交删除请求 |
get(url, params, args) | 获取html网页的主要方法 |
head(url, args) | 获取html网页头信息 |
patch(url, data, args) | 向html网页提交局部修改请求 |
post(url, data, json, args) | 向网页提交post请求 |
put(url, data, args) | 向网页提交put请求 |
request(method, url, args) | 构造一个请求,支撑其他各种方法的基础方法 |
1.4 响应对象
请求方法返回的是Response为响应对象,使用格式为:
"""get()"""
url = "http://www.baidu.com/s"
params = {'wd':'伊朗'}
"""
当然也可以是req : Response = requests.get(url,params=params),采用“变量 : 类型”
的好处是,在程序编程时,能够自动提醒(提示)该变量附有的方法或属性。
"""
req = requests.get(url,params=params)
print(type(req))
# 输出结果
# <class 'requests.models.Response'>
通过调用Response对象附带属性能够查看请求网页返回的相关内容。具体属性有:
encoding:查看响应正文的编码格式
text:查看响应正文,形式为字符串
content:查看响应正文的文本内容,形式为字节。如果获取图片数据或者音视频数据,通过该属性能够直接获取二进制数据进行保存
status_code:查看响应状态码,如果是200,则为成功;如果为4__,则为失败
headers:查看响应的响应头
url:查看响应的url
request.headers:查看请求头信息
request.url:查看请求的url
request.method:查看请求的方法
cookies:获取响应中的cookies,得到RequestCookiesJar对象
req.encoding='utf-8'
print(req.cookies)
# 通过以下语句,将cookie对象转为name=value形式的字符串,以便后续请求使用
print(';'.join(['%s=%s' %(cookie.name,cookie.value) for cookie in req.cookies]))
2、html解析
在pyhon中有两大解析库,即Xpath和BeautifulSoup,二者各有所长,本次学习的是xpath。
2.1 xpath
Xpath是一种用于在XML文档中定位节点的语言,可以用于从XML文档中提取数据,以及在XML文档中进行搜索和过滤操作,被广泛应用于XML文档的处理和分析。
Xpath使用路径(类似于文件系统中的路径)表达式来描述节点的位置。Xpath还提供了一些内置函数和运算符,可以对XML文档中的数据进行操作和计算。
2.2 安装与使用
在Python中Xpath通过lxml库来实现。Lxml的etree模块包含了Xpath,并支持Xpath语法,能够对XML文档进行解析和操作。因此,使用Xpath需安装lxml库,安装方法如下(笔者使用VSCODE):
# 在VSCODE终端输入安装语句即可
pip install lxml
Xpath基本使用方法为:
from lxml import etree
import requests
# 爬取数据,返回Response响应对象
res = requests.get("https://baijiahao.baidu.com/s?id=1798934055638272794&wfr=spider&for=pc")
res.encoding='utf-8'
# 将Response响应对象文本,处理为Element对象,供xpath使用
html = etree.HTML(res.text)
# 使用xpath获取title标签中的文本,返回list对象
title = html.xpath('//title/text()')
print(title)
2.3 语法规则
语法规则 | 说明 |
. | 表示当前节点,一般出现在路径表达式的开头 |
.. | 表示当前节点的父节点 |
/ | 当‘/’出现在路径表达式的开头,表示根;当‘/’出现在表达式中,与节点名称配合使用,格式为‘./nodename’,例如'./head',表示选择当前节点下的所有head子节点 |
// | 与配合节点名称,用于选择当前节点下所有符合条件的节点,无论是什么“辈分”,格式为‘//nodename’ |
Nodename | 用于选择节点,一般与其他语法规则一并使用 |
[] | 表示谓词,用于更精准的选择节点。可以匹配节点属性,也支持使用内置函数 |
@ | 用于选择标签属性,在谓词中使用,格式为‘[@class]’ |
Node() | 用于选择所有节点。如果配合参数,可获取特定节点,例如:'td/node()[self::text() or self::sup]',选择td标签下文本节点和标签名称为sup的节点 |
* | 一般与‘/’配合使用,表示筛选当前节点下所有子节点 |
2.4 常用元素定位方法
1.根据属性值模糊匹配定位元素
格式为:[contains(@属性,’匹配值’)]
2.使用逻辑运算符配合属性定位元素
格式为:[@属性1 逻辑运算符 @属性2]
3.使用特定索引定位元素
格式为:元素标签[数字]、元素标签[position() = 数字]、元素标签[last()]
分别表示第几个元素、第几个元素、最后一个元素
4.通过匹配元素值定位元素
格式为:元素名称[text() = ‘元素值’]
5.通过匹配元素值或属性值前缀定位元素
格式为:元素名称[starts-with(text(),’前缀值’)]
四、爬虫实例
实例仅用于学习爬虫技术,不考虑开发规范。
import requests
from lxml import etree
from lxml import html
import xlwt,xlrd
from xlutils.copy import copy
import datetime
# 爬取入口
HtmlUrl = "http://bjjs.zjw.beijing.gov.cn/eportal/ui?pageId=307670&isTrue=1"
# 项目详细信息链接前缀
BasisHtmlUrl = "http://bjjs.zjw.beijing.gov.cn"
# 请求头
Headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0"}
# PagePath = '//table[@class="page"]//td[@calss="Normal"]/text()'
# 控制项目展示分页数据的,即跳转到哪个分页
JumpPagePath = '//form[@id="FDCJYFORM"]//input[@id="jumpPageBox"]/@value'
def Request(url,header, data):
"""请求网页数据"""
res = requests.post(url, headers = header, data = data)
res.encoding="utf-8"
return res
def Xpath_Html(url, header, data):
"""将response对象字符串转化为xpath可解析对象"""
res = Request(url, header, data)
html = etree.HTML(res.text)
return html
def Page_Number(html,path):
"""获取总记录数和分页展示记录数"""
PageText = html.xpath(path)
PageText = PageText[1].replace('\t','').replace('\r\n','')
inx = PageText.index(',')
inxs = PageText.index('示')+1
inxt = PageText.index('条')
Count = PageText[5:inx]
PageSize = PageText[inxs:inxt]
pages = int(round(int(Count)/int(PageSize),0))
return pages
def Form_Data(html, path):
"""用于获取页面Form表单提交数据"""
Data = html.xpath(path)
return Data
def Create_Book():
#创建工作簿对象
time_str = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
ExcelName = 'D:\\file\\workspace_vscode\\xlwt_test{}.xls'.format(time_str)
book = xlwt.Workbook(encoding='utf-8')
sheet1 = book.add_sheet("项目楼盘")
headers1 = ['项目名称','项目地址','土地用途和使用年限','项目楼盘','批准销售套数','批准销售面积(m²)','销售状态','预售住宅拟售均价(元/m²),','自然楼层','销售楼层','房间号','规划设计用途','户型','建筑面积','套内面积','按建筑面积拟售单价','按套内面积拟售单价']
# 创建一个带有自定义字体的样式
font = xlwt.Font()
font.name = '等线' # 字体名称
font.bold = True # 字体加粗
# font.underline = True # 下划线
# font.italic = True # 斜体
style = xlwt.XFStyle()
style.font = font
for i in range(len(headers1)):
sheet1.write(0,i+1,headers1[i],style)
# for col_index in range(10):
sheet1.col(i+1).width = 4800
# sheet2 = book.add_sheet("详细信息")
# headers2 = ['项目名称','项目楼盘']
# for i in range(len(headers2)):
# sheet2.write(0,i+1,headers2[i])
book.save(ExcelName)
return ExcelName
def open_and_modify_excel(file_path):
# 打开已有的Excel文件
rb = xlrd.open_workbook(file_path, formatting_info=True)
worksheet = rb.sheet_by_name('项目楼盘')
row = 1
while True:
try:
cellvalue=worksheet.cell(row, 1).value
row = row +1
except:
break
return row
def wt_data(file_path,sheet_name,row,data):
workbook = xlrd.open_workbook(file_path)
workbooknew = copy(workbook)
font = xlwt.Font()
font.name = '等线' # 字体名称
font.bold = False # 字体加粗
# font.underline = True # 下划线
# font.italic = True # 斜体
style = xlwt.XFStyle()
style.font = font
if sheet_name == '项目楼盘':
sheetnew = workbooknew.get_sheet(sheet_name)
sheetnew.write(row,1,data['ProName'],style)
sheetnew.write(row,2,data['ProADD'],style)
sheetnew.write(row,3,data['ProQX'],style)
sheetnew.write(row,4,data['LpName'],style)
sheetnew.write(row,5,data['LpTS'],style)
sheetnew.write(row,6,data['LpMJ'],style)
sheetnew.write(row,7,data['LpZT'],style)
sheetnew.write(row,8,data['LpJJ'],style)
sheetnew.write(row,9,data['zrlc'],style)
sheetnew.write(row,10,data['xslc'],style)
sheetnew.write(row,11,data['roomname'],style)
sheetnew.write(row,12,data['room_yt'],style)
sheetnew.write(row,13,data['room_hx'],style)
sheetnew.write(row,14,data['room_jzmj'],style)
sheetnew.write(row,15,data['room_tnmj'],style)
sheetnew.write(row,16,data['room_dj1'],style)
sheetnew.write(row,17,data['room_dj2'],style)
# elif sheet_name == '详细信息':
# sheetnew = workbooknew.get_sheet(sheet_name)
# sheetnew.write(row,1,data['ProName'])
# sheetnew.write(row,2,data['ProADD'])
# sheetnew.write(row,3,data['ProQX'])
# sheetnew.write(row,4,data['LpName'])
# sheetnew.write(row,5,data['LpTS'])
# sheetnew.write(row,6,data['LpMJ'])
# sheetnew.write(row,7,data['LpZT'])
# sheetnew.write(row,8,data['LpJJ'])
workbooknew.save(file_path)
def Cfg_Form_Data(currentPage):
FormData = {}
FormData['currentPage'] = currentPage
return FormData
def cfg_none(value):
if len(value)==0:
valuemr = '--'
else:
valuemr = value[0].replace('\r\n','').replace('\t','').replace(' ','')
return valuemr
def open_web_page(url,head,form_data):
"""该函数用于跳转页面"""
new_web_html = Xpath_Html(url,head,form_data)
return new_web_html
def get_count_lab(html,path):
list_count_tr = html.xpath(path)
if len(list_count_tr)==0:
print('未获取到该页面有效行数')
count_tr = len(list_count_tr)
else:
count_tr = len(list_count_tr)
return count_tr
def get_pro_data(html, path, currentpage):
"""获取每个项目的简介信息"""
# 获取当前分页中项目个数
DataPageCount = len(html.xpath(path))
# 循环获取每个项目的跳转链接
for y in range(DataPageCount):
y = y + 1
if y > 1:
# # 获取每个项目对应的序号
# ProXuHao = (int(currentpage)-1)*15+y-1
# 获取每个项目的详细信息跳转链接
href_pro = html.xpath(path + '[{}]/td[1]/a/@href'.format(y))
# 拼接项目的详细信息跳转链接字符串
url_pro = BasisHtmlUrl + href_pro[0]
print("项目跳转链接:" + url_pro)
html_pro_data = open_web_page(url_pro,Headers,'')
get_building_data(html_pro_data)
def get_building_data_more(url,pro_data,):
html_building = Xpath_Html(url, Headers, '')
# 获取项目楼盘总记录数
count_building_page = Page_Number(html_building,'//table[@class="page"]/tbody/tr/td[last()]/text()')
print("楼盘表分页数:" + str(count_building_page))
# 循环获取分页数据
for i in range(count_building_page):
i=i+1
# 设置form表单
FormData = Cfg_Form_Data(i)
# 获取分页数据
html_page_building = Xpath_Html(url, Headers, FormData)
path_building = '//span[@id="Span1"]/div/table/tr'
count_page_building = get_count_lab(html_page_building,path_building)
print('当前页楼盘数:' + str(count_page_building))
for j in range(count_page_building):
j = j + 1
if j >0:
LpName = html_page_building.xpath('//span[@id="Span1"]/div/table/tr[{}]/td[1]/a/text()'.format(j))
pro_data['LpName'] = cfg_none(LpName)
LpName = html_page_building.xpath('//span[@id="Span1"]/div/table/tr[{}]/td[2]/text()'.format(j))
pro_data['LpTS'] = cfg_none(LpName)
LpName = html_page_building.xpath('//span[@id="Span1"]/div/table/tr[{}]/td[3]/text()'.format(j))
pro_data['LpMJ'] = cfg_none(LpName)
LpName = html_page_building.xpath('//span[@id="Span1"]/div/table/tr[{}]/td[4]/text()'.format(j))
pro_data['LpZT'] = cfg_none(LpName)
LpName = html_page_building.xpath('//span[@id="Span1"]/div/table/tr[{}]/td[5]/text()'.format(j))
pro_data['LpJJ'] = cfg_none(LpName)
print(pro_data)
# LpBeginRow = open_and_modify_excel(FilePath)
# print('楼盘序号:' + str(LpBeginRow))
# 跳转到楼盘明细页面
url_building = BasisHtmlUrl + html_page_building.xpath('//span[@id="Span1"]/div/table/tr[{}]/td[6]/a/@href'.format(j))[0]
html_building = open_web_page(url_building,Headers,'')
get_floor_data(html_building,pro_data)
# for l in range(count_floor):
# l = l+1
# path_floor_room = '//table[@id="table_Buileing"]/tbody/tr[{}]/td'.format(l)
# count_floor_room = get_count_lab(url_building,path_floor_room)
# wt_data(FilePath,'项目楼盘',LpBeginRow,pro_data)
def get_floor_data(html,building_data):
html = html
building_data=building_data
path_floor = '//table[@id="table_Buileing"]/tbody/tr'
count_floor = get_count_lab(html,path_floor)
print('当前楼盘楼层数:' + str(count_floor))
if count_floor > 0:
for l in range(count_floor):
if l > 0:
l = l+1
LcName = html.xpath('//div[@id="show"]/div/table/tbody/tr[{}]/td[1]/text()'.format(l))
building_data['zrlc'] = cfg_none(LcName)
LcName = html.xpath('//div[@id="show"]/div/table/tbody/tr[{}]/td[2]/text()'.format(l))
building_data['xslc'] = cfg_none(LcName)
path_room = '//div[@id="show"]/div/table/tbody/tr[{}]/td[3]/div'.format(l)
count_room = get_count_lab(html,path_room)
print('当前楼层户数:' + str(count_room))
if count_room > 0 :
for i in range(count_room):
i = i+1
roomname = html.xpath('//div[@id="show"]/div/table/tbody/tr[{}]/td[3]/div[{}]/a/text()'.format(l,i))
building_data['roomname'] = cfg_none(roomname)
url_room = BasisHtmlUrl + html.xpath('//div[@id="show"]/div/table/tbody/tr[{}]/td[3]/div[{}]/a/@href'.format(l,i))[0]
html_room = open_web_page(url_room,Headers,'')
count_xx = get_count_lab(html_room,'//div[@id="showDiv"]/table/tr')
print('当前房屋信息条数:' + str(count_xx))
if count_xx >0:
room_yt = html_room.xpath('//div[@id="showDiv"]/table/tr[3]/td[2]/text()')
building_data['room_yt'] = cfg_none(room_yt)
room_hx = html_room.xpath('//div[@id="showDiv"]/table/tr[4]/td[2]/text()')
building_data['room_hx'] = cfg_none(room_hx)
room_jzmj = html_room.xpath('//div[@id="showDiv"]/table/tr[5]/td[2]/text()')
building_data['room_jzmj'] = cfg_none(room_jzmj)
room_tnmj = html_room.xpath('//div[@id="showDiv"]/table/tr[6]/td[2]/text()')
building_data['room_tnmj'] = cfg_none(room_tnmj)
room_dj1 = html_room.xpath('//div[@id="showDiv"]/table/tr[7]/td[2]/text()')
building_data['room_dj1'] = cfg_none(room_dj1)
room_dj2 = html_room.xpath('//div[@id="showDiv"]/table/tr[8]/td[2]/text()')
building_data['room_dj2'] = cfg_none(room_dj2)
print(building_data)
LpBeginRow = open_and_modify_excel(FilePath)
wt_data(FilePath,'项目楼盘',LpBeginRow,building_data)
# path_floor_room = '//table[@id="table_Buileing"]/tbody/tr[{}]/td'.format(l)
# count_floor_room = get_count_lab(html,path_floor_room)
def get_building_data(html_pro):
"""获取每个项目的楼盘信息"""
# 获取项目信息,并存到字典中
ProData = {}
ProData['ProName'] = html_pro.xpath('//td[@id="项目名称"]/text()')[0].replace('\r\n','').replace('\t','')
ProData['ProADD'] = html_pro.xpath('//td[@id="坐落位置"]/text()')[0].replace('\r\n','').replace('\t','')
ProData['ProQX'] = html_pro.xpath('//td[@id="土地用途和使用年限"]/text()')[0].replace('\r\n','').replace('\t','')
print("项目信息:")
print(ProData)
# 判断项目楼盘信息是否涉及分页
ProDetailPath = '//div[@class="portlet" and @id="1f9196f8f2ea4346b5ac615ae825afa0"]/div[last()]/div/div[2]/table[last()]//a/@href'
pro_more_href = html_pro.xpath(ProDetailPath)
# 判断是否有更多楼盘信息
if len(pro_more_href)>0:
# 拼接“查看更多”跳转链接
url_pro_more = BasisHtmlUrl + pro_more_href[0]
print("项目楼盘更多信息链接:" + url_pro_more)
# 获取楼盘信息,用于检查是否存在分页数据
get_building_data_more(url_pro_more,ProData)
else:
# HouseUrlR = html.xpath(ProDetailPath + '[1]//a/@href')
LPList = html_pro.xpath('//span[@id="Span1"]/table/tr')
# print(LPList)
for j in range(len(LPList)):
j = j+ 1
if j >0:
LpName = html_pro.xpath('//span[@id="Span1"]/table/tr[{}]/td[1]/text()'.format(j))
ProData['LpName'] = cfg_none(LpName)
LpName = html_pro.xpath('//span[@id="Span1"]/table/tr[{}]/td[2]/text()'.format(j))
ProData['LpTS'] = cfg_none(LpName)
LpName = html_pro.xpath('//span[@id="Span1"]/table/tr[{}]/td[3]/text()'.format(j))
ProData['LpMJ'] = cfg_none(LpName)
LpName = html_pro.xpath('//span[@id="Span1"]/table/tr[{}]/td[4]/text()'.format(j))
ProData['LpZT'] = cfg_none(LpName)
LpName = html_pro.xpath('//span[@id="Span1"]/table/tr[{}]/td[5]/text()'.format(j))
ProData['LpJJ'] = cfg_none(LpName)
print(ProData)
# LpBeginRow = open_and_modify_excel(FilePath)
# print('楼盘序号:' + str(LpBeginRow))
# 跳转到楼盘明细页面
url_building = BasisHtmlUrl + html_pro.xpath('//span[@id="Span1"]/table/tr[{}]/td[6]/a/@href'.format(j))[0]
html_building = open_web_page(url_building,Headers,'')
get_floor_data(html_building,ProData)
def get_pagedata_pro(count_page):
# 循环获取分页项目列表
for i in range(count_page):
CurrentPage = i + 1
print("当前页码:" + str(CurrentPage))
FormData = Cfg_Form_Data(CurrentPage) # 设置form表单值
html_page_pro = Xpath_Html(HtmlUrl, Headers, FormData) # 获取分页数据
# 循环获取项目简介
# 分页中项目列表的html路径
ProListPath = '//form[@id="FDCJYFORM"]/table[last()]/tr[last()]/td/table/tr'
get_pro_data(html_page_pro, ProListPath, CurrentPage)
if __name__=='__main__':
try:
# xpath可解析对象
html_begin = Xpath_Html(HtmlUrl, Headers, '')
"""获取全部项目涉及分页码"""
path_pro_page = '//div[@id="pagingDiv"]/table/tbody/tr/td[last()]/text()' # 获取项目总记录数的路径
count_pro_page = Page_Number(html_begin, path_pro_page)
print("项目分页列表:" + str(count_pro_page))
"""创建文件,并将表头存入excel中"""
FilePath = Create_Book()
"""按分页页码获取项目数据"""
get_pagedata_pro(count_pro_page)
except Exception as e:
error = e.__traceback__
print(error)