File size: 10,603 Bytes
2e901fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import gradio as gr
import arxiv
import re
import os
import requests # 用于进行 HTTP 请求,获取 DOI 和通用 URL 元数据
import datetime # 用于获取当前日期作为访问日期
import hashlib # 用于为通用 URL 生成唯一的 BibTeX 键

# --- 核心逻辑函数 ---

def extract_identifiers(input_string):
    """
    从输入的字符串中提取 arXiv ID、DOI 或通用 URL。
    支持新旧格式的 arXiv 链接/ID、DOI 链接/ID 和常见的 URL 格式。
    返回一个包含 (type, id) 元组的列表,例如 [('arxiv', '2006.01234'), ('doi', '10.1007/s11227-020-03299-8'), ('web', 'https://example.com/blog')]
    """
    input_string = input_string.strip()
    identifiers = []

    # 尝试提取 arXiv ID
    arxiv_new_format_match = re.search(r'(?P<id>\d{4}\.\d{5})(v\d+)?', input_string)
    if arxiv_new_format_match:
        identifiers.append(('arxiv', arxiv_new_format_match.group('id')))
        return identifiers

    arxiv_old_format_match = re.search(r'(?P<id>[a-z\-]+/\d{7})(v\d+)?', input_string)
    if arxiv_old_format_match:
        identifiers.append(('arxiv', arxiv_old_format_match.group('id')))
        return identifiers

    if re.match(r'^\d{4}\.\d{5}(v\d+)?$', input_string):
        identifiers.append(('arxiv', input_string.split('v')[0]))
        return identifiers

    if re.match(r'^[a-z\-]+/\d{7}(v\d+)?$', input_string):
        identifiers.append(('arxiv', input_string.split('v')[0]))
        return identifiers

    # 尝试提取 DOI
    doi_match = re.search(r'(10\.\d{4,9}/[-._;()/:A-Z0-9]+)', input_string, re.IGNORECASE)
    if doi_match:
        identifiers.append(('doi', doi_match.group(1)))
        return identifiers

    # 如果以上都不是,尝试作为通用 URL
    # 简单的 URL 匹配,确保它看起来像一个 URL (http/https开头或www.开头)
    if re.match(r'^(https?://|www\.)[^\s/$.?#].[^\s]*$', input_string, re.IGNORECASE):
        identifiers.append(('web', input_string))
        return identifiers

    return identifiers

def get_bibtex_from_arxiv(arxiv_id):
    """
    根据 arXiv ID 获取 BibTeX 条目。
    """
    try:
        search_results = arxiv.Search(id_list=[arxiv_id]).results()
        paper = next(search_results)

        authors = " and ".join([author.name for author in paper.authors])
        title = paper.title.replace("\n", " ").strip()
        year = paper.published.year
        arxiv_category = paper.primary_category
        doi_field = f",\n  doi = {{{paper.doi}}}" if paper.doi else ""

        bib_key = arxiv_id.replace('.', '_').replace('/', '_').replace('-', '_')

        bibtex_entry = f"""
@article{{{bib_key},
  title={{ {title} }},
  author={{ {authors} }},
  journal={{ arXiv preprint arXiv:{paper.entry_id.split('/')[-1]} [{arxiv_category}] }},
  year={{ {year} }}{doi_field}
}}
"""
        return bibtex_entry, None # 返回 BibTeX 和 None (无错误)
    except StopIteration:
        return None, f"错误: 未找到 arXiv ID '{arxiv_id}' 的论文。请检查 ID 是否正确或已发布。"
    except Exception as e:
        return None, f"无法获取 arXiv ID '{arxiv_id}' 的 BibTeX 条目: {e}"

def get_bibtex_from_doi(doi):
    """
    根据 DOI 获取 BibTeX 条目,通过 CrossRef API。
    """
    try:
        # CrossRef API 直接提供 BibTeX 格式的转换
        url = f"https://api.crossref.org/v1/works/{doi}/transform/application/x-bibtex"
        headers = {'Accept': 'application/x-bibtex'} # 请求 BibTeX 格式
        response = requests.get(url, headers=headers, timeout=10) # 增加超时

        if response.status_code == 200:
            return response.text, None # 返回 BibTeX 文本和 None (无错误)
        elif response.status_code == 404:
            return None, f"错误: 未找到 DOI '{doi}' 的论文。请检查 DOI 是否正确。"
        else:
            return None, f"错误: 获取 DOI '{doi}' 的 BibTeX 失败,状态码: {response.status_code},响应: {response.text[:100]}..."
    except requests.exceptions.RequestException as e:
        return None, f"网络请求错误,无法获取 DOI '{doi}' 的 BibTeX: {e}"
    except Exception as e:
        return None, f"处理 DOI '{doi}' 时发生未知错误: {e}"

def get_bibtex_from_url(url):
    """
    根据通用 URL 获取 BibTeX 条目。
    尝试从网页标题中提取信息,并生成 @misc 类型的 BibTeX。
    注意:从通用网页提取准确的元数据(作者、日期等)非常困难且不可靠。
    此函数主要提取标题和 URL,并记录访问日期。
    """
    try:
        # 确保 URL 包含协议,否则 requests 可能无法处理
        if not url.startswith('http://') and not url.startswith('https://'):
            url = 'http://' + url # 默认使用 http,requests 会自动重定向到 https

        response = requests.get(url, timeout=10)
        response.raise_for_status() # 对 4xx/5xx 响应抛出 HTTPError
        html_content = response.text

        # 尝试从 <title> 标签中提取标题
        title_match = re.search(r'<title>(.*?)</title>', html_content, re.IGNORECASE | re.DOTALL)
        title = title_match.group(1).strip() if title_match else url # 如果未找到标题,则使用 URL 作为标题

        # 尝试从 Open Graph 协议中提取标题 (优先级更高)
        og_title_match = re.search(r'<meta\s+property="og:title"\s+content="([^"]*)"', html_content, re.IGNORECASE)
        if og_title_match:
            title = og_title_match.group(1).strip()
        
        # 简单的 bib_key 生成,基于 URL 的 MD5 哈希值,取前8位
        bib_key = "web_" + hashlib.md5(url.encode('utf-8')).hexdigest()[:8]

        access_date = datetime.date.today().strftime("%Y-%m-%d")

        bibtex_entry = f"""
@misc{{{bib_key},
  title={{ {title} }},
  howpublished={{ \\url{{{url}}} }},
  note={{ Accessed: {access_date} }}
}}
"""
        return bibtex_entry, None
    except requests.exceptions.RequestException as e:
        return None, f"网络请求错误,无法获取 URL '{url}' 的内容: {e}"
    except Exception as e:
        return None, f"处理 URL '{url}' 时发生未知错误: {e}"


def get_bibtex_entries_and_display(input_string):
    """
    根据输入的字符串(每行一个 arXiv ID/链接、DOI/链接或通用 URL)获取 BibTeX 条目,
    返回日志信息和所有BibTeX条目的字符串形式。
    """
    input_lines = input_string.split('\n')
    
    processed_identifiers = [] # 存储 (type, id) 元组
    log_messages = []
    all_bibtex_content = [] # 存储所有BibTeX条目

    log_messages.append("正在从输入中提取 arXiv ID、DOI 或通用 URL...")
    for line in input_lines:
        line = line.strip()
        if not line:
            continue
        extracted = extract_identifiers(line)
        if extracted:
            for id_type, identifier in extracted:
                processed_identifiers.append((id_type, identifier))
        else:
            log_messages.append(f"警告: 无法从 '{line}' 中提取有效的 arXiv ID、DOI 或 URL。将跳过此项。")

    # 去重处理,确保每个唯一的 (type, id) 对只处理一次
    unique_identifiers = list(set(processed_identifiers))
    
    if not unique_identifiers:
        log_messages.append("没有检测到有效的 arXiv ID、DOI 或 URL。请检查输入。")
        return "\n".join(log_messages), "", None

    log_messages.append(f"\n成功提取到 {len(unique_identifiers)} 个唯一的标识符。正在获取 BibTeX 条目...")

    for id_type, identifier in unique_identifiers:
        bibtex_entry = None
        error_message = None

        if id_type == 'arxiv':
            log_messages.append(f"正在获取 arXiv ID '{identifier}' 的 BibTeX 条目...")
            bibtex_entry, error_message = get_bibtex_from_arxiv(identifier)
        elif id_type == 'doi':
            log_messages.append(f"正在获取 DOI '{identifier}' 的 BibTeX 条目...")
            bibtex_entry, error_message = get_bibtex_from_doi(identifier)
        elif id_type == 'web': # 新增的通用 URL 处理
            log_messages.append(f"正在获取通用 URL '{identifier}' 的 BibTeX 条目...")
            bibtex_entry, error_message = get_bibtex_from_url(identifier)
        else:
            error_message = f"未知标识符类型: {id_type} (ID: {identifier})"

        if bibtex_entry:
            all_bibtex_content.append(bibtex_entry)
            log_messages.append(f"成功获取 {id_type.upper()} '{identifier}' 的 BibTeX 条目。")
        else:
            log_messages.append(error_message)
            
    final_bibtex_string = "\n".join(all_bibtex_content)

    temp_bib_filepath = None
    if final_bibtex_string.strip():
        temp_bib_filepath = "temp_references.bib" # 修改文件名以反映更广泛的来源
        try:
            with open(temp_bib_filepath, 'w', encoding='utf-8') as f:
                f.write(final_bibtex_string)
            log_messages.append(f"\nBibTeX 内容已保存到临时文件 '{temp_bib_filepath}' 以供下载。")
        except Exception as e:
            log_messages.append(f"错误: 无法将BibTeX内容写入临时文件: {e}")
            temp_bib_filepath = None

    if not final_bibtex_string.strip():
        log_messages.append("\n没有成功获取任何 BibTeX 条目。")

    return "\n".join(log_messages), final_bibtex_string, temp_bib_filepath

# --- Gradio 界面部分 ---

# 定义 Gradio 界面
iface = gr.Interface(
    fn=get_bibtex_entries_and_display,
    inputs=[
        gr.Textbox(
            lines=10, 
            label="粘贴 arXiv 链接/ID、DOI 或通用 URL (每行一个)", 
            placeholder="例如:\nhttps://arxiv.org/abs/2006.01234\n1904.08133v1\n10.1109/TNNLS.2020.3006789\nhttps://doi.org/10.1038/s41586-023-06670-w\nhttps://www.example.com/blog-post\nwww.anotherblog.org/article"
        )
    ],
    outputs=[
        gr.Textbox(label="处理日志", lines=8, interactive=False),
        gr.Textbox(label="在此复制 BibTeX 内容", lines=10, interactive=True),
        gr.File(label="下载 BibTeX 文件")
    ],
    title="Anything2Bib: 通用 BibTeX 获取工具",
    description="在此粘贴 arXiv 论文的链接/ID、DOI 或通用网页链接 (每行一个),获取其 BibTeX 条目。",
    allow_flagging="never"
)

# 启动 Gradio 应用
if __name__ == "__main__":
    print("Gradio 应用正在启动...")
    print("你可以在浏览器中访问以下地址:")
    iface.launch(share=False)
    print("Gradio 应用已关闭。")