综合项目:爬完整个书店
3 道练习题·预计 60 分钟·做对一题解锁下一段
前言
各位童鞋学到这里,爬虫的四步已经全部学完了:获取、解析、提取、存储。不过前面八章,是一章学一样本事,每一样都是单独练的。这一章不学新东西,而是把这八章的本事串起来,写一个完整的书店爬虫。
那么,从哪儿下手呢?各位童鞋还记得实战 2 吗?写记账本之前,咱们没有急着写代码,而是先用大白话把要做的事拆开,再给每一步找一个学过的零件。写爬虫也是一样,而且更要这样做。因为爬虫要和别人的网站打交道,要是一上来就写一大坨代码,出了问题,连是哪一步错的都不知道。
所以这一章的节奏是这样的:先拆需求,再一个函数一个函数地写,每写完一个,就单独试一试,最后才把它们拼起来。写完以后,还要检查一下数据对不对。这一步,很多人都会忘。
这一章做出来的 books.csv ,就是数据分析课要用的那份数据。到时候你会亲眼看到:自己抓下来的文件,和数据分析课那边准备好的,一模一样。
学这一章之前,需要先学完爬虫课第 1~8 章,尤其是 第 5 章 的 parse_list() 、第 6 章 的重试、第 7 章 的接口和 第 8 章 的 csv,这几样这一章都会用到。
目录
这一章会做:
- 一、先拆需求——要什么数据,从哪儿取,分几步
- 二、可靠地取——
fetch() - 三、取全部图书——
crawl_api() - 四、和网页对一对——数据校验
- 五、存成 csv——
save_csv() - 六、完整的爬虫——把函数拼起来
- 七、常见坑
- 八、自己查一次文档——只存一部分字段
- 九、小结与回看
做完这一章,你会有一个结构清楚、取得稳、存得对的书店爬虫,和一份可以交给数据分析课的 books.csv 。
一、先拆需求
动手写代码之前,先用一句大白话说清楚,这个爬虫到底要做什么:
把两点书店的 120 本书,每本的编号、书名、作者、分类、价格、评分、评价数、库存、页数和出版年份,存成一个 csv 文件。
要做的事说清楚了,下一个问题是:数据从哪儿取?前几章其实试过两条路,放在一起比一比:
| 办法 | 请求次数 | 数据 |
|---|---|---|
| 网页:6 个列表页,再加 120 个详情页 | 126 次 | 要用选择器一个个取,再清洗 |
接口:page-1.json 到 page-6.json | 6 次 | 全部字段都在,已经是数字和 None |
这么一比,差别就很明显了。第 7 章说过,有接口就先用接口:请求次数少,对网站友好,取到的数据也干净。所以这个爬虫,主力用接口。
不过,这里还有一个疑问:接口的数据,和网页上显示的一致吗?会不会漏了什么?光靠猜是不行的,这就要做校验:挑一页网页抓下来,和接口的数据对一对。
从哪儿取想好了,接下来把要做的事拆成几个函数。每个函数只做一件事,而且大多能在前面的章节里找到来历:
| 函数 | 做什么 | 用到哪一章 |
|---|---|---|
fetch(url) | 取回一个网址,出错就重试,不行就返回 None | 第 6 章 |
crawl_api() | 顺着 next 取完全部图书,返回列表 | 第 7 章 |
parse_list(html, url) | 从列表页的网页里取出每本书 | 第 5 章 |
check(books, page_books) | 拿网页上的书和接口的书对一对 | 新写的 |
save_csv(books, path) | 存成 csv | 第 8 章 |
再把这几个函数按先后顺序排一排,画成流程:
① crawl_api():从 page-1.json 开始,顺着 next 取完 6 页
② 取第 1 页的网页,parse_list() 取出 20 本
③ check():对一对书名和价格,不一致就停下来查
④ save_csv():存成 csv整个爬虫,就是这四步。下面就一个函数一个函数地写,每写完一个,就单独试一个。先从 fetch(url) 写起,因为后面所有的请求,都要经过它。
二、可靠地取
为什么要先写 fetch() 呢?因为这个爬虫里所有的请求,都要经过它。只要它够可靠,后面的函数就不用再操心「请求出错了怎么办」。
它要做到这么几件事:
- 请求成功了,就返回响应。
- 遇到
404这类错误,重试也没有用,就不再重试。 - 遇到
429和 5 开头的状态码,等一会儿可能就好了,所以等一会儿再试。 - 每次请求之后,都停一停。
这其实就是第 6 章的写法,只是多加了一个统一的停顿:
import time
import requests
def fetch(url, retries=3, pause=0.2):
"""取回 url,成功返回响应;不能重试的错误或者试了几次都不行,返回 None。"""
for attempt in range(1, retries + 1):
response = requests.get(url, timeout=10)
time.sleep(pause)
if response.status_code == 200:
return response
if response.status_code != 429 and response.status_code < 500:
print(f'跳过 {url}:{response.status_code}')
return None
time.sleep(pause * attempt)
print(f'放弃 {url}:试了 {retries} 次')
return None
print(fetch('https://learn-py.org/lab/bookstore/api/page-1.json').status_code)
print(fetch('https://learn-py.org/lab/bookstore/api/page-9.json'))输出的结果:
200
跳过 https://learn-py.org/lab/bookstore/api/page-9.json:404
None先看输出。第一个网址取到了,打印出状态码 200 。第二个网址 page-9.json ,网站返回的是 404 ,fetch() 没有重试,打印一行「跳过」的提示,然后返回 None 。
这段代码里,还有一处值得说一说,就是 time.sleep(pause) 放的位置。它放在 fetch() 里面,以后不管在哪儿请求,都会自动停一停,不会忘。pause 写成了参数,停多久可以随时调:在网页里用 0.2 秒;在自己电脑上抓真实的网站,就调成 1 秒以上。
fetch() 写好了,也试过了。下面先做一道练习,自己动手写一遍 fetch() 。下一节,就用它去取全部的图书。
写函数 fetch(url, retries=3, pause=0.2) :
- 每次请求都设
timeout=10,请求之后停pause秒; - 状态码是 200,返回响应;
- 不是 429,也不是 5 开头的,直接返回
None; - 其他情况再等
pause * 第几次秒重试,最多试retries次,都不行返回None。
编辑器下面已经写好了调用,运行后的输出应该是:
200
None
3推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai