利用scrapy抓取图集(day4)

整体理解

scrapy是一个爬虫框架,通过它,可以快速构建一个高效且合格的爬虫。

接下来,通过下图理解scrapy框架的组成。

**Scrapy Engine (引擎):**整个爬虫系统的cpu,负责处理所有组件之间的数据流动。

**Spiders(爬虫):**留给用户写代码的地方(接口),一般用户需要写如何发送请求、如何解析响应等。

**Scheduler (调度器):**接收来自引擎发来的请求,将请求放到队列中,并对它们去重。当引擎需要执行一个请求(交给Downloader下载)时,调度器需要从队列中拿出一个任务(链接)给引擎。

**Downloader (下载器):**它唯一的职责就是根据引擎给它的请求,去互联网 (Internet) 上下载网页,然后把下载结果包装成一个响应 (Response) 对象返回给引擎。

Item Pipeline (项目管道): 数据的“加工厂”。当 Spider 从网页中提取出数据 (Items) 后,这些数据会被送到这里进行一系列处理,比如:

  • 数据清洗和验证。
  • 丢弃无用的数据。
  • 将最终干净的数据存入数据库、CSV 文件或 JSON 文件中。

Downloader Middlewares (下载器中间件):位于引擎和下载器之间的“安检通道”。当请求从引擎发往下载器,以及响应从下载器返回引擎时,都会经过这里。可以用它来:

  • 处理请求: 修改或添加请求头 (如 User-Agent)、设置代理 IP、处理 robots.txt 规则等。

  • 处理响应: 在响应被 Spider 解析前进行一些预处理。

Spider Middlewares (爬虫中间件): 位于引擎和 Spider 之间的**“安检通道”**。它主要处理 Spider 的输入(响应)和输出(请求和数据项)。

image-20250709200244954

运作流程

代码写好,程序开始运行…

  • 1 引擎:Hi!Spider, 你要处理哪一个网站?
  • 2 Spider:老大要我处理xxxx.com。
  • 3 引擎:你把第一个需要处理的URL给我吧。
  • 4 Spider:给你,第一个URL是xxxxxxx.com。
  • 5 引擎:Hi!调度器,我这有request请求你帮我排序入队一下。
  • 6 调度器:好的,正在处理你等一下。
  • 7 引擎:Hi!调度器,把你处理好的request请求给我。
  • 8 调度器:给你,这是我处理好的request
  • 9 引擎:Hi!下载器,你按照老大的下载中间件的设置帮我下载一下这个request请求
  • 10 下载器:好的!给你,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后引擎告诉调度器,这个request下载失败了,你记录一下,我们待会儿再下载)
  • 11 引擎:Hi!Spider,这是下载好的东西,并且已经按照老大的下载中间件处理过了,你自己处理一下(注意!这儿responses默认是交给def parse()这个函数处理的)
  • 12 Spider:(处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的Item数据。
  • 13 引擎:Hi !管道 我这儿有个item你帮我处理一下!调度器!这是需要跟进URL你帮我处理下。然后从第四步开始循环,直到获取完老大需要全部信息。
  • 14 管道调度器:好的,现在就做!

基本使用

对于用户来说,在Scrapy的这些组件中,留给用户编写的接口主要是:Spiders、Item Pipeline、Downloader Middlewares。

一般情况下,要使用Scrapy爬取内容,用户需要实现以下3个模块的代码编写:

Spiders (爬虫): 这是最核心、必须由用户编写的部分。所有的爬取逻辑,包括从哪个 URL 开始、如何跟踪链接以及如何从页面中提取数据,都在这里定义。

Item Pipeline (项目管道): 这是第二常用的用户接口。当从 Spider 中提取出数据(Items)后,如果想进行数据清洗、验证、去重或将其存储到数据库(如 MySQL, MongoDB)等特定地方,就需要自己编写 Pipeline。

Downloader Middlewares (下载器中间件): 这是非常常用的用户接口,尤其是在应对反爬虫策略时。你需要通过编写或启用下载器中间件来执行以下操作:

  • 设置随机的 User-Agent
  • 添加代理 IP。
  • 处理复杂的 Cookie 或 Javascript 逻辑。

使用教程如下:

创建项目

创建一个名为scrapy_tutorial的项目。

1
scrapy startproject scrapy_tutorial

目录结果是这样的。

1
2
3
4
5
6
7
8
9
10
scrapy_tutorial/
scrapy.cfg # 部署配置文件
scrapy_tutorial/ # 项目的Python模块
__init__.py
items.py # 定义Item
middlewares.py # 定义中间件
pipelines.py # 定义项目管道
settings.py # 设置文件
spiders/ # 放置爬虫的目录
__init__.py

添加代码

往文件spiders/__init__.py中添加代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# This package will contain the spiders of your Scrapy project
#
# Please refer to the documentation for information on how to create and manage
# your spiders.
import scrapy


class QuotesSpider(scrapy.Spider):
name = 'quotes'
start_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/'
]

def parse(self, response):
page = response.url.split('/')[-2]
print("hello")
filename = f'quotes-{page}.html'
with open(filename, 'wb') as f:
f.write(response.body)
self.log(f'Saved file {filename}')

爬取

执行爬虫,scrapy框架回根据输入的quotes(爬虫名字)去爬取数据。

1
scrapy crawl quotes

案例爬取:站酷

目标网站:站酷ZCOOL-设计师互动平台-打开站酷,发现更好的设计!

这次爬取的目标是:图片、标题、点赞数、用户名,以用户名、标题、点赞数作为文件夹,将页面内的目标图片爬取下来,放到对应的文件夹内。

image-20250710150941140

初步分析

先不着急写爬虫,先看看有没有需要逆向的参数。

根据抓包,首页的请求标头是这样的,没有加密字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET / HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: max-age=0
Connection: keep-alive
Cookie: HWWAFSESID=625ae7945294c79738; HWWAFSESTIME=1752131201045; _sm=197f32865943484-051bea0fbaabfd8-4c657b58-1822436-197f3286595616f; meitustat={%22wgid%22:%22197f32865943484-051bea0fbaabfd8-4c657b58-1822436-197f3286595616f%22}; Hm_lvt_e6331320ac7de17020046faecd5fa6b8=1752131201; HMACCOUNT=67DBE82315EBBF65; psid=197f328686a917-0b9d0a9aef36da-4c657b58-1bcee4-197f328686b3a80; sensorsdata2015jssdkchannel=%7B%22prop%22%3A%7B%22_sa_channel_landing_url%22%3A%22%22%7D%7D; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22197f328686e1b39-0af9bfce8e4ade8-4c657b58-1822436-197f328686f35a%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTk3ZjMyODY4NmUxYjM5LTBhZjliZmNlOGU0YWRlOC00YzY1N2I1OC0xODIyNDM2LTE5N2YzMjg2ODZmMzVhIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%22%2C%22value%22%3A%22%22%7D%2C%22%24device_id%22%3A%22197f328686e1b39-0af9bfce8e4ade8-4c657b58-1822436-197f328686f35a%22%7D; newMeitustat_wgid=197f32865943484-051bea0fbaabfd8-4c657b58-1822436-197f3286595616f; customer=2; z_law_undefined=false; session=eyJjdXJyZW50VXNlciI6bnVsbH0=; session.sig=1VmncKqjATz0GMOcrellG1zEeXo; recommend_tip=1; r_drefresh_count=2; Hm_lpvt_e6331320ac7de17020046faecd5fa6b8=1752132340; eventPushTime=1664159429345
Host: www.zcool.com.cn
Referer: https://www.zcool.com.cn/home
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

而在返回的页面中,存在指向存放插画具体信息的链接。

image-20250710153157204

但在页面源代码中,并没有类似的内容,大概率是ajax。

搜索“近期四幅复古美漫风插画”,看看哪个请求的响应是这个。

image-20250710173402694

请求参数p=1&ps=20&column=4不像加密,请求头也没有加密字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /p1/discover/first?p=1&ps=20&column=4 HTTP/1.1
AbCurrentList:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Cookie: HWWAFSESID=ef49ebbddb7f9f374c; HWWAFSESTIME=1752135440608; _sm=197f369207f2968-0319a2bceab60b8-26011151-1822436-197f3692080551f; meitustat={%22wgid%22:%22197f369207f2968-0319a2bceab60b8-26011151-1822436-197f3692080551f%22}; psid=197f369220f262b-0b4e2b67741871-26011151-1bcee4-197f369221038bb; sensorsdata2015jssdkchannel=%7B%22prop%22%3A%7B%22_sa_channel_landing_url%22%3A%22%22%7D%7D; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22197f36922131c0b-0b4e2b67741871-26011151-1822436-197f36922142e22%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTk3ZjM2OTIyMTMxYzBiLTBiNGUyYjY3NzQxODcxLTI2MDExMTUxLTE4MjI0MzYtMTk3ZjM2OTIyMTQyZTIyIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%22%2C%22value%22%3A%22%22%7D%2C%22%24device_id%22%3A%22197f36922131c0b-0b4e2b67741871-26011151-1822436-197f36922142e22%22%7D; newMeitustat_wgid=197f369207f2968-0319a2bceab60b8-26011151-1822436-197f3692080551f; customer=2; z_law_undefined=false; session=eyJjdXJyZW50VXNlciI6bnVsbH0=; session.sig=1VmncKqjATz0GMOcrellG1zEeXo; z_ex_group=%7B%7D
Host: www.zcool.com.cn
Referer: https://www.zcool.com.cn/?page=1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

根据分析,请求参数p=1指的页数为1;ps未知,但ps是不变的,大概率无关紧要;column=4指的是展示的页面中,一行4个item。

image-20250710174134510

接下来,打开具体的作品链接搜集信息,发现页面内无法使用F12(开发者工具),也无法右键获得图片的链接地址。

image-20250710153307591

然而,在搜索栏中,可以打开开发者工具。

image-20250710153844494

请求的链接是一个静态页面,这回不涉及ajax了,真好。

image-20250710154117910

同时,这个请求标头同样没有加密字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /work/ZNzIyMTQwMzY=.html HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: max-age=0
Connection: keep-alive
Cookie: HWWAFSESID=625ae7945294c79738; HWWAFSESTIME=1752131201045; _sm=197f32865943484-051bea0fbaabfd8-4c657b58-1822436-197f3286595616f; meitustat={%22wgid%22:%22197f32865943484-051bea0fbaabfd8-4c657b58-1822436-197f3286595616f%22}; Hm_lvt_e6331320ac7de17020046faecd5fa6b8=1752131201; HMACCOUNT=67DBE82315EBBF65; psid=197f328686a917-0b9d0a9aef36da-4c657b58-1bcee4-197f328686b3a80; sensorsdata2015jssdkchannel=%7B%22prop%22%3A%7B%22_sa_channel_landing_url%22%3A%22%22%7D%7D; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22197f328686e1b39-0af9bfce8e4ade8-4c657b58-1822436-197f328686f35a%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTk3ZjMyODY4NmUxYjM5LTBhZjliZmNlOGU0YWRlOC00YzY1N2I1OC0xODIyNDM2LTE5N2YzMjg2ODZmMzVhIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%22%2C%22value%22%3A%22%22%7D%2C%22%24device_id%22%3A%22197f328686e1b39-0af9bfce8e4ade8-4c657b58-1822436-197f328686f35a%22%7D; newMeitustat_wgid=197f32865943484-051bea0fbaabfd8-4c657b58-1822436-197f3286595616f; customer=2; z_law_undefined=false; session=eyJjdXJyZW50VXNlciI6bnVsbH0=; session.sig=1VmncKqjATz0GMOcrellG1zEeXo; r_drefresh_count=1; Hm_lpvt_e6331320ac7de17020046faecd5fa6b8=1752131669; recommend_tip=1
Host: www.zcool.com.cn
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

利用scrapy框架写爬虫

在文件夹spiders的__init__.py中。

将默认的start_urls换成函数start_requests。

1
2
3
4
5
6
7
8
9
10
11
12
# 爬取页数
num_pages = 1
# 基础URL
page_url = "https://www.zcool.com.cn/p1/discover/first?p=XXX&ps=20&column=4"

# 生成起始URL列表
# start_urls = [

# ]
def start_requests(self):
for i in range(1, self.num_pages + 1):
yield scrapy.Request(url=self.page_url.replace("XXX", str(i)), callback=self.parse)

简单的伪装一下headers,在文件settings.py中,修改成下面的语句。

1
2
3
4
# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
}

下面插入一个小插曲,之前没分析出ajax的时候做的一些操作。


为了防止出现下面这个情况,还需要修改设置。(我之前爬主页的截图)

image-20250710164802113

在settings.py中,修改ROBOTSTXT_OBEY

1
ROBOTSTXT_OBEY = False

这回没问题了,接下来定位首页的每个Item。

image-20250710165139324


回到正轨继续分析,修改请求链接后能访问到目标链接了。

image-20250710182122220

收集pageUrl。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def parse(self, response):
"""
解析列表页的JSON,为每个作品生成一个详情页的抓取请求。
"""
try:
data = response.json()
except json.JSONDecodeError:
self.logger.error(f"Failed to decode JSON from {response.url}")
return

if data and data.get('datas'):
for item in data['datas']:
content = item.get('content')
if not content:
continue

page_url = content.get('pageUrl')

if page_url and '/work/' in page_url:
creator_obj = content.get('creatorObj', {})

title = content.get('title', '无标题')
username = creator_obj.get('username', '匿名用户')
recommend_count = content.get('recommendCount', 0)

# 使用 cb_kwargs 将数据传递给回调函数,同时对于返回的response,使用self.parse_work_details进行解析
yield scrapy.Request(
url=page_url,
callback=self.parse_work_details,
cb_kwargs={
'title': title,
'username': username,
'recommend_count': recommend_count
}
)
image-20250710183325448

针对pageUrl返回的页面,需要用另一个parser来解析,用来定位图片链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  def parse_work_details(self, response, title, username, recommend_count):
# 获得title
self.logger.info(f"Parsing details for: {title}")

json_data_string = response.css('script#__NEXT_DATA__::text').get()

image_urls = []
if json_data_string:
try:

data = json.loads(json_data_string)

product_images = data.get('props', {}).get('pageProps', {}).get('data', {}).get('productImages', [])

# 定位并获得url
image_urls = [img.get('url') for img in product_images if img.get('url')]
print(f"Found {len(image_urls)} images for {title} by {username}")

except json.JSONDecodeError:
self.logger.error(f"Failed to parse JSON from page: {response.url}")

if image_urls:
# 清理文件名中的非法字符
clean_title = re.sub(r'[\\/*?:"<>|]', "", title).strip()
clean_username = re.sub(r'[\\/*?:"<>|]', "", username).strip()

# 构建文件夹名称
folder_name = f"{clean_title}-{clean_username}-{recommend_count}赞"

# 传给管道
yield {
'image_urls': image_urls,
'folder_name': folder_name
}
else:
self.logger.warning(f"No images found on page: {response.url}")

函数parse_work_details会为管道带来图片链接、文件夹名字。

接下来在管道类中,写对图片链接的处理方式。

定义了一个新类ZcoolImagePipeline,继承ImagesPipeline——ImagesPipeline是Scrapy专门爬图片的类,所以这里覆写函数get_media_requests和file_path,自己定义对图片的处理逻辑即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ZcoolImagePipeline(ImagesPipeline):
# 负责统一进行图片下载
def get_media_requests(self, item, info):
# 获得图片链接
for i, image_url in enumerate(item.get('image_urls', [])):
# 下载图片
print(f"Requesting image {i} from URL: {image_url}")
# 使用meta的方式,传递数据,以便保存时可以取出来定义文件夹名
yield scrapy.Request(image_url, meta={'item': item, 'image_index': i})

# 负责图片的保存路径
def file_path(self, request, response=None, info=None, *, item=None):
# 获得item,即url和文件夹名的字典
image_item = request.meta['item']
# 获得图片名,这里以序号作为图片名
image_index = request.meta['image_index']
# 从item中获得文件夹名,早在parse_work_details设置的文件夹名
folder = image_item['folder_name']

# 创建文件
file_extension = os.path.splitext(urlparse(request.url).path)

# 如果无后缀名的话,默认用jpg
if not file_extension:
file_extension = '.jpg'

# 返回存储的文件名(带路径的那种)
return f'{folder}/{image_index}{file_extension}'

如果使用了ImagesPipeline,还需要再Setting中定义刚才的管道类和存储路径。

1
2
3
4
5
6
7
8
ITEM_PIPELINES = {
# "zcool.pipelines.ZcoolPipeline": 300,
# 我们定义的管道类,100是优先级,越小优先级越高
"zcool.pipelines.ZcoolImagePipeline": 100,
}

# 最终的存储路径是IMAGES_STORE + 函数file_path的返回值
IMAGES_STORE = "C:\\Users\\19415\\Desktop\\爬虫\\Scrapy_Projects\\zcool\\downloads"

跑结果!跑不出来,做了好久的检查,代码没问题,最后看日志,发现ImagesPipeline需要按照Pillow。

image-20250710225446915

重新启动!

image-20250710234121141

这回完美了,下载到了很多图片。

image-20250710234721065

如果爬取太慢,可以设置一下这个。

image-20250710234234050

最后,申明一下,只是做一个爬虫学习!不是想引起DDoS攻击!侵权删!!