(Python网络爬虫);抓取B站404页面小漫画

目录

一. 分析网页

二. 准备工作

三. 实现爬虫

1. 抓取工作

2. 分析工作

3. 拼接主函数&运行结果

四. 完整代码清单

1.多线程版本spider.py: 

2.异步版本async_spider.py:


经常逛B站的同志们可能知道,B站的404页面做得别具匠心:

可以通过https://www.bilibili.com/404来访问这个页面,我们还能换一换:

不过,能刷出哪个小漫画都是随机的,究竟有多少张我确实不知道。所以我突发奇想,想把这些东西爬下来,正好作为爬虫案例练手也不错呀!

一. 分析网页

我们先“御驾亲征”看看应该怎么下手,这时就需要用 F12 召唤开发者工具看网页代码了。

通过元素检查,可以发现img对象的src属性包含了一个链接,这就是图片的来源。

每次换页,这个链接就会变更。说明网页资源是利用JavaScript动态渲染的,不应该像基础爬虫那样直接从网页源代码抓取信息。那么首先,先看看是不是通过Ajax请求动态渲染的,如果是的话,直接模拟Ajax请求即可,不需要用selenium之类的工具。

经过筛查,有三个.xhr类型的请求:

其中中间的那个请求,它的响应是json数据,里面就是我们要找的信息——图片id和链接: 

那现在问题就解决了,我们的思路很简单——先模拟这个ajax请求,获取响应的json数据;然后根据数据中的url,请求图片并保存到计算机上。 我看了看一共只有20张图片,所以不强要求异步爬虫。正好我的异步编程水平很拉跨,没个几千条数据我是不会用异步的!

二. 准备工作

那么,用什么工具呢?首先是http库,我们之前看到、这些请求的协议类型清一色都是"h2":

那么我们就不能用requests或者标准库urllib,它们不支持Http/2.0协议,但是可以用httpx这个第三方库。httpx的API和requests很类似,在简单的请求方面几乎是完全一样。不过它可以支持Http/2.0协议,还自带异步框架,确实非常好用。

其次是解析库,这里处理的是json数据不是html数据,什么靓汤呀正则呀都不需要了。不过json库可能会用上,暂时把它考虑上吧。

最后是保存图片,pathlib肯定会有用的,它太棒了。

至于其它的,我会用logging这个日志库。合理记录程序运行情况有助于调试爬虫程序,没个日志还要修爬虫的BUG太折磨了;虽然不用异步,但可能会用concurrent库来进行并发,提高爬取效率。

老B毕竟是大网站,没个UA伪装它还是会把我们当场擒获的:

幸好,只加个UA就过了,毕竟404页面没必要下太多功夫防御吧:

好,有了这些分析,下面我们来实现这个程序。 

三. 实现爬虫

新建一个文件夹,在其中创建spider.py文件,现在开始实现它。

1. 抓取工作

首先是导入工作和必要的准备。为了便于阅读,我直接在代码片段中写(部分1)之类的提示,尽管这是不合法的:

(部分1)
import logging
from pathlib import Path
from concurrent import futuresimport httpxfrom my_modules.clock import clock
(部分2)
logging.basicConfig(level=logging.INFO,format='%(asctime)s-%(levelname)s:%(message)s'
)
logging.getLogger('httpx').setLevel(logging.WARNING)
(部分3)
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
}
def get_client(): return httpx.Client(http2=True, headers=headers, timeout=10)
BASE_URL = 'https://api.bilibili.com/x/activity/operation/list?source_id=630edcfddbd0b39ca7371ad2'
SAVED_DIR = Path('images')

(1):这一部分导入要用的库。根据PEP8的建议,在Python中导入模块时,标准库、第三方库、和自己编写的模块要分开写,并用空行隔开。这个clock是个装饰器,用来计时,看看这个爬虫程序的效率如何。

(2):这一部分配置logging。首先,level=logging.INFO指出会报告'info'及以上级别的日志;format规定日志输出的形式;最后那个getLogger('httpx')……是为了阻止httpx库报告warning以下级别的日志,不然它每次请求成功都会自动给我发一个info日志,直接就刷屏了,烦人。

(3):上来就是请求头headers,只做了一下UA伪装。这个get_client()函数返回一个可以执行请求的client对象,为什么我不直接写client = httpx.Client(……)然后在全局使用它呢?

  • 一来,这样的话client需要用close()关闭,用with语句更好。然而,成品client对象是不能在with中使用的,所以我选择用这个函数动态创建client;
  • 二来,我害怕client不是线程安全的,共用一个可能导致意外BUG。为了避免麻烦还是为每个请求动态分配一个吧。不过我没有考证,没准是线程安全的?

当然还有其他策略,还可以参考最后我给出的异步版本的那种方案。那两个常量BASE_URL和SAVED_DIR分别是 Ajax请求的url 和 图片保存的路径。

然后,我们定义一个通用爬取函数,返回的是响应。 这样后面不管是获取text,json()还是content都能复用它:

def scrape_url(url: str) -> httpx.Response|None:"""抓取url的页面,返回响应;超时则返回None"""logging.info('开始抓取:%s', url)try:with get_client() as client:response = client.get(url)except httpx.TimeoutException:logging.warning('警告!请求超时:%s', url)return Noneexcept httpx.RequestError:logging.critical('重大错误!抓取时发生异常:%s', url, exc_info=True)raiseelse:logging.info('抓取成功:%s', url)return response

这里关键点只有try-else语句,仅在try语句完美地执行下来没有出错时,才会执行else子句的内容。logging.info()这样的函数以不同的等级进行日志报告,比如warning是警告,critical是严重错误。

然后,复用它来定义两个具体抓取函数,一个模拟ajax请求、一个用来抓取图片。这里的注解用到了Any,需要从typing导入它:

def scrape_ajax(url: str) -> Any|None:"""抓取ajax请求返回的响应,返回其中的json数据"""response = scrape_url(url)if response is not None:return response.json()else:return None
def scrape_img(url: str) -> bytes|None:"""抓取图片url的响应,获取其二进制数据"""response = scrape_url(url)if response is not None:logging.info('获取图片数据成功:%s', url)return response.contentelse:return None

下一步我们来看看怎么从scrape_ajax返回的数据中获取我们想要的信息。

2. 分析工作

通过源码观察ajax请求的响应,返回的东西是这样的:

这是个json数据,首先我们应该提取“data”,然后提取里面的“list”,这里面放着的是各个图片的数据。对于每张图片,我想要“id”和“object_id”两个数据,前者拿来在保存时命名,后者供scrape_img使用来获取二进制数据。那么我可以先把“Image”封装为一个数据类,每个Image有id和url两个属性。我喜欢用typing的NamedTuple,它能很方便地创造一个轻量级数据类:

class Image(NamedTuple):"""封装图片数据的类"""id: strurl: str

 将这段代码写在准备工作那里,现在就可以开始写分析数据的函数了。

def parse_ajax(data: Any) -> list[Image]:"""分析ajax请求的响应数据,封装为Image对象列表返回"""imgs = []img_lst = data.get('data').get('list')for img in img_lst:id = img.get('id')url = img.get('object_id')new_img = Image(id, url)imgs.append(new_img)return imgs

这个函数分析ajax响应的json数据,从中提取信息封装为Image对象,并打包为列表返回。 

然后,是一个保存图片用的函数,负责消费这些Images对象:

def save_img(image: Image) -> None:"""保存单张图片"""name, url = imagedata = scrape_img(url)if data is not None:(SAVED_DIR / f'{name}.png').write_bytes(data)logging.info('图片保存成功:%s', name)else:logging.error('图片获取失败:%s', name)

 第一行直接拆包Image对象拿到id和url,然后尝试用scrape_img抓取二进制数据。如果不是None,则保存到SAVED_DIR中并给出一条报告。否则,说明请求超时,也给出一条报告。

最后,我们把这些“积木”拼起来写成主函数,这个程序就完成啦。

3. 拼接主函数&运行结果

下面是主函数,它“奋六世之余烈”,把我们一直以来做的工作协调起来:

@clock(report_upon_exit=True)
def main():"""启动!"""SAVED_DIR.mkdir(exist_ok=True)ajax_json = scrape_ajax(BASE_URL)if ajax_json is None:raise RuntimeError('第一个请求都超时了,还想做爬虫?')images = parse_ajax(ajax_json)with futures.ProcessPoolExecutor() as executor:executor.map(save_img, images)if __name__ == '__main__':main()

这里有很多老面孔打赢复活赛了,尤其是concurrent.futures。

  • 首先,用了我自己的装饰器来计时。
  • 然后,SAVED_DIR.mkdir()函数是让程序创建这个目录。但是如果目录已经存在这一句就会报错,指定exist_ok=True可以避免这种行为。
  • 获取ajax请求的响应,如果是None的话说明超时了。要是这么重要的数据都得不到,那我们也没有继续运行程序的理由了,直接报错吧。
  • 用parse_ajax来获取封装的图片数据。
  • 用concurrent.futures.ProcessPoolExecutor来多进程地执行save_img任务,可以提高效率。

很好,下面我们来运行一下看看,终端的结果如下:

11秒!这个效率我觉得还是不行。我试了试完全顺序执行,用时15秒。这里多进程的提升确实有限。感觉get_client也有锅,在异步爬虫里必须更改这部分逻辑,否则它会阻塞协程。

把上面的ProcessPoolExecutor改为ThreadPoolExecutor就能改为多线程版本,它的运行时间如下:

用时9.4s,看来多线程在这里强于多进程。即使因为GIL,多线程常被人诟病,但是它轻量,在这里就超过了开销更大的多进程版本。

我还实现了一版异步爬虫,它的用时如下:

用时7.9s接近8s,可以看到,尽管中间存在阻塞步骤,异步的效率还是更强。它是真适合干这个呀,异步拿了MVP!异步的脚本跟多进程的相比改动有些大、更改了一些逻辑、我就不详细说了,最后我会给出异步的代码清单。

不管怎样,成功把图片下载下来了,这就是成功!还要啥自行车啊?

 好了,完事儿(* ̄▽ ̄)~*。下面我来放一下多线程和异步版本的完整代码清单。

四. 完整代码清单

1.多线程版本spider.py: 

import logging
from pathlib import Path
from concurrent import futures
from typing import Any, NamedTupleimport httpxfrom my_modules.clock import clocklogging.basicConfig(level=logging.INFO,format='%(asctime)s-%(levelname)s:%(message)s'
)
logging.getLogger('httpx').setLevel(logging.WARNING)headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
}
def get_client(): return httpx.Client(http2=True, headers=headers, timeout=10)
BASE_URL = 'https://api.bilibili.com/x/activity/operation/list?source_id=630edcfddbd0b39ca7371ad2'
SAVED_DIR = Path('images')class Image(NamedTuple):"""封装图片数据的类"""id: strurl: strdef scrape_url(url: str) -> httpx.Response|None:"""抓取url的页面,返回响应;超时则返回None"""logging.info('开始抓取:%s', url)try:with get_client() as client:response = client.get(url)except httpx.TimeoutException:logging.warning('警告!请求超时:%s', url)return Noneexcept httpx.RequestError:logging.critical('重大错误!抓取时发生异常:%s', url, exc_info=True)raiseelse:logging.info('抓取成功:%s', url)return response
def scrape_ajax(url: str) -> Any|None:"""抓取ajax请求返回的响应,返回其中的json数据"""response = scrape_url(url)if response is not None:return response.json()else:return None
def scrape_img(url: str) -> bytes|None:"""抓取图片url的响应,获取其二进制数据"""response = scrape_url(url)if response is not None:logging.info('获取图片数据成功:%s', url)return response.contentelse:return None
def parse_ajax(data: Any) -> list[Image]:"""分析ajax请求的响应数据,封装为Image对象列表返回"""imgs = []img_lst = data.get('data').get('list')for img in img_lst:id = img.get('id')url = img.get('object_id')new_img = Image(id, url)imgs.append(new_img)return imgs
def save_img(image: Image) -> None:"""保存单张图片"""name, url = imagedata = scrape_img(url)if data is not None:(SAVED_DIR / f'{name}.png').write_bytes(data)logging.info('图片保存成功:%s', name)else:logging.error('图片获取失败:%s', name)@clock(report_upon_exit=True)
def main():"""启动!"""SAVED_DIR.mkdir(exist_ok=True)ajax_json = scrape_ajax(BASE_URL)if ajax_json is None:raise RuntimeError('第一个请求都超时了,还想做爬虫?')images = parse_ajax(ajax_json)with futures.ThreadPoolExecutor() as executor:executor.map(save_img, images)if __name__ == '__main__':main()

2.异步版本async_spider.py:

import asyncio
import logging
from pathlib import Path
from typing import Any, NamedTupleimport httpxfrom my_modules.clock import clocklogging.basicConfig(level=logging.INFO,format='%(asctime)s-%(levelname)s:%(message)s'
)
logging.getLogger('httpx').setLevel(logging.WARNING)headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
}
BASE_URL = 'https://api.bilibili.com/x/activity/operation/list?source_id=630edcfddbd0b39ca7371ad2'
SAVED_DIR = Path('async_images')class Image(NamedTuple):"""封装图片数据的类"""id: strurl: strasync def scrape_url(client: httpx.AsyncClient, url: str) -> httpx.Response|None:"""异步抓取url的页面,返回响应;超时则返回None"""logging.info('开始抓取:%s', url)try:response = await client.get(url)except httpx.TimeoutException:logging.warning('警告!请求超时:%s', url)return Noneexcept httpx.RequestError:logging.critical('重大错误!抓取时发生异常:%s', url, exc_info=True)raiseelse:logging.info('抓取成功:%s', url)return response
async def scrape_ajax(client: httpx.AsyncClient, url: str) -> Any|None:"""异步抓取ajax请求返回的响应,返回其中的json数据"""response = await scrape_url(client, url)if response is not None:return response.json()return None
async def scrape_img(client: httpx.AsyncClient, url: str) -> bytes|None:"""异步抓取图片url的响应,获取其二进制数据"""response = await scrape_url(client, url)if response is not None:logging.info('获取图片数据成功:%s', url)return response.contentreturn None
def parse_ajax(data: Any) -> list[Image]:"""分析ajax请求的响应数据,封装为Image对象列表返回"""imgs = []img_lst = data.get('data', {}).get('list', [])for img in img_lst:id = img.get('id')url = img.get('object_id')if id and url:imgs.append(Image(id, url))return imgs
async def save_img(client: httpx.AsyncClient, image: Image) -> None:"""异步保存单张图片"""name, url = imagedata = await scrape_img(client, url)if data is not None:(SAVED_DIR / f'{name}.png').write_bytes(data)logging.info('图片保存成功:%s', name)else:logging.error('图片获取失败:%s', name)@clock(report_upon_exit=True)
async def async_main():"""启动!异步爬虫"""SAVED_DIR.mkdir(exist_ok=True)async with httpx.AsyncClient(http2=True, headers=headers, timeout=10) as client:ajax_json = await scrape_ajax(client, BASE_URL)if ajax_json is None:raise RuntimeError('第一个请求都超时了,还想做爬虫?')images = parse_ajax(ajax_json)tasks = [save_img(client, image) for image in images]await asyncio.gather(*tasks)if __name__ == '__main__':asyncio.run(async_main())

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/83872.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实战设计模式之模板方法模式

概述 模板方法模式定义了一个操作中的算法骨架,并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法中的某些步骤。简单来说,就是在一个方法中定义了要执行的步骤顺序或算法框架,但允许子类…

ROS1: 使用rosbag的方式将点云topic保存为pcd文件

ROS1: 使用rosbag的方式将点云topic保存为pcd文件。 分为两步:步骤1:通过rosbag录制点云 ,步骤2:通过ros1将rosbag保存为点云pcd文件。 ------------------------ 步骤一:指令示例如下: # topic 名称&a…

MySQL 高级学习篇

一、连结(Join) 1.1 概念 联结(Join)操作用于将多个表中的列组合在一起,形成一个新的查询结果集。它允许我们从多个表中提取数据,并基于表之间的关系进行查询。 1.2 类型 1. 内联结(INNER J…

clickhouse 学习总结

在 ClickHouse 中,配置文件通常位于 /etc/clickhouse 目录下。这个目录包含了多个配置文件,用于控制 ClickHouse 的各种服务(如服务器、用户、远程服务等)的配置。 数据存储目录/var/lib/clickhouse 配置 文件目录 /etc/clickho…

理解JavaScript中map和parseInt的陷阱:一个常见的面试题解析

前言 在JavaScript面试中,map和parseInt的组合常常被用作考察候选人对这两个方法理解深度的题目。让我们通过一个简单的例子来深入探讨其中的原理。 问题现象 [1, 2, 3].map(parseInt) // 输出结果是什么?很多人可能会预期输出[1, 2, 3],但…

字符串 金额转换

package heima.Test09;import java.util.Scanner;public class Money {public static void main(String[] args) {//1。键盘录入一个金额Scanner sc new Scanner(System.in);//请输入一个数据String result "";int money;while (true) {System.out.println("请…

静态相机中的 CCD和CMOS的区别

文章目录 CCD处理方式CMOS处理方式两者区别 首先根据 成像原理,CCD和CMOS的作用是一致的,都是为了将光子转化为数字图像,只是 转换的方式出现差异。 CCD处理方式 获取光子: 在电荷耦合器件(CCD)传感器中…

Pycharm的终端无法使用Anaconda命令行问题详细解决教程

很多初学者在Windows系统上安装了Anaconda后,在PyCharm终端中运行Conda命令时,会遇到以下错误: conda : 无法将“conda”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。 请检查名称的拼写,如果包括路径,请确保…

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.

ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #&#xff1a…

TDengine 替换 Hadoop,彻底解决数据丢失问题 !

完全替换 Hadoop,彻底解决写入丢数问题 !TDengine 助力积成电子更好服务电力客户! 小T导读:在内蒙古某新能源集控项目中,三区需接入并分析大量风电、光伏逆变器及储能设备的监测数据。随着数据规模不断扩大&#xff0c…

从0到1认识ElasticStack

一、ES集群部署 操作系统Ubuntu22.04LTS/主机名IP地址主机配置elk9110.0.0.91/244Core8GB100GB磁盘elk9210.0.0.92/244Core8GB100GB磁盘elk9310.0.0.93/244Core8GB100GB磁盘 1. 什么是ElasticStack? # 官网 https://www.elastic.co/ ElasticStack早期名称为elk。 elk分别…

MySQL账号权限管理指南:安全创建账户与精细授权技巧

在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则&#xf…

DFT测试之TAP/SIB/TDR

TAP的作用 tap全称是test access port,是将jtag接口转为reset、sel、ce、ue、se、si、tck和so这一系列测试组件接口的模块。 jtag的接口主要是下面几个信号: 信号名称信号方向信号描述TCK(测试时钟)输入测试时钟,同…

Python对接印度股票数据源实战指南

Python对接印度股票数据源实战指南 基于StockTV API实现印度证券市场数据对接,覆盖实时行情、K线、指数等核心功能,提供完整开发方案与避坑指南 一、数据源选型要点(技术维度对比) 根据2025年最新实测数据,印度市场主…

usbutils工具的使用帮助

作为嵌入式系统开发中的常用工具,usbutils 是一套用于管理和调试USB设备的Linux命令行工具集。以下是其核心功能和使用方法的详细说明: 1. 工具组成 核心命令: lsusb:列出所有连接的USB设备及详细信息(默认安装&#…

k8s入门教程(集群部署、使用,镜像拉取失败网络问题排查)

文章目录 K8S基础创建centos虚拟机K3S部署配置k3s容器containerd镜像2025年4月测试可用镜像源配置 Pod容器Deployment(部署)和ReplicaSet(副本集)镜像拉取失败问题排查 Service服务ServiceType取值 NameSpace命名空间声明式对象配…

使用VuePress2.X构建个人知识博客,并且用个人域名部署到GitHub Pages中

使用VuePress2.X构建个人知识博客,并且用个人域名部署到GitHub Pages中 什么是VuePress VuePress 是一个以 Markdown 为中心的静态网站生成器。你可以使用 Markdown 来书写内容(如文档、博客等),然后 VuePress 会帮助你生成一个…

Vue3 + Element Plus 防止按钮重复点击的解决方案

在 Vue3 和 Element Plus 项目中,防止按钮重复点击是一个常见的需求,特别是在表单提交、支付等场景下。以下是几种实现方式: 1. 使用 Element Plus 的 loading 状态 Element Plus 的按钮组件本身就支持 loading 状态,这是最简单…

ES101系列09 | 运维、监控与性能优化

本篇文章主要讲解 ElasticSearch 中 DevOps 与性能优化的内容,包括集群部署最佳实践、容量规划、读写性能优化和缓存、熔断器等。 集群部署最佳实践 在生产环境中建议设置单一角色的节点。 Dedicated master eligible nodes:负责集群状态的管理。使用…

如何基于Mihomo Party http端口配置git与bash命令行代理

如何基于Mihomo Party http端口配置git与bash命令行代理 1. 确定Mihomo Party http端口配置 点击内核设置后即可查看 默认7892端口,开启允许局域网连接 2. 配置git代理 配置本机代理可以使用 127.0.0.1 配置局域网内其它机代理需要使用本机的非回环地址 IP&am…