从 JavaScript 角度学 Python(30) - 爬虫

前言

前面我们也已经学了不少的知识点,而这边也进入了铁人赛的最後一天,所以就来试着写一个简单的 ithelp 铁人赛爬虫。

作业需求

一开始一样先讲一下我们要做什麽吧~

这边我们主要会使用 Requests + BeautifulSoup 套件来实作一些东西,至於实作什麽就直接来看需求吧。

  • 爬取铁人赛首页获取主题资讯,栏位必须有以下:
    • 主题网址
    • 主题标题
    • 参赛人数
  • 透过刚才的主题资讯捞取特别主题的参赛资料,栏位必须有以下:
    • id
    • 参赛者名称
    • 铁人赛名称
    • 参赛者铁人赛连结
    • 敲碗数

爬虫目标网址:2021 iThome 铁人赛首页

爬取铁人赛列表

首先请建立一个资料夹叫做 get-iThome,然後我们一样要先建立虚拟环境

python3 -m venv .venv

那麽由於我们会须需要做爬虫,因此要使用 Requests 来去请求页面,然後再搭配 BeautifulSoup 分析 HTML(不要忘了 lxml 解析器):

pip3 install requests2 beautifulsoup4 lxml

接下来就是准备进入撰写程序码了,那麽这边可以先引入相关套件:

import requests
import bs4 from BeautifulSoup

那麽一开始我们要先请求 iThome 的铁人赛列表页面,铁人赛的网址是 https://ithelp.ithome.com.tw/2021ironman/signup/list,因此就会先使用 requests 套件请求该页面回来:

r = requests.get('https://ithelp.ithome.com.tw/2021ironman/signup/list')

if r.status_code == 200:
  print(r.text) # HTML 结构

这样你已经取得了 iThome 铁人赛的首页罗~

那麽现在就要准备引入 BeautifulSoup 来解析刚刚捞回来的 HTML:

soup = BeautifulSoup(r.text, 'lxml')

超简单的对吧!

https://ithelp.ithome.com.tw/upload/images/20210928/20119486FAkmVGxLuV.png

接下来我们所有关於 HTML 选取都会跟 soup 有关,那我们其中一个需求会是需要绕取主题列表的连结与报名人数,所以这边就请先打开浏览器到 iThome 页面,然後使用开发者工具透过复制「CSS 路径」取得 HTML 的位置,那这边我们要选取到 .group-nav 就好,因为我们要它的连结与子元素:

https://ithelp.ithome.com.tw/upload/images/20210928/20119486fIhUX2L65P.png

oh!你问我这是什麽浏览器吗?因为我习惯开发使用 Firefox 浏览器,所以如果你有兴趣的人可以考虑阅读我这一篇关於 Firefox 的介绍唷~

那麽拉回到 Python 中,当你点选了复制 「CSS 路径」之後你会取得以下这一大串 CSS 路径:

html.fa-events-icons-ready body div.wrapper div.border-frame.clearfix div.contestants-nav ul.list-unstyled.contestants-nav__ul li.contestants-nav__item a.group-nav

那这边会建议你删除两个地方,分别是开头的这一段:

html.fa-events-icons-ready

与结尾的 .active,因此这边只会保留以下这一段,否则你有可能会无法正确选取到元素唷~

body div.wrapper div.border-frame.clearfix div.contestants-nav ul.list-unstyled.contestants-nav__ul li.contestants-nav__item a.group-nav

那接下来就会使用到前一章节所介绍的 CSS 选取方式,也就是 select 方法来选取元素并储存到另一个变数中:

tags = soup.select('body div.wrapper div.border-frame.clearfix div.contestants-nav ul.list-unstyled.contestants-nav__ul li.contestants-nav__item a.group-nav')

那麽我们都知道使用 select 方法选取出来的 HTML 元素会是一个串列,因此这边必须使用 for loop 回圈来取出主题、连结与参赛人数,所以就会需要使用一个变数暂存整理後的资料,那我们虽然已经选取出特定区块的元素,但是为了让选取更精准所以这边就会使用 BeautifulSoup 的 find 方法,然後指定 class 组合成一个新的字典并推入到暂存的串列中,但这边我有特别忽略全部铁人这个选项:

cacheData = []
for item in tags:
  if item.find('div', { "class": "group-nav__text"}).text.strip() != "全部参赛铁人":
    cacheData.append({
      'name': item.find('div', { "class": "group-nav__text"}).text.strip(),
      'num': item.find('div', { "class": "group-nav__num"}).text.strip(),
      'url': item.get('href', None)
    })

对了,这边建议要取出主题名称与参赛人数时结尾要补上 strip() 否则你会发现多一大推空白唷~

https://ithelp.ithome.com.tw/upload/images/20210928/20119486ufB9D48usO.png

最後就是使用 JSON 模组将 cacheData 变数储存到 .json 档案中:

f = open('menu.json', 'w', encoding='utf8')
f.write(json.dumps(cacheData))
f.close()

那这边也提供完整的范例给予参考:

import requests
from bs4 import BeautifulSoup
import json

r = requests.get('https://ithelp.ithome.com.tw/2021ironman/signup/list')

if r.status_code == 200:
  soup = BeautifulSoup(r.text, 'lxml')
  tags = soup.select('body div.wrapper div.border-frame.clearfix div.contestants-nav ul.list-unstyled.contestants-nav__ul li.contestants-nav__item a.group-nav')
  cacheData = []
  for item in tags:
    if item.find('div', { "class": "group-nav__text"}).text.strip() != "全部参赛铁人":
      cacheData.append({
        'name': item.find('div', { "class": "group-nav__text"}).text.strip(),
        'num': item.find('div', { "class": "group-nav__num"}).text.strip(),
        'url': item.get('href', None)
      })
  f = open('menu.json', 'w')
  f.write(json.dumps(cacheData))
  f.close()

获取每一个主题的参赛者连结与主题

前面我们已经完成了主体列表的取得,所以接下来就是获取每一份主题列表中的参赛主题罗~

刚刚我们已经整理了一份 menu.json,所以这边我们将会透过这一份资料来去请求参赛者的主题与连结,一开始我们先建立一个 getUserData.py 档案,然後先引入我们会需要使用的套件:

import requests
from bs4 import BeautifulSoup
import json

接下来我们要使用 open 函式打开 menu.json,这边要注意读出来 JSON 档案後要记得字典,否则会出现错误:

f = open('menu.json', 'r')
data = json.loads(f.read())[0]

接下来就是使用 for loop 搭配 requests 取得主题的页面:

for item in data:
  r = requests.get(item['url'])

这时候你应该会发现一件事情,就是我们只能获取那个主题列表的首页,但是却没有取得分页的问题,你只要到主题列表往下滑可以看到分页按钮

https://ithelp.ithome.com.tw/upload/images/20210928/20119486sMBwQwHvyo.png

那你可能会想说我们只有取得每一个主题的首页,那这边我们该如何取得分页?其实非常简单,首先你先到主题列表,然後算一下一页大概几笔资料,然後用我们刚刚整理的参赛人数去算出分页有几个,这样子再搭配回圈就可以多次请求了,废话不多说上 Code:

for item in data:
  pages = math.ceil(int(item['num']) / 10)
  for page in range(pages):
    r = requests.get(f'{item["num"]}&page={page}')

(这边建议使用 math 模组的 ceil 方法,而不是 round,否则会出错。)

透过以上写法就可以拿回每一个分页的资料与页面,接下来就是依据主题资料来整理成一份新的资料并储存到各自的 JSON,这边逻辑也与前面类似,所以这边最後就直接看完整范例:

import requests
from bs4 import BeautifulSoup
import json
import math

f = open('menu.json', 'r')
data = json.loads(f.read())

for item in data:
  pages = math.ceil(int(item['num']) / 10)
  cacheData = []
  for page in range(pages):
    r = requests.get(f'{item["url"]}&page={page + 1}')
    if r.status_code == 200:
      soup = BeautifulSoup(r.text, 'lxml')
      tags = soup.select('body div.wrapper div.border-frame.clearfix div.contestants-wrapper div.contestants-list.clearfix')
      for tag in tags:
        cacheData.append({
          'id': tag.find('a', { "class": "contestants-expect"}).get('data-id', None),
          'name': tag.find('div', { "class": "contestants-list__name"}).text.strip(),
          'title': tag.find('a', { "class": "contestants-list__title"}).text.strip(),
          'url': tag.find('a', { "class": "contestants-list__title"}).get('href', None),
          'like': tag.find('span', { "class": "contestants-expect__number"}).text.strip()
        })
  print(f'已经取得 {item["name"]} 主题,共 {len(cacheData)} 参赛资料。')
  f = open(f'./data/{item["name"]}.json', 'a')
  f.write(json.dumps(cacheData))
  f.close()

那麽这边就是一个完整的爬虫范例,当然还可以做更进阶的,例如捞取每一份主题的订阅数、计算总阅览数等这类型铁人赛所没有提供的功能,而这部分的实作就当作我出给你的最终作业罗。

作者的话

前阵子朋友问了我一下如何入门投资与创造被动收入,後来我想了一下就将之前自己的一些小小心得写成了一篇文章分享给他,虽然不知道对他是否有帮助,但是最主要期望他在思考这问题之前先解决自己金钱上管控的问题,毕竟他真的不太会控管自己金流 QQ

如果有兴趣的人也可以参考看看(?)

创造被动收入之前的准备与理财入门

关於兔兔们

兔法无边


<<:  Day 0x1E UVa11321 Sort! Sort!! and Sort!!!

>>:  DAY16-EXCEL统计分析:Z检定实例

【从实作学习ASP.NET Core】Day24 | 前台 | Session 购物车 (2)

接续昨天的内容,今天要配合 SessionHelper 来完成购物车的主要功能 需要完成购物车的模型...

Day10 Android - Toast快显元件

今天讲的内容属於简单的元件使用,而我在前面几天已经先有拿来用几次来观察结果,但我一直没有好好提到,今...

【C++】One, Two and Three Dimensional Array

阵列是一群相同资料型态的变数集合~ 就是将相同资料型态的varaible装在一起~ 学习目标: On...

LeetCode解题 Day29

725. Split Linked List in Parts https://leetcode.c...

TailwindCSS 从零开始 - 完赛心得

完赛的内心小剧场 心得 感谢坚持到最後的自己,本次铁人赛完赛了,最後一篇就来点软性的心得文吧! 因...