在处理 Azure 文档分析服务时,很多开发者会遇到一些常见的问题,尤其是在处理 PDF 文件时。这些问题可能涉及如何正确使用 LROPoller
以及如何避免常见的错误。本文将详细讨论如何在异步代码中使用 Azure 文档分析服务,特别是如何正确使用 LROPoller
,并分析遇到的 TypeError: 'LROPoller' object is not callable
错误的原因和解决方案。
1. 什么是 LROPoller
?
在 Azure SDK 中,LROPoller
是一种用来处理长时间运行操作(Long Running Operation, LRO)的工具。Azure 中的许多操作,特别是涉及到文件分析或机器学习任务的操作,通常需要较长时间才能完成。为了避免阻塞线程,Azure SDK 引入了 LROPoller
,它允许开发者异步地检查操作的状态,直到操作完成。
LROPoller
是一个可以轮询操作结果的对象。当你调用像 begin_analyze_document
这样的方法时,它会返回一个 LROPoller
对象。该对象需要你进行轮询,直到你获得操作的最终结果。
2. 问题背景
在使用 Azure 文档分析服务时,开发者通常需要异步地处理文件分析任务。以下是一个典型的代码示例,其中使用 LROPoller
来处理 PDF 文档的分析:
async def process_pdf(self, pdf_path: str, prompt: str) -> list:
try:
# 提取文本
with open(pdf_path, "rb") as f:
poller = await asyncio.to_thread(self.doc_client.begin_analyze_document, "prebuilt-document", document=f)
doc_result = poller.result()
extracted_text = " ".join([p.content for p in doc_result.paragraphs]) if doc_result.paragraphs else ""
if not extracted_text:
return [False, "文档内容提取失败:未能提取到文本内容"]
return [True, extracted_text]
except Exception as e:
print(f"处理PDF文件时出错: {str(e)}")
import traceback
traceback.print_exc()
return [False, "处理PDF文件时出错"]
然而,在执行时,你可能会遇到类似以下的错误:
TypeError: 'LROPoller' object is not callable
这个错误通常是由于错误地使用了 LROPoller
对象,下面我们将深入分析这个问题的原因,并提供解决方案。
3. 错误分析:TypeError: 'LROPoller' object is not callable
错误原因
LROPoller
是一个对象,而不是一个函数。你无法像调用普通函数那样直接调用 poller()
。在上面的代码中,开发者试图直接调用 poller.result()
,这是不正确的使用方式。正确的做法是使用 LROPoller
的 result()
方法来获取最终结果,但不能像调用函数那样直接调用它。
具体来说,begin_analyze_document
方法返回的是一个 LROPoller
对象,它不是一个可以直接调用的函数,而是一个管理长时间运行操作(LRO)的对象。因此,我们需要通过异步方式等待操作完成,并正确地调用 result()
来获取操作结果。
错误示例
在原始代码中,错误发生在如下这一行:
poller = await asyncio.to_thread(self.doc_client.begin_analyze_document, "prebuilt-document", document=f)
doc_result = poller.result() # 错误:不能直接调用 poller
这里的 poller.result()
是不正确的使用方式,因为 poller
是一个 LROPoller
对象,而不是一个函数。你不能直接调用它。
4. 正确的使用方法
4.1 使用 LROPoller
获取结果
为了正确地使用 LROPoller
,你需要使用 poller.result()
来获取操作的最终结果,但必须通过 await
来异步等待操作完成。你可以使用 asyncio.to_thread()
方法将阻塞操作放入线程池中执行,确保不会阻塞主线程。
下面是修正后的代码:
async def process_pdf(self, pdf_path: str, prompt: str) -> list:
try:
# 提取文本
with open(pdf_path, "rb") as f:
# 使用 asyncio.to_thread 异步调用 begin_analyze_document 方法
poller = await asyncio.to_thread(self.doc_client.begin_analyze_document, "prebuilt-document", document=f)
# 异步等待 poller 完成操作并返回结果
result = await asyncio.to_thread(poller.result) # 使用 .result() 获取结果
# 提取文本内容
extracted_text = " ".join([p.content for p in result.paragraphs]) if result.paragraphs else ""
if not extracted_text:
return [False, "文档内容提取失败:未能提取到文本内容"]
return [True, extracted_text]
except Exception as e:
print(f"处理PDF文件时出错: {str(e)}")
import traceback
traceback.print_exc()
return [False, "处理PDF文件时出错"]
4.2 使用 await
等待 LROPoller
完成操作
在修正后的代码中,我们首先通过 asyncio.to_thread
异步地调用 begin_analyze_document
,然后通过 await asyncio.to_thread(poller.result)
等待 poller.result()
完成操作并返回最终结果。
4.3 异步和同步的结合
在处理异步代码时,await
关键字非常重要,它确保了我们在等待外部操作(如 Azure API 请求)的同时,不会阻塞事件循环。通过将同步的操作放入 asyncio.to_thread()
中执行,我们可以保持异步代码的流畅执行,同时避免阻塞主线程。
5. 进一步优化
5.1 处理长时间运行的操作
当操作时间较长时,使用 LROPoller
进行轮询是很有必要的。通过 poller.result()
方法,我们可以在操作完成后获得最终结果。然而,如果操作非常耗时,可能需要考虑增加超时机制或者使用轮询来定期检查操作状态,以避免长时间的阻塞。
5.2 错误处理与日志记录
在实际开发中,良好的错误处理和日志记录非常重要。我们应该记录详细的错误信息,以便在生产环境中快速定位问题。例如,在代码中使用 traceback.print_exc()
输出错误堆栈信息,能够帮助我们准确了解错误发生的上下文。
5.3 流式处理响应
如果 Azure 文档分析的结果非常庞大,我们可以使用流式响应逐步返回结果,而不是等待所有操作完成。这可以避免内存占用过大,并提供更快的响应体验。
from fastapi.responses import StreamingResponse
async def generate_stream(taskNo, files, handleCode, content, db):
# 逐步生成并返回结果
yield b"Initial processing..."
result = await some_async_processing(taskNo, files, handleCode, content, db)
yield result
@router.post("/ai/chat", response_model=str)
async def aiChat(request: ChatRequest, db: AsyncSession = Depends(get_db)):
return StreamingResponse(generate_stream(request.taskNo, request.files, request.handleCode, request.content, db), media_type="application/octet-stream")
6. 总结
在使用 LROPoller
时,最常见的错误是将其当作普通的函数调用。实际上,LROPoller
是一个用于处理长时间运行操作的对象,正确的使用方式是通过异步地轮询操作,直到操作完成。在遇到 TypeError: 'LROPoller' object is not callable
错误时,开发者需要调整代码,正确地使用 poller.result()
方法。
通过将同步操作放入线程池、使用 await
异步等待操作完成,我们可以避免阻塞事件循环,确保程序能够高效地处理并发请求。同时,错误处理和日志记录是必不可少的,它们能够帮助我们在开发过程中快速定位和解决问题。