apaper-mcp 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apaper_mcp/__init__.py +3 -0
- apaper_mcp/__main__.py +7 -0
- apaper_mcp/formatters.py +176 -0
- apaper_mcp/models.py +42 -0
- apaper_mcp/platforms/__init__.py +1 -0
- apaper_mcp/platforms/arxiv.py +257 -0
- apaper_mcp/platforms/cnki.py +103 -0
- apaper_mcp/platforms/dblp.py +57 -0
- apaper_mcp/platforms/iacr.py +171 -0
- apaper_mcp/platforms/scholar.py +62 -0
- apaper_mcp/proxy.py +92 -0
- apaper_mcp/server.py +388 -0
- apaper_mcp-0.3.0.dist-info/METADATA +176 -0
- apaper_mcp-0.3.0.dist-info/RECORD +17 -0
- apaper_mcp-0.3.0.dist-info/WHEEL +4 -0
- apaper_mcp-0.3.0.dist-info/entry_points.txt +2 -0
- apaper_mcp-0.3.0.dist-info/licenses/LICENSE +21 -0
apaper_mcp/__init__.py
ADDED
apaper_mcp/__main__.py
ADDED
apaper_mcp/formatters.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _year_filter(minimum: int | None, maximum: int | None) -> str:
|
|
6
|
+
if minimum is None and maximum is None:
|
|
7
|
+
return ""
|
|
8
|
+
return f" in year range ({minimum if minimum is not None else 'earliest'}-{maximum if maximum is not None else 'latest'})"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_iacr_search_response(
|
|
12
|
+
papers: list[dict[str, Any]],
|
|
13
|
+
query: str,
|
|
14
|
+
year_min: int | None = None,
|
|
15
|
+
year_max: int | None = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
if not papers:
|
|
18
|
+
return f"No papers found for query: {query}{_year_filter(year_min, year_max)}"
|
|
19
|
+
lines = [
|
|
20
|
+
f"Found {len(papers)} IACR papers for query '{query}'{_year_filter(year_min, year_max)}:",
|
|
21
|
+
"",
|
|
22
|
+
]
|
|
23
|
+
for index, paper in enumerate(papers, 1):
|
|
24
|
+
lines += [
|
|
25
|
+
f"{index}. **{paper['title']}**",
|
|
26
|
+
f" - Paper ID: {paper['paper_id']}",
|
|
27
|
+
f" - Authors: {', '.join(paper['authors'])}",
|
|
28
|
+
f" - URL: {paper['url']}",
|
|
29
|
+
f" - PDF: {paper['pdf_url']}",
|
|
30
|
+
]
|
|
31
|
+
if paper["categories"]:
|
|
32
|
+
lines.append(f" - Categories: {', '.join(paper['categories'])}")
|
|
33
|
+
if paper["keywords"]:
|
|
34
|
+
lines.append(f" - Keywords: {', '.join(paper['keywords'])}")
|
|
35
|
+
if paper["abstract"]:
|
|
36
|
+
lines.append(f" - Abstract: {paper['abstract']}")
|
|
37
|
+
lines.append("")
|
|
38
|
+
return "\n".join(lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_dblp_search_response(
|
|
42
|
+
results: list[dict[str, Any]], query: str, options: dict[str, Any]
|
|
43
|
+
) -> str:
|
|
44
|
+
filters = []
|
|
45
|
+
if options.get("year_from") is not None or options.get("year_to") is not None:
|
|
46
|
+
filters.append(
|
|
47
|
+
f"year range ({options.get('year_from', 'earliest')}-{options.get('year_to', 'latest')})"
|
|
48
|
+
)
|
|
49
|
+
if options.get("venue_filter"):
|
|
50
|
+
filters.append(f"venue '{options['venue_filter']}'")
|
|
51
|
+
suffix = f" with filters: {', '.join(filters)}" if filters else ""
|
|
52
|
+
if not results:
|
|
53
|
+
return f"No papers found for query: {query}{suffix}"
|
|
54
|
+
if options.get("include_bibtex"):
|
|
55
|
+
lines = [
|
|
56
|
+
f"Found {len(results)} DBLP BibTeX entries for query '{query}'{suffix}:",
|
|
57
|
+
"",
|
|
58
|
+
]
|
|
59
|
+
for index, result in enumerate(results, 1):
|
|
60
|
+
lines += [
|
|
61
|
+
f"{index}. DBLP Key: {result['dblp_key']}",
|
|
62
|
+
"```bibtex",
|
|
63
|
+
result["bibtex"],
|
|
64
|
+
"```",
|
|
65
|
+
"",
|
|
66
|
+
]
|
|
67
|
+
return "\n".join(lines)
|
|
68
|
+
lines = [f"Found {len(results)} DBLP papers for query '{query}'{suffix}:", ""]
|
|
69
|
+
for index, result in enumerate(results, 1):
|
|
70
|
+
lines += [
|
|
71
|
+
f"{index}. **{result['title'] or 'Untitled'}**",
|
|
72
|
+
f" - DBLP Key: {result['dblp_key']}",
|
|
73
|
+
f" - Authors: {', '.join(result['authors'])}",
|
|
74
|
+
]
|
|
75
|
+
if result.get("venue"):
|
|
76
|
+
lines.append(f" - Venue: {result['venue']}")
|
|
77
|
+
if result.get("year"):
|
|
78
|
+
lines.append(f" - Year: {result['year']}")
|
|
79
|
+
if result.get("doi"):
|
|
80
|
+
lines.append(f" - DOI: {result['doi']}")
|
|
81
|
+
if result.get("url"):
|
|
82
|
+
lines.append(f" - URL: {result['url']}")
|
|
83
|
+
lines.append("")
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def format_google_scholar_search_response(
|
|
88
|
+
papers: list[dict[str, Any]],
|
|
89
|
+
query: str,
|
|
90
|
+
year_low: int | None = None,
|
|
91
|
+
year_high: int | None = None,
|
|
92
|
+
) -> str:
|
|
93
|
+
if not papers:
|
|
94
|
+
return f"No papers found for query: {query}{_year_filter(year_low, year_high)}"
|
|
95
|
+
lines = [
|
|
96
|
+
f"Found {len(papers)} Google Scholar papers for query '{query}'{_year_filter(year_low, year_high)}:",
|
|
97
|
+
"",
|
|
98
|
+
]
|
|
99
|
+
for index, paper in enumerate(papers, 1):
|
|
100
|
+
lines += [
|
|
101
|
+
f"{index}. **{paper['title']}**",
|
|
102
|
+
f" - Authors: {', '.join(paper['authors'])}",
|
|
103
|
+
]
|
|
104
|
+
if paper["citations"] > 0:
|
|
105
|
+
lines.append(f" - Citations: {paper['citations']}")
|
|
106
|
+
if paper.get("published_date"):
|
|
107
|
+
try:
|
|
108
|
+
lines.append(
|
|
109
|
+
f" - Year: {datetime.fromisoformat(paper['published_date'].replace('Z', '+00:00')).year}"
|
|
110
|
+
)
|
|
111
|
+
except ValueError:
|
|
112
|
+
pass
|
|
113
|
+
if paper.get("url"):
|
|
114
|
+
lines.append(f" - URL: {paper['url']}")
|
|
115
|
+
if paper.get("abstract"):
|
|
116
|
+
lines.append(f" - Abstract: {paper['abstract']}")
|
|
117
|
+
lines.append("")
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_arxiv_search_response(
|
|
122
|
+
papers: list[dict[str, Any]], query: str, options: dict[str, Any] | None = None
|
|
123
|
+
) -> str:
|
|
124
|
+
options = options or {}
|
|
125
|
+
filters = []
|
|
126
|
+
if options.get("date_from") or options.get("date_to"):
|
|
127
|
+
filters.append(
|
|
128
|
+
f"date range ({options.get('date_from', 'earliest')} to {options.get('date_to', 'latest')})"
|
|
129
|
+
)
|
|
130
|
+
if options.get("categories"):
|
|
131
|
+
filters.append(f"categories [{', '.join(options['categories'])}]")
|
|
132
|
+
suffix = f" with filters: {', '.join(filters)}" if filters else ""
|
|
133
|
+
if not papers:
|
|
134
|
+
return f"No arXiv papers found for query: {query}{suffix}"
|
|
135
|
+
lines = [f"Found {len(papers)} arXiv papers for query '{query}'{suffix}:", ""]
|
|
136
|
+
for index, p in enumerate(papers, 1):
|
|
137
|
+
lines += [
|
|
138
|
+
f"{index}. **{p['title'] or 'Untitled'}**",
|
|
139
|
+
f" - Paper ID: {p['paper_id']}",
|
|
140
|
+
]
|
|
141
|
+
if p["authors"]:
|
|
142
|
+
lines.append(f" - Authors: {', '.join(p['authors'])}")
|
|
143
|
+
if p["primary_category"]:
|
|
144
|
+
lines.append(f" - Primary Category: {p['primary_category']}")
|
|
145
|
+
if p["categories"]:
|
|
146
|
+
lines.append(f" - Categories: {', '.join(p['categories'])}")
|
|
147
|
+
if p["published_date"]:
|
|
148
|
+
lines.append(f" - Published: {p['published_date']}")
|
|
149
|
+
if p["doi"]:
|
|
150
|
+
lines.append(f" - DOI: {p['doi']}")
|
|
151
|
+
if p["journal_ref"]:
|
|
152
|
+
lines.append(f" - Journal Ref: {p['journal_ref']}")
|
|
153
|
+
if p["url"]:
|
|
154
|
+
lines.append(f" - URL: {p['url']}")
|
|
155
|
+
if p["pdf_url"]:
|
|
156
|
+
lines.append(f" - PDF: {p['pdf_url']}")
|
|
157
|
+
if p["abstract"]:
|
|
158
|
+
lines.append(f" - Abstract: {p['abstract']}")
|
|
159
|
+
lines.append("")
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def format_cnki_search_response(papers: list[dict[str, Any]], query: str) -> str:
|
|
164
|
+
if not papers:
|
|
165
|
+
return f"No CNKI papers found for query: {query}"
|
|
166
|
+
lines = [f"Found {len(papers)} CNKI papers for query '{query}':", ""]
|
|
167
|
+
for index, p in enumerate(papers, 1):
|
|
168
|
+
lines.append(f"{index}. **{p['title'] or 'Untitled'}**")
|
|
169
|
+
if p["authors"]:
|
|
170
|
+
lines.append(f" - Authors: {', '.join(p['authors'])}")
|
|
171
|
+
if p["source"]:
|
|
172
|
+
lines.append(f" - Source: {p['source']}")
|
|
173
|
+
if p["href"]:
|
|
174
|
+
lines.append(f" - URL: {p['href']}")
|
|
175
|
+
lines.append("")
|
|
176
|
+
return "\n".join(lines)
|
apaper_mcp/models.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Paper(TypedDict):
|
|
5
|
+
paper_id: str
|
|
6
|
+
title: str
|
|
7
|
+
authors: list[str]
|
|
8
|
+
abstract: str
|
|
9
|
+
doi: str
|
|
10
|
+
published_date: str | None
|
|
11
|
+
pdf_url: str
|
|
12
|
+
url: str
|
|
13
|
+
source: str
|
|
14
|
+
categories: list[str]
|
|
15
|
+
keywords: list[str]
|
|
16
|
+
citations: int
|
|
17
|
+
references: list[str]
|
|
18
|
+
extra: dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ArxivPaper(TypedDict):
|
|
22
|
+
paper_id: str
|
|
23
|
+
title: str
|
|
24
|
+
authors: list[str]
|
|
25
|
+
abstract: str
|
|
26
|
+
categories: list[str]
|
|
27
|
+
primary_category: str
|
|
28
|
+
published_date: str
|
|
29
|
+
updated_date: str
|
|
30
|
+
pdf_url: str
|
|
31
|
+
url: str
|
|
32
|
+
doi: str
|
|
33
|
+
journal_ref: str
|
|
34
|
+
comment: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CnkiPaper(TypedDict):
|
|
38
|
+
title: str
|
|
39
|
+
authors: list[str]
|
|
40
|
+
source: str
|
|
41
|
+
source_link: str
|
|
42
|
+
href: str
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Platform-specific academic paper clients and parsers."""
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
from ..models import ArxivPaper
|
|
10
|
+
|
|
11
|
+
_GROUPS = {
|
|
12
|
+
"cs": "computer_science",
|
|
13
|
+
"econ": "economics",
|
|
14
|
+
"eess": "eess",
|
|
15
|
+
"math": "mathematics",
|
|
16
|
+
"physics": "physics",
|
|
17
|
+
"astro-ph": "physics",
|
|
18
|
+
"cond-mat": "physics",
|
|
19
|
+
"gr-qc": "physics",
|
|
20
|
+
"hep-ex": "physics",
|
|
21
|
+
"hep-lat": "physics",
|
|
22
|
+
"hep-ph": "physics",
|
|
23
|
+
"hep-th": "physics",
|
|
24
|
+
"math-ph": "physics",
|
|
25
|
+
"nlin": "physics",
|
|
26
|
+
"nucl-ex": "physics",
|
|
27
|
+
"nucl-th": "physics",
|
|
28
|
+
"quant-ph": "physics",
|
|
29
|
+
"q-bio": "q_biology",
|
|
30
|
+
"q-fin": "q_finance",
|
|
31
|
+
"stat": "statistics",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_arxiv_search_params(
|
|
36
|
+
query: str, options: dict[str, object] | None = None
|
|
37
|
+
) -> dict[str, str]:
|
|
38
|
+
options = options or {}
|
|
39
|
+
term = query.strip()
|
|
40
|
+
field = "all"
|
|
41
|
+
for prefix, name in (
|
|
42
|
+
("ti:", "title"),
|
|
43
|
+
("au:", "author"),
|
|
44
|
+
("abs:", "abstract"),
|
|
45
|
+
("cat:", "all"),
|
|
46
|
+
("doi:", "doi"),
|
|
47
|
+
):
|
|
48
|
+
if term.lower().startswith(prefix):
|
|
49
|
+
term, field = term[len(prefix) :].strip(), name
|
|
50
|
+
break
|
|
51
|
+
date_from, date_to = options.get("date_from"), options.get("date_to")
|
|
52
|
+
categories = options.get("categories") or []
|
|
53
|
+
if not term and not date_from and not date_to:
|
|
54
|
+
raise ValueError("No search criteria provided")
|
|
55
|
+
max_results = max(1, int(options.get("max_results", 10)))
|
|
56
|
+
size = (
|
|
57
|
+
25
|
|
58
|
+
if max_results <= 25
|
|
59
|
+
else 50
|
|
60
|
+
if max_results <= 50
|
|
61
|
+
else 100
|
|
62
|
+
if max_results <= 100
|
|
63
|
+
else 200
|
|
64
|
+
)
|
|
65
|
+
if not categories and not date_from and not date_to and field == "all":
|
|
66
|
+
result = {"searchtype": "all", "query": term, "start": "0", "size": str(size)}
|
|
67
|
+
if options.get("sort_by") == "date":
|
|
68
|
+
result["order"] = "-announced_date_first"
|
|
69
|
+
return result
|
|
70
|
+
result = {
|
|
71
|
+
"advanced": "",
|
|
72
|
+
"terms-0-operator": "AND",
|
|
73
|
+
"terms-0-term": term,
|
|
74
|
+
"terms-0-field": field,
|
|
75
|
+
}
|
|
76
|
+
groups = {
|
|
77
|
+
_GROUPS.get(str(category).split(".")[0].lower()) for category in categories
|
|
78
|
+
}
|
|
79
|
+
groups.discard(None)
|
|
80
|
+
for group in groups:
|
|
81
|
+
result[f"classification-{group}"] = "y"
|
|
82
|
+
result.update(
|
|
83
|
+
{
|
|
84
|
+
"classification-physics_archives": "all",
|
|
85
|
+
"classification-include_cross_list": "include",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
if date_from or date_to:
|
|
89
|
+
for label, value in (("date_from", date_from), ("date_to", date_to)):
|
|
90
|
+
if value and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", str(value)):
|
|
91
|
+
raise ValueError(f"Invalid {label} format. Use YYYY-MM-DD: {value}")
|
|
92
|
+
result.update(
|
|
93
|
+
{
|
|
94
|
+
"date-filter_by": "date_range",
|
|
95
|
+
"date-year": "",
|
|
96
|
+
"date-from_date": str(date_from or ""),
|
|
97
|
+
"date-to_date": str(date_to or ""),
|
|
98
|
+
"date-date_type": "submitted_date",
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
result.update(
|
|
103
|
+
{
|
|
104
|
+
"date-filter_by": "all_dates",
|
|
105
|
+
"date-year": "",
|
|
106
|
+
"date-from_date": "",
|
|
107
|
+
"date-to_date": "",
|
|
108
|
+
"date-date_type": "submitted_date",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
result.update(
|
|
112
|
+
{
|
|
113
|
+
"abstracts": "show",
|
|
114
|
+
"size": str(size),
|
|
115
|
+
"order": "-announced_date_first"
|
|
116
|
+
if options.get("sort_by") == "date"
|
|
117
|
+
else "",
|
|
118
|
+
"start": "0",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_arxiv_search_html(html: str) -> list[ArxivPaper]:
|
|
125
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
126
|
+
papers = []
|
|
127
|
+
for item in soup.select("li.arxiv-result"):
|
|
128
|
+
link = item.select_one("p.list-title a[href*='/abs/']")
|
|
129
|
+
if not link:
|
|
130
|
+
continue
|
|
131
|
+
match = re.search(r"/abs/([^/?#]+)", link.get("href", ""))
|
|
132
|
+
if not match:
|
|
133
|
+
continue
|
|
134
|
+
paper_id = re.sub(r"v\d+$", "", match.group(1), flags=re.I)
|
|
135
|
+
clean = lambda value: re.sub(r"\s+", " ", value or "").strip()
|
|
136
|
+
tags = [
|
|
137
|
+
clean(x.get_text())
|
|
138
|
+
for x in item.select("div.tags span.tag")
|
|
139
|
+
if clean(x.get_text())
|
|
140
|
+
]
|
|
141
|
+
pdf = next(
|
|
142
|
+
(
|
|
143
|
+
x.get("href", "")
|
|
144
|
+
for x in item.select("p.list-title a")
|
|
145
|
+
if "/pdf/" in x.get("href", "")
|
|
146
|
+
),
|
|
147
|
+
f"https://arxiv.org/pdf/{paper_id}",
|
|
148
|
+
)
|
|
149
|
+
date_match = re.search(
|
|
150
|
+
r"Submitted\s+(\d{1,2}\s+\w+,?\s+\d{4})",
|
|
151
|
+
clean(
|
|
152
|
+
item.select_one("p.is-size-7").get_text()
|
|
153
|
+
if item.select_one("p.is-size-7")
|
|
154
|
+
else ""
|
|
155
|
+
),
|
|
156
|
+
re.I,
|
|
157
|
+
)
|
|
158
|
+
published = ""
|
|
159
|
+
if date_match:
|
|
160
|
+
try:
|
|
161
|
+
published = (
|
|
162
|
+
datetime.strptime(date_match.group(1).replace(",", ""), "%d %B %Y")
|
|
163
|
+
.replace(tzinfo=timezone.utc)
|
|
164
|
+
.isoformat()
|
|
165
|
+
.replace("+00:00", "Z")
|
|
166
|
+
)
|
|
167
|
+
except ValueError:
|
|
168
|
+
pass
|
|
169
|
+
comments = {"comment": "", "journal_ref": "", "doi": ""}
|
|
170
|
+
for paragraph in item.select("p.comments"):
|
|
171
|
+
text = clean(paragraph.get_text())
|
|
172
|
+
for label, key in (
|
|
173
|
+
("Comments", "comment"),
|
|
174
|
+
("Journal ref", "journal_ref"),
|
|
175
|
+
("doi", "doi"),
|
|
176
|
+
):
|
|
177
|
+
if re.match(label + r"\s*:", text, re.I):
|
|
178
|
+
comments[key] = re.sub(label + r"\s*:\s*", "", text, flags=re.I)
|
|
179
|
+
papers.append(
|
|
180
|
+
{
|
|
181
|
+
"paper_id": paper_id,
|
|
182
|
+
"title": clean(
|
|
183
|
+
item.select_one("p.title").get_text()
|
|
184
|
+
if item.select_one("p.title")
|
|
185
|
+
else ""
|
|
186
|
+
),
|
|
187
|
+
"authors": [
|
|
188
|
+
clean(x.get_text())
|
|
189
|
+
for x in item.select("p.authors a")
|
|
190
|
+
if clean(x.get_text())
|
|
191
|
+
],
|
|
192
|
+
"abstract": clean(
|
|
193
|
+
(
|
|
194
|
+
item.select_one("span.abstract-full")
|
|
195
|
+
or item.select_one("span.abstract-short")
|
|
196
|
+
).get_text()
|
|
197
|
+
if (
|
|
198
|
+
item.select_one("span.abstract-full")
|
|
199
|
+
or item.select_one("span.abstract-short")
|
|
200
|
+
)
|
|
201
|
+
else ""
|
|
202
|
+
)
|
|
203
|
+
.removesuffix("▽ More")
|
|
204
|
+
.removesuffix("△ Less")
|
|
205
|
+
.strip(),
|
|
206
|
+
"categories": tags,
|
|
207
|
+
"primary_category": tags[0] if tags else "",
|
|
208
|
+
"published_date": published,
|
|
209
|
+
"updated_date": published,
|
|
210
|
+
"pdf_url": pdf,
|
|
211
|
+
"url": f"https://arxiv.org/abs/{paper_id}",
|
|
212
|
+
**comments,
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
return papers
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse_retry_after_ms(value: str | None, now: float | None = None) -> int | None:
|
|
219
|
+
if not value or not value.strip():
|
|
220
|
+
return None
|
|
221
|
+
if value.strip().isdigit():
|
|
222
|
+
return int(value.strip()) * 1000
|
|
223
|
+
if " " not in value:
|
|
224
|
+
return None
|
|
225
|
+
try:
|
|
226
|
+
return max(
|
|
227
|
+
0,
|
|
228
|
+
int(
|
|
229
|
+
datetime.strptime(value.strip(), "%a, %d %b %Y %H:%M:%S GMT")
|
|
230
|
+
.replace(tzinfo=timezone.utc)
|
|
231
|
+
.timestamp()
|
|
232
|
+
* 1000
|
|
233
|
+
- (now if now is not None else time.time() * 1000)
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
except ValueError:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def compute_backoff_ms(
|
|
241
|
+
attempt: int,
|
|
242
|
+
retry_after_ms: int | None,
|
|
243
|
+
options: dict[str, int],
|
|
244
|
+
rand=random.random,
|
|
245
|
+
) -> int:
|
|
246
|
+
delay = (
|
|
247
|
+
max(options["base_ms"] * 2**attempt, retry_after_ms or 0)
|
|
248
|
+
+ rand() * options["jitter_ms"]
|
|
249
|
+
)
|
|
250
|
+
return int(min(delay, options["max_ms"]))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def looks_like_ip(text: str) -> bool:
|
|
254
|
+
return bool(
|
|
255
|
+
re.fullmatch(r"(\d{1,3}\.){3}\d{1,3}", text)
|
|
256
|
+
or (":" in text and re.fullmatch(r"[0-9a-fA-F:]{2,45}", text))
|
|
257
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from urllib.parse import unquote, urljoin
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_cnki_query_json(value: str) -> dict:
|
|
7
|
+
return {
|
|
8
|
+
"Platform": "",
|
|
9
|
+
"Resource": "CROSSDB",
|
|
10
|
+
"Classid": "WD0FTY92",
|
|
11
|
+
"Products": "CJFQ,CAPJ,WWJD,CJTL,CDFD,CMFD,CPFD,IPFD,CPVD,WWPD,CCND,SCSF,SCHF,SCSD,SOSD,SNAD,CCJD,WBFD,WWBD,CCVD,CJFN",
|
|
12
|
+
"QNode": {
|
|
13
|
+
"QGroup": [
|
|
14
|
+
{
|
|
15
|
+
"Key": "Subject",
|
|
16
|
+
"Title": "",
|
|
17
|
+
"Logic": 0,
|
|
18
|
+
"Items": [
|
|
19
|
+
{
|
|
20
|
+
"Field": "SU",
|
|
21
|
+
"Value": value,
|
|
22
|
+
"Operator": "TOPRANK",
|
|
23
|
+
"Logic": 0,
|
|
24
|
+
"Title": "主题",
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"ChildItems": [],
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"ExScope": 1,
|
|
32
|
+
"SearchType": 2,
|
|
33
|
+
"Rlang": "BOTH",
|
|
34
|
+
"KuaKuCode": "YSTT4HG0,LSTPFY1C,JUP3MUPD,MPMFIG1A,WQ0UVIAA,BLZOG7CK,PWFIRAGL,EMRPGLPA,NLBO1Z6R,NN3FJMUV",
|
|
35
|
+
"Expands": {},
|
|
36
|
+
"SearchFrom": 4,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_cnki_search_html(
|
|
41
|
+
html: str, base_url: str = "https://kns.cnki.net/"
|
|
42
|
+
) -> list[dict]:
|
|
43
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
44
|
+
papers = []
|
|
45
|
+
for row in soup.select(
|
|
46
|
+
"table.result-table-list tbody tr, table.result-table-list tr"
|
|
47
|
+
):
|
|
48
|
+
title = row.select_one("td.name a.fz14")
|
|
49
|
+
if (
|
|
50
|
+
not title
|
|
51
|
+
or not title.get("href")
|
|
52
|
+
or title["href"].startswith("javascript:")
|
|
53
|
+
or "/kcms2/article/abstract" not in title["href"]
|
|
54
|
+
):
|
|
55
|
+
continue
|
|
56
|
+
source = row.select_one("td.source")
|
|
57
|
+
source_link = ""
|
|
58
|
+
source_name = ""
|
|
59
|
+
if source:
|
|
60
|
+
source_a = next(
|
|
61
|
+
(
|
|
62
|
+
x
|
|
63
|
+
for x in source.select("a")
|
|
64
|
+
if x.get("href")
|
|
65
|
+
and ("knavi/detail" in x["href"] or "thinker.cnki.net" in x["href"])
|
|
66
|
+
),
|
|
67
|
+
None,
|
|
68
|
+
)
|
|
69
|
+
if source_a:
|
|
70
|
+
source_name, source_link = (
|
|
71
|
+
source_a.get_text(strip=True),
|
|
72
|
+
urljoin(base_url, source_a["href"]),
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
source_name = next(
|
|
76
|
+
(
|
|
77
|
+
x.get_text(strip=True)
|
|
78
|
+
for x in source.select("span")
|
|
79
|
+
if x.get_text(strip=True) not in ("—",)
|
|
80
|
+
),
|
|
81
|
+
source.get_text(" ", strip=True),
|
|
82
|
+
)
|
|
83
|
+
papers.append(
|
|
84
|
+
{
|
|
85
|
+
"title": title.get_text(strip=True),
|
|
86
|
+
"authors": [
|
|
87
|
+
x.get_text(strip=True)
|
|
88
|
+
for x in row.select("td.author a.KnowledgeNetLink")
|
|
89
|
+
],
|
|
90
|
+
"source": source_name,
|
|
91
|
+
"source_link": source_link,
|
|
92
|
+
"href": urljoin(base_url, title["href"]),
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
return papers
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_filename_from_content_disposition(disposition: str) -> str:
|
|
99
|
+
match = re.search(r"filename\*\s*=\s*[^']*''([^;]+)", disposition, re.I)
|
|
100
|
+
if match:
|
|
101
|
+
return unquote(match.group(1).strip())
|
|
102
|
+
match = re.search(r"filename\s*=\s*\"?([^\";]+)", disposition, re.I)
|
|
103
|
+
return unquote(match.group(1).strip()) if match else ""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_dblp_api_response(payload: Any) -> list[dict]:
|
|
5
|
+
hits = (
|
|
6
|
+
payload.get("result", {}).get("hits", {}).get("hit", [])
|
|
7
|
+
if isinstance(payload, dict)
|
|
8
|
+
else []
|
|
9
|
+
)
|
|
10
|
+
if not isinstance(hits, list):
|
|
11
|
+
hits = [hits] if hits else []
|
|
12
|
+
result = []
|
|
13
|
+
for publication in hits:
|
|
14
|
+
info = publication.get("info", {})
|
|
15
|
+
authors = info.get("authors", {}).get("author", [])
|
|
16
|
+
if not isinstance(authors, list):
|
|
17
|
+
authors = [authors] if authors else []
|
|
18
|
+
authors = [a if isinstance(a, str) else a.get("text", "") for a in authors]
|
|
19
|
+
url = info.get("url", "")
|
|
20
|
+
result.append(
|
|
21
|
+
{
|
|
22
|
+
"title": info.get("title", ""),
|
|
23
|
+
"authors": [a for a in authors if a],
|
|
24
|
+
"venue": info.get("venue", ""),
|
|
25
|
+
"year": int(info["year"])
|
|
26
|
+
if str(info.get("year", "")).isdigit()
|
|
27
|
+
else None,
|
|
28
|
+
"type": info.get("type", ""),
|
|
29
|
+
"doi": info.get("doi", ""),
|
|
30
|
+
"ee": info.get("ee", ""),
|
|
31
|
+
"url": url,
|
|
32
|
+
"dblp_key": url.replace("https://dblp.org/rec/", "")
|
|
33
|
+
or publication.get("key", "").removeprefix("dblp:"),
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def filter_dblp_results(results: list[dict], options: dict[str, Any]) -> list[dict]:
|
|
40
|
+
return [
|
|
41
|
+
r
|
|
42
|
+
for r in results
|
|
43
|
+
if not (
|
|
44
|
+
options.get("year_from") is not None
|
|
45
|
+
and r.get("year") is not None
|
|
46
|
+
and r["year"] < options["year_from"]
|
|
47
|
+
)
|
|
48
|
+
and not (
|
|
49
|
+
options.get("year_to") is not None
|
|
50
|
+
and r.get("year") is not None
|
|
51
|
+
and r["year"] > options["year_to"]
|
|
52
|
+
)
|
|
53
|
+
and not (
|
|
54
|
+
options.get("venue_filter")
|
|
55
|
+
and options["venue_filter"].lower() not in r.get("venue", "").lower()
|
|
56
|
+
)
|
|
57
|
+
]
|