笔记:Python 爬虫/数据分析简易教程
楔子
我相信点进这篇文章的你肯定听说过爬虫及其原理,简单来说,就是模拟浏览器,向网站发送请求,然后,解析网站返回的数据,从中提取需要的信息。
爬虫还有更加丰富的功能与技巧,我们今天不讲。就讲最基础的:读取 HTML 和 JSON,然后,提取信息。这个功能,足以应付大部分的爬虫需求。
既然选择 Python,那么我们也可以非常轻松地把爬虫和数据分析结合起来。例如,爬取某个网站的排行榜,把统计数据输出到 CSV 文件,同时用 matplotlib
绘制图表。
本文中的代码全部来自我的项目 mojimoon/bangumi-anime-ranking,顺便给我点个 star 吧。
希望对你有所帮助。
工具
包括后续分析数据的步骤在内,你很有可能需要用到下表中的第三方库:
库名 | 用途 |
---|---|
requests |
发送 HTTP 请求 |
beautifulsoup4 |
解析 HTML |
regex |
正则表达式 |
numpy |
处理数值 |
pandas |
处理表格 |
matplotlib |
绘制图表 |
scipy |
拟合曲线 |
fake-useragent |
生成随机 UA |
还有可能用到的内置库:
json
:解析 JSONcsv
:解析和生成 CSVtime
:计时或者等待
你不知道有没有安装这些库,也不成问题。我们可以一行命令安装所有需要的库:
1 | pip install requests beautifulsoup4 regex numpy pandas matplotlib scipy |
分析网页
有必要说明,解析 HTML 和解析 JSON 是两个完全不同的工作,但是,本质都是一样的:一般,先手动获取一个文件,分析文件结构,找到需要的信息在文件中的位置,然后,写代码,把这个位置的信息提取出来。
作为例子,我们今天的项目是爬取 Bangumi 的动画排行榜,然后获取每部动画的详细数据,后续进行分析。爬取排行榜需要解析 HTML,获取详细数据用的是官方 API,所以需要解析 JSON。
解析 HTML
首先,我们手动获取排行榜第一页的 HTML:
1 | wget https://bgm.tv/anime/browser?sort=rank -O rank.html |
我们想要知道如何获得排行榜上每个条目的 ID,可以考虑通过某些 id
或者 href
属性来定位这些条目。先把注意力集中在排行榜主体:
1 | <ul id="browserItemList" class="browserFull"> |
因为网页源代码是动态生成的,格式上很混乱是正常现象。(此处的 HTML 代码是我整理过的,实际的源代码,每个标签都是一行,没有缩进。)
仍然不难发现,我们要找的信息在 #browserItemList
下的 li
中,li
的 id
属性是 item_
加上条目的 ID。
尽量使用id
来定位元素,因为id
是唯一的。要检查你的选择器是否正确,请用浏览器的开发者工具。
我们使用 BeautifulSoup 来解析 HTML:
1 | for li in soup.select('#browserItemList > li'): |
——不会用 BeautifulSoup?没关系,下文会介绍用法。这里只需要了解怎么解析 HTML。
解析 JSON
查阅 Bangumi 官方 API 文档,得知
- API 服务器的地址是
https://api.bgm.tv/
- 获取条目信息的 API 是
v0/subjects/{id}
,其中{id}
是条目的 ID
因此,我们再次手动爬取第一个条目的数据:
1 | wget https://api.bgm.tv/v0/subjects/326 -O subject.json |
结果是一个 JSON 文件,关键信息如下:
1 | { |
此处的 JSON 代码也是我整理过的,实际的源代码只有一行。
因此,我们可以通过 json
库来解析 JSON:
1 | # 此处我预先下载了 JSON 在 data/sub/{id}.json |
获取这些信息后,我们可以用 pandas 进行整合,保存到 CSV 文件,再进行后续处理。
我们初步了解怎么爬取数据,那么接下来讲解具体的实现。
用法快速入门
虽然文档非常全面详尽,但也不是每个人都有耐性看完的。我个人认为,入门级别的爬虫,只需要知道一些基本用法即可:
import requests
1 | from fake_useragent import UserAgent |
发送 HTTP 请求,url
(str
) 是请求的网址,headers
(dict
) 是请求头,User-Agent
是浏览器标识,有些网站会根据这个标识来判断你是不是爬虫,如果是爬虫,就不给你数据了。
from bs4 import BeautifulSoup
1 | soup = BeautifulSoup(r.text, 'html.parser') |
解析 HTML。r.text
(str
) 是 HTTP 响应的正文,html.parser
是解析器,这里使用的是 Python 内置的解析器。
1 | for li in soup.select('#browserItemList > li'): |
使用 CSS 选择器,选中 HTML 中的元素。然后,可以把这个元素当作字典来使用,例如,li['id']
是这个元素的 id
属性,li.select('a')[0]['href']
是这个元素的第一个 a
子元素的 href
属性。
import json
1 | j = json.load(jfile) |
解析 JSON,jfile
(file
) 是一个文件对象,这一步后就可以关闭文件了。然后,可以把这个 JSON 当作字典来使用,例如,j['data']['list'][0]['name']
是这个 JSON 的 data
字段的 list
字段的第一个元素的 name
字段。
import csv
1 | writer = csv.writer(ofile) |
生成 CSV,ofile
(file
) 是一个文件对象;然后 writer.writerow()
写入一行数据,参数是一个列表。
csv
本身的功能完全可以用 pandas
来代替。
import re
1 | id = re.search(r'item_(\d+)', li['id']).group(1) |
这里给出一个很浅显的例子:在 li['id']
中匹配 item_
后面的数字,然后,把这个数字提取出来。re.search()
返回一个 Match
对象,group(1)
是这个对象的第一个分组,也就是第一个括号(即 (\d+)
)匹配到的内容。
正则表达式太复杂了,而且很容易出错,我这儿推荐一个网站:regex101。这个网站可以帮助你调试正则表达式,外加很有用的『翻译成人话』功能。例如,<a href="(.+?)">(.+?)</a>
这个正则表达式,可以翻译成人话:『匹配 <a href="
,然后,匹配任意字符,直到遇到第一个 "
,然后,匹配 ">
,然后,匹配任意字符,直到遇到第一个 </a>
』。
import numpy as np
1 | mean, std, hi, uq, med, lq, lo= \ |
应该是非常常用的一些统计量了,其中 s
(list
) 是一组数值。
numpy
非常神通广大,由于这几个库经常一起用,所以我不单独列出,下文会展示更多 numpy
的用法。
import matplotlib.pyplot as plt
1 | plt.hist(s, bins=250, density=True, alpha=.75, color='g') # 绘制频率密度直方图,数据分为 250 组 |
这里给出一个绘制频率密度直方图的例子,s
(list
) 是一组数值,name
(str
) 是这组数值的名称,pre
(str
) 是图片的路径,fname
(str
) 是图片的文件名。为了绘制出更好看的图,plt
提供了非常多的参数,如有必要还是得查文档。标签的水平和垂直方向的位置调整,我觉得是比较不错的技巧。
1 | plt.bar(range(1, 11), s / n, width=.8, color='g', alpha=.75) |
绘制条形图。和直方图不同,直方图给出的是不同的 x
,而条形图给出的是不同的 y
。因此我们将 range(1, 11)
作为 x
,将 s / n
作为 y
(其中,s
是一个 numpy 数组;这样写是因为条形图不支持自动转换为频率密度)。
from scipy.stats import norm, pearsonr
1 | r, p = pearsonr(x, y) |
scipy.stats
中有很多种分布和拟合工具。
正态分布,上文已提到用 plt.plot(x, norm.pdf(x, mean, std), color='r', linewidth=.5)
绘制正态分布密度曲线,这相当于是传入一个 numpy 数组 x
,直接计算对应的正态分布密度。另外的例子:_P3S = norm.cdf(3) * 100
,这相当于是计算正态分布的累积分布函数,即 P(X <= 3)
的百分数。
pearsonr
是计算皮尔逊相关系数的方法。
import pandas as pd
1 | df.loc[:,'bayes'] = (VOT_MIN * AVG_AVG + df['vote'] * df['avg']) / (VOT_MIN + df['vote']) # 计算贝叶斯平均 |
pandas
的最强大之处就是可以把『数组当作数处理』,尽管 df
这个 Dataframe 有很多行,但是 df['vote']
之类的数组都可以像数字一样参与运算,会对每一行都进行相同的操作。
顺便一提,df.loc[:,'bayes']
会返回一个 Series,而 df['bayes']
会返回一个 numpy 数组。这两者的区别在于,Series 会保留原来的索引,而 numpy 数组不会。这里我们需要保留原来的索引,因为我们要把贝叶斯平均和原来的数据一起保存到 csv 文件中。
实例
为供读者参考,这里给出这个项目的实例。受篇幅所限,省略了部分功能的实现与文件操作,但是保留了核心代码。
爬取排行榜
1 | def get_html(page, ofile): |
获取条目信息
1 | def get_json(sid): |
进行数据分析
- 上文
matplotlib
部分已经相当详细地介绍了如何绘制图表。 - 至于输出统计数据到 CSV,只需要会用
writer.writerow
方法就可以了。 - 上文
pandas
部分也已经介绍了如何使用贝叶斯平均进行排序。
此处不再赘述,若有需要可以参考仓库中的代码。
结语
最后一提,不要硬编码。这是一个非常严肃的问题,硬编码让你这次写起来方便,以后要改就麻烦了。Python 提供了非常强大的 try...except...
语法,提高代码的鲁棒性。另外,在判断异常的时候,也不能只考虑数据在多少页结束这个问题,还要纳入网络不稳定等因素,例如,requests
库提供了 r.raise_for_status()
方法,可以在请求失败的时候抛出异常。
好吧,这期简易教程就写到这里。
mojimoon/bangumi-anime-ranking 这个项目的统计结果可以在项目的 README.md
中找到,点进去看看吧。
都看到这里了就麻烦点个 Star 吧!