最近开始学习Python的爬虫应用,个人比较喜欢用Xpath的方式来爬取数据,今天就结合一下Xpath方式,以“神奇宝贝百科”为素材,制作一个爬取每只宝可梦数据的工程项目
准备工作
神奇宝贝百科地址:https://wiki.52poke.com/wiki/主页
工程项目的目标是,获取每只精灵的名字、编号、属性、特性、以及蛋群,比如这个妙蛙花页面的例子
我们可以先看看这个网站逻辑是怎么样的,妙蛙花的图鉴地址是这个:https://wiki.52poke.com/wiki/妙蛙花
和主页相比,wiki后面跟着的内容实际上是精灵的文本编码,那么我们就需要得到每只精灵的文本编码是多少了,而在神奇宝贝百科中,在全国图鉴页面中,刚好就可以查看全部宝可梦的名字
全国图鉴页面地址:https://wiki.52poke.com/wiki/宝可梦列表(按全国图鉴编号)
在这个页面里面,点击每只精灵的名字,就可以跳转到对应精灵的图鉴页面中去,也就是说这个页面里面,应该会包含每只宝可梦图鉴页面的超链接,我们可以用开发者模式看看
用元素查找器点击妙蛙种子的名字,查找到了这只宝可梦的超链接,点击进去,果然就是跳转到了妙蛙种子的图鉴页面了,这边爬虫的基本逻辑就是,先获取每只宝可梦的超链接,再逐个超链接来解析宝可梦的数据,并爬取获得
开始编码
#urllib中的URL拼接库
from urllib.parse import urljoin
#导入request库
import requests
#导入re库
import re
#导入日志信息库
import logging
#导入文件写入库
import json
from os import makedirs
from os.path import exists
#导入lxml库
from lxml import etree
#日志输出级别和输出格式
logging.basicConfig(level=logging.INFO,format='%(asctime)s-%(levelname)s:%(message)s')
#打开文件夹results,如果没有,就创造一个这样的文件夹
RESULTS_DIR = 'results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)
首先,导入一些基本的库,以方便我们使用对应的功能,由于最终的结果是导出JSON格式的文件,所以也导入了JSON的库
之后按照上一步的分析,把网址的根部URL,以及全国图鉴的URL以常量的方式作为一个常量编写到程序中,:
#当前站点的根目录
BASE_URL = 'https://wiki.52poke.com'
#测试用网站
GOAL_URL = 'https://wiki.52poke.com/wiki/%E5%AE%9D%E5%8F%AF%E6%A2%A6%E5%88%97%E8%A1%A8%EF%BC%88%E6%8C%89%E5%85%A8%E5%9B%BD%E5%9B%BE%E9%89%B4%E7%BC%96%E5%8F%B7%EF%BC%89'
第一步,先发送get请求,获取目标网页的代码,创建一个这样的函数来实现功能:
#抓取网页源代码函数
def scrape_page(url):
#打印日志信息
logging.info('scraping %s...',url)
try:
response = requests.get(url)
if response.status_code == 200:
#如果状态码是200,直接返回源代码,否则捕捉异常
return response.text
logging.error('get invalid status code %s while scraping %s',response.status_code,url)
except requests.RequestException:
logging.error('error occurred while scraping %s',url,exc_info=True)
第二步,创建每只宝可梦的地址列表,我们可以通过解析全国图鉴的url,并根据起初观察的地址结构,来获得每只宝可梦的超链接
#获取每只宝可梦的网页代码函数
def parse_index(html):
#将网页源码解析为lxml树
tree = etree.HTML(html)
#用lxml树来获取目标a节点的href属性
elements = tree.xpath('//td[@class = "rdexn-name"]/a/@href')
#如果没有数据,返回空列表,否则返回处理后的地址列表
if not elements:
return []
for element in elements:
#把根目录和href属性的值结合成每只宝可梦的页面地址
detail_url = urljoin(BASE_URL,element)
logging.info('get detail url %s',detail_url)
yield detail_url
根据网页的逻辑,精灵的超链接都放在了class属性为rdexn-name的td节点下的a节点的href属性里面,所以这里用Xpath的结构//td[@class = "rdexn-name"]/a/@href
来获取里面的值,通过urllib库下的urljion方法,把BASE_URL和href属性值合起来,就能得到每只宝可梦的页面链接,之后再以生成器对象返回
#抓取每只宝可梦的详细页面代码函数
def scrape_detail(url):
return scrape_page(url)
创建一个专门抓取每只宝可梦详细页面的函数,这里直接调用之前解析网页的函数就可以了,之后就是抓取最重要的数据部分了,也是最复杂的一步
重点分析
第一步最简单,先创建一个解析精灵数据的函数,依然使用Xpath的对象树的方法来创建解析对象
#解析每只宝可梦的内容,包括属性、编号、名字、特性、蛋群
def parse_details(html):
#将网页源码解析为lxml树
tree = etree.HTML(html)
接下来就是重点环节了,要获取精灵的属性,就要观察它的网页结构是怎么样的:
每只宝可梦基本上有1~2种属性,根据网页结构,属性的路径表达可以是这样子:
#属性
types = tree.xpath('//table[@class = "roundy bgwhite fulltable"]//td[contains(@class,"roundy")]/a//span[@class = "type-box-9-text"]/text()')
编号就更简单了,直接找到title属性为“宝可梦列表(按全国图鉴编号)”的a标签,获取文本属性就可以了
#编号
number = tree.xpath('//a[@title = "宝可梦列表(按全国图鉴编号)"]/text()')
名字也很简单,直接获取style等于font-size:1.5em的span标签下的b标签的文本属性就可以了
#名字
name = tree.xpath('//span[@style = "font-size:1.5em"]/b/text()')
特性的话会相对比较麻烦,每只精灵至少有1种基本特性,而至多有两种基本特性,但是隐藏特性也属于精灵的特性,而且并不是每只精灵都会有隐藏特性的,例如铁甲蛹这只精灵
所以网页的结构也因此而不同,可以观察到,有隐藏特性和没隐藏特性的路径,是不一样的,没有隐藏特性的精灵,td的标签没有colspan这个属性,如果用同一种写法,可能会引起获取不到特性的问题
因此这就需要加上逻辑判断,根据情况来决定路径的执行,思路是根据上面td属性的区别来作出判断,找到这边标签的Xpath路径之后,作为if语句的判断条件,特性的路径语句也不难,找到包含“特性”字符的title属性就可以了
#特性
#如果没有隐藏特性
hide_feature = tree.xpath('/table[contians(@class,"roundy")]/tbody/tr[4]/td[@colspan = "2"]')
if not hide_feature:
features = tree.xpath('//td/table[@class = "roundy bgwhite fulltable"]/tbody//a[contains(@title,"(特性)")]/text()')
else:
features = tree.xpath('//td[@colspan = "2"]//a[contains(@title,"(特性)")]/text()')
蛋群属性也不难,按照页面逻辑,找到colspan属性的td标签下,包含“蛋群”字眼的title属性的a标签,获取文本就可以了
#蛋群
eggs = tree.xpath('//td[@colspan = "2"]//a[contains(@title,"(蛋群)")]/text()')
最后再将获取到的数据,以字典的形式返回,这样一来,就能得到一只宝可梦的属性、编号、名字、特性、蛋群的信息
return{
'types':types,
'number':number,
'name':name,
'features':features,
'eggs':eggs
}
这样的代码看似没问题,不过宝可梦有个设定就是,有些宝可梦可能会有其他形态,比如妙蛙花这只精灵,有超级进化,以及超极巨化的形态,那么要获取它的信息的时候,可能会出现数据重复的问题,这种情况是因为具有多种形态的宝可梦,网页结构不一样的原因
可以看到,妙蛙花因为具有多个形态的原因,网页结构上多了几个class属性为_toggle from的属性,那么就需要多做一步判断处理,这里只取他最基本的形态的数据为主,代码如下:
#解析每只宝可梦的内容,包括属性、编号、名字、特性、蛋群
def parse_details(html):
#将网页源码解析为lxml树
tree = etree.HTML(html)
#判断是否有额外形态
toggle = tree.xpath('//tr[@class = "_toggle form1"]')
#根据是否有额外形态,来进行解析筛选
if toggle:
#属性
types = tree.xpath('//tr[@class = "_toggle form1"]//table[@class = "roundy bgwhite fulltable"]//td[contains(@class,"roundy")]/a//span[@class = "type-box-9-text"]/text()')
#编号
number = tree.xpath('//tr[@class = "_toggle form1"]//a[@title = "宝可梦列表(按全国图鉴编号)"]/text()')
#名字
name = tree.xpath('//tr[@class = "_toggle form1"]//span[@style = "font-size:1.5em"]/b/text()')
#特性
#如果没有隐藏特性
hide_feature = tree.xpath('/table[contians(@class,"roundy")]/tbody/tr[4]/td[@colspan = "2"]')
if not hide_feature:
features = tree.xpath('//tbody/tr[contains(@class,"_toggle form1")]//td//a[contains(@title,"(特性)")]/text()')
else:
features = tree.xpath('//tbody/tr[contains(@class,"_toggle form1")]//td[@colspan = "2"]//a[contains(@title,"(特性)")]/text()')
#蛋群
eggs = tree.xpath('//tr[@class = "_toggle form1"]//td[@colspan = "2"]//a[contains(@title,"(蛋群)")]/text()')
else:
#属性
types = tree.xpath('//table[@class = "roundy bgwhite fulltable"]//td[contains(@class,"roundy")]/a//span[@class = "type-box-9-text"]/text()')
#编号
number = tree.xpath('//a[@title = "宝可梦列表(按全国图鉴编号)"]/text()')
#名字
name = tree.xpath('//span[@style = "font-size:1.5em"]/b/text()')
#特性
#如果没有隐藏特性
hide_feature = tree.xpath('/table[contians(@class,"roundy")]/tbody/tr[4]/td[@colspan = "2"]')
if not hide_feature:
features = tree.xpath('//td/table[@class = "roundy bgwhite fulltable"]/tbody//a[contains(@title,"(特性)")]/text()')
else:
features = tree.xpath('//td[@colspan = "2"]//a[contains(@title,"(特性)")]/text()')
#蛋群
eggs = tree.xpath('//td[@colspan = "2"]//a[contains(@title,"(蛋群)")]/text()')
return{
'types':types,
'number':number,
'name':name,
'features':features,
'eggs':eggs
}
最后一步是把数据导出为JSON文件格式
#保存为JSON格式
def save_data(data):
name = data.get('name')
data_path = f'{RESULTS_DIR}/{name}.json'
json.dump(data,open(data_path,'w',encoding='utf-8'),ensure_ascii=False,indent=2)
所有的功能性代码就已经全部完成了,最后只需要执行主函数,让程序运行起来,就可以自动抓取每只宝可梦的数据了
#主函数
def main():
#获取全国图鉴的页面信息
html = scrape_page(GOAL_URL)
#获取每只精灵的超链接代码
detail_urls = parse_index(html)
#把每只宝可梦的详情页逐个分析,用data的变量来存取
for detail_url in detail_urls:
detail_html = scrape_detail(detail_url)
data = parse_details(detail_html)
logging.info('get detail data %s',data)
logging.info('saving data to json file')
save_data(data)
logging.info('data saved successfully')
#运行程序
if __name__=='__main__':
main()
之后就可以获得每只宝可梦的JSON文件了,里面包括每只精灵的属性、编号、名字、特性、蛋群信息