paper_analysis.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. """
  2. 论文分析蓝图 - 使用 Deepseek AI 分析研究论文的 Flask 蓝图
  3. """
  4. from flask import Blueprint, render_template, request, jsonify
  5. import requests
  6. import json
  7. import os
  8. import tempfile
  9. from werkzeug.utils import secure_filename
  10. # 创建蓝图
  11. paper_analysis_bp = Blueprint('paper_analysis', __name__, url_prefix='/paper-analysis')
  12. @paper_analysis_bp.route('/')
  13. def paper_analysis():
  14. """论文分析页面"""
  15. return render_template('paper_analysis.html')
  16. @paper_analysis_bp.route('/api/test-deepseek', methods=['POST'])
  17. def test_deepseek():
  18. """测试 Deepseek API 连接"""
  19. try:
  20. api_key = request.headers.get('X-API-Key')
  21. if not api_key:
  22. return jsonify({'error': '需要 API 密钥'}), 401
  23. # 使用简单提示测试 API
  24. headers = {
  25. 'Authorization': f'Bearer {api_key}',
  26. 'Content-Type': 'application/json'
  27. }
  28. test_response = requests.post(
  29. 'https://api.deepseek.com/v1/chat/completions', # 使用聊天补全端点
  30. headers=headers,
  31. json={
  32. 'model': 'deepseek-chat',
  33. 'messages': [
  34. {'role': 'user', 'content': '打个招呼'}
  35. ],
  36. 'max_tokens': 10
  37. },
  38. timeout=10
  39. )
  40. if test_response.ok:
  41. return jsonify({
  42. 'success': True,
  43. 'message': 'Deepseek API 连接成功',
  44. 'response': test_response.json()
  45. })
  46. else:
  47. return jsonify({
  48. 'success': False,
  49. 'error': f'API 错误: {test_response.status_code}',
  50. 'details': test_response.text
  51. }), test_response.status_code
  52. except requests.exceptions.RequestException as e:
  53. return jsonify({
  54. 'success': False,
  55. 'error': '连接错误',
  56. 'details': str(e)
  57. }), 500
  58. except Exception as e:
  59. return jsonify({
  60. 'success': False,
  61. 'error': '意外错误',
  62. 'details': str(e)
  63. }), 500
  64. @paper_analysis_bp.route('/api/analyze-paper', methods=['POST'])
  65. def analyze_paper():
  66. """使用 Deepseek API 分析论文"""
  67. try:
  68. # 从请求头获取 API 密钥
  69. api_key = request.headers.get('X-API-Key')
  70. if not api_key:
  71. return jsonify({'error': '需要 API 密钥'}), 401
  72. # 获取分析选项
  73. extract_keywords = request.form.get('extract_keywords') == 'true'
  74. generate_summary = request.form.get('generate_summary') == 'true'
  75. find_related = request.form.get('find_related') == 'true'
  76. # 获取上传的文件
  77. if 'file' not in request.files:
  78. return jsonify({'error': '没有上传文件'}), 400
  79. file = request.files['file']
  80. if file.filename == '':
  81. return jsonify({'error': '没有选择文件'}), 400
  82. # 检查文件大小(限制为 50MB)
  83. file.seek(0, 2) # 定位到文件末尾
  84. file_size = file.tell()
  85. file.seek(0) # 重置到开头
  86. if file_size > 50 * 1024 * 1024: # 50MB 限制
  87. return jsonify({'error': '文件过大。最大大小为 50MB'}), 400
  88. if file_size == 0:
  89. return jsonify({'error': '文件为空'}), 400
  90. # 临时保存文件
  91. filename = secure_filename(file.filename)
  92. print(f"正在处理文件: {filename} (大小: {file_size} 字节)")
  93. with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as temp_file:
  94. file.save(temp_file.name)
  95. file_path = temp_file.name
  96. try:
  97. # 初始化结果字典
  98. results = {
  99. 'keywords': [],
  100. 'summary': '',
  101. 'related_works': []
  102. }
  103. # 从文件提取文本
  104. text = extract_text_from_file(file_path, filename)
  105. if not text or not text.strip():
  106. return jsonify({'error': '无法从文件中提取文本。文件可能为空或格式不受支持。'}), 400
  107. # 清理文本
  108. text = text.strip()
  109. print(f"截断前的最终文本长度: {len(text)}")
  110. # 检查是否有足够的文本
  111. if len(text) < 100:
  112. return jsonify({
  113. 'error': '提取的文本过短。这可能是没有 OCR 文本的扫描 PDF。请确保您的 PDF 包含可选择的文本,而不仅仅是图像。'
  114. }), 400
  115. # 处理大型文档
  116. text = process_large_document(text)
  117. # 为每个请求的分析调用 Deepseek API
  118. headers = {
  119. 'Authorization': f'Bearer {api_key}',
  120. 'Content-Type': 'application/json'
  121. }
  122. if extract_keywords:
  123. results['keywords'] = extract_keywords_with_deepseek(text, headers)
  124. if generate_summary:
  125. results['summary'] = generate_summary_with_deepseek(text, headers)
  126. if find_related:
  127. results['related_works'] = extract_formulas_with_deepseek(text, headers)
  128. return jsonify(results)
  129. finally:
  130. # 清理临时文件
  131. try:
  132. os.unlink(file_path)
  133. except Exception as e:
  134. print(f"删除临时文件时出错: {str(e)}")
  135. except Exception as e:
  136. print(f"分析论文时出错: {str(e)}")
  137. return jsonify({'error': str(e)}), 500
  138. def extract_text_from_file(file_path, filename):
  139. """从各种文件格式中提取文本"""
  140. text = ''
  141. file_ext = os.path.splitext(filename)[1].lower()
  142. try:
  143. if file_ext == '.pdf':
  144. text = extract_pdf_text(file_path)
  145. elif file_ext in ['.docx', '.doc']:
  146. text = extract_word_text(file_path, file_ext)
  147. elif file_ext == '.rtf':
  148. text = extract_rtf_text(file_path)
  149. elif file_ext in ['.tex', '.latex']:
  150. text = extract_latex_text(file_path)
  151. elif file_ext in ['.md', '.markdown']:
  152. text = extract_markdown_text(file_path)
  153. else:
  154. text = extract_plain_text(file_path)
  155. except Exception as e:
  156. print(f"文件处理错误: {str(e)}")
  157. raise Exception(f"读取文件时出错: {str(e)}")
  158. return text
  159. def extract_pdf_text(file_path):
  160. """从 PDF 文件提取文本"""
  161. try:
  162. from PyPDF2 import PdfReader
  163. reader = PdfReader(file_path)
  164. text = ''
  165. num_pages = len(reader.pages)
  166. print(f"PDF 有 {num_pages} 页")
  167. for i, page in enumerate(reader.pages):
  168. try:
  169. page_text = page.extract_text()
  170. if page_text:
  171. text += page_text + '\n'
  172. print(f"已提取第 {i+1}/{num_pages} 页")
  173. except Exception as page_error:
  174. print(f"提取第 {i+1} 页时出错: {str(page_error)}")
  175. continue
  176. print(f"提取的总文本长度: {len(text)}")
  177. return text
  178. except ImportError:
  179. # 尝试替代的 PDF 库
  180. try:
  181. import pdfplumber
  182. text = ''
  183. with pdfplumber.open(file_path) as pdf:
  184. for page in pdf.pages:
  185. page_text = page.extract_text()
  186. if page_text:
  187. text += page_text + '\n'
  188. return text
  189. except ImportError:
  190. raise Exception('PDF 处理不可用。请安装 PyPDF2 或 pdfplumber。')
  191. except Exception as pdf_error:
  192. print(f"PDF 提取错误: {str(pdf_error)}")
  193. # 尝试使用 PyMuPDF 作为后备方案
  194. try:
  195. import fitz # PyMuPDF
  196. pdf_document = fitz.open(file_path)
  197. text = ''
  198. for page_num in range(pdf_document.page_count):
  199. page = pdf_document[page_num]
  200. text += page.get_text() + '\n'
  201. pdf_document.close()
  202. return text
  203. except ImportError:
  204. raise Exception(f'无法从 PDF 提取文本: {str(pdf_error)}。请尝试安装 PyMuPDF。')
  205. except Exception as mupdf_error:
  206. raise Exception(f'PDF 提取失败: {str(pdf_error)}')
  207. def extract_word_text(file_path, file_ext):
  208. """从 Word 文档提取文本"""
  209. try:
  210. if file_ext == '.docx':
  211. from docx import Document
  212. doc = Document(file_path)
  213. return '\n'.join([paragraph.text for paragraph in doc.paragraphs])
  214. else:
  215. # .doc 文件
  216. try:
  217. import docx2txt
  218. return docx2txt.process(file_path)
  219. except ImportError:
  220. raise Exception('DOC 文件支持需要 docx2txt。请使用以下命令安装: pip install docx2txt')
  221. except ImportError:
  222. raise Exception('Word 文档支持需要 python-docx。请使用以下命令安装: pip install python-docx')
  223. except Exception as docx_error:
  224. raise Exception(f'读取 Word 文档时出错: {str(docx_error)}')
  225. def extract_rtf_text(file_path):
  226. """从 RTF 文件提取文本"""
  227. try:
  228. import striprtf
  229. with open(file_path, 'r', encoding='utf-8') as f:
  230. rtf_content = f.read()
  231. return striprtf.rtf_to_text(rtf_content)
  232. except ImportError:
  233. raise Exception('RTF 支持需要 striprtf。请使用以下命令安装: pip install striprtf')
  234. except Exception as rtf_error:
  235. raise Exception(f'读取 RTF 文件时出错: {str(rtf_error)}')
  236. def extract_latex_text(file_path):
  237. """从 LaTeX 文件提取文本"""
  238. try:
  239. with open(file_path, 'r', encoding='utf-8') as f:
  240. tex_content = f.read()
  241. # 基础 LaTeX 清理 - 移除常见命令
  242. import re
  243. text = tex_content
  244. # 移除注释
  245. text = re.sub(r'%.*$', '', text, flags=re.MULTILINE)
  246. # 移除常见 LaTeX 命令但保留内容
  247. text = re.sub(r'\\(begin|end)\{[^}]+\}', '', text)
  248. text = re.sub(r'\\[a-zA-Z]+\*?\{([^}]+)\}', r'\1', text)
  249. text = re.sub(r'\\[a-zA-Z]+\*?', '', text)
  250. return text
  251. except Exception as tex_error:
  252. raise Exception(f'读取 LaTeX 文件时出错: {str(tex_error)}')
  253. def extract_markdown_text(file_path):
  254. """从 Markdown 文件提取文本"""
  255. try:
  256. with open(file_path, 'r', encoding='utf-8') as f:
  257. text = f.read()
  258. # 清理 markdown 语法
  259. import re
  260. # 移除图片链接
  261. text = re.sub(r'!\[[^\]]*\]\([^)]+\)', '', text)
  262. # 将链接转换为纯文本
  263. text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
  264. return text
  265. except Exception as md_error:
  266. raise Exception(f'读取 Markdown 文件时出错: {str(md_error)}')
  267. def extract_plain_text(file_path):
  268. """从纯文本文件提取文本"""
  269. encodings = ['utf-8', 'utf-16', 'gbk', 'gb2312', 'big5', 'latin-1']
  270. text = None
  271. for encoding in encodings:
  272. try:
  273. with open(file_path, 'r', encoding=encoding) as f:
  274. text = f.read()
  275. print(f"使用 {encoding} 编码成功读取文件")
  276. break
  277. except UnicodeDecodeError:
  278. continue
  279. except Exception as e:
  280. print(f"使用 {encoding} 读取时出错: {str(e)}")
  281. continue
  282. if text is None:
  283. # 尝试以二进制方式读取并解码
  284. with open(file_path, 'rb') as f:
  285. binary_content = f.read()
  286. try:
  287. text = binary_content.decode('utf-8', errors='ignore')
  288. except:
  289. text = str(binary_content)
  290. return text
  291. def process_large_document(text):
  292. """通过优先处理公式提取来处理大型文档"""
  293. if len(text) > 98000:
  294. print("检测到大型文档,优先处理内容以进行公式提取")
  295. # 尝试找到包含公式的章节(常见模式)
  296. import re
  297. # 查找数学内容指示符
  298. math_sections = []
  299. lines = text.split('\n')
  300. for i, line in enumerate(lines):
  301. if re.search(r'[=+\-*/∫∑∏√∂∇∆λμσπ]|equation|formula|theorem|lemma|proof', line, re.IGNORECASE):
  302. # 包含周围上下文
  303. start = max(0, i-5)
  304. end = min(len(lines), i+6)
  305. math_sections.extend(lines[start:end])
  306. if math_sections:
  307. # 使用数学内容丰富的章节以更好地提取公式
  308. math_text = '\n'.join(math_sections)
  309. if len(math_text) > 50000: # 仍然太长
  310. text = math_text[:98000]
  311. else:
  312. # 将数学章节与文档开头结合
  313. remaining_space = 98000 - len(math_text)
  314. text = text[:remaining_space] + '\n\n[数学内容章节:]\n' + math_text
  315. else:
  316. # 未找到数学指示符,使用第一部分
  317. text = text[:98000]
  318. return text
  319. def extract_keywords_with_deepseek(text, headers):
  320. """使用 Deepseek API 提取关键词"""
  321. try:
  322. keyword_messages = [
  323. {
  324. 'role': 'system',
  325. 'content': '你是一个有助于从学术论文中提取关键词的助手。始终以有效的 JSON 格式响应。'
  326. },
  327. {
  328. 'role': 'user',
  329. 'content': f"""分析以下学术论文并提取关键术语和概念。
  330. 对于每个关键词,提供一个介于 0 到 1 之间的相关性分数。
  331. 仅返回具有 'text' 和 'score' 属性的有效 JSON 对象数组。
  332. 示例格式: [{{"text": "machine learning", "score": 0.95}}, {{"text": "neural networks", "score": 0.85}}]
  333. 论文文本:
  334. {text}"""
  335. }
  336. ]
  337. keyword_response = requests.post(
  338. 'https://api.deepseek.com/v1/chat/completions',
  339. headers=headers,
  340. json={
  341. 'model': 'deepseek-chat',
  342. 'messages': keyword_messages,
  343. 'temperature': 0.3,
  344. 'max_tokens': 4000
  345. },
  346. timeout=60
  347. )
  348. if keyword_response.ok:
  349. response_content = keyword_response.json()['choices'][0]['message']['content']
  350. try:
  351. # 尝试从响应中提取 JSON
  352. import re
  353. json_match = re.search(r'\[.*\]', response_content, re.DOTALL)
  354. if json_match:
  355. return json.loads(json_match.group())
  356. else:
  357. return json.loads(response_content)
  358. except json.JSONDecodeError:
  359. print(f"来自关键词 API 的无效 JSON: {response_content}")
  360. return []
  361. else:
  362. print(f"关键词 API 错误: {keyword_response.text}")
  363. return []
  364. except Exception as e:
  365. print(f"关键词提取错误: {str(e)}")
  366. return []
  367. def generate_summary_with_deepseek(text, headers):
  368. """使用 Deepseek API 生成摘要"""
  369. try:
  370. summary_messages = [
  371. {
  372. 'role': 'system',
  373. 'content': '你是一个有助于总结学术论文的助手。'
  374. },
  375. {
  376. 'role': 'user',
  377. 'content': f"""提供以下学术论文的全面摘要。
  378. 重点关注主要贡献、方法和关键发现。
  379. 保持回应简洁且结构良好。
  380. 论文文本:
  381. {text}"""
  382. }
  383. ]
  384. summary_response = requests.post(
  385. 'https://api.deepseek.com/v1/chat/completions',
  386. headers=headers,
  387. json={
  388. 'model': 'deepseek-chat',
  389. 'messages': summary_messages,
  390. 'temperature': 0.3,
  391. 'max_tokens': 4000
  392. },
  393. timeout=60
  394. )
  395. if summary_response.ok:
  396. return summary_response.json()['choices'][0]['message']['content']
  397. else:
  398. print(f"摘要 API 错误: {summary_response.text}")
  399. return "生成摘要时出错"
  400. except Exception as e:
  401. print(f"摘要生成错误: {str(e)}")
  402. return "生成摘要时出错"
  403. def extract_formulas_with_deepseek(text, headers):
  404. """使用 Deepseek API 提取公式"""
  405. try:
  406. related_messages = [
  407. {
  408. 'role': 'system',
  409. 'content': '''你是一位专业的数学家和 AI 助手,专门从学术论文中提取数学公式。
  410. 你的任务是识别并提取给定文本中所有的数学公式、方程式和数学表达式,尽可能多地提取。
  411. 重要说明:
  412. 1. 提取你找到的每一个数学公式、方程式或表达式
  413. 2. 包括内联公式、显示方程和数学定义
  414. 3. 尽可能保留原始符号
  415. 4. 对于每个公式,提供其表示内容的上下文
  416. 5. 始终以有效的 JSON 格式响应
  417. 你必须彻底并提取所有公式,而不仅仅是主要的公式。'''
  418. },
  419. {
  420. 'role': 'user',
  421. 'content': f"""从以下论文文本中提取所有的数学公式和方程式。
  422. 对于找到的每个公式,请提供:
  423. - 公式本身(尽可能使用 LaTeX 表示法)
  424. - 详细描述,解释公式表示的内容以及每个变量的含义
  425. - 出现位置的上下文或章节
  426. - 它是定义、定理、引理还是一般方程
  427. - 解释公式目的的中文描述
  428. 返回一个 JSON 数组,其中每个元素具有以下属性:
  429. - "formula": 数学表达式(使用 LaTeX 表示法)
  430. - "description": 公式表示或计算的内容
  431. - "variables": 对公式中每个变量/符号含义的详细解释
  432. - "variables_chinese": 变量解释的中文翻译(与 variables 结构相同)
  433. - "type": 其中之一 ["definition", "theorem", "lemma", "equation", "inequality", "identity", "other"]
  434. - "context": 关于其使用位置/方式的简要上下文
  435. - "chinese_description": 关于公式及其目的的综合中文描述
  436. 示例格式:
  437. [
  438. {{
  439. "formula": "E = mc^2",
  440. "description": "爱因斯坦质能等价关系",
  441. "variables": {{"E": "energy (joules)", "m": "mass (kilograms)", "c": "speed of light in vacuum (≈3×10^8 m/s)"}},
  442. "variables_chinese": {{"E": "能量 (焦耳)", "m": "质量 (千克)", "c": "真空中的光速 (≈3×10^8 m/s)"}},
  443. "type": "equation",
  444. "context": "狭义相对论基本方程",
  445. "chinese_description": "爱因斯坦质能等价公式,表示质量和能量之间的等价关系"
  446. }},
  447. {{
  448. "formula": "F = ma",
  449. "description": "牛顿第二运动定律",
  450. "variables": {{"F": "net force (newtons)", "m": "mass (kilograms)", "a": "acceleration (m/s²)"}},
  451. "variables_chinese": {{"F": "净力 (牛顿)", "m": "质量 (千克)", "a": "加速度 (m/s²)"}},
  452. "type": "equation",
  453. "context": "经典力学基本定律",
  454. "chinese_description": "牛顿第二定律,描述物体受力与加速度的关系"
  455. }}
  456. ]
  457. 论文文本:
  458. {text}
  459. 重要说明:
  460. 1. 提取每一个公式,即使是简单的如 "x + y = z" 或 "f(x) = ax + b"
  461. 2. 对于公式中的每个变量或符号,解释其代表什么
  462. 3. 相关时包括测量单位
  463. 4. 提供解释公式重要性的全面中文描述
  464. 5. 在变量解释中要彻底且详细"""
  465. }
  466. ]
  467. related_response = requests.post(
  468. 'https://api.deepseek.com/v1/chat/completions',
  469. headers=headers,
  470. json={
  471. 'model': 'deepseek-chat',
  472. 'messages': related_messages,
  473. 'temperature': 0.1, # 较低的温度以获得更一致的提取
  474. 'max_tokens': 4000 # 增加令牌限制以获取更多公式
  475. },
  476. timeout=120 # 增加超时时间以处理大型文档
  477. )
  478. if related_response.ok:
  479. response_content = related_response.json()['choices'][0]['message']['content']
  480. try:
  481. # 尝试从响应中提取 JSON
  482. import re
  483. # 在响应中查找 JSON 数组
  484. json_match = re.search(r'\[[\s\S]*\]', response_content)
  485. if json_match:
  486. formulas = json.loads(json_match.group())
  487. return formulas
  488. else:
  489. # 尝试直接 JSON 解析
  490. return json.loads(response_content)
  491. except json.JSONDecodeError as e:
  492. print(f"来自公式 API 的无效 JSON: {response_content}")
  493. print(f"JSON 错误: {str(e)}")
  494. return []
  495. else:
  496. print(f"公式 API 错误: {related_response.text}")
  497. return []
  498. except Exception as e:
  499. print(f"公式提取错误: {str(e)}")
  500. return []