从列表页提取多条数据
3 道练习题·预计 50 分钟·做对一题解锁下一段
前言
各位童鞋在上一章学会了用 BeautifulSoup,从网页里取出一个书名、一个价格。可是一个列表页上有 20 本书,咱们真正想要的,是每一本书的书名、作者、价格、评分和库存,整整齐齐地排成一张表。
这一章就来做这件事。要取的东西听上去很多,其实套路很简单,一句话就能说完:先找到每本书的外壳,再在每个外壳里面取各个字段。
不过,光把字段取出来还不够,因为取出来的都还只是文字:「¥104.40」不能直接加减,「★★★★☆」也没法算平均分。所以还要多做一步,把文字清洗成真正的数据:价格变成浮点数,星星变成数字,「有货」「缺货」变成 True 和 False 。
那么,最后做出来的东西是什么样子呢?各位童鞋还记得实战 2 的记账本吗?每一笔账是一个字典,很多笔账装进一个列表。这一章做出来的,正是同一个样子:每本书是一个字典,一页书是一个列表。
学这一章之前,需要先学完 爬虫课第 4 章 ,会用 select() 、select_one() 、get_text() 。另外,还要熟悉 实战 2 里「列表里装字典」的写法。
目录
这一章会学到:
- 一、先找到每本书的外壳——在外壳里面取字段
- 二、把文字变成数据——价格、星级和库存
- 三、补全链接——
urljoin() - 四、包成一个函数——
parse_list() - 五、常见坑
- 六、自己查一次文档——
urljoin()是怎么拼网址的 - 七、小结与回看
学完这一章,你能把一整页书变成一个整整齐齐的列表,每本书的数据都能直接拿来计算。
一、先找到每本书的外壳
前言里说了,套路是先找外壳。那么,一本书的外壳在哪儿呢?在书店的网页里,一本书的所有信息,都装在一个 <article class="book-card"> 里,这就是它的外壳。所以第一步,先把这一页上的 20 个外壳都找出来:
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 里:
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') ,各拿到一个列表,再一个一个对上号,不也行吗?问题在于,万一哪本书少了一个字段,两个列表的长度就不一样了。从那本书开始,后面的书名和价格就全都错了位。而按外壳来取,一本书的字段永远在一起,就不会出这种事。
字段都取到了。可仔细看看输出,取出来的值全是字符串,还不能直接拿来算,下一节就来解决这个问题。
二、把文字变成数据
上一节拿到的全是字符串。比如价格 '¥104.40' ,前面还带着一个 ¥ ,既不能直接比大小,也没法算平均。要清洗的有三样:价格、评分和库存,咱们一样一样来。
先说价格:去掉 ¥ ,再转成浮点数。
text = '¥104.40'
price = float(text.replace('¥', ''))
print(price, price * 2)输出的结果:
104.4 208.8转换以后,price 就是一个真正的数了,乘以 2 也没问题。
再看评分。评分是用星星画出来的,数一数有几颗实心的星星,就知道是几星。可有的书还没人评过,网页上写的是「暂无评分」,这样的书就用 None 来表示「没有这个值」:
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 :
for text in ['有货', '缺货']:
in_stock = text == '有货'
print(text, in_stock)输出的结果:
有货 True
缺货 False这里有个小窍门。text == '有货' 本身就是一个比较,比较的结果就是 True 或 False ,直接存起来就行,不用再写 if 去判断。
三样清洗都会了,现在把它们合到一起,整理第一页。整理好以后,顺手算一算平均价格、平均评分和缺货的本数,看看数据是不是真的能算了:
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 ,才能这样把它们挑出去,平均分也就不会被拉低。
取字段和清洗都会了,先做两道练习,把这两步练熟。做完再回来,看看书名上的链接该怎么处理。
取回第 4 页,按「先找外壳、再在外壳里取字段」的办法,把每本书整理成字典:书名、价格(转成浮点数)、有没有评分。然后打印一共几本、最贵的是哪本、没有评分的有几本。
运行后的输出应该是:
20 本
最贵的是 字体手记,105.40 元
没有评分的 2 本推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai