「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」
数据爬取背景
口袋妖怪,90 后小学初中接触的一款动画片~,在橡皮擦年少的时候,那可是深信现实中会有皮卡丘的。 当然我首次接触口袋妖怪的时候,它还叫做 “宠物小精灵”/"神奇宝贝"。 现在知道了,它准确的名字叫做 宝可梦(日文︰ポケットモンスター,英文︰Pokémon)
初中时一个元素周期表都背不下来,初代神奇宝贝 151 只到是背的很熟,甚至连进化之后的技能名字都能背诵。果然兴趣才是最好的老师。
本篇博客就要验证童年的记忆是否还依据存在,我们要爬取一下现在的 宝可梦 到底出现了多少以前没有见过的新宝贝。
数据爬取前的分析
目标网站经过我的筛选之后,likexia.gitee.io/piddb/# 进入了视线,因为橡皮擦喜欢爬取表格类的,但是真正写代码的时候,发现这网站还真有点意思,下面依次为大家说明一下,你可以学习到一个爬虫老鸟的问题解决思路。
首先拿到目标网站之后,我会先查阅数据是后台动态加载的,还是直接渲染的,这两种问题的解决思路区分方式,就是查看网页源码之后检索待爬取的某个关键要素。
例如,人眼在网页上看到了如下数据:
查看源码之后,检索并未在源码中发现任何和 妙蛙种子 相关的信息,那此时就可以猜测数据由接口返回的,而不是在页面中直接渲染,但是当我检索所有的 API 之后,发现出问题了,并没有任何接口在向前台动态的输出数据。
这就奇怪了,两种方案都没有找到数据,那问题的真相只有一个,数据在前端。
经过一番查找,发现数据竟然是 JS 直接渲染了...这个... 也太好爬了吧。
怪不得我放大缩小窗口,数据是及时变化的呢。
发现核心 JS
翻阅所有的 JS 发现,核心的 JS 名字叫做 db.js。
本次案例核心使用 db.js 与 zh.js 即可。
db.js 代码部分如下:
const POKEDEX = [
{
"pokemon": [
{
"Pokemon": "Bulbasaur"
}
],
"stats": [
{
"catch rate": "45",
"growth rate": "Medium Slow",
"hp": "45",
"attack": "49",
"defense": "49",
"sp atk": "65",
"sp def": "65",
"speed": "45",
"types": [
"Grass",
"Poison"
]
}
],
"exp": [
{
"base exp": "64"
}
],
"images": {
"normal": {
"front": "https://img.pokemondb.net/sprites/black-white/normal/bulbasaur.png",
"back": "https://img.pokemondb.net/sprites/black-white/back-normal/bulbasaur.png"
},
"shiny": {
"front": "https://img.pokemondb.net/sprites/black-white/shiny/bulbasaur.png",
"back": "https://img.pokemondb.net/sprites/black-white/back-shiny/bulbasaur.png"
}
}
},
zh.js 代码部分如下:
function cnText(text) {
//数组里面有的,返回中文
for (var i in obj) {
if (text == i) {
return obj[i];
}
}
//数组里面没有的,原样返回
for (var i in obj) {
if (text != i) {
return text;
}
}
}
var obj = {
'pokeball': '精灵球',
'greatball': '超级球',
'ultraball': '高级球',
//1
'Bulbasaur': '妙蛙种子',
'Ivysaur': '妙蛙草',
'Venusaur': '妙蛙花',
'Charmander': '小火龙',
'Charmeleon': '火恐龙',
'Charizard': '喷火龙',
'Squirtle': '杰尼龟',
'Wartortle': '卡咪龟',
'Blastoise': '水箭龟',
'Caterpie': '绿毛虫',
'Metapod': '铁甲蛹',
'Butterfree': '巴大蝶',
通过 requests 获取之后,在对其进行英文转中文的替换。
爬取部分代码
当真正爬取的时候,发现虽然案例比较简单,但是坑却不少,例如由于 JS 文件过大,在爬取的时候设置了 requests 的等待时间为 3s,结果超时了...,通过正则去匹配数据的时候,发现正则无法格式化,后来转用 execjs 模块解析 JS,又碰到的乱码问题。
不到 50 行代码,竟然碰到了这么多有趣的技术问题,简直是以外收货啊,编程学习遇到 BUG,解决 BUG,才能真正的学习到知识。
获取 JS 文件部分代码:
def get_db():
db_jd_url = "http://likexia.gitee.io/pokemon/db.js"
res = requests.get(db_jd_url, headers=headers)
js_db = res.text
这部分代码非常简单,注意 get()方法不要添加 timeout 参数,如果想要添加,把时间设置长一些。
接下来就比较惨了,获取到的 JS 文件,如下图所示:
原方案是采用 re 模块,将数据部分匹配出来,在转换成 JSON 格式,结果通过一系列的操作之后,放弃了,大家可以尝试一下,过程中碰到的问题,我已经单独补充了两篇博客去说明,试一下就可以碰到。
正则表达式如果匹配不出来,可以直接采用截取字符串的方式进行。
一般会碰到 JSON 解析问题,具体的解决方法可以参照:
dream.blog.csdn.net/article/det…
dream.blog.csdn.net/article/det…
原有思路没有实现,那么采用 Python 碰到 JS 常见的思路,直接执行 JS,在获取 JS 中的变量。
代码如下:
def get_db():
db_jd_url = "http://likexia.gitee.io/pokemon/db.js"
res = requests.get(db_jd_url, headers=headers)
js_db = res.text
# 通过 compile 命令转成一个js对象
docjs = execjs.compile(js_db)
# 调用变量
dbs = docjs.eval('POKEDEX')
print(dbs)
if __name__ == '__main__':
get_db()
代码运行效果如下图所示:
execjs 模块主要用于 Python 解析 JS,如果没有该模块,安装一下即可,pip install PyExecJS。
你可以通过execjs执行 JS 代码,获取返回值:
# 直接执行JS代码
e = execjs.eval('a = new Array(1,2,3)')
# 直接运行结果
print(e)
可以执行 JS 函数,在调用:
# execjs.compil 执行更复杂的js代码
func = execjs.compile('''
function add(x,y){
return x+y;
};
''')
print(func.call('add', '1', '2'))
当然,你也可以把 JS 代码存储成单独的一个文件,在通过文件读取函数,读取之后进行解析。
在本文上述代码中,我们抓取 db.js 文件之后,通过 execjs 直接编译 JS,并获取到了 POKEDEX,得到了所有宝可梦的对象。
requests 发起请求之后获取的参数
截止到现在,我们使用 requests.get()方法之后,获取了响应对象,也就是常说的 response 对象,运行下述代码,重点看代码之后的说明。
import requests
headers = {
"Referer": "http://likexia.gitee.io/piddb/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}
def get_db():
db_jd_url = "http://likexia.gitee.io/pokemon/db.js"
res = requests.get(db_jd_url, headers=headers)
print(res)
if __name__ == "__main__":
get_db()
运行结果展示如下,该输入表示 get()方法返回的是一个状态码为 200 的响应对象。
对于该对象,有如下属性可以获取。
response 对象属性
import requests
headers = {
"Referer": "http://likexia.gitee.io/piddb/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}
def get_db():
db_jd_url = "http://likexia.gitee.io/pokemon/db.js"
res = requests.get(db_jd_url, headers=headers)
print(res.status_code)
print(res.encoding)
print(res.headers)
print(res.url)
print(res.cookies)
if __name__ == "__main__":
get_db()
属性说明清单如下:
- res.status_code 请求状态码
- res.encoding 网页编码
- res.headers 响应头
- res.url 响应地址
- res.cookies 响应 cookie
对于上述内容,大家在学习的时候,可以多次尝试即可掌握。
对于响应内容,有以下三种格式存在。
文本响应内容
res.text,调用 res 的 text 属性,可以直接获取网页内容,但是存在的风险是编码问题,所以一般在使用的时候,我们需要配合 res.encoding 使用,当然是先设置编码,在进行文本数据获取。
二进制响应内容
在我们爬取图片或者音频视频等文件的时候,就会用到二进制响应内容,它调用的是属性是 res.content,它会以字节的方式访问请求响应体,在本案例中,除了采集到宝可梦数据信息以外,还可以去抓取每个宝可梦对应的图片数据,由于本案例不是为了获取,后续会有专门的一篇博客去介绍图片抓取,那本案例参照下述代码进行学习一下即可。
待抓取的图片地址为:img.pokemondb.net/sprites/bla…,妙蛙种子的正面照。
def get_img():
res = requests.get(
"https://img.pokemondb.net/sprites/black-white/normal/bulbasaur.png", headers=headers)
print(res.content)
if __name__ == "__main__":
get_img()
代码运行之后,你会看到一堆字节输出,可以通过代码对其进行解析与展示。代码如下:
import requests
from PIL import Image
from io import BytesIO
def get_img():
res = requests.get(
"https://img.pokemondb.net/sprites/black-white/normal/bulbasaur.png", headers=headers)
print(res.content)
i = Image.open(BytesIO(res.content))
i.show()
if __name__ == "__main__":
get_img()
代码运行之后会自动打开图片。(此技术在爬虫领域可以用在验证码的展示)
JSON 响应内容
如果网页或者接口返回的是 JSON 格式的数据,可以直接通过 res.json() 将返回的数据格式化成 JSON 格式,方便后续编码使用。
本篇文章恰好解决了之前一直遗留的问题,使用 requests 获取 JS 代码,并对 JS 代码进行解析操作,希望你可以掌握该案例的整体思路。
原始响应内容
在 罕见 的情况下,如果你想获取来自服务器的原始套接字响应,那么可以访问 res.raw。该内容不在入门阶段对大家进行进行说明了,使用场景还是比较少见。
除数据存储外的完整代码
本案例进行到此,涉及的知识已经扩展完毕,相信你对 requests 模块又多了一点点了解,接下来我们在补齐一下剩余部分的代码,顺便说一句,这里面还涉及到一个编码的小坑,下面我们详细说明。
在前面的代码中我们获取到的宝可梦的名字是英文的,需要将其转换成中文,这里就涉及到一个新的 JS 文件获取了,也就是 zh.js。
代码如下:
def zh(name):
zh_url = "http://likexia.gitee.io/piddb/zh.js"
res = requests.get(zh_url, headers=headers)
res.encoding = "utf-8"
zh_data = res.text
# 通过compile命令转成一个js对象
docjs = execjs.compile(zh_data)
# # 调用变量
obj_dict = docjs.eval('obj')
本代码在执行的时候,会出现错误。错误提示如下:
UnicodeEncodeError: 'gbk' codec can't encode character '\u30fb' in position 19196: illegal multibyte sequence
数字部分忽略即可,核心的问题其实还是 execjs 模块对中文的支持问题。
关于该问题的解法比较现成的,需要修改 Python 内置的一些源码,这个问题会出现在 windows 上,mac 或者 linux 无此问题。
原因是 gbk 编码读取不了 utf-8 编码的字符。
解决办法就修改一下让其默认编码方式为 utf-8 即可。
你可以通过各种办法修改 subprocess.py 文件,我使用的是 everything 进行查询。
该文件在 python 安装目录下的 lib 文件夹中。
subprocess.py 文件所在(在你电脑上有多个文件,注意查找):
找到文件之后,全局检索 encoding,找到下图所示区域,进行修改:
修改编码,保存再次运行即可:
再次运行上述代码,发现数据已经获取完毕:
中文编码修改完毕之后,难点已经不再存在了,剩下的就是 Python 基于语法了。
import json
import requests
import execjs
headers = {
"Referer": "http://likexia.gitee.io/piddb/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}
def zh(name):
zh_url = "http://likexia.gitee.io/piddb/zh.js"
res = requests.get(zh_url, headers=headers)
res.encoding = "utf-8"
zh_data = res.text
# 通过compile命令转成一个js对象
docjs = execjs.compile(zh_data)
# # 调用变量
obj_dict = docjs.eval('obj')
return obj_dict[name]
def get_db():
db_jd_url = "http://likexia.gitee.io/pokemon/db.js"
res = requests.get(db_jd_url, headers=headers)
js_db = res.text
# 通过 compile 命令转成一个js对象
docjs = execjs.compile(js_db)
# 调用变量
dbs = docjs.eval('POKEDEX')
for item in dbs:
name = item["pokemon"][0]["Pokemon"]
zh_name = zh(name)
item["pokemon"][0]["Pokemon"] = zh_name
print(item)
if __name__ == "__main__":
get_db()
最终得到的结果如图所示:
最后在说两句
看着一个个熟悉的宝可梦名字,想起少年时每天追剧的场景,那是我的少年啊。
皮卡丘,小火龙,妙蛙种子,杰尼龟,可达鸭... ... 每看到一个熟悉的名字,都会 会心一笑,抓完宝可梦,心血来潮,抽时间把数码宝贝抓取一下。
在很多时候,爬虫程序需要对 JS 进行解析操作,本篇文章涉及到的只是最简单的实现方式之一,把 JS 文件当成一个 HTML 网页进行操作即可。