【Python从入门到进阶】45、Scrapy框架核心组件介绍

news2024/9/30 1:29:21

接上篇《44、Scrapy的基本介绍和安装》
上一篇我们学习了Scrapy框架的基础介绍以及环境的搭建,本篇我们来学习一下Scrapy框架的核心组件的使用。

下面的核心组件的介绍,仍是基于这幅图的机制,大家可以再回顾一下:

注:图片来自[寻_觅]博主

一、Item定义与处理

1.1 Item的概述

在Scrapy框架中,Item是数据的主要载体,用于存储Spider从网页爬取的数据。Item定义了数据的结构,并通过Item Loader对爬取到的数据进行加载和清洗。

1.2 Item的定义

Item使用Python类来定义,通常继承自scrapy.item.Item基础类。通过定义Item类,我们可以明确数据的结构,如字段、类型等。例如:

import scrapy  
  
class MyprojectItem(scrapy.Item):  
    link = scrapy.Field()  # 字段名,数据类型默认为str  
    description = scrapy.Field()  # 字段名,数据类型默认为str

1.3 Item的处理

Item的处理主要涉及两个部分:Item Loader和Pipeline。Item Loader用于加载和清洗数据,Pipeline则负责数据的持久化存储。
Item Loader:Item Loader是用于加载和清洗数据的组件。通过使用Item Loader,我们可以对爬取到的数据进行预处理,如删除不需要的空白字符、转换数据类型等。使用Item Loader需要定义Loader类,并指定需要加载的字段。例如新建一个MySpider定义一个ItemLoader对象:

import scrapy
from scrapy.loader import ItemLoader
from ..items import MyprojectItem

class MySpider(scrapy.Spider):  
    name = 'myspider'  
    start_urls = ['http://example.com']  
  
    def parse(self, response):  
        loader = ItemLoader(item=MyprojectItem(), response=response)  
        # 使用XPath选择器选择<h1>标签中的内容作为标题。  
        loader.add_xpath('title', '//h1/text()')  
        # 使用CSS选择器选择class为content的元素中的内容作为内容。 
        loader.add_css('content', '.content')  
        return loader.load_item()

而Pipeline负责数据的持久化存储,如将爬取到的数据存储到数据库、文件等,我们在下面的小结详细讲解。

二、Spider爬虫定义与使用

2.1 Spider的概述

Spider是Scrapy框架中用于爬取网页数据的组件。Spider通过模拟浏览器行为,自动解析网页结构,提取所需的数据,并将其封装为Item对象。

2.2 Spider的编写

编写Spider需要继承scrapy.Spider基础类,并实现以下方法:

●name:Spider的唯一标识符,用于在Scrapy项目中区分不同的Spider。
●start_urls:爬取的起始URL列表。Scrapy会从这些URL开始爬取数据。
●parse():用于解析单个网页的回调函数。该函数将接收一个Response对象作为参数,并返回一个Item对象或一个Item生成器。

例如,以下是一个简单的Spider,用于爬取网站上的所有链接:

import scrapy
from ..items import MyprojectItem


class BaiduUrlSpider(scrapy.Spider):
    name = "baidu_url"
    allowed_domains = ["www.baidu.com"]
    start_urls = ["https://www.baidu.com"]

    def parse(self, response):
        # 抓取所有的链接
        for link in response.css('a::attr(href)').getall():
            if "http" in link or "https" in link:
                print("link: ", link)  # 打印出来抓取的链接地址
                item = MyprojectItem(link=link, description='test')
                yield item  # 把数据交给管道  piplines.py  进行数据的下载

2.3 Spider的使用

在Scrapy项目中,可以通过运行Scrapy命令来启动Spider:

●运行scrapy crawl <spider_name>命令,其中<spider_name>是Spider的名称。该命令将启动指定的Spider,并开始爬取数据。
●可以使用-o选项指定输出的文件名,例如scrapy crawl myspider -o output.csv将爬取的数据输出到output.csv文件中。
●可以使用-t选项指定输出数据的格式,例如scrapy crawl myspider -t csv将输出CSV格式的数据。

三、Pipeline数据处理管道

3.1 Pipeline的概述

Pipeline是Scrapy框架中负责处理已爬取数据的组件。它负责将爬取到的数据从Spider传递到最终的存储位置,并在传递过程中对数据进行清洗、验证和持久化。

3.2 Pipeline的工作流程

Pipeline的工作流程如下:

(1)Spider爬取数据完成后,将Item对象传递给Pipeline。
(2)Pipeline接收到Item对象后,会按照设定的顺序逐一进行处理。每个Pipeline都有一个唯一的优先级,优先级高的Pipeline会先于优先级低的Pipeline进行处理。
(3)在Pipeline中,可以对Item对象进行各种处理,如清洗数据、验证数据、持久化存储等。
(4)处理完成后,Pipeline将Item对象返回给Engine。
(5)Engine将Item对象传递给下一个Pipeline进行处理,或者将其传递给最终的存储位置。

3.3 自定义Pipeline

在Scrapy框架中,我们可以自定义Pipeline来满足特定的数据处理需求。要自定义Pipeline,需要创建一个Python类,并实现process_item()方法。process_item()方法接受一个Item对象作为参数,并返回一个Item对象。例如,我们可以创建一个将Item对象存储到CSV文件的Pipeline:
在settings.py中启用Pipeline:

ITEM_PIPELINES = {  
   'myproject.pipelines.MyPipeline': 300,  
}

分配给每个类的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,通过pipeline,通常将这些数字定义在0-1000范围内(0-1000随意设置,数值越低,组件的优先级越高)
创建Pipeline类,item pipiline组件是一个独立的Python类,其中process_item()方法必须实现:

from scrapy.exporters import CsvItemExporter
import json  # 需要导入的
from collections import OrderedDict  # 需要导入的


class MyPipeline(object):
    def __init__(self):
        # 可选实现,做参数初始化等
        self.file = open('news_data1.csv', 'wb')
        self.exporter = CsvItemExporter(self.file, encoding='gbk')
        self.exporter.start_exporting()

    def process_item(self, item, spider):
        # item (Item 对象) – 被爬取的item
        # spider (Spider 对象) – 爬取该item的spider
        # 这个方法必须实现,每个item pipeline组件都需要调用该方法,
        # 这个方法必须返回一个 Item 对象,被丢弃的item将不会被之后的pipeline组件所处理。
        print('默认的字段数据:{}\n'.format(item))
        item = OrderedDict(item)
        item = json.dumps(item, ensure_ascii=False)
        print('调整后的字段数据:{}\n'.format(item))
        self.exporter.export_item(eval(item))
        return item  # 返回一个Item对象,该对象将被持久化存储到CSV文件

    def open_spider(self, spider):
        # spider (Spider 对象) – 被开启的spider
        # 可选实现,当spider被开启时,这个方法被调用。
        pass

    def close_spider(self, spider):
        # spider (Spider 对象) – 被关闭的spider
        # 可选实现,当spider被关闭时,这个方法被调用
        self.exporter.finish_exporting()
        self.file.close()

四、Scheduler调度器

4.1 Scheduler的概述

Scheduler是Scrapy框架中的调度器组件,负责管理爬取任务的队列。它根据一定的优先级和规则,将爬取请求放入队列中,并按照顺序执行。

4.2 Scheduler的工作原理

Scheduler的工作原理如下:

(1)Spider爬取完一个网页后,将生成的请求(Request)对象传递给Scheduler。
(2)Scheduler根据请求的优先级、规则等因素,将其放入内部的队列中。
(3)Engine启动后,会定期从Scheduler中获取请求,并将其传递给下载器进行下载。
(4)下载完成后,Engine将下载的响应(Response)对象传递给Scheduler。
(5)Scheduler将响应对象传递给相应的Spider进行处理。

4.3 Scheduler的配置

在Scrapy项目的settings.py文件中,可以通过设置以下参数来配置Scheduler的行为:

●SCHEDULER:用于指定使用的Scheduler类。Scrapy框架提供了多种Scheduler的实现,如DumbScheduler、SqlalchemyScheduler等。可以根据需求选择合适的Scheduler。
●SCHEDULER_QUEUE_CLASS:用于指定请求队列的实现方式。Scrapy框架提供了多种队列的实现,如PriorityQueue、FifoQueue等。可以根据需求选择合适的队列。
●SCHEDULER_QUEUE_URL:用于指定请求队列的持久化存储方式。可以使用Redis、RabbitMQ等消息队列服务作为存储后端。
●SCHEDULER_PERSIST:用于指定是否启用请求队列的持久化存储。设置为True时,请求队列将在爬虫重启后继续存在,避免重复爬取。

4.4 自定义Scheduler

如果默认的Scheduler不能满足需求,我们可以自定义Scheduler来实现特定的调度逻辑。要自定义Scheduler,需要创建一个Python类,并实现以下方法:

●enqueue_request(request):将请求加入队列中。该方法需要返回一个布尔值,表示请求是否成功加入队列。
●dequeued_request():从队列中获取请求。该方法需要返回一个请求对象。
●has_pending_requests():检查队列中是否有待处理的请求。该方法需要返回一个布尔值。
●stats:返回调度器的统计信息。该方法需要返回一个字典,包含调度器的各种统计数据。

五、Downloader下载器

5.1 Downloader的概述

Downloader是Scrapy框架中负责下载网页数据的组件。它接收Engine传递的下载请求(Request),并发送HTTP请求到目标网站,获取网页的响应(Response)数据,并将其返回给Engine。

5.2 Downloader的工作流程

Downloader的工作流程如下:

(1)Engine将下载请求(Request)对象传递给Downloader。
(2)Downloader根据请求的URL、方法、头信息等,构建HTTP请求。
(3)Downloader发送HTTP请求到目标网站,并等待响应。
(4)一旦收到响应,Downloader将响应数据封装为Response对象,并将其返回给Engine。
(5)Engine将Response对象传递给相应的Spider进行处理。

5.3 Downloader的配置

在Scrapy项目的settings.py文件中,可以通过设置以下参数来配置Downloader的行为:

●DOWNLOADER:用于指定使用的Downloader类。Scrapy框架默认使用scrapy.core.downloader.handlers.http.HTTPDownloader作为Downloader的实现。
●DOWNLOAD_DELAY:用于设置下载器在连续下载请求之间的延迟时间(秒)。这可以避免对目标网站造成过大的访问压力。
●DOWNLOAD_TIMEOUT:用于设置下载请求的超时时间(秒)。如果请求在超时时间内没有响应,Downloader将引发超时异常。
●DOWNLOADER_MIDDLEWARES:用于指定使用的Downloader中间件列表。Downloader中间件可以对下载请求和响应进行预处理和后处理,例如添加代理、处理重定向等。
●DOWNLOADER_MIDDLEWARES_BASE:Scrapy框架提供的默认Downloader中间件列表。可以在自定义的Downloader中间件中继承或覆盖默认的中间件。

5.4 自定义Downloader

如果默认的Downloader不能满足需求,我们可以自定义Downloader来实现特定的下载逻辑。要自定义Downloader,需要创建一个Python类,并实现以下方法:

●fetch(request, spider):用于处理下载请求。该方法需要接收一个Request对象和一个Spider对象作为参数,并返回一个包含响应数据的Response对象。在该方法中,可以使用Python标准库中的HTTP客户端库(如urllib、requests等)来发送HTTP请求,并获取响应数据。
●close():用于在Downloader关闭时进行清理操作。该方法可选,可以根据需要进行实现。

通过自定义Downloader,我们可以实现更复杂的下载逻辑,例如使用代理、处理验证码、处理动态加载等。

六、Engine引擎

6.1 Engine的概述

Engine是Scrapy框架的核心组件,负责协调和控制整个爬取过程。它管理着Spider、Scheduler、Downloader和Pipeline等组件,确保它们按照正确的顺序和逻辑进行工作。

6.2 Engine的工作流程

Engine的工作流程如下:

(1)初始化Engine,并加载Scrapy项目的配置。
(2)加载并启动Spider,将Spider的起始URL传递给Scheduler。
(3)从Scheduler中获取下一个要爬取的请求(Request)。
(3)将请求传递给Downloader进行下载。
(4)等待Downloader返回响应(Response)。
(5)将响应传递给相应的Spider进行处理。
(6)Spider解析响应数据,提取所需的信息,并生成新的请求或Item对象。
(7)将新生成的请求传递给Scheduler进行调度。
(8)将生成的Item对象传递给Pipeline进行处理。
(9)重复步骤3-9,直到Scheduler中没有更多的请求或满足停止条件。
(10)关闭Engine,释放资源。

6.3 Engine的配置

在Scrapy项目的settings.py文件中,可以通过设置以下参数来配置Engine的行为:

●CONCURRENT_REQUESTS:用于设置同时进行的下载请求数量。该参数可以控制并发度,即Downloader同时处理的请求数量。
●CONCURRENT_REQUESTS_PER_DOMAIN:用于设置每个域名同时进行的下载请求数量。该参数可以限制对同一网站的并发访问量,避免对目标网站造成过大的访问压力。
●CONCURRENT_REQUESTS_PER_IP:用于设置每个IP地址同时进行的下载请求数量。该参数可以进一步细化对并发访问量的控制,限制对同一IP地址的请求频率。
●DOWNLOAD_TIMEOUT:用于设置下载请求的超时时间(秒)。如果请求在超时时间内没有响应,Engine将引发超时异常。
●ENGINE_STOPPER:用于指定Engine的停止条件。可以自定义一个类,并实现should_stop()方法来判断是否满足停止条件。

6.4 自定义Engine

在Scrapy框架中,一般情况下不需要自定义Engine,因为Engine的核心功能已经由框架提供,并且可以通过配置参数进行调整。但是,如果需要实现特殊的爬取逻辑或集成其他功能,可以通过继承Scrapy的Engine类来自定义Engine。自定义Engine时,可以重写start()和stop()方法来实现自定义的启动和停止逻辑,也可以重写其他方法来扩展Engine的功能。需要注意的是,自定义Engine需要熟悉Scrapy框架的内部工作原理和各个组件之间的交互方式。

七、Logging日志系统

7.1 日志系统的概述

日志系统是Scrapy框架中用于记录和跟踪爬虫运行信息的组件。通过日志系统,我们可以方便地查看爬虫的运行状态、调试问题、监控异常等。

7.2 日志级别的概念

Scrapy的日志系统使用Python的标准库logging模块实现,支持多种日志级别,包括:

●DEBUG:详细信息,用于开发和调试阶段。
●INFO:一般信息,用于了解爬虫的运行情况。
●WARNING:警告信息,用于指出可能的问题或异常。
●ERROR:错误信息,用于记录发生的错误。
●CRITICAL:严重错误,用于记录严重的问题或异常。

7.3 日志配置

在Scrapy项目的settings.py文件中,可以通过设置以下参数来配置日志系统的行为:

●LOG_LEVEL:用于设置日志级别。可选值包括DEBUG、INFO、WARNING、ERROR、CRITICAL。默认级别为WARNING。
●LOG_FILE:用于指定日志文件的路径。如果设置了这个参数,日志将输出到指定的文件中。
●LOG_ENABLED:用于启用或禁用日志功能。设置为True时,启用日志功能;设置为False时,禁用日志功能。默认值为True。
●LOG_STDOUT:用于将日志输出到标准输出(stdout)。设置为True时,将日志输出到控制台;设置为False时,不输出到控制台。默认值为False。
●LOG_FORMAT:用于设置日志的格式。格式化字符串可以根据需要自行定制,支持%levelname、%message等占位符。

7.4 自定义日志处理程序

如果默认的日志处理程序不能满足需求,我们可以自定义一个处理程序来自定义日志的行为。要自定义日志处理程序,需要创建一个Python类,并实现以下方法:

●__init__(self, logger, log_level, log_format):初始化方法,接收一个logger对象、一个log_level参数和一个log_format参数。
●handle(self, record):处理日志记录的方法。该方法接收一个Record对象作为参数,并返回一个处理后的日志记录对象。可以对该方法进行重写,实现自定义的日志处理逻辑。

通过自定义日志处理程序,我们可以对日志进行更详细的处理和定制化输出,例如将日志发送到远程服务器、将日志写入数据库等。

八、测试

刚刚上面定义的item、itemloder、spiders、pipelines跑一下试试:

在控制台输入:scrapy crawl baidu_url,效果(日志LOG_LEVEL = "ERROR"):

下载的csv文件内容:

成功的获取了百度首页的所有链接信息。

至此,我们对Scrapy框架的核心组件介绍就完毕了。下一篇我们以58同城网站抓取为样例,写一个实际案例来训练Scrapy框架的使用。

转载请注明出处:https://guangzai.blog.csdn.net/article/details/135187400

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

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

相关文章

数学的雨伞下:理解世界的乐趣

这本书没有一个公式&#xff0c;却讲透了数学的本质&#xff01; 《数学的雨伞下&#xff1a;理解世界的乐趣》。一本足以刷新观念的好书&#xff0c;从超市到对数再到相对论&#xff0c;娓娓道来。对于思维空间也给出了一个更容易理解的角度。 作者&#xff1a;米卡埃尔•洛奈…

Ubuntu20.04纯命令配置PCL(点云库)

Ubuntu20.04纯命令配置PCL&#xff08;点云库&#xff09; 最近在学习点云库&#xff08;PCL&#xff09;的使用&#xff0c;第一步就是在自己的电脑安装配置PCL。 首先&#xff0c;对于ubuntu 16.04以上版本&#xff0c;可以直接使用命令进行安装&#xff0c;新建好一个文件夹…

分析冒泡排序

#include <stdio.h> int main() { int arr[10] { 2,5,1,3,6,4,7,8,9,0 }; int i 0; int j 0; for( i 0 ;i < sizeof(arr)/sizeof(arr[0]) - 1 ; i) 红色的代表数组一共有n个元素&#xff0c;则需要n-1次 { for( j 0 // 这里可以让数组从哪一…

人工智能轨道交通行业周刊-第69期(2023.12.11-12.24)

本期关键词&#xff1a;集装箱智能管理、智慧工地、智能应急机器人、车辆构造、大模型推理 1 整理涉及公众号名单 1.1 行业类 RT轨道交通人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟VSTR铁路与城市轨道交通RailMetro轨…

UG在实体上刻字

当我们想在实体上显示文字的时候&#xff0c;需要用到文本命令&#xff0c;菜单-插入-曲线-文本 文本命令中的具体用法 当在曲线和平面上显示文字的时候&#xff0c;只需要输入文字&#xff0c;并选中相应的曲线或者平面即可 当在曲面上显示文字的时候&#xff0c;设置如下 当文…

Vue3中的混入(mixins)

本文主要介绍Vue3中的混入&#xff08;mixins&#xff09;。 目录 一、在普通写法中使用混入&#xff1a;二、在setup写法中使用混入&#xff1a; 混入是Vue中一种用于在组件中共享可复用功能的特性。在Vue 3中&#xff0c;混入的使用方式有所改变。 一、在普通写法中使用混入…

c++11--类型自动推导

1.自动类型推断 1.1.auto a.auto声明变量的类型必须由编译器在编译时期推导而得。 int main(){double foo();auto x 1;//x类型为intauto y foo();// y类型为doubleauto z;// errreturn 0; }b.auto声明得变量必须被初始化。 c.针对指针和引用 推导类型是指针类型时&#xff0…

抠图、换背景、正装图证件照制作方法

本篇灵感是最近又要使用别的底色的正装照的图片。上学的时候&#xff0c;要求证件照的底色是蓝底、党员档案里要求图片的底色是红底、 将来上班的证件照要求是白底&#xff0c;并且无论是考研还是找工作都是制作简历的时候&#xff0c;根据简历的样板不同需要更换不同的底色。 …

HBase 集群搭建

文章目录 安装前准备兼容性官方网址 集群搭建搭建 Hadoop 集群搭建 Zookeeper 集群解压缩安装配置文件高可用配置分发 HBase 文件 服务的启停启动顺序停止顺序 验证进程查看 Web 端页面 安装前准备 兼容性 1&#xff09;与 Zookeeper 的兼容性问题&#xff0c;越新越好&#…

jQuery实现响应式瀑布流 - 实现灯箱效果

在这之前&#xff0c;有写过一篇关于实现瀑布流的文章&#xff0c;后期有人留言提出需要添加灯箱效果的功能&#xff0c;所以这次则讲述下如何实现此功能。由于该篇接上篇写的&#xff1a;jQuery实现响应式瀑布流效果&#xff08;jQueryflex&#xff09;_jquery瀑布流插件-CSDN…

驾驶未来:百度Apollo自动驾驶技术的探索与实践(文末赠送apollo周边)

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《linux深造日志》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! ⛳️ 粉丝福利活动 ✅参与方式&#xff1a;通过连接报名观看课程&#xff0c;即可免费获取精美周边 ⛳️活动链接&#xf…

Java之Synchronized与锁升级

Synchronized与锁升级 一、概述 在多线程并发编程中 synchronized 一直是元老级角色&#xff0c;很多人都会称呼它为重量级锁。但是&#xff0c;随着 Java SE 1.6 对 synchronized 进行了各种优化之后&#xff0c;有些情况下它就并不那么重了。 本文详细介绍 Java SE 1.6 中为…

智能算法(GA、DBO等)求解阻塞流水车间调度问题(BFSP)

先做一个声明&#xff1a;文章是由我的个人公众号中的推送直接复制粘贴而来&#xff0c;因此对智能优化算法感兴趣的朋友&#xff0c;可关注我的个人公众号&#xff1a;启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法&#xff0c;经典的&#xff0c;或者是近几年…

七天搞定java接口自动化测试实战,一文搞定...

前言 无论是自动化测试还是自动化部署&#xff0c;撸码肯定少不了&#xff0c;所以下面的基于java语言的接口自动化测试&#xff0c;要想在业务上实现接口自动化&#xff0c;前提是要有一定的java基础。 如果没有java基础&#xff0c;也没关系。这里小编也为大家提供了一套jav…

Gaussian-Splatting 训练并导入Unity中

这个周末玩点啥~&#x1f41e; &#x1f354;资源下载&#x1f365;环境安装&#x1f4a1;安装C编译工具&#x1f4a1;安装Python&#x1f4a1;安装CUDA&#x1f4a1;添加ffmpeg到环境变量Path&#x1f4a1;pytorch安装&#x1f4a1;tqdm 安装&#x1f4a1;diff-gaussian-raste…

元素的显示与隐藏(常用)

场景&#xff1a;类似网站广告&#xff0c;当我们点击关闭就不见了&#xff0c;但是我们重新刷新页面&#xff0c;会重新出现&#xff01; 本质&#xff1a;让一个元素在页面中隐藏或者显示出来。 1. display 显示隐藏元素 但是不保留位置 2. visibility 显示隐藏元素 但是保留…

luceda ipkiss教程 53:在版图上加中文

要在版图上加中文&#xff0c;如&#xff1a; 可以通过如下方法实现&#xff1a; 首先&#xff0c;可以在ppt中加入文本框&#xff0c;在文本框中输入想要加到版图上的中文内容&#xff0c;如&#xff0c;复旦大学&#xff0c;并将文本框存为windows位图。 其次&#xff0c;通…

智能优化算法应用:基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.金豺算法4.实验参数设定5.算法结果6.参考文献7.MA…

在Redis客户端设置连接密码 并演示密码登录

我们先连接到Redis服务 然后 我们要输入 CONFIG SET requirepass “新密码” 例如 CONFIG SET requirepass "A15167"这样 密码就被设置成立 A15167 我们 输入 AUTH 密码 例如 AUTH A15167这里 返回OK说明成功了 然后 我们退出在登录就真的需要 redis-cli -h IP地…

C语言沉浸式刷题【C语言必刷题】

1.猜凶手 某地发生了一起谋杀案&#xff0c;警察通过排查确定杀人凶手必为四个嫌疑犯的一个&#xff0c;以下是4个嫌犯的供词。已知&#xff08;请编写代码找出凶手&#xff09; A说&#xff1a;不是我。 B说&#xff1a;是C。C说&#xff1a;是D。D说&#xff1a;C再胡说。 程…