selenium并发请求的实现-并发日志篇
前言
前面我们使用了线程、进程、协程实现了并发。在实践操作中,报错时发现定位问题很麻烦:不清楚报错日志属于哪个并发请求的操作。
那么有没有什么办法好区分日志属于哪个的请求呢:答案是在日志格式中加上请求ID,这样每个请求就能根据ID来分辨是哪个的请求了。
实现:
首先用到的是contextvars库,contextvars模块提供了管理、存储和访问上下文局部状态的API,它在Python 3.7版本中引入。
其次用到的是UUID,它是一个通用唯一标识符,用来标明当前。
接着需要用的是 FastAPI 框架中的 HTTP 中间件(middleware),中间件可以用作日志记录、CORS处理,认证授权、性能监控、异常处理、请求/响应修改等
下面我们使用fastapi框架结合loguru日志模块来演示下如何实现并发日志。
示例代码:
1.测试日志是否正常,代码如下:
1 from uuid import uuid4 2 from contextvars import ContextVar 3 from fastapi import FastAPI, Request 4 from loguru import logger 5 6 7 # 初始化FastAPI应用 8 app = FastAPI() 9 10 # 创建线程安全的上下文变量 11 request_id_ctx = ContextVar("request_id", default="system") 12 13 # 配置loguru日志格式(使用extra字段前需要先绑定) 14 logger.configure(extra={"request_id": ""}) # 初始化extra字段 15 logger.add("app.log", 16 format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {extra[request_id]} | {message}", 17 enqueue=True) # enqueue确保多进程安全 18 19 20 @app.middleware("http") #中间件 21 async def request_middleware(request: Request, call_next): 22 # 生成唯一请求ID 23 rid = f"REQ-{uuid4()}" 24 25 # 设置到上下文变量(需要保存token防止提前回收) 26 token = request_id_ctx.set(rid) 27 28 # 将request_id存入请求对象(可选,方便在路由中获取) 29 request.state.request_id = rid 30 31 try: 32 # 处理请求前日志(通过上下文获取) 33 with logger.contextualize(request_id=rid): 34 logger.info("Request started") 35 response = await call_next(request) 36 37 finally: 38 # 确保清理上下文(关键修正:避免内存泄漏) 39 request_id_ctx.reset(token) 40 41 # 添加请求ID到响应头(可选) 42 response.headers["X-Request-ID"] = rid 43 return response 44 45 46 # 获取当前请求日志器(通过contextvars获取) 47 def get_logger(): 48 return logger.bind(request_id=request_id_ctx.get()) 49 50 51 # 测试接口 52 @app.get("/test") 53 async def test_endpoint(request: Request): 54 current_logger = get_logger() 55 current_logger.info("logger进入处理流程") 56 # 模拟业务处理 57 import time 58 time.sleep(0.1) 59 current_logger.info("logger处理完成") 60 61 return { 62 "request_id": request.state.request_id, 63 "status": "success" 64 } 65 66 if __name__ == "__main__": 67 import uvicorn 68 uvicorn.run(app, host="0.0.0.0", port=8000)
2. 修改第一篇的百度请求,使用并发实现,并加入日志,代码如下:
1 import time 2 from selenium import webdriver 3 from selenium.webdriver.common.by import By 4 from selenium.webdriver.chrome.options import Options 5 from concurrent.futures import ThreadPoolExecutor 6 import asyncio 7 from uuid import uuid4 8 from contextvars import ContextVar 9 from loguru import logger 10 from fastapi import FastAPI, Request 11 import sys 12 13 app = FastAPI() 14 executor = ThreadPoolExecutor(max_workers=10) # 创建一个线程池,最大线程数设置为10 15 request_id_ctx = ContextVar("request_id", default="system") 16 logger.configure(handlers=[], extra={"request_id": ""}) # 初始化extra字段.handlers清空默认日志输出 17 logger.add("app.log",format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {extra[request_id]} | {file.name}:{line} | {message}",colorize=True,enqueue=True) # enqueue确保多进程安全 18 logger.add(sys.stderr,format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level}</level> | <cyan>{extra[request_id]}</cyan> | <blue>{file.name}:{line}</blue> | <level>{message}</level>",colorize=True,enqueue=True) # enqueue确保多进程安全 19 20 21 @app.middleware("http") #中间件 22 async def request_middleware(request: Request, call_next): 23 # 生成唯一请求ID 24 rid = f"REQ-{uuid4()}" 25 # 设置到上下文变量(需要保存token防止提前回收) 26 token = request_id_ctx.set(rid) 27 # 将request_id存入请求对象(可选,方便在路由中获取) 28 request.state.request_id = rid 29 try: 30 # 处理请求前日志(通过上下文获取) 31 with logger.contextualize(request_id=rid): 32 logger.info("Request started") 33 response = await call_next(request) 34 finally: 35 # 确保清理上下文,避免内存泄漏 36 request_id_ctx.reset(token) 37 # 添加请求ID到响应头(可选) 38 response.headers["X-Request-ID"] = rid 39 return response 40 41 42 def sync_search_baidu(key_word: str, request_id: str): 43 """实际的搜索逻辑""" 44 # 在子线程中设置上下文变量 45 logger_ctx = logger.bind(request_id=request_id) 46 options = Options() 47 # options.add_argument('--headless') # 启用无头模式 48 options.add_argument('--disable-gpu') # 禁用GPU加速 49 driver = webdriver.Chrome(options=options) 50 driver.maximize_window() # 最大化窗口 51 search_result = [] 52 try: 53 driver.get("https://www.baidu.com") 54 logger_ctx.info(f"browser is opened,Searching for '{key_word}'...") 55 driver.find_element(By.ID, "kw").send_keys(key_word) 56 driver.find_element(By.ID, "su").click() 57 time.sleep(10) 58 search_results = driver.find_elements(By.XPATH, "//div[@id='content_left']//h3//p/span/span") 59 search_result = [result.text for result in search_results] 60 logger_ctx.info(f"Search completed for '{key_word}'. Results: {search_result}") 61 return search_result 62 except Exception as e: 63 return search_result.append(f"Error occurred while searching for '{key_word}': {e}") 64 finally: 65 driver.quit() 66 67 68 @app.get("/search/{key_word}") 69 async def search_baidu(key_word: str, request: Request): 70 """执行百度搜索的异步封装""" 71 loop = asyncio.get_event_loop() # 获取当前事件循环 72 request_id = request.state.request_id 73 try: 74 # 在线程池中执行同步的Selenium操作 75 return await loop.run_in_executor( 76 executor, 77 lambda: sync_search_baidu(key_word, request_id) 78 ) 79 except Exception as e: 80 return {"error": str(e)} 81 82 83 if __name__ == '__main__': 84 import uvicorn 85 uvicorn.run(app, host="0.0.0.0", port=8000)
浙公网安备 33010602011771号