
Ieee Export
- 4 installs
- 31 repo stars
- Updated March 14, 2026
- cookjohn/ieee-skills
Exports IEEE Xplore article citations in RIS, BibTeX, or plain text and can push them to Zotero.
About
Exports article citations from IEEE Xplore in multiple formats and supports pushing to Zotero. A developer uses it to save citation data for papers into a reference manager.
- Extracts citations via the 'Cite This' modal on the document page
- Supports plain text, BibTeX, RIS, RefWorks, and Zotero push
Ieee Export by the numbers
- 4 all-time installs (skills.sh)
- Ranked #1,780 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cookjohn/ieee-skills --skill ieee-exportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 31 |
| Last updated | March 14, 2026 |
| Repository | cookjohn/ieee-skills ↗ |
What it does
Exports IEEE Xplore article citations in RIS, BibTeX, or plain text and can push them to Zotero.
Files
IEEE Xplore Citation Export
Export article citations from IEEE Xplore. Supports Plain Text, BibTeX, RIS, RefWorks formats, and Zotero push.
Single Article Export (from Document Page)
Step 1: Navigate to article page (if needed)
If not already on the article page:
navigate_page({
url: "{BASE_URL}/document/{ARNUMBER}/",
initScript: "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})Step 2: Open "Cite This" modal and extract citation
Click the "Cite This" button, select the desired format tab, and extract the citation text:
async (format) => {
// format: 'text', 'bibtex', 'ris', 'refworks'
// Wait for page to load
for (let i = 0; i < 20; i++) {
if (document.querySelector('.document-title')) break;
await new Promise(r => setTimeout(r, 500));
}
// Click "Cite This" button
const citeBtn = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === 'Cite This');
if (!citeBtn) return { error: 'Cite This button not found.' };
citeBtn.click();
// Wait for modal to appear
await new Promise(r => setTimeout(r, 1500));
// Map format to tab name
const tabMap = {
'text': 'Plain Text',
'bibtex': 'BibTeX',
'ris': 'RIS',
'refworks': 'Refworks'
};
const tabName = tabMap[format] || 'BibTeX';
// Click the desired format tab
const tab = [...document.querySelectorAll('.cite-this-container a')].find(a => a.textContent.trim() === tabName);
if (tab) tab.click();
// Wait for content to load
await new Promise(r => setTimeout(r, 1000));
// Extract citation text
const citeContent = document.querySelector('.cite-this-container .ql-editor') ||
document.querySelector('.cite-this-container [xplmathjax]') ||
document.querySelector('.cite-this-container pre');
const citationText = citeContent?.textContent?.trim() || '';
// Get article info for metadata
const title = document.querySelector('.document-title span')?.textContent?.trim() || '';
const arnumber = window.location.pathname.match(/\/document\/(\d+)/)?.[1] || '';
// Close modal
const closeBtn = document.querySelector('.cite-this-container .fa-times') ||
document.querySelector('.modal .close');
if (closeBtn) closeBtn.click();
return { citationText, format: tabName, title, arnumber };
}Step 3: Save or push citation
Save to file: Write the citation text to a local file using the appropriate extension (.bib, .ris, .txt).
Push to Zotero: See Zotero section below.
Batch Export (from Search Results)
Step 1: On search results page, select articles and open Export modal
async (arnumbers) => {
// Select articles
const items = document.querySelectorAll('.List-results-items .result-item');
let selected = 0;
items.forEach(item => {
const titleLink = item.querySelector('h3 a[href*="/document/"]');
const docNum = titleLink?.href?.match(/\/document\/(\d+)/)?.[1] || '';
if (arnumbers.includes(docNum)) {
const checkbox = item.querySelector('input[type="checkbox"]');
if (checkbox && !checkbox.checked) {
checkbox.click();
selected++;
}
}
});
// Click Export button — it's a button with class xpl-btn-primary and text "Export"
// Note: The button exists in the results-actions bar, NOT inside a dropdown
const exportBtn = [...document.querySelectorAll('button.xpl-btn-primary, button')]
.find(b => b.textContent.trim() === 'Export');
if (!exportBtn) return { error: 'Export button not found. Make sure you are on a search results page.' };
// Must select at least one article before Export works
if (selected === 0) {
return { error: 'No articles selected. Select articles first, then export.' };
}
exportBtn.click();
await new Promise(r => setTimeout(r, 2000));
return { selected, message: 'Export modal opened. Proceed with format selection.' };
}Step 2: Select Citations tab and download
async (format) => {
// Click Citations tab in export modal
const citationsTab = [...document.querySelectorAll('.nav-link')].find(a => a.textContent.trim() === 'Citations');
if (citationsTab) citationsTab.click();
await new Promise(r => setTimeout(r, 1000));
// Select format radio button
// IMPORTANT: Radio buttons are DISABLED until articles are selected in the Results tab first.
// If radios are disabled, switch to Results tab, select articles, then come back to Citations tab.
const formatMap = {
'text': 'download-ascii',
'bibtex': 'download-bibtex',
'ris': 'download-ris',
'refworks': 'download-refworks'
};
const labelFor = formatMap[format] || 'download-bibtex';
// Find the radio by label text as fallback since for= attribute may not match id
const allLabels = document.querySelectorAll('.export-form label, .tab-pane.active label');
const targetLabel = [...allLabels].find(l => l.getAttribute('for') === labelFor ||
l.textContent.trim().toLowerCase().includes(format));
const radio = targetLabel?.querySelector('input[type="radio"]');
if (radio) {
if (radio.disabled) {
return { error: 'Format radio buttons are disabled. Articles must be selected first. Use Step 1 to select articles before exporting citations.' };
}
radio.click();
}
// Select "Citation and Abstract" option
const citAbsLabel = [...allLabels].find(l => l.getAttribute('for') === 'citation-abstract' ||
l.textContent.trim().includes('Citation and Abstract'));
const citAbsRadio = citAbsLabel?.querySelector('input[type="radio"]');
if (citAbsRadio && !citAbsRadio.disabled) citAbsRadio.click();
await new Promise(r => setTimeout(r, 500));
// Click Download button
const downloadBtn = [...document.querySelectorAll('.tab-pane.active button, .modal button')].find(b =>
b.textContent.trim().includes('Download') && !b.textContent.trim().includes('Cancel')
);
if (downloadBtn) {
downloadBtn.click();
return { success: true, format };
}
return { error: 'Download button not found in export panel.' };
}Zotero Push
To push citations to a locally running Zotero instance. Two modes are supported:
Prerequisites: Zotero desktop must be running with the Connector API enabled (default on port 23119).
Mode 1: RIS import (simple, no PDF)
After extracting RIS citation text from the Cite This modal:
python ${CLAUDE_SKILL_DIR}/scripts/push_to_zotero.py --ris-data "{RIS_CONTENT}"Or save to a file first and import:
python ${CLAUDE_SKILL_DIR}/scripts/push_to_zotero.py --ris-file "{RIS_FILE_PATH}"Mode 2: JSON import (structured data with optional PDF attachment)
Save paper data as a JSON file, then run:
python ${CLAUDE_SKILL_DIR}/scripts/push_to_zotero.py --json "{JSON_FILE_PATH}"JSON format (single paper or array):
{
"title": "Paper Title",
"authors": ["Author One", "Author Two"],
"journal": "IEEE Transactions on ...",
"date": "2026",
"doi": "10.1109/...",
"volume": "46",
"issue": "3",
"pages": "1234-1245",
"abstract": "...",
"keywords": ["keyword1", "keyword2"],
"url": "https://ieeexplore.ieee.org/document/{ARNUMBER}",
"pdfUrl": "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber={ARNUMBER}",
"cookies": "..."
}Listing Zotero collections
python ${CLAUDE_SKILL_DIR}/scripts/push_to_zotero.py --listExport Format Reference
| Format | File Extension | Use Case |
|---|---|---|
| Plain Text | .txt | Human-readable citation |
| BibTeX | .bib | LaTeX documents |
| RIS | .ris | Reference managers (Zotero, Mendeley, EndNote) |
| RefWorks | .txt | RefWorks import |
Notes
- The "Cite This" modal on document pages provides citation data for individual articles.
- The "Export" button on search results pages handles batch citation export.
- For batch export, articles must first be selected via checkboxes.
- Citation formats include: Plain Text, BibTeX, RIS, RefWorks.
- Include options: "Citation Only" or "Citation and Abstract".
- For Zotero push, ensure Zotero desktop is running before invoking.
#!/usr/bin/env python3
"""Push IEEE Xplore citation data to Zotero via local Connector API (localhost:23119).
Supports two modes:
1. RIS import: --ris-file or --ris-data (backward compatible)
2. JSON import: --json (structured data with optional PDF attachment)
Session strategy: deterministic sessionID derived from content hash.
- 201 = saved successfully
- 409 = SESSION_EXISTS = already saved (idempotent, treat as success)
- Zotero's session gc/remove are buggy, sessions persist until restart.
Deterministic IDs turn this bug into a feature: same content → same ID → 409 = already done.
"""
import argparse
import hashlib
import io
import json
import sys
import urllib.error
import urllib.request
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
ZOTERO_API = "http://127.0.0.1:23119/connector"
HTTP_TIMEOUT = 15 # seconds, matching Zotero Connector extension
# ---------------------------------------------------------------------------
# Zotero API helpers
# ---------------------------------------------------------------------------
def zotero_request(endpoint, data=None, timeout=HTTP_TIMEOUT):
"""Send JSON request to Zotero local API with timeout."""
url = f"{ZOTERO_API}/{endpoint}"
body = json.dumps(data or {}, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"X-Zotero-Connector-API-Version": "3",
})
try:
resp = urllib.request.urlopen(req, timeout=timeout)
text = resp.read().decode("utf-8")
return resp.status, json.loads(text) if text else None
except urllib.error.HTTPError as e:
resp_body = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(resp_body) if resp_body else None
except json.JSONDecodeError:
return e.code, {"error": resp_body}
except urllib.error.URLError:
return 0, None
except TimeoutError:
return -1, {"error": f"Request timed out ({timeout}s)"}
def make_session_id(content_key):
"""Generate deterministic 12-char sessionID from content key.
Same content always produces the same ID, so:
- First call: creates session, saves items → 201
- Repeat call: session exists → 409 → treat as already saved
"""
return hashlib.md5(
content_key.encode("utf-8", errors="surrogateescape")
).hexdigest()[:12]
def get_selected_collection():
"""Get currently selected Zotero collection."""
status, data = zotero_request("getSelectedCollection")
if status != 200 or not data:
return None
return data
# ---------------------------------------------------------------------------
# RIS import (backward compatible)
# ---------------------------------------------------------------------------
def push_ris(ris_data):
"""Push RIS data to Zotero via /connector/import with deterministic session.
Returns:
dict with 'success' boolean and 'message' string.
"""
if not ris_data.strip():
return {"success": False, "message": "Empty RIS data."}
session_id = make_session_id(ris_data.strip())
url = f"{ZOTERO_API}/import?session={session_id}"
payload = json.dumps(ris_data).encode("utf-8")
req = urllib.request.Request(url, data=payload, headers={
"Content-Type": "application/json",
"Accept": "application/json",
})
try:
resp = urllib.request.urlopen(req, timeout=HTTP_TIMEOUT)
body = resp.read().decode("utf-8", errors="replace")
return {"success": True, "message": f"Saved to Zotero (session: {session_id}). Response: {body}"}
except urllib.error.HTTPError as e:
resp_body = e.read().decode("utf-8", errors="replace")
if e.code == 409:
return {"success": True, "message": f"Already saved, no duplicates added (session: {session_id})"}
return {"success": False, "message": f"HTTP {e.code}: {resp_body}"}
except urllib.error.URLError as e:
return {
"success": False,
"message": f"Cannot connect to Zotero. Is Zotero desktop running? Error: {e.reason}",
}
except TimeoutError:
return {"success": False, "message": f"Request timed out ({HTTP_TIMEOUT}s)"}
except Exception as e:
return {"success": False, "message": f"Unexpected error: {e}"}
# ---------------------------------------------------------------------------
# JSON / structured item import with PDF attachment support
# ---------------------------------------------------------------------------
def build_zotero_item(paper):
"""Build Zotero journalArticle/conferencePaper item from IEEE paper data."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Determine item type based on info
info = paper.get("info", "").lower()
if "conference" in info:
item_type = "conferencePaper"
elif "book" in info:
item_type = "book"
else:
item_type = "journalArticle"
item = {
"itemType": item_type,
"title": paper.get("title", ""),
"abstractNote": paper.get("abstract", ""),
"date": paper.get("date", paper.get("year", "")),
"url": paper.get("url", ""),
"DOI": paper.get("doi", ""),
"volume": paper.get("volume", ""),
"issue": paper.get("issue", ""),
"pages": paper.get("pages", ""),
"publicationTitle": paper.get("journal", paper.get("publication", "")),
"libraryCatalog": "IEEE Xplore",
"accessDate": now,
"creators": [
{"name": a, "creatorType": "author"}
for a in paper.get("authors", [])
],
"tags": [
{"tag": k, "type": 1}
for k in paper.get("keywords", [])
],
"attachments": [],
}
if paper.get("issn"):
item["ISSN"] = paper["issn"]
if paper.get("arnumber"):
item["extra"] = f"arnumber: {paper['arnumber']}"
if item_type == "conferencePaper":
item["conferenceName"] = paper.get("publication", "")
return item
def download_pdf(pdf_url, cookies="", referer="https://ieeexplore.ieee.org"):
"""Download PDF from IEEE Xplore using provided cookies.
Returns (bytes, content_type) or (None, error_message).
"""
req = urllib.request.Request(pdf_url, headers={
"Cookie": cookies,
"Referer": referer,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/145.0.0.0",
})
try:
resp = urllib.request.urlopen(req, timeout=60)
content_type = resp.headers.get("Content-Type", "application/pdf")
data = resp.read()
if len(data) < 1024:
return None, f"PDF file too small ({len(data)} bytes), may require authentication"
return data, content_type
except Exception as e:
return None, str(e)
def save_attachment(session_id, item_id, pdf_bytes, pdf_url,
content_type="application/pdf", title="Full Text PDF"):
"""Upload PDF binary to Zotero via /connector/saveAttachment (Zotero 7.x workflow)."""
metadata = json.dumps({
"id": item_id + "_pdf",
"parentItemID": item_id,
"title": title,
"url": pdf_url,
"contentType": content_type,
})
url = f"{ZOTERO_API}/saveAttachment?sessionID={session_id}"
req = urllib.request.Request(url, data=pdf_bytes, headers={
"Content-Type": content_type,
"X-Metadata": metadata,
"Content-Length": str(len(pdf_bytes)),
"X-Zotero-Connector-API-Version": "3",
})
try:
resp = urllib.request.urlopen(req, timeout=60)
return resp.status, None
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="replace")
except Exception as e:
return 0, str(e)
def save_items(items, uri="", attachments=None, cookies=""):
"""Push items to Zotero via saveItems API, optionally with PDF attachments.
Uses deterministic sessionID (content hash) for idempotency:
- 201 = saved successfully
- 409 = same items already saved in this Zotero session (success)
"""
key = "|".join(sorted(item.get("title", "") for item in items))
session_id = make_session_id(key)
# Assign IDs to items (needed for attachment parentItemID mapping)
for i, item in enumerate(items):
if "id" not in item:
item["id"] = f"ieee_{session_id}_{i}"
data = {
"sessionID": session_id,
"uri": uri,
"items": items,
}
status, resp = zotero_request("saveItems", data)
already_saved = False
if status == 201:
msg = f"Saved to Zotero (session: {session_id})"
elif status == 409:
already_saved = True
msg = f"Already saved, no duplicates added (session: {session_id})"
elif status == 500:
detail = resp.get("error", "") if resp else ""
if "libraryEditable" in str(resp):
return 500, "Target library is read-only. Switch to a writable collection in Zotero."
return 500, f"Zotero internal error: {detail}"
elif status == 0:
return 0, "Zotero is not running or connection refused"
elif status == -1:
return -1, f"Request timed out ({HTTP_TIMEOUT}s)"
else:
return status, f"Unknown error, HTTP {status}"
# Handle PDF attachments (only for new saves, skip if already saved)
if attachments and not already_saved:
col = get_selected_collection()
files_editable = col.get("filesEditable", True) if col else True
if files_editable:
pdf_results = []
for att in attachments:
idx = att.get("itemIndex", 0)
pdf_url = att.get("pdfUrl", "")
title = att.get("title", "Full Text PDF")
if not pdf_url:
continue
item_id = items[idx]["id"] if idx < len(items) else items[0]["id"]
print(f" Downloading PDF: {pdf_url[:80]}...", file=sys.stderr)
pdf_bytes, ct = download_pdf(pdf_url, cookies=cookies)
if pdf_bytes is None:
pdf_results.append(f" PDF download failed: {ct}")
continue
print(f" Uploading PDF to Zotero ({len(pdf_bytes)} bytes)...", file=sys.stderr)
att_status, att_err = save_attachment(
session_id, item_id, pdf_bytes, pdf_url, title=title
)
if att_status == 201:
pdf_results.append(f" PDF attached: {title} ({len(pdf_bytes) // 1024}KB)")
else:
pdf_results.append(f" PDF upload failed: HTTP {att_status} {att_err or ''}")
if pdf_results:
msg += "\n" + "\n".join(pdf_results)
else:
msg += "\n (Target collection does not support file attachments, skipping PDF)"
return 201, msg
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Push IEEE Xplore citations to Zotero"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--ris-file", help="Path to an RIS file to import")
group.add_argument("--ris-data", help="RIS data as a string")
group.add_argument(
"--json",
help="Path to JSON file with structured paper data (supports PDF attachment)",
)
group.add_argument("--list", action="store_true", help="List Zotero collections")
args = parser.parse_args()
# Check Zotero is running
status, _ = zotero_request("ping")
if status == 0:
print("Error: Zotero is not running. Please start Zotero desktop.")
sys.exit(1)
if args.list:
col = get_selected_collection()
if col:
print(f"Current collection: {col.get('name', '?')} (ID: {col.get('id', '?')})")
print(f"Library: {col.get('libraryName', '?')}")
for t in col.get("targets", []):
indent = " " * t.get("level", 0)
print(f" {indent}{t['name']} (ID: {t['id']})")
return
# Show current collection
col = get_selected_collection()
if col:
print(f"Zotero collection: {col.get('name', '?')}")
# Mode 1: RIS import
if args.ris_file or args.ris_data:
if args.ris_file:
with open(args.ris_file, "r", encoding="utf-8") as f:
ris_data = f.read()
else:
ris_data = args.ris_data
result = push_ris(ris_data)
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0 if result["success"] else 1)
# Mode 2: JSON structured import
if args.json:
with open(args.json, "r", encoding="utf-8") as f:
paper_data = json.load(f)
# Handle both single paper and array
if isinstance(paper_data, list):
papers = paper_data
elif "items" in paper_data:
# Already in Zotero format
status, msg = save_items(
paper_data["items"], paper_data.get("uri", "")
)
if status == 201:
print(f"Success: {msg} ({len(paper_data['items'])} items)")
else:
print(f"Failed: {msg}")
sys.exit(1)
return
else:
papers = [paper_data]
# Build Zotero items
items = []
for p in papers:
if "itemType" in p:
items.append(p)
elif "title" in p:
items.append(build_zotero_item(p))
if not items:
print("Error: No valid paper data found.")
sys.exit(1)
# Collect attachment info and cookies from input
attachments = []
cookies = ""
for i, p in enumerate(papers):
if p.get("pdfUrl"):
attachments.append({
"itemIndex": i,
"pdfUrl": p["pdfUrl"],
"title": p.get("pdfTitle", "Full Text PDF"),
})
if p.get("cookies") and not cookies:
cookies = p["cookies"]
uri = papers[0].get("url", "")
status, msg = save_items(items, uri, attachments=attachments, cookies=cookies)
if status == 201:
print(f"Success: {msg} ({len(items)} items)")
for item in items:
print(f" - {item.get('title', '?')}")
else:
print(f"Failed: {msg}")
sys.exit(1)
if __name__ == "__main__":
main()