Python 环境加载中…

从列表页提取多条数据

3 道练习题·预计 50 分钟·做对一题解锁下一段

完整通读本章 →

教学 01 / 08· 已读

前言

各位童鞋在上一章学会了用 BeautifulSoup,从网页里取出一个书名、一个价格。可是一个列表页上有 20 本书,咱们真正想要的,是每一本书的书名、作者、价格、评分和库存,整整齐齐地排成一张表。

这一章就来做这件事。要取的东西听上去很多,其实套路很简单,一句话就能说完:先找到每本书的外壳,再在每个外壳里面取各个字段。

不过,光把字段取出来还不够,因为取出来的都还只是文字:「¥104.40」不能直接加减,「★★★★☆」也没法算平均分。所以还要多做一步,把文字清洗成真正的数据:价格变成浮点数,星星变成数字,「有货」「缺货」变成 True 和 False 。

那么,最后做出来的东西是什么样子呢?各位童鞋还记得实战 2 的记账本吗?每一笔账是一个字典,很多笔账装进一个列表。这一章做出来的,正是同一个样子:每本书是一个字典,一页书是一个列表。

学这一章之前,需要先学完 爬虫课第 4 章 ,会用 select() 、select_one() 、get_text() 。另外,还要熟悉 实战 2 里「列表里装字典」的写法。

目录

这一章会学到:

  • 一、先找到每本书的外壳——在外壳里面取字段
  • 二、把文字变成数据——价格、星级和库存
  • 三、补全链接——urljoin()
  • 四、包成一个函数——parse_list()
  • 五、常见坑
  • 六、自己查一次文档——urljoin() 是怎么拼网址的
  • 七、小结与回看

学完这一章,你能把一整页书变成一个整整齐齐的列表,每本书的数据都能直接拿来计算。

教学 02 / 08· 已读

一、先找到每本书的外壳

前言里说了,套路是先找外壳。那么,一本书的外壳在哪儿呢?在书店的网页里,一本书的所有信息,都装在一个 <article class="book-card"> 里,这就是它的外壳。所以第一步,先把这一页上的 20 个外壳都找出来:

Python· 可在网页运行
import requests
from bs4 import BeautifulSoup

html = requests.get('https://learn-py.org/lab/bookstore/').text
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('article.book-card')
print(len(cards))

输出的结果:

文本
20

正好 20 个,一本书一个外壳。外壳找齐了,接下来对每个外壳,在它里面取出书名、作者、价格、评分和库存,拼成一个字典,再放进列表 books 里:

Python· 可在网页运行
import requests
from bs4 import BeautifulSoup

html = requests.get('https://learn-py.org/lab/bookstore/').text
soup = BeautifulSoup(html, 'html.parser')
books = []
for card in soup.select('article.book-card'):
    books.append({
        'title': card.select_one('.title').get_text(strip=True),
        'author': card.select_one('.author').get_text(strip=True),
        'price': card.select_one('.price').get_text(strip=True),
        'rating': card.select_one('.rating').get_text(strip=True),
        'stock': card.select_one('.stock').get_text(strip=True),
    })
print(len(books))
print(books[0])
print(books[2])

输出的结果:

文本
20
{'title': '轻松玩转自动化', 'author': '陈星河', 'price': '¥104.40', 'rating': '★★★★★', 'stock': '有货'}
{'title': '早起养花', 'author': '林子墨', 'price': '¥56.20', 'rating': '暂无评分', 'stock': '有货'}

这段代码里,最要紧的是循环里的 card.select_one() ,注意不是 soup.select_one() 。为什么呢?因为 card 是一本书的外壳,在它里面找,找到的一定是这本书自己的书名和价格,不会和别的书搞混。

有的童鞋可能会想:何必这么麻烦?分别 select('.title') 、select('.price') ,各拿到一个列表,再一个一个对上号,不也行吗?问题在于,万一哪本书少了一个字段,两个列表的长度就不一样了。从那本书开始,后面的书名和价格就全都错了位。而按外壳来取,一本书的字段永远在一起,就不会出这种事。

字段都取到了。可仔细看看输出,取出来的值全是字符串,还不能直接拿来算,下一节就来解决这个问题。

教学 03 / 08

二、把文字变成数据

上一节拿到的全是字符串。比如价格 '¥104.40' ,前面还带着一个 ¥ ,既不能直接比大小,也没法算平均。要清洗的有三样:价格、评分和库存,咱们一样一样来。

先说价格:去掉 ¥ ,再转成浮点数。

Python· 可在网页运行
text = '¥104.40'
price = float(text.replace('¥', ''))
print(price, price * 2)

输出的结果:

文本
104.4 208.8

转换以后,price 就是一个真正的数了,乘以 2 也没问题。

再看评分。评分是用星星画出来的,数一数有几颗实心的星星,就知道是几星。可有的书还没人评过,网页上写的是「暂无评分」,这样的书就用 None 来表示「没有这个值」:

Python· 可在网页运行
def parse_rating(text):
    if text == '暂无评分':
        return None
    return text.count('★')


print(parse_rating('★★★★☆'))
print(parse_rating('暂无评分'))

输出的结果:

文本
4
None

看到这里,很多童鞋会问:「没有评分,记成 0 不就行了,为什么非要用 None ?」因为 0 星本身也是一个评分,而「没人评过」完全是另一回事。要是记成 0 ,算平均分的时候,就会把没人评过的书也算进去,平均分就被白白拉低了。

最后是库存。网页上写的是「有货」或者「缺货」,咱们把「有货」变成 True ,「缺货」变成 False :

Python· 可在网页运行
for text in ['有货', '缺货']:
    in_stock = text == '有货'
    print(text, in_stock)

输出的结果:

文本
有货 True
缺货 False

这里有个小窍门。text == '有货' 本身就是一个比较,比较的结果就是 True 或 False ,直接存起来就行,不用再写 if 去判断。

三样清洗都会了,现在把它们合到一起,整理第一页。整理好以后,顺手算一算平均价格、平均评分和缺货的本数,看看数据是不是真的能算了:

Python· 可在网页运行
import requests
from bs4 import BeautifulSoup


def parse_rating(text):
    if text == '暂无评分':
        return None
    return text.count('★')


html = requests.get('https://learn-py.org/lab/bookstore/').text
soup = BeautifulSoup(html, 'html.parser')
books = []
for card in soup.select('article.book-card'):
    books.append({
        'title': card.select_one('.title').get_text(strip=True),
        'price': float(card.select_one('.price').get_text(strip=True).replace('¥', '')),
        'rating': parse_rating(card.select_one('.rating').get_text(strip=True)),
        'in_stock': card.select_one('.stock').get_text(strip=True) == '有货',
    })

rated = [book['rating'] for book in books if book['rating'] is not None]
print(books[2])
print(f'平均价格 {sum(book["price"] for book in books) / len(books):.2f} 元')
print(f'有评分的 {len(rated)} 本,平均 {sum(rated) / len(rated):.1f} 星')
print(f'缺货 {sum(1 for book in books if not book["in_stock"])} 本')

输出的结果:

文本
{'title': '早起养花', 'price': 56.2, 'rating': None, 'in_stock': True}
平均价格 61.27 元
有评分的 19 本,平均 3.5 星
缺货 1 本

清洗之后,数据就能拿来算了:这一页书的平均价格是 61.27 元,有评分的 19 本平均 3.5 星,缺货的有 1 本。

代码里 rated 那一行,用的是 [... for book in books if ...] 的写法。这是基础课第 7 章学过的列表生成式,后面加上 if ,表示只要满足条件的,在这里就是只要有评分的书。正因为没有评分的书记的是 None ,才能这样把它们挑出去,平均分也就不会被拉低。

取字段和清洗都会了,先做两道练习,把这两步练熟。做完再回来,看看书名上的链接该怎么处理。

练习 1 / 3整理第 4 页题目有问题?

取回第 4 页,按「先找外壳、再在外壳里取字段」的办法,把每本书整理成字典:书名、价格(转成浮点数)、有没有评分。然后打印一共几本、最贵的是哪本、没有评分的有几本。

运行后的输出应该是:

文本
20 本
最贵的是 字体手记,105.40 元
没有评分的 2 本
main.py
可编辑
做对当前题解锁下一段 ·03

推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai

学习目录