部署在Heroku的Flask应用首次访问正常,页面重载后报连接拒绝错误
故障原因
- 核心触发点:Selenium的Chrome驱动实例被定义为全局启动对象,且在第一次请求的
finally块中执行了driver.quit()。首次请求到达时,全局初始化的驱动处于可用状态,因此访问正常;但第一次请求处理完成后,驱动进程被直接销毁,后续所有重载/新请求再调用已经退出的驱动发起访问时,无法连接到本地Chrome服务,最终从底层抛出ConnectionRefusedError,逐层往上包装为NewConnectionError、MaxRetryError。 - 代码附带的其他隐患:
- 缺少
import os模块导入,依赖环境变量读取Chrome、驱动路径的逻辑在部署环境下会直接抛出名称错误 - 异常处理逻辑存在漏洞:如果
WebDriverWait等待元素超时,element变量不会被赋值,此时直接在finally块读取element.text会触发变量未定义错误 - 启动逻辑未适配Heroku部署规则:Flask默认监听
127.0.0.1:5000,无法接收Heroku平台动态路由转发的外部请求 - 代码中使用的
xpath变量未提前定义,运行时会直接抛出名称错误
- 缺少
修复方案
- 移除全局初始化的driver实例,改为每个请求单独创建driver实例,请求处理完成(无论成功失败)后再销毁当前请求对应的driver,从根源避免跨请求调用已销毁实例的问题。
- 补上缺失的模块导入,修正异常处理逻辑,保证driver初始化成功后才会执行退出操作,避免空对象报错。
- 适配Heroku的端口分配规则,监听平台动态下发的
PORT环境变量,绑定0.0.0.0地址保证外部请求可达。
修复后的可直接部署代码如下:
import os from flask import Flask from flask import request from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException app = Flask(__name__) def init_chrome_driver(): chrome_options = webdriver.ChromeOptions() chrome_options.binary_location = os.environ.get("GOOGLE_CHROME_BIN") chrome_options.add_argument("--headless") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-gpu") chrome_options.add_argument("--remote-debugging-port=9222") driver = webdriver.Chrome( executable_path=os.environ.get("CHROMEDRIVER_PATH"), options=chrome_options ) return driver @app.route('/', methods=['GET', 'POST']) def handleRequests(): driver = None resp_data = "" target_url = str(request.args.get('url')) # 替换为实际业务需要匹配的XPath规则 target_xpath = "//*[@id='content']" try: driver = init_chrome_driver() driver.get(target_url) element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, target_xpath)) ) resp_data = element.text except TimeoutException: resp_data = "页面目标元素加载超时" except Exception as e: resp_data = f"请求处理失败: {str(e)}" finally: if driver: driver.quit() return str(resp_data) if __name__ == "__main__": listen_port = int(os.environ.get("PORT", 5000)) app.run(host="0.0.0.0", port=listen_port)
补充说明:如果是低流量场景,单请求创建销毁driver实例的逻辑足够稳定;如果后续请求量上升,可以自行实现driver连接池复用实例,降低频繁启动销毁浏览器带来的性能开销。
内容的提问来源于stack exchange,提问作者Stack_Sucks
相关产品推荐
相关产品推荐

