doi_to_bibtex.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/env python3
  2. """
  3. DOI to BibTeX Converter
  4. Quick utility to convert DOIs to BibTeX format using CrossRef API.
  5. """
  6. import sys
  7. import requests
  8. import argparse
  9. import time
  10. import json
  11. from typing import Optional, List
  12. class DOIConverter:
  13. """Convert DOIs to BibTeX entries using CrossRef API."""
  14. def __init__(self):
  15. self.session = requests.Session()
  16. self.session.headers.update({
  17. 'User-Agent': 'DOIConverter/1.0 (Citation Management Tool; mailto:support@example.com)'
  18. })
  19. def doi_to_bibtex(self, doi: str) -> Optional[str]:
  20. """
  21. Convert a single DOI to BibTeX format.
  22. Args:
  23. doi: Digital Object Identifier
  24. Returns:
  25. BibTeX string or None if conversion fails
  26. """
  27. # Clean DOI (remove URL prefix if present)
  28. doi = doi.strip()
  29. if doi.startswith('https://doi.org/'):
  30. doi = doi.replace('https://doi.org/', '')
  31. elif doi.startswith('http://doi.org/'):
  32. doi = doi.replace('http://doi.org/', '')
  33. elif doi.startswith('doi:'):
  34. doi = doi.replace('doi:', '')
  35. # Request BibTeX from CrossRef content negotiation
  36. url = f'https://doi.org/{doi}'
  37. headers = {
  38. 'Accept': 'application/x-bibtex',
  39. 'User-Agent': 'DOIConverter/1.0 (Citation Management Tool)'
  40. }
  41. try:
  42. response = self.session.get(url, headers=headers, timeout=15)
  43. if response.status_code == 200:
  44. bibtex = response.text.strip()
  45. # CrossRef sometimes returns entries with @data type, convert to @misc
  46. if bibtex.startswith('@data{'):
  47. bibtex = bibtex.replace('@data{', '@misc{', 1)
  48. return bibtex
  49. elif response.status_code == 404:
  50. print(f'Error: DOI not found: {doi}', file=sys.stderr)
  51. return None
  52. else:
  53. print(f'Error: Failed to retrieve BibTeX for {doi} (status {response.status_code})', file=sys.stderr)
  54. return None
  55. except requests.exceptions.Timeout:
  56. print(f'Error: Request timeout for DOI: {doi}', file=sys.stderr)
  57. return None
  58. except requests.exceptions.RequestException as e:
  59. print(f'Error: Request failed for {doi}: {e}', file=sys.stderr)
  60. return None
  61. def convert_multiple(self, dois: List[str], delay: float = 0.5) -> List[str]:
  62. """
  63. Convert multiple DOIs to BibTeX.
  64. Args:
  65. dois: List of DOIs
  66. delay: Delay between requests (seconds) for rate limiting
  67. Returns:
  68. List of BibTeX entries (excludes failed conversions)
  69. """
  70. bibtex_entries = []
  71. for i, doi in enumerate(dois):
  72. print(f'Converting DOI {i+1}/{len(dois)}: {doi}', file=sys.stderr)
  73. bibtex = self.doi_to_bibtex(doi)
  74. if bibtex:
  75. bibtex_entries.append(bibtex)
  76. # Rate limiting
  77. if i < len(dois) - 1: # Don't delay after last request
  78. time.sleep(delay)
  79. return bibtex_entries
  80. def main():
  81. """Command-line interface."""
  82. parser = argparse.ArgumentParser(
  83. description='Convert DOIs to BibTeX format using CrossRef API',
  84. epilog='Example: python doi_to_bibtex.py 10.1038/s41586-021-03819-2'
  85. )
  86. parser.add_argument(
  87. 'dois',
  88. nargs='*',
  89. help='DOI(s) to convert (can provide multiple)'
  90. )
  91. parser.add_argument(
  92. '-i', '--input',
  93. help='Input file with DOIs (one per line)'
  94. )
  95. parser.add_argument(
  96. '-o', '--output',
  97. help='Output file for BibTeX (default: stdout)'
  98. )
  99. parser.add_argument(
  100. '--delay',
  101. type=float,
  102. default=0.5,
  103. help='Delay between requests in seconds (default: 0.5)'
  104. )
  105. parser.add_argument(
  106. '--format',
  107. choices=['bibtex', 'json'],
  108. default='bibtex',
  109. help='Output format (default: bibtex)'
  110. )
  111. args = parser.parse_args()
  112. # Collect DOIs from command line and/or file
  113. dois = []
  114. if args.dois:
  115. dois.extend(args.dois)
  116. if args.input:
  117. try:
  118. with open(args.input, 'r', encoding='utf-8') as f:
  119. file_dois = [line.strip() for line in f if line.strip()]
  120. dois.extend(file_dois)
  121. except FileNotFoundError:
  122. print(f'Error: Input file not found: {args.input}', file=sys.stderr)
  123. sys.exit(1)
  124. except Exception as e:
  125. print(f'Error reading input file: {e}', file=sys.stderr)
  126. sys.exit(1)
  127. if not dois:
  128. parser.print_help()
  129. sys.exit(1)
  130. # Convert DOIs
  131. converter = DOIConverter()
  132. if len(dois) == 1:
  133. bibtex = converter.doi_to_bibtex(dois[0])
  134. if bibtex:
  135. bibtex_entries = [bibtex]
  136. else:
  137. sys.exit(1)
  138. else:
  139. bibtex_entries = converter.convert_multiple(dois, delay=args.delay)
  140. if not bibtex_entries:
  141. print('Error: No successful conversions', file=sys.stderr)
  142. sys.exit(1)
  143. # Format output
  144. if args.format == 'bibtex':
  145. output = '\n\n'.join(bibtex_entries) + '\n'
  146. else: # json
  147. output = json.dumps({
  148. 'count': len(bibtex_entries),
  149. 'entries': bibtex_entries
  150. }, indent=2)
  151. # Write output
  152. if args.output:
  153. try:
  154. with open(args.output, 'w', encoding='utf-8') as f:
  155. f.write(output)
  156. print(f'Successfully wrote {len(bibtex_entries)} entries to {args.output}', file=sys.stderr)
  157. except Exception as e:
  158. print(f'Error writing output file: {e}', file=sys.stderr)
  159. sys.exit(1)
  160. else:
  161. print(output)
  162. # Summary
  163. if len(dois) > 1:
  164. success_rate = len(bibtex_entries) / len(dois) * 100
  165. print(f'\nConverted {len(bibtex_entries)}/{len(dois)} DOIs ({success_rate:.1f}%)', file=sys.stderr)
  166. if __name__ == '__main__':
  167. main()