| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- """
- 论文分析蓝图 - 使用 Deepseek AI 分析研究论文的 Flask 蓝图
- """
- from flask import Blueprint, render_template, request, jsonify
- import requests
- import json
- import os
- import tempfile
- from werkzeug.utils import secure_filename
- # 创建蓝图
- paper_analysis_bp = Blueprint('paper_analysis', __name__, url_prefix='/paper-analysis')
- @paper_analysis_bp.route('/')
- def paper_analysis():
- """论文分析页面"""
- return render_template('paper_analysis.html')
- @paper_analysis_bp.route('/api/test-deepseek', methods=['POST'])
- def test_deepseek():
- """测试 Deepseek API 连接"""
- try:
- api_key = request.headers.get('X-API-Key')
- if not api_key:
- return jsonify({'error': '需要 API 密钥'}), 401
- # 使用简单提示测试 API
- headers = {
- 'Authorization': f'Bearer {api_key}',
- 'Content-Type': 'application/json'
- }
- test_response = requests.post(
- 'https://api.deepseek.com/v1/chat/completions', # 使用聊天补全端点
- headers=headers,
- json={
- 'model': 'deepseek-chat',
- 'messages': [
- {'role': 'user', 'content': '打个招呼'}
- ],
- 'max_tokens': 10
- },
- timeout=10
- )
- if test_response.ok:
- return jsonify({
- 'success': True,
- 'message': 'Deepseek API 连接成功',
- 'response': test_response.json()
- })
- else:
- return jsonify({
- 'success': False,
- 'error': f'API 错误: {test_response.status_code}',
- 'details': test_response.text
- }), test_response.status_code
- except requests.exceptions.RequestException as e:
- return jsonify({
- 'success': False,
- 'error': '连接错误',
- 'details': str(e)
- }), 500
- except Exception as e:
- return jsonify({
- 'success': False,
- 'error': '意外错误',
- 'details': str(e)
- }), 500
- @paper_analysis_bp.route('/api/analyze-paper', methods=['POST'])
- def analyze_paper():
- """使用 Deepseek API 分析论文"""
- try:
- # 从请求头获取 API 密钥
- api_key = request.headers.get('X-API-Key')
- if not api_key:
- return jsonify({'error': '需要 API 密钥'}), 401
- # 获取分析选项
- extract_keywords = request.form.get('extract_keywords') == 'true'
- generate_summary = request.form.get('generate_summary') == 'true'
- find_related = request.form.get('find_related') == 'true'
- # 获取上传的文件
- if 'file' not in request.files:
- return jsonify({'error': '没有上传文件'}), 400
- file = request.files['file']
- if file.filename == '':
- return jsonify({'error': '没有选择文件'}), 400
- # 检查文件大小(限制为 50MB)
- file.seek(0, 2) # 定位到文件末尾
- file_size = file.tell()
- file.seek(0) # 重置到开头
- if file_size > 50 * 1024 * 1024: # 50MB 限制
- return jsonify({'error': '文件过大。最大大小为 50MB'}), 400
- if file_size == 0:
- return jsonify({'error': '文件为空'}), 400
- # 临时保存文件
- filename = secure_filename(file.filename)
- print(f"正在处理文件: {filename} (大小: {file_size} 字节)")
- with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as temp_file:
- file.save(temp_file.name)
- file_path = temp_file.name
- try:
- # 初始化结果字典
- results = {
- 'keywords': [],
- 'summary': '',
- 'related_works': []
- }
- # 从文件提取文本
- text = extract_text_from_file(file_path, filename)
- if not text or not text.strip():
- return jsonify({'error': '无法从文件中提取文本。文件可能为空或格式不受支持。'}), 400
- # 清理文本
- text = text.strip()
- print(f"截断前的最终文本长度: {len(text)}")
- # 检查是否有足够的文本
- if len(text) < 100:
- return jsonify({
- 'error': '提取的文本过短。这可能是没有 OCR 文本的扫描 PDF。请确保您的 PDF 包含可选择的文本,而不仅仅是图像。'
- }), 400
- # 处理大型文档
- text = process_large_document(text)
- # 为每个请求的分析调用 Deepseek API
- headers = {
- 'Authorization': f'Bearer {api_key}',
- 'Content-Type': 'application/json'
- }
- if extract_keywords:
- results['keywords'] = extract_keywords_with_deepseek(text, headers)
- if generate_summary:
- results['summary'] = generate_summary_with_deepseek(text, headers)
- if find_related:
- results['related_works'] = extract_formulas_with_deepseek(text, headers)
- return jsonify(results)
- finally:
- # 清理临时文件
- try:
- os.unlink(file_path)
- except Exception as e:
- print(f"删除临时文件时出错: {str(e)}")
- except Exception as e:
- print(f"分析论文时出错: {str(e)}")
- return jsonify({'error': str(e)}), 500
- def extract_text_from_file(file_path, filename):
- """从各种文件格式中提取文本"""
- text = ''
- file_ext = os.path.splitext(filename)[1].lower()
- try:
- if file_ext == '.pdf':
- text = extract_pdf_text(file_path)
- elif file_ext in ['.docx', '.doc']:
- text = extract_word_text(file_path, file_ext)
- elif file_ext == '.rtf':
- text = extract_rtf_text(file_path)
- elif file_ext in ['.tex', '.latex']:
- text = extract_latex_text(file_path)
- elif file_ext in ['.md', '.markdown']:
- text = extract_markdown_text(file_path)
- else:
- text = extract_plain_text(file_path)
- except Exception as e:
- print(f"文件处理错误: {str(e)}")
- raise Exception(f"读取文件时出错: {str(e)}")
- return text
- def extract_pdf_text(file_path):
- """从 PDF 文件提取文本"""
- try:
- from PyPDF2 import PdfReader
- reader = PdfReader(file_path)
- text = ''
- num_pages = len(reader.pages)
- print(f"PDF 有 {num_pages} 页")
- for i, page in enumerate(reader.pages):
- try:
- page_text = page.extract_text()
- if page_text:
- text += page_text + '\n'
- print(f"已提取第 {i+1}/{num_pages} 页")
- except Exception as page_error:
- print(f"提取第 {i+1} 页时出错: {str(page_error)}")
- continue
- print(f"提取的总文本长度: {len(text)}")
- return text
- except ImportError:
- # 尝试替代的 PDF 库
- try:
- import pdfplumber
- text = ''
- with pdfplumber.open(file_path) as pdf:
- for page in pdf.pages:
- page_text = page.extract_text()
- if page_text:
- text += page_text + '\n'
- return text
- except ImportError:
- raise Exception('PDF 处理不可用。请安装 PyPDF2 或 pdfplumber。')
- except Exception as pdf_error:
- print(f"PDF 提取错误: {str(pdf_error)}")
- # 尝试使用 PyMuPDF 作为后备方案
- try:
- import fitz # PyMuPDF
- pdf_document = fitz.open(file_path)
- text = ''
- for page_num in range(pdf_document.page_count):
- page = pdf_document[page_num]
- text += page.get_text() + '\n'
- pdf_document.close()
- return text
- except ImportError:
- raise Exception(f'无法从 PDF 提取文本: {str(pdf_error)}。请尝试安装 PyMuPDF。')
- except Exception as mupdf_error:
- raise Exception(f'PDF 提取失败: {str(pdf_error)}')
- def extract_word_text(file_path, file_ext):
- """从 Word 文档提取文本"""
- try:
- if file_ext == '.docx':
- from docx import Document
- doc = Document(file_path)
- return '\n'.join([paragraph.text for paragraph in doc.paragraphs])
- else:
- # .doc 文件
- try:
- import docx2txt
- return docx2txt.process(file_path)
- except ImportError:
- raise Exception('DOC 文件支持需要 docx2txt。请使用以下命令安装: pip install docx2txt')
- except ImportError:
- raise Exception('Word 文档支持需要 python-docx。请使用以下命令安装: pip install python-docx')
- except Exception as docx_error:
- raise Exception(f'读取 Word 文档时出错: {str(docx_error)}')
- def extract_rtf_text(file_path):
- """从 RTF 文件提取文本"""
- try:
- import striprtf
- with open(file_path, 'r', encoding='utf-8') as f:
- rtf_content = f.read()
- return striprtf.rtf_to_text(rtf_content)
- except ImportError:
- raise Exception('RTF 支持需要 striprtf。请使用以下命令安装: pip install striprtf')
- except Exception as rtf_error:
- raise Exception(f'读取 RTF 文件时出错: {str(rtf_error)}')
- def extract_latex_text(file_path):
- """从 LaTeX 文件提取文本"""
- try:
- with open(file_path, 'r', encoding='utf-8') as f:
- tex_content = f.read()
- # 基础 LaTeX 清理 - 移除常见命令
- import re
- text = tex_content
- # 移除注释
- text = re.sub(r'%.*$', '', text, flags=re.MULTILINE)
- # 移除常见 LaTeX 命令但保留内容
- text = re.sub(r'\\(begin|end)\{[^}]+\}', '', text)
- text = re.sub(r'\\[a-zA-Z]+\*?\{([^}]+)\}', r'\1', text)
- text = re.sub(r'\\[a-zA-Z]+\*?', '', text)
- return text
- except Exception as tex_error:
- raise Exception(f'读取 LaTeX 文件时出错: {str(tex_error)}')
- def extract_markdown_text(file_path):
- """从 Markdown 文件提取文本"""
- try:
- with open(file_path, 'r', encoding='utf-8') as f:
- text = f.read()
- # 清理 markdown 语法
- import re
- # 移除图片链接
- text = re.sub(r'!\[[^\]]*\]\([^)]+\)', '', text)
- # 将链接转换为纯文本
- text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
- return text
- except Exception as md_error:
- raise Exception(f'读取 Markdown 文件时出错: {str(md_error)}')
- def extract_plain_text(file_path):
- """从纯文本文件提取文本"""
- encodings = ['utf-8', 'utf-16', 'gbk', 'gb2312', 'big5', 'latin-1']
- text = None
- for encoding in encodings:
- try:
- with open(file_path, 'r', encoding=encoding) as f:
- text = f.read()
- print(f"使用 {encoding} 编码成功读取文件")
- break
- except UnicodeDecodeError:
- continue
- except Exception as e:
- print(f"使用 {encoding} 读取时出错: {str(e)}")
- continue
- if text is None:
- # 尝试以二进制方式读取并解码
- with open(file_path, 'rb') as f:
- binary_content = f.read()
- try:
- text = binary_content.decode('utf-8', errors='ignore')
- except:
- text = str(binary_content)
- return text
- def process_large_document(text):
- """通过优先处理公式提取来处理大型文档"""
- if len(text) > 98000:
- print("检测到大型文档,优先处理内容以进行公式提取")
- # 尝试找到包含公式的章节(常见模式)
- import re
- # 查找数学内容指示符
- math_sections = []
- lines = text.split('\n')
- for i, line in enumerate(lines):
- if re.search(r'[=+\-*/∫∑∏√∂∇∆λμσπ]|equation|formula|theorem|lemma|proof', line, re.IGNORECASE):
- # 包含周围上下文
- start = max(0, i-5)
- end = min(len(lines), i+6)
- math_sections.extend(lines[start:end])
- if math_sections:
- # 使用数学内容丰富的章节以更好地提取公式
- math_text = '\n'.join(math_sections)
- if len(math_text) > 50000: # 仍然太长
- text = math_text[:98000]
- else:
- # 将数学章节与文档开头结合
- remaining_space = 98000 - len(math_text)
- text = text[:remaining_space] + '\n\n[数学内容章节:]\n' + math_text
- else:
- # 未找到数学指示符,使用第一部分
- text = text[:98000]
- return text
- def extract_keywords_with_deepseek(text, headers):
- """使用 Deepseek API 提取关键词"""
- try:
- keyword_messages = [
- {
- 'role': 'system',
- 'content': '你是一个有助于从学术论文中提取关键词的助手。始终以有效的 JSON 格式响应。'
- },
- {
- 'role': 'user',
- 'content': f"""分析以下学术论文并提取关键术语和概念。
- 对于每个关键词,提供一个介于 0 到 1 之间的相关性分数。
- 仅返回具有 'text' 和 'score' 属性的有效 JSON 对象数组。
- 示例格式: [{{"text": "machine learning", "score": 0.95}}, {{"text": "neural networks", "score": 0.85}}]
- 论文文本:
- {text}"""
- }
- ]
- keyword_response = requests.post(
- 'https://api.deepseek.com/v1/chat/completions',
- headers=headers,
- json={
- 'model': 'deepseek-chat',
- 'messages': keyword_messages,
- 'temperature': 0.3,
- 'max_tokens': 4000
- },
- timeout=60
- )
- if keyword_response.ok:
- response_content = keyword_response.json()['choices'][0]['message']['content']
- try:
- # 尝试从响应中提取 JSON
- import re
- json_match = re.search(r'\[.*\]', response_content, re.DOTALL)
- if json_match:
- return json.loads(json_match.group())
- else:
- return json.loads(response_content)
- except json.JSONDecodeError:
- print(f"来自关键词 API 的无效 JSON: {response_content}")
- return []
- else:
- print(f"关键词 API 错误: {keyword_response.text}")
- return []
- except Exception as e:
- print(f"关键词提取错误: {str(e)}")
- return []
- def generate_summary_with_deepseek(text, headers):
- """使用 Deepseek API 生成摘要"""
- try:
- summary_messages = [
- {
- 'role': 'system',
- 'content': '你是一个有助于总结学术论文的助手。'
- },
- {
- 'role': 'user',
- 'content': f"""提供以下学术论文的全面摘要。
- 重点关注主要贡献、方法和关键发现。
- 保持回应简洁且结构良好。
- 论文文本:
- {text}"""
- }
- ]
- summary_response = requests.post(
- 'https://api.deepseek.com/v1/chat/completions',
- headers=headers,
- json={
- 'model': 'deepseek-chat',
- 'messages': summary_messages,
- 'temperature': 0.3,
- 'max_tokens': 4000
- },
- timeout=60
- )
- if summary_response.ok:
- return summary_response.json()['choices'][0]['message']['content']
- else:
- print(f"摘要 API 错误: {summary_response.text}")
- return "生成摘要时出错"
- except Exception as e:
- print(f"摘要生成错误: {str(e)}")
- return "生成摘要时出错"
- def extract_formulas_with_deepseek(text, headers):
- """使用 Deepseek API 提取公式"""
- try:
- related_messages = [
- {
- 'role': 'system',
- 'content': '''你是一位专业的数学家和 AI 助手,专门从学术论文中提取数学公式。
- 你的任务是识别并提取给定文本中所有的数学公式、方程式和数学表达式,尽可能多地提取。
- 重要说明:
- 1. 提取你找到的每一个数学公式、方程式或表达式
- 2. 包括内联公式、显示方程和数学定义
- 3. 尽可能保留原始符号
- 4. 对于每个公式,提供其表示内容的上下文
- 5. 始终以有效的 JSON 格式响应
- 你必须彻底并提取所有公式,而不仅仅是主要的公式。'''
- },
- {
- 'role': 'user',
- 'content': f"""从以下论文文本中提取所有的数学公式和方程式。
- 对于找到的每个公式,请提供:
- - 公式本身(尽可能使用 LaTeX 表示法)
- - 详细描述,解释公式表示的内容以及每个变量的含义
- - 出现位置的上下文或章节
- - 它是定义、定理、引理还是一般方程
- - 解释公式目的的中文描述
- 返回一个 JSON 数组,其中每个元素具有以下属性:
- - "formula": 数学表达式(使用 LaTeX 表示法)
- - "description": 公式表示或计算的内容
- - "variables": 对公式中每个变量/符号含义的详细解释
- - "variables_chinese": 变量解释的中文翻译(与 variables 结构相同)
- - "type": 其中之一 ["definition", "theorem", "lemma", "equation", "inequality", "identity", "other"]
- - "context": 关于其使用位置/方式的简要上下文
- - "chinese_description": 关于公式及其目的的综合中文描述
- 示例格式:
- [
- {{
- "formula": "E = mc^2",
- "description": "爱因斯坦质能等价关系",
- "variables": {{"E": "energy (joules)", "m": "mass (kilograms)", "c": "speed of light in vacuum (≈3×10^8 m/s)"}},
- "variables_chinese": {{"E": "能量 (焦耳)", "m": "质量 (千克)", "c": "真空中的光速 (≈3×10^8 m/s)"}},
- "type": "equation",
- "context": "狭义相对论基本方程",
- "chinese_description": "爱因斯坦质能等价公式,表示质量和能量之间的等价关系"
- }},
- {{
- "formula": "F = ma",
- "description": "牛顿第二运动定律",
- "variables": {{"F": "net force (newtons)", "m": "mass (kilograms)", "a": "acceleration (m/s²)"}},
- "variables_chinese": {{"F": "净力 (牛顿)", "m": "质量 (千克)", "a": "加速度 (m/s²)"}},
- "type": "equation",
- "context": "经典力学基本定律",
- "chinese_description": "牛顿第二定律,描述物体受力与加速度的关系"
- }}
- ]
- 论文文本:
- {text}
- 重要说明:
- 1. 提取每一个公式,即使是简单的如 "x + y = z" 或 "f(x) = ax + b"
- 2. 对于公式中的每个变量或符号,解释其代表什么
- 3. 相关时包括测量单位
- 4. 提供解释公式重要性的全面中文描述
- 5. 在变量解释中要彻底且详细"""
- }
- ]
- related_response = requests.post(
- 'https://api.deepseek.com/v1/chat/completions',
- headers=headers,
- json={
- 'model': 'deepseek-chat',
- 'messages': related_messages,
- 'temperature': 0.1, # 较低的温度以获得更一致的提取
- 'max_tokens': 4000 # 增加令牌限制以获取更多公式
- },
- timeout=120 # 增加超时时间以处理大型文档
- )
- if related_response.ok:
- response_content = related_response.json()['choices'][0]['message']['content']
- try:
- # 尝试从响应中提取 JSON
- import re
- # 在响应中查找 JSON 数组
- json_match = re.search(r'\[[\s\S]*\]', response_content)
- if json_match:
- formulas = json.loads(json_match.group())
- return formulas
- else:
- # 尝试直接 JSON 解析
- return json.loads(response_content)
- except json.JSONDecodeError as e:
- print(f"来自公式 API 的无效 JSON: {response_content}")
- print(f"JSON 错误: {str(e)}")
- return []
- else:
- print(f"公式 API 错误: {related_response.text}")
- return []
- except Exception as e:
- print(f"公式提取错误: {str(e)}")
- return []
|