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

如何终止并退出由exec()调用的异步脚本?

解决Scrapy管道调用异步脚本后进程无法退出的问题

问题场景

我有一个bs4_scraper.py文件,其中的bs4_scraper是异步函数,单独在终端执行python bs4_scraper.py时能正常完成并优雅退出。但在Scrapy的WritePidErrorsPipeline的close_spider方法中,通过exec(open(bs4_file).read())调用该脚本后,任务虽能运行完成,但进程会僵死无法自动退出。

相关代码

bs4_scraper.py

async def bs4_scraper():
    limit = Semaphore(8)
    tasks = []
    pids = get_pids_from_file_generator()
    for pid in pids:
        task = create_task(scrap(pid, fake_header(), limit))
        tasks.append(task)
    result = await gather(*tasks)
    return result


if __name__ == "__main__":
    try:
        run(bs4_scraper())
    except Exception as e:
        logger.error(e)

Scrapy管道代码

class WritePidErrorsPipeline:
    def close_spider(self, spider):
        pid_errors_file = generate_pid_errors_file()
        pg = PostgresDB()
        non_inserted_ids = pg.select_non_inserted_ids(pid_errors_file)
        if non_inserted_ids:
            self.insertion_errors_file(non_inserted_ids)
            bs4_file = os.path.abspath("bs4/bs4_scraper.py")
            exec(open(bs4_file).read()) # 问题所在
        else:
            logger.info("[SUCCESS]: There are no items missing")

    def insertion_errors_file(
        self,
        non_inserted_ids: List[Tuple[str]],
        output_file: str = "insertion_errors.log",
    ) -> str:
        with open(output_file, "w", encoding="utf-8") as f:
            for non_inserted_id in non_inserted_ids:
                f.write(f"{non_inserted_id[0]}\n")
        return output_file

补充说明:scrap是发起异步请求的异步函数,get_pids_from_file_generator是读取txt文件生成pid的生成器函数。

解决方案

1. 重构异步脚本为可导入模块

修改bs4_scraper.py,将异步执行逻辑封装成独立的同步调用函数,方便外部模块导入使用:

import asyncio
from asyncio import Semaphore, create_task, gather

# 原异步函数保持不变
async def bs4_scraper():
    limit = Semaphore(8)
    tasks = []
    pids = get_pids_from_file_generator()
    for pid in pids:
        task = create_task(scrap(pid, fake_header(), limit))
        tasks.append(task)
    result = await gather(*tasks)
    return result

# 封装同步调用函数,负责管理事件循环
def run_scraper():
    try:
        asyncio.run(bs4_scraper())
    except Exception as e:
        logger.error(e)

# 保留原终端执行入口
if __name__ == "__main__":
    run_scraper()

2. 修改Scrapy管道调用方式

替换exec的调用方式,改为导入模块并调用封装好的函数,确保事件循环正确关闭:

import os
import sys
# 将bs4目录添加到Python路径,确保能导入模块
sys.path.append(os.path.abspath("bs4"))
from bs4_scraper import run_scraper

class WritePidErrorsPipeline:
    def close_spider(self, spider):
        pid_errors_file = generate_pid_errors_file()
        pg = PostgresDB()
        non_inserted_ids = pg.select_non_inserted_ids(pid_errors_file)
        if non_inserted_ids:
            self.insertion_errors_file(non_inserted_ids)
            # 替换exec为函数调用
            run_scraper()
        else:
            logger.info("[SUCCESS]: There are no items missing")

    # insertion_errors_file方法保持不变
    def insertion_errors_file(
        self,
        non_inserted_ids: List[Tuple[str]],
        output_file: str = "insertion_errors.log",
    ) -> str:
        with open(output_file, "w", encoding="utf-8") as f:
            for non_inserted_id in non_inserted_ids:
                f.write(f"{non_inserted_id[0]}\n")
        return output_file

原因说明

  • 使用exec(open(bs4_file).read())会让异步代码运行在Scrapy进程的上下文环境中,异步事件循环的资源无法被正确回收,导致进程僵死。
  • 改为导入模块并调用asyncio.run()封装的函数后,asyncio.run()会负责创建独立的事件循环,在任务完成后自动关闭循环并释放所有相关资源,确保进程能正常退出。

内容的提问来源于stack exchange,提问作者Guilherme Massoqueto

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 07:12:53