You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scrapy回调问题:需获取数据库自增ID爬取后续页面

问题:Scrapy实现父子条目关联爬取与数据库存储

我需要完成以下爬取流程:

  • 从目标页面爬取包含子页面URL的父条目;
  • 将父条目插入MySQL数据库,获取其auto_increment自增ID;
  • 通过父条目里的URL爬取子页面;
  • 将子页面的条目插入数据库,并用父条目的自增ID作为外键关联。

这是爬虫开发中的常见场景,但尝试用item_scraped信号在父条目处理完成后触发子页面爬取时,发现信号回调无法直接生成Request。有人提到用Spider Middleware,但不确定具体实现,且Middleware本不应该负责发起请求。我的环境是Scrapy 2.6.2 + Python 3.8,简化代码如下:

爬虫代码

import scrapy
from scrapy import signals

class MySpider(scrapy.Spider):
    name = 'MySpider'
    allowed_domains = ['www.somedomain.com']
    start_urls = [
        'https://www.somedomain.com/someurl/', 
        ]

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = cls()
        # 尝试绑定条目处理完成后的回调
        crawler.signals.connect(spider.item_scraped, signals.item_scraped)
        return spider

    def parse(self, response):
        for node in response.xpath(".//tr/td[@class='foo']"):
            yield {
                'a': ...,
                'b': ...,
                'url': ...
            }

    def item_scraped(self, item, response, spider):
###
### 此处无法正常生成Request
###
        # 尝试传递自增ID并发起子页面请求
        yield Request(item['url'], meta={'id':item['id']}, callback=self.parse_page)

    def parse_page(self, response):
        for node in response.xpath("//div[@class='something']//a"):
            yield {
                'id': response.meta['id'],
                'title': ...
            }

管道代码

import mysql.connector

class WinebotPipeline(object):

    def __init__(self):
        self.conf = {'host': ..., 'user': ..., 'password': ..., 'database': ...}
        self.connection = self.mysql_connect()
        self.cursor = self.connection.cursor()
    
    def mysql_connect(self):
        return mysql.connector.connect(**self.conf)

    def process_item(self, item, spider):
        sql = "insert into `some_table` (`a`, `b`, `url`) values (%s, %s, %s)" 
        self.cursor.execute(sql, list(item.values()))
        self.connection.commit()

        # 获取刚插入条目的自增ID
        item['id'] = self.cursor.lastrowid
        return item

解决方案

方法一:在管道中直接调度请求

管道是处理完父条目并拿到自增ID的最佳节点,可通过Scrapy的引擎直接调度子页面请求,无需依赖信号。

修改后的管道代码:

import mysql.connector
from scrapy.http import Request

class WinebotPipeline(object):

    def __init__(self):
        self.conf = {'host': ..., 'user': ..., 'password': ..., 'database': ...}
        self.connection = self.mysql_connect()
        self.cursor = self.connection.cursor()
        self.crawler = None

    # 通过from_crawler获取爬虫引擎上下文
    @classmethod
    def from_crawler(cls, crawler):
        pipeline = cls()
        pipeline.crawler = crawler
        return pipeline

    def mysql_connect(self):
        return mysql.connector.connect(**self.conf)

    def process_item(self, item, spider):
        sql = "insert into `some_table` (`a`, `b`, `url`) values (%s, %s, %s)" 
        self.cursor.execute(sql, list(item.values()))
        self.connection.commit()

        # 获取自增ID
        item['id'] = self.cursor.lastrowid
        
        # 构造子页面请求并通过引擎调度
        req = Request(
            item['url'],
            meta={'id': item['id']},
            callback=spider.parse_page
        )
        self.crawler.engine.crawl(req, spider=spider)
        
        return item

修改后的爬虫代码(移除无用的信号绑定):

import scrapy

class MySpider(scrapy.Spider):
    name = 'MySpider'
    allowed_domains = ['www.somedomain.com']
    start_urls = [
        'https://www.somedomain.com/someurl/', 
        ]

    def parse(self, response):
        for node in response.xpath(".//tr/td[@class='foo']"):
            yield {
                'a': ...,
                'b': ...,
                'url': ...
            }

    def parse_page(self, response):
        for node in response.xpath("//div[@class='something']//a"):
            yield {
                'id': response.meta['id'],
                'title': ...
            }

方法二:在信号回调中用Twisted异步调度请求

如果坚持使用item_scraped信号,可借助Twisted的Deferred实现异步请求调度,避免直接生成Request的限制。

修改后的爬虫代码:

import scrapy
from scrapy import signals
from scrapy.http import Request
from twisted.internet.defer import Deferred

class MySpider(scrapy.Spider):
    name = 'MySpider'
    allowed_domains = ['www.somedomain.com']
    start_urls = [
        'https://www.somedomain.com/someurl/', 
        ]

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = cls()
        spider.crawler = crawler
        crawler.signals.connect(spider.item_scraped, signals.item_scraped)
        return spider

    def parse(self, response):
        for node in response.xpath(".//tr/td[@class='foo']"):
            yield {
                'a': ...,
                'b': ...,
                'url': ...
            }

    def item_scraped(self, item, response, spider):
        # 通过引擎直接调度子页面请求
        req = Request(
            item['url'],
            meta={'id': item['id']},
            callback=self.parse_page
        )
        self.crawler.engine.crawl(req, spider=self)
        # 返回Deferred确保异步处理完成
        return Deferred.succeed(None)

    def parse_page(self, response):
        for node in response.xpath("//div[@class='something']//a"):
            yield {
                'id': response.meta['id'],
                'title': ...
            }

管道代码保持不变即可。


关键说明

  • crawler.engine.crawl()是Scrapy内部调度请求的核心方法,可直接将Request加入爬取队列,规避信号/管道回调无法生成Request的限制;
  • 管道中通过from_crawler获取crawler上下文是Scrapy组件的标准写法,用于获取引擎、信号等核心资源;
  • 两种方案都能保证父条目入库并拿到自增ID后,再发起子页面爬取,确保外键关联的正确性。

内容的提问来源于stack exchange,提问作者Wolf D.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 04:01:07