fccoelho aider (anthropic/claude-sonnet-4-20250514) commited on
Commit
53b56be
·
1 Parent(s): dc33182

refactor: melhorar extração de referências por regex em todo o texto

Browse files

Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>

Files changed (1) hide show
  1. app.py +122 -112
app.py CHANGED
@@ -117,55 +117,71 @@ def extract_references_with_llm(text, model_name):
117
  return [{"error": f"Erro ao processar com LLM ({model_name}): {str(e)}"}]
118
 
119
  def extract_references_with_regex(text):
120
- """Extrai referências usando expressões regulares"""
121
  try:
122
- # Encontrar a seção de referências
123
- references_section = ""
124
 
125
- # Padrões para identificar início da seção de referências
126
- ref_patterns = [
127
- r'(?i)references?\s*\n',
128
- r'(?i)bibliography\s*\n',
129
- r'(?i)literatura\s+citada\s*\n',
130
- r'(?i)referências\s+bibliográficas\s*\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  ]
132
 
133
- for pattern in ref_patterns:
134
- match = re.search(pattern, text)
135
- if match:
136
- references_section = text[match.end():]
137
- break
138
-
139
- if not references_section:
140
- # Se não encontrou seção específica, usar últimos 30% do texto
141
- references_section = text[int(len(text) * 0.7):]
142
-
143
- # Padrões para extrair referências individuais
144
- # Padrão básico: Autor(es). (Ano). Título. Journal/Editora.
145
- ref_pattern = r'([A-Z][^.]*?)\.\s*\((\d{4})\)\.\s*([^.]+)\.\s*([^.]+?)(?:\.|$)'
146
 
147
- # Padrão alternativo para referências numeradas
148
- numbered_pattern = r'\[\d+\]\s*([A-Z][^.]*?)\.\s*\((\d{4})\)\.\s*([^.]+)\.\s*([^.]+?)(?:\.|$)'
149
-
150
- # Padrão para referências com formato diferente
151
- alt_pattern = r'([A-Z][A-Za-z\s,&]+)\s+\((\d{4})\)[.,]\s*([^.]+)[.,]\s*([^.]+?)(?:\.|$)'
152
-
153
- references = []
154
-
155
- # Tentar diferentes padrões
156
- for pattern in [ref_pattern, numbered_pattern, alt_pattern]:
157
- matches = re.findall(pattern, references_section, re.MULTILINE | re.DOTALL)
158
 
159
- for match in matches:
160
- if len(match) >= 4:
161
- # Limpar e processar os dados extraídos
162
- authors = match[0].strip()
163
- year = match[1].strip()
164
- title = match[2].strip()
165
- journal = match[3].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  # Extrair DOI se presente
168
- doi_match = re.search(r'doi[:\s]*([^\s]+)', journal, re.IGNORECASE)
169
  doi = doi_match.group(1) if doi_match else ""
170
 
171
  # Extrair volume e páginas
@@ -173,26 +189,43 @@ def extract_references_with_regex(text):
173
  volume = vol_pages_match.group(1) if vol_pages_match else ""
174
  pages = vol_pages_match.group(2) if vol_pages_match else ""
175
 
176
- references.append({
 
 
 
 
 
177
  "authors": authors,
178
  "title": title,
179
  "journal": journal,
180
  "year": year,
181
  "volume": volume,
182
  "pages": pages,
183
- "doi": doi
184
- })
 
 
 
 
185
 
186
- # Remover duplicatas baseadas no título
187
- seen_titles = set()
188
  unique_references = []
 
189
  for ref in references:
190
- title_key = ref["title"].lower().strip()
191
- if title_key not in seen_titles and len(title_key) > 10:
192
- seen_titles.add(title_key)
193
- unique_references.append(ref)
 
 
 
 
 
 
 
194
 
195
- return unique_references[:50] # Limitar a 50 referências para evitar ruído
196
 
197
  except Exception as e:
198
  return [{"error": f"Erro na extração por regex: {str(e)}"}]
@@ -200,69 +233,44 @@ def extract_references_with_regex(text):
200
  def create_highlighted_text(text, regex_references):
201
  """Cria HTML com texto destacado onde foram encontradas referências por regex"""
202
  try:
203
- # Encontrar a seção de referências
204
- references_section = ""
205
- section_start = 0
206
-
207
- # Padrões para identificar início da seção de referências
208
- ref_patterns = [
209
- r'(?i)references?\s*\n',
210
- r'(?i)bibliography\s*\n',
211
- r'(?i)literatura\s+citada\s*\n',
212
- r'(?i)referências\s+bibliográficas\s*\n'
213
- ]
214
 
215
- for pattern in ref_patterns:
216
- match = re.search(pattern, text)
217
- if match:
218
- section_start = match.start()
219
- references_section = text[match.end():]
220
- break
221
-
222
- if not references_section:
223
- # Se não encontrou seção específica, usar últimos 30% do texto
224
- section_start = int(len(text) * 0.7)
225
- references_section = text[section_start:]
226
-
227
- # Criar HTML base
228
- html_text = text.replace('\n', '<br>')
229
-
230
- # Cores para diferentes tipos de matches
231
- colors = ['#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#9c27b0']
232
-
233
- # Padrões para destacar
234
  patterns = [
235
- (r'([A-Z][^.]*?)\.\s*\((\d{4})\)\.\s*([^.]+)\.\s*([^.]+?)(?:\.|$)', 'Padrão básico'),
236
- (r'\[\d+\]\s*([A-Z][^.]*?)\.\s*\((\d{4})\)\.\s*([^.]+)\.\s*([^.]+?)(?:\.|$)', 'Padrão numerado'),
237
- (r'([A-Z][A-Za-z\s,&]+)\s+\((\d{4})\)[.,]\s*([^.]+)[.,]\s*([^.]+?)(?:\.|$)', 'Padrão alternativo'),
238
- (r'(?i)references?\s*\n', 'Seção de referências'),
239
- (r'(?i)bibliography\s*\n', 'Bibliografia')
 
240
  ]
241
 
242
- # Aplicar destaques
243
- for i, (pattern, description) in enumerate(patterns):
244
- color = colors[i % len(colors)]
 
 
 
245
 
246
- # Encontrar matches no texto da seção de referências
247
- section_html = references_section.replace('\n', '<br>')
248
- matches = list(re.finditer(pattern, references_section, re.MULTILINE | re.DOTALL))
 
 
 
 
 
 
 
249
 
250
- # Destacar matches (processar de trás para frente para não afetar posições)
251
- for match in reversed(matches):
252
- start, end = match.span()
253
- matched_text = references_section[start:end]
254
- highlighted = f'<span style="background-color: {color}; padding: 2px; border-radius: 3px;" title="{description}">{matched_text.replace(chr(10), "<br>")}</span>'
255
-
256
- # Calcular posição no texto completo
257
- full_start = section_start + start
258
- full_end = section_start + end
259
-
260
- # Substituir no HTML completo
261
- before = html_text[:full_start].replace('\n', '<br>')
262
- after = html_text[full_end:].replace('\n', '<br>')
263
- html_text = before + highlighted + after
264
 
265
- # Criar HTML final com estilo
266
  styled_html = f"""
267
  <div style="
268
  font-family: 'Courier New', monospace;
@@ -280,12 +288,14 @@ def create_highlighted_text(text, regex_references):
280
  📄 Texto Extraído com Destaques das Referências
281
  </div>
282
  <div style="margin-bottom: 15px; font-size: 11px; color: #666;">
283
- <span style="background-color: #ffeb3b; padding: 2px;">■</span> Padrão básico &nbsp;
284
- <span style="background-color: #4caf50; padding: 2px;">■</span> Padrão numerado &nbsp;
285
- <span style="background-color: #2196f3; padding: 2px;">■</span> Padrão alternativo &nbsp;
286
- <span style="background-color: #ff9800; padding: 2px;">■</span> Seção referências
 
 
287
  </div>
288
- {html_text}
289
  </div>
290
  """
291
 
 
117
  return [{"error": f"Erro ao processar com LLM ({model_name}): {str(e)}"}]
118
 
119
  def extract_references_with_regex(text):
120
+ """Extrai referências usando expressões regulares em todo o texto"""
121
  try:
122
+ references = []
 
123
 
124
+ # Padrões melhorados para extrair referências individuais
125
+ patterns = [
126
+ # Padrão 1: Autor(es). (Ano). Título. Journal/Editora.
127
+ r'^([A-Z][A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
128
+
129
+ # Padrão 2: Referências numeradas [1] Autor...
130
+ r'^\[\d+\]\s*([A-Z][A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
131
+
132
+ # Padrão 3: Autor, A. (Ano). Título. Journal.
133
+ r'^([A-Z][A-Za-z\s,&.-]+?)\s+\((\d{4}[a-z]?)\)[.,]\s*([^.]+?)[.,]\s*([^.]+?)\.?\s*$',
134
+
135
+ # Padrão 4: Autor et al. (Ano) Título. Journal
136
+ r'^([A-Z][A-Za-z\s,&.-]*?et\s+al\.?)\s*\((\d{4}[a-z]?)\)[.,]?\s*([^.]+?)[.,]\s*([^.]+?)\.?\s*$',
137
+
138
+ # Padrão 5: Sobrenome, Nome (Ano). Título. Journal.
139
+ r'^([A-Z][a-z]+,\s*[A-Z][A-Za-z\s,&.-]*?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
140
+
141
+ # Padrão 6: Múltiplos autores com &
142
+ r'^([A-Z][A-Za-z\s,&.-]+?&[A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$'
143
  ]
144
 
145
+ # Dividir texto em linhas
146
+ lines = text.split('\n')
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ # Processar cada linha
149
+ for line_num, line in enumerate(lines):
150
+ line = line.strip()
 
 
 
 
 
 
 
 
151
 
152
+ # Pular linhas muito curtas ou que não começam com letra maiúscula
153
+ if len(line) < 20 or not line[0].isupper():
154
+ continue
155
+
156
+ # Pular linhas que são claramente títulos de seção
157
+ if re.match(r'^(abstract|introduction|methods?|results?|discussion|conclusion|references?|bibliography|acknowledgments?)\.?\s*$', line, re.IGNORECASE):
158
+ continue
159
+
160
+ # Tentar cada padrão
161
+ for pattern in patterns:
162
+ match = re.match(pattern, line, re.MULTILINE | re.IGNORECASE)
163
+
164
+ if match and len(match.groups()) >= 4:
165
+ authors = match.group(1).strip()
166
+ year = match.group(2).strip()
167
+ title = match.group(3).strip()
168
+ journal = match.group(4).strip()
169
+
170
+ # Validações adicionais
171
+ # Verificar se tem pelo menos um autor válido
172
+ if not re.search(r'[A-Z][a-z]+', authors):
173
+ continue
174
+
175
+ # Verificar se o título não é muito curto
176
+ if len(title) < 10:
177
+ continue
178
+
179
+ # Verificar se não é uma linha de cabeçalho ou rodapé
180
+ if re.search(r'(page|vol|volume|number|issue)\s*\d+', line, re.IGNORECASE):
181
+ continue
182
 
183
  # Extrair DOI se presente
184
+ doi_match = re.search(r'doi[:\s]*([^\s,]+)', journal, re.IGNORECASE)
185
  doi = doi_match.group(1) if doi_match else ""
186
 
187
  # Extrair volume e páginas
 
189
  volume = vol_pages_match.group(1) if vol_pages_match else ""
190
  pages = vol_pages_match.group(2) if vol_pages_match else ""
191
 
192
+ # Limpar campos
193
+ authors = re.sub(r'\s+', ' ', authors)
194
+ title = re.sub(r'\s+', ' ', title)
195
+ journal = re.sub(r'\s+', ' ', journal)
196
+
197
+ reference = {
198
  "authors": authors,
199
  "title": title,
200
  "journal": journal,
201
  "year": year,
202
  "volume": volume,
203
  "pages": pages,
204
+ "doi": doi,
205
+ "line_number": line_num + 1 # Para debug
206
+ }
207
+
208
+ references.append(reference)
209
+ break # Parar na primeira correspondência para esta linha
210
 
211
+ # Remover duplicatas baseadas no título e ano
212
+ seen_refs = set()
213
  unique_references = []
214
+
215
  for ref in references:
216
+ # Criar chave única baseada em título e ano
217
+ key = (ref["title"].lower().strip()[:50], ref["year"])
218
+
219
+ if key not in seen_refs:
220
+ seen_refs.add(key)
221
+ # Remover campo de debug antes de retornar
222
+ ref_clean = {k: v for k, v in ref.items() if k != "line_number"}
223
+ unique_references.append(ref_clean)
224
+
225
+ # Ordenar por ano (mais recente primeiro)
226
+ unique_references.sort(key=lambda x: x.get("year", "0"), reverse=True)
227
 
228
+ return unique_references[:100] # Limitar a 100 referências
229
 
230
  except Exception as e:
231
  return [{"error": f"Erro na extração por regex: {str(e)}"}]
 
233
  def create_highlighted_text(text, regex_references):
234
  """Cria HTML com texto destacado onde foram encontradas referências por regex"""
235
  try:
236
+ # Dividir texto em linhas
237
+ lines = text.split('\n')
238
+ highlighted_lines = []
 
 
 
 
 
 
 
 
239
 
240
+ # Padrões para destacar (mesmos da extração)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  patterns = [
242
+ r'^([A-Z][A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
243
+ r'^\[\d+\]\s*([A-Z][A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
244
+ r'^([A-Z][A-Za-z\s,&.-]+?)\s+\((\d{4}[a-z]?)\)[.,]\s*([^.]+?)[.,]\s*([^.]+?)\.?\s*$',
245
+ r'^([A-Z][A-Za-z\s,&.-]*?et\s+al\.?)\s*\((\d{4}[a-z]?)\)[.,]?\s*([^.]+?)[.,]\s*([^.]+?)\.?\s*$',
246
+ r'^([A-Z][a-z]+,\s*[A-Z][A-Za-z\s,&.-]*?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
247
+ r'^([A-Z][A-Za-z\s,&.-]+?&[A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$'
248
  ]
249
 
250
+ colors = ['#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#9c27b0', '#e91e63']
251
+
252
+ # Processar cada linha
253
+ for line in lines:
254
+ original_line = line
255
+ line_stripped = line.strip()
256
 
257
+ # Verificar se a linha corresponde a algum padrão
258
+ matched = False
259
+ for i, pattern in enumerate(patterns):
260
+ if re.match(pattern, line_stripped, re.MULTILINE | re.IGNORECASE):
261
+ if len(line_stripped) >= 20 and line_stripped[0].isupper():
262
+ color = colors[i % len(colors)]
263
+ highlighted_line = f'<span style="background-color: {color}; padding: 2px; border-radius: 3px; display: block; margin: 1px 0;" title="Padrão {i+1}">{original_line}</span>'
264
+ highlighted_lines.append(highlighted_line)
265
+ matched = True
266
+ break
267
 
268
+ if not matched:
269
+ highlighted_lines.append(original_line)
270
+
271
+ # Criar HTML final
272
+ html_content = '<br>'.join(highlighted_lines)
 
 
 
 
 
 
 
 
 
273
 
 
274
  styled_html = f"""
275
  <div style="
276
  font-family: 'Courier New', monospace;
 
288
  📄 Texto Extraído com Destaques das Referências
289
  </div>
290
  <div style="margin-bottom: 15px; font-size: 11px; color: #666;">
291
+ <span style="background-color: #ffeb3b; padding: 2px;">■</span> Padrão 1 &nbsp;
292
+ <span style="background-color: #4caf50; padding: 2px;">■</span> Padrão 2 &nbsp;
293
+ <span style="background-color: #2196f3; padding: 2px;">■</span> Padrão 3 &nbsp;
294
+ <span style="background-color: #ff9800; padding: 2px;">■</span> Padrão 4 &nbsp;
295
+ <span style="background-color: #9c27b0; padding: 2px;">■</span> Padrão 5 &nbsp;
296
+ <span style="background-color: #e91e63; padding: 2px;">■</span> Padrão 6
297
  </div>
298
+ {html_content}
299
  </div>
300
  """
301