151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

962 阅读9分钟

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

数据爬取背景

口袋妖怪,90 后小学初中接触的一款动画片~,在橡皮擦年少的时候,那可是深信现实中会有皮卡丘的。 当然我首次接触口袋妖怪的时候,它还叫做 “宠物小精灵”/"神奇宝贝"。 现在知道了,它准确的名字叫做 宝可梦(日文︰ポケットモンスター,英文︰Pokémon

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表背不过 -- Python 爬虫小课 3-9

初中时一个元素周期表都背不下来,初代神奇宝贝 151 只到是背的很熟,甚至连进化之后的技能名字都能背诵。果然兴趣才是最好的老师。

本篇博客就要验证童年的记忆是否还依据存在,我们要爬取一下现在的 宝可梦 到底出现了多少以前没有见过的新宝贝。

数据爬取前的分析

目标网站经过我的筛选之后,likexia.gitee.io/piddb/# 进入了视线,因为橡皮擦喜欢爬取表格类的,但是真正写代码的时候,发现这网站还真有点意思,下面依次为大家说明一下,你可以学习到一个爬虫老鸟的问题解决思路。

首先拿到目标网站之后,我会先查阅数据是后台动态加载的,还是直接渲染的,这两种问题的解决思路区分方式,就是查看网页源码之后检索待爬取的某个关键要素。

例如,人眼在网页上看到了如下数据:

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

查看源码之后,检索并未在源码中发现任何和 妙蛙种子 相关的信息,那此时就可以猜测数据由接口返回的,而不是在页面中直接渲染,但是当我检索所有的 API 之后,发现出问题了,并没有任何接口在向前台动态的输出数据。

这就奇怪了,两种方案都没有找到数据,那问题的真相只有一个,数据在前端。

经过一番查找,发现数据竟然是 JS 直接渲染了...这个... 也太好爬了吧。

怪不得我放大缩小窗口,数据是及时变化的呢。

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

发现核心 JS

翻阅所有的 JS 发现,核心的 JS 名字叫做 db.js

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

本次案例核心使用 db.jszh.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 文件,如下图所示:

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

原方案是采用 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()

代码运行效果如下图所示:

image.png

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 的响应对象。

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

对于该对象,有如下属性可以获取。

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…,妙蛙种子的正面照。

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

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 文件所在(在你电脑上有多个文件,注意查找):

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

找到文件之后,全局检索 encoding,找到下图所示区域,进行修改:

image.png

修改编码,保存再次运行即可:

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

再次运行上述代码,发现数据已经获取完毕:

image.png

中文编码修改完毕之后,难点已经不再存在了,剩下的就是 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()

最终得到的结果如图所示:

151只宝可梦(神奇宝贝)倒背的我,却连元素周期表都背不过 -- Python 爬虫小课 3-9

最后在说两句

看着一个个熟悉的宝可梦名字,想起少年时每天追剧的场景,那是我的少年啊。

皮卡丘,小火龙,妙蛙种子,杰尼龟,可达鸭... ... 每看到一个熟悉的名字,都会 会心一笑,抓完宝可梦,心血来潮,抽时间把数码宝贝抓取一下。

在很多时候,爬虫程序需要对 JS 进行解析操作,本篇文章涉及到的只是最简单的实现方式之一,把 JS 文件当成一个 HTML 网页进行操作即可。