bioconverters
1__docformat__ = "google" 2 3from .pmc_types import PMCArticle 4from .pmcxml import parse_pmcxml, pmcxml2bioc, pmcxml2txt 5from .pubmed_types import ( 6 Chemical, 7 MeshHeading, 8 MeshQualifier, 9 PublicationType, 10 PubMedArticle, 11 SupplementaryMeshConcept, 12) 13from .pubmedxml import parse_pubmedxml, pubmedxml2bioc, pubmedxml2txt 14 15__all__ = [ 16 "parse_pmcxml", 17 "pmcxml2bioc", 18 "pmcxml2txt", 19 "PMCArticle", 20 "parse_pubmedxml", 21 "pubmedxml2bioc", 22 "pubmedxml2txt", 23 "PubMedArticle", 24 "Chemical", 25 "MeshHeading", 26 "MeshQualifier", 27 "SupplementaryMeshConcept", 28 "PublicationType", 29]
344def parse_pmcxml( 345 source: Union[str, TextIO], 346 return_xml: bool = False, 347 keep_tags=set(), 348 trim_buggy_sentences: bool = True, 349 inject_citations: bool = False, 350 clean_numeric_citations: bool = True, 351 clean_xrefs_in_brackets: bool = True, 352 clear_empty_brackets: bool = True, 353 fix_exponentials: bool = True, 354) -> Iterable[PMCArticle]: 355 """ 356 Parse a PMC XML file into a series of PMCArticle objects (one per article/sub-article). 357 358 Args: 359 source: The text or file handle containing the PMC XML 360 return_xml: return each passage's text as a marked-up XML string if True, or as 361 plain, unescaped text with any markup stripped if False (default). 362 keep_tags: with return_xml=True, tags whose markup is preserved inline in each 363 passage's text (e.g. "sup", "italic") - has no effect when return_xml=False. 364 Defaults to an empty set (no markup kept). Pass `pmc_constants.PMC_KEEP_TAGS` 365 for the common formatting tags (sup, sub, italic, bold, underline, monospace, 366 sc, overline, strike). 367 trim_buggy_sentences: trim overly long, unbroken runs of text to a maximum length, 368 to avoid issues with buggy sentences in some PMC articles. 369 inject_citations: resolve each in-text bibr citation's pmid/doi and retag it to 370 `<citation>`, kept in the output instead of dropped. Must not be combined with 371 clean_numeric_citations=True - injection enriches bibr citations, 372 clean_numeric_citations deletes them, so having both on is almost certainly a mistake. 373 clean_numeric_citations: drop a numeric bibr citation marker outright, e.g. "1", "[1,2]", 374 regardless of context - this is what catches a citation glued directly onto a 375 word with no separating space at all. 376 clean_xrefs_in_brackets: drop bracket-wrapped xref clutter, e.g. "(Table 1)". 377 clear_empty_brackets: remove any "(...)"/"[...]"/"{...}" left containing no word 378 characters, e.g. from clean_numeric_citations/clean_xrefs_in_brackets, or from an 379 unrelated ignore_tag (like ext-link) that happened to be parenthesised. 380 fix_exponentials: with return_xml=False, recover a digit-preceded numeric `<sup>` 381 as "^N" instead of losing it to plain concatenation, e.g. `"10<sup>8</sup>"` -> 382 "10^8". 383 384 These defaults match pmcxml2bioc/pmcxml2txt, so behavior is consistent regardless of 385 which entry point is used. 386 """ 387 assert not (inject_citations and clean_numeric_citations), ( 388 "inject_citations and clean_numeric_citations can't both be True - injection enriches bibr " 389 "citations, clean_numeric_citations deletes them. Pass clean_numeric_citations=False." 390 ) 391 392 source = _apply_pmc_xlink_fix(source) 393 394 # Skip to the article element in the file 395 for event, elem in etree.iterparse(source, events=("start", "end", "start-ns", "end-ns")): 396 if event == "end" and elem.tag == "article": 397 if inject_citations: 398 citation_lookup = _build_citation_lookup(elem) 399 _inject_citations(elem, citation_lookup) 400 401 meta = _get_meta_info_for_pmc_article(elem) 402 403 # We're going to process the main article along with any subarticles 404 # And if any of the subarticles have distinguishing IDs (e.g. PMID), then 405 # that'll be used, otherwise the parent article's metadata will be used 406 subarticles = [elem] + elem.findall("./sub-article") 407 408 for article_elem in subarticles: 409 if article_elem is elem: 410 # This is the main parent article. Just use its metadata 411 sub_meta = meta 412 else: 413 # Check if this subarticle has any distinguishing IDs and use them instead 414 sub_meta = _get_meta_info_for_pmc_article(article_elem) 415 if not (sub_meta.pmid or sub_meta.pmcid or sub_meta.doi): 416 sub_meta.pmid = meta.pmid 417 sub_meta.pmcid = meta.pmcid 418 sub_meta.doi = meta.doi 419 if sub_meta.pub_year is None: 420 sub_meta.pub_year = meta.pub_year 421 sub_meta.pub_month = meta.pub_month 422 sub_meta.pub_day = meta.pub_day 423 if not sub_meta.journal: 424 sub_meta.journal = meta.journal 425 sub_meta.journal_iso = meta.journal_iso 426 427 content = _extract_article_content( 428 article_elem, 429 keep_tags, 430 return_xml, 431 trim_buggy_sentences, 432 inject_citations, 433 clean_numeric_citations, 434 clean_xrefs_in_brackets, 435 clear_empty_brackets, 436 fix_exponentials, 437 ) 438 439 yield PMCArticle( 440 pmid=sub_meta.pmid, 441 pmcid=sub_meta.pmcid, 442 doi=sub_meta.doi, 443 pub_year=sub_meta.pub_year, 444 pub_month=sub_meta.pub_month, 445 pub_day=sub_meta.pub_day, 446 journal=sub_meta.journal, 447 journal_iso=sub_meta.journal_iso, 448 **content, 449 ) 450 451 # Less important here (compared to abstracts) as each article file is not too big 452 elem.clear()
Parse a PMC XML file into a series of PMCArticle objects (one per article/sub-article).
Arguments:
- source: The text or file handle containing the PMC XML
- return_xml: return each passage's text as a marked-up XML string if True, or as plain, unescaped text with any markup stripped if False (default).
- keep_tags: with return_xml=True, tags whose markup is preserved inline in each
passage's text (e.g. "sup", "italic") - has no effect when return_xml=False.
Defaults to an empty set (no markup kept). Pass
pmc_constants.PMC_KEEP_TAGSfor the common formatting tags (sup, sub, italic, bold, underline, monospace, sc, overline, strike). - trim_buggy_sentences: trim overly long, unbroken runs of text to a maximum length, to avoid issues with buggy sentences in some PMC articles.
- inject_citations: resolve each in-text bibr citation's pmid/doi and retag it to
<citation>, kept in the output instead of dropped. Must not be combined with clean_numeric_citations=True - injection enriches bibr citations, clean_numeric_citations deletes them, so having both on is almost certainly a mistake. - clean_numeric_citations: drop a numeric bibr citation marker outright, e.g. "1", "[1,2]", regardless of context - this is what catches a citation glued directly onto a word with no separating space at all.
- clean_xrefs_in_brackets: drop bracket-wrapped xref clutter, e.g. "(Table 1)".
- clear_empty_brackets: remove any "(...)"/"[...]"/"{...}" left containing no word characters, e.g. from clean_numeric_citations/clean_xrefs_in_brackets, or from an unrelated ignore_tag (like ext-link) that happened to be parenthesised.
- fix_exponentials: with return_xml=False, recover a digit-preceded numeric
<sup>as "^N" instead of losing it to plain concatenation, e.g."10<sup>8</sup>"-> "10^8".
These defaults match pmcxml2bioc/pmcxml2txt, so behavior is consistent regardless of which entry point is used.
455def pmcxml2bioc( 456 source: Union[str, TextIO], 457 sections: Iterable[str] = ("title", "subtitle", "abstract", "article", "back", "floating"), 458 trim_buggy_sentences: bool = True, 459 clean_numeric_citations: bool = True, 460 clean_xrefs_in_brackets: bool = True, 461 clear_empty_brackets: bool = True, 462 fix_exponentials: bool = True, 463) -> Iterator[bioc.BioCDocument]: 464 """ 465 Convert a PMC XML file into its Bioc equivalent 466 467 Args: 468 source: The text or file handle containing the PMC XML 469 sections: which of the six PMCArticle text fields ("title", "subtitle", "abstract", 470 "article", "back", "floating") to include as passages, and in what order. 471 trim_buggy_sentences: trim overly long, unbroken runs of text to a maximum length, 472 to avoid issues with buggy sentences in some PMC articles. 473 clean_numeric_citations: see parse_pmcxml. 474 clean_xrefs_in_brackets: see parse_pmcxml. 475 clear_empty_brackets: see parse_pmcxml. 476 fix_exponentials: see parse_pmcxml. 477 478 Raises: 479 RuntimeError: On any parsing errors 480 481 Returns: 482 An iterator over the newly generated Bioc documents 483 """ 484 try: 485 for pmc_doc in parse_pmcxml( 486 source, 487 keep_tags=set(), 488 return_xml=False, 489 inject_citations=False, 490 trim_buggy_sentences=trim_buggy_sentences, 491 clean_numeric_citations=clean_numeric_citations, 492 clean_xrefs_in_brackets=clean_xrefs_in_brackets, 493 clear_empty_brackets=clear_empty_brackets, 494 fix_exponentials=fix_exponentials, 495 ): 496 bioc_doc = bioc.BioCDocument() 497 bioc_doc.id = pmc_doc.pmid 498 bioc_doc.infons["title"] = pmc_doc.title 499 bioc_doc.infons["pmid"] = pmc_doc.pmid 500 bioc_doc.infons["pmcid"] = pmc_doc.pmcid 501 bioc_doc.infons["doi"] = pmc_doc.doi 502 bioc_doc.infons["year"] = pmc_doc.pub_year 503 bioc_doc.infons["month"] = pmc_doc.pub_month 504 bioc_doc.infons["day"] = pmc_doc.pub_day 505 bioc_doc.infons["journal"] = pmc_doc.journal 506 bioc_doc.infons["journal_iso"] = pmc_doc.journal_iso 507 508 offset = 0 509 for group_name in sections: 510 value = getattr(pmc_doc, group_name) 511 texts = (value,) if isinstance(value, str) else value 512 for text_source in texts: 513 if not text_source: 514 continue 515 516 passage = bioc.BioCPassage() 517 518 passage.infons["section"] = group_name 519 520 passage.text = text_source 521 passage.offset = offset 522 523 offset += len(text_source) 524 bioc_doc.add_passage(passage) 525 526 yield bioc_doc 527 528 except etree.ParseError: 529 raise RuntimeError("Parsing error in PMC xml file: %s" % source)
Convert a PMC XML file into its Bioc equivalent
Arguments:
- source: The text or file handle containing the PMC XML
- sections: which of the six PMCArticle text fields ("title", "subtitle", "abstract", "article", "back", "floating") to include as passages, and in what order.
- trim_buggy_sentences: trim overly long, unbroken runs of text to a maximum length, to avoid issues with buggy sentences in some PMC articles.
- clean_numeric_citations: see parse_pmcxml.
- clean_xrefs_in_brackets: see parse_pmcxml.
- clear_empty_brackets: see parse_pmcxml.
- fix_exponentials: see parse_pmcxml.
Raises:
- RuntimeError: On any parsing errors
Returns:
An iterator over the newly generated Bioc documents
532def pmcxml2txt( 533 source: Union[str, TextIO], 534 sections: Iterable[str] = ("title", "subtitle", "abstract", "article", "back", "floating"), 535 include_metadata: bool = False, 536 passage_separator: str = "\n\n", 537 trim_buggy_sentences: bool = True, 538 clean_numeric_citations: bool = True, 539 clean_xrefs_in_brackets: bool = True, 540 clear_empty_brackets: bool = True, 541 fix_exponentials: bool = True, 542) -> Iterator[str]: 543 """ 544 Convert a PMC XML file into plain text, one string per article/sub-article. 545 546 Args: 547 source: The text or file handle containing the PMC XML 548 sections: which of the six PMCArticle text fields ("title", "subtitle", "abstract", 549 "article", "back", "floating") to include, and in what order. 550 include_metadata: prepend a "label: value" header block (pmid, pmcid, doi, year, 551 month, day, journal) before the text, separated by passage_separator like any 552 other passage. Fields that are empty/missing are omitted. 553 passage_separator: string used to join the header (if any), and every extracted 554 passage, into the single returned string. 555 trim_buggy_sentences: trim overly long, unbroken runs of text to a maximum length, 556 to avoid issues with buggy sentences in some PMC articles. 557 clean_numeric_citations: see parse_pmcxml. 558 clean_xrefs_in_brackets: see parse_pmcxml. 559 clear_empty_brackets: see parse_pmcxml. 560 fix_exponentials: see parse_pmcxml. 561 562 Returns: 563 An iterator over one plain text string per article/sub-article 564 """ 565 for doc in parse_pmcxml( 566 source, 567 keep_tags=set(), 568 return_xml=False, 569 trim_buggy_sentences=trim_buggy_sentences, 570 clean_numeric_citations=clean_numeric_citations, 571 clean_xrefs_in_brackets=clean_xrefs_in_brackets, 572 clear_empty_brackets=clear_empty_brackets, 573 fix_exponentials=fix_exponentials, 574 ): 575 parts = [] 576 if include_metadata: 577 header = _format_metadata_header( 578 { 579 "pmid": doc.pmid, 580 "pmcid": doc.pmcid, 581 "doi": doc.doi, 582 "year": doc.pub_year, 583 "month": doc.pub_month, 584 "day": doc.pub_day, 585 "journal": doc.journal, 586 } 587 ) 588 if header: 589 parts.append(header) 590 591 parts.extend(doc.iter_text(sections)) 592 593 yield passage_separator.join(parts)
Convert a PMC XML file into plain text, one string per article/sub-article.
Arguments:
- source: The text or file handle containing the PMC XML
- sections: which of the six PMCArticle text fields ("title", "subtitle", "abstract", "article", "back", "floating") to include, and in what order.
- include_metadata: prepend a "label: value" header block (pmid, pmcid, doi, year, month, day, journal) before the text, separated by passage_separator like any other passage. Fields that are empty/missing are omitted.
- passage_separator: string used to join the header (if any), and every extracted passage, into the single returned string.
- trim_buggy_sentences: trim overly long, unbroken runs of text to a maximum length, to avoid issues with buggy sentences in some PMC articles.
- clean_numeric_citations: see parse_pmcxml.
- clean_xrefs_in_brackets: see parse_pmcxml.
- clear_empty_brackets: see parse_pmcxml.
- fix_exponentials: see parse_pmcxml.
Returns:
An iterator over one plain text string per article/sub-article
37@dataclass 38class PMCArticle(_PMCMeta): 39 """One PMC article or sub-article, as extracted by `parse_pmcxml`.""" 40 41 # Redeclared here (not just inherited from _PMCMeta) purely so pdoc documents them on 42 # this class - it doesn't inline a base class's fields into a subclass's page. Dataclass 43 # field order/behavior is unaffected: re-annotating an inherited field updates its type 44 # in place without moving it, since ordering is fixed by each name's first occurrence 45 # when walking the MRO. 46 pmid: str 47 """PubMed ID, or an empty string if not found.""" 48 49 pmcid: str 50 """PubMed Central ID, or an empty string if not found.""" 51 52 doi: str 53 """DOI, or an empty string if not found.""" 54 55 pub_year: Optional[str] 56 """Publication year, or None if not found.""" 57 58 pub_month: Optional[Union[str, int]] 59 """Publication month, or None if not found.""" 60 61 pub_day: Optional[str] 62 """Publication day, or None if not found.""" 63 64 journal: str 65 """Journal title, or an empty string if not found.""" 66 67 journal_iso: str 68 """ISO abbreviation of the journal title, or an empty string if not found.""" 69 70 title: str 71 """Article title, or an empty string if not found.""" 72 73 subtitle: str 74 """Article subtitle, or an empty string if not present.""" 75 76 abstract: Iterable[str] 77 """Abstract passages.""" 78 79 article: Iterable[str] 80 """Body text passages, extracted from the article's `<body>` element.""" 81 82 back: Iterable[str] 83 """Back-matter text passages (e.g. appendices), extracted from the article's `<back>` element.""" 84 85 floating: Iterable[str] 86 """Text passages from blocks outside the main flow (e.g. figure/table captions), 87 extracted from the article's `<floats-group>` element.""" 88 89 def iter_text(self, sections: Iterable[str] = _ALL_SECTIONS) -> Iterator[str]: 90 """ 91 Yield each non-empty passage of text from the given fields, in order. 92 93 Args: 94 sections: which of the six text fields to pull text from, and in what order. 95 Defaults to all six ("title", "subtitle", "abstract", "article", "back", 96 "floating"). 97 """ 98 for section in sections: 99 value = getattr(self, section) 100 texts = (value,) if isinstance(value, str) else value 101 for text in texts: 102 if text: 103 yield text
One PMC article or sub-article, as extracted by parse_pmcxml.
Back-matter text passages (e.g. appendices), extracted from the article's <back> element.
Text passages from blocks outside the main flow (e.g. figure/table captions),
extracted from the article's <floats-group> element.
89 def iter_text(self, sections: Iterable[str] = _ALL_SECTIONS) -> Iterator[str]: 90 """ 91 Yield each non-empty passage of text from the given fields, in order. 92 93 Args: 94 sections: which of the six text fields to pull text from, and in what order. 95 Defaults to all six ("title", "subtitle", "abstract", "article", "back", 96 "floating"). 97 """ 98 for section in sections: 99 value = getattr(self, section) 100 texts = (value,) if isinstance(value, str) else value 101 for text in texts: 102 if text: 103 yield text
Yield each non-empty passage of text from the given fields, in order.
Arguments:
- sections: which of the six text fields to pull text from, and in what order. Defaults to all six ("title", "subtitle", "abstract", "article", "back", "floating").
135def parse_pubmedxml( 136 source: Union[str, TextIO], 137 clear_empty_brackets: bool = True, 138 fix_exponentials: bool = True, 139) -> Iterable[PubMedArticle]: 140 """ 141 Args: 142 source: path to the MEDLINE xml file 143 clear_empty_brackets: remove any "(...)"/"[...]"/"{...}" left containing no word 144 characters. 145 fix_exponentials: recover a digit-preceded numeric `<sup>` as "^N" instead of losing 146 it to plain concatenation, e.g. `"10<sup>8</sup>"` -> "10^8". Same default as 147 pubmedxml2txt/pubmedxml2bioc. 148 """ 149 for event, elem in etree.iterparse(source, events=("start", "end", "start-ns", "end-ns")): 150 if event == "end" and elem.tag == "PubmedArticle": # MedlineCitation'): 151 # Try to extract the pmid_id 152 pmid_field = elem.find("./MedlineCitation/PMID") 153 assert pmid_field is not None 154 pmid = pmid_field.text 155 156 journal_year, journal_month, journal_day = _get_journal_date_for_medline_file(elem, pmid) 157 entry_year, entry_month, entry_day = _get_pubmed_entry_date(elem) 158 159 j_comparison = tuple( 160 9999 if d is None else d for d in [journal_year, journal_month, journal_day] 161 ) 162 e_comparison = tuple( 163 9999 if d is None else d for d in [entry_year, entry_month, entry_day] 164 ) 165 if ( 166 j_comparison < e_comparison 167 ): # The PubMed entry has been delayed for some reason so let's try the journal data 168 pub_year, pub_month, pub_day = journal_year, journal_month, journal_day 169 else: 170 pub_year, pub_month, pub_day = entry_year, entry_month, entry_day 171 172 # Extract the authors 173 author_elems = elem.findall("./MedlineCitation/Article/AuthorList/Author") 174 authors = [] 175 for author_elem in author_elems: 176 forename = author_elem.find("./ForeName") 177 lastname = author_elem.find("./LastName") 178 collectivename = author_elem.find("./CollectiveName") 179 180 name = None 181 if ( 182 forename is not None 183 and lastname is not None 184 and forename.text is not None 185 and lastname.text is not None 186 ): 187 name = "%s %s" % (forename.text, lastname.text) 188 elif lastname is not None and lastname.text is not None: 189 name = lastname.text 190 elif forename is not None and forename.text is not None: 191 name = forename.text 192 elif collectivename is not None and collectivename.text is not None: 193 name = collectivename.text 194 else: 195 raise RuntimeError("Unable to find authors in Pubmed citation (PMID=%s)" % pmid) 196 authors.append(name) 197 198 chemicals = [] 199 chemical_elems = elem.findall("./MedlineCitation/ChemicalList/Chemical") 200 for chemical_elem in chemical_elems: 201 substance_elem = chemical_elem.find("./NameOfSubstance") 202 chemicals.append( 203 Chemical( 204 ui=substance_elem.attrib["UI"], 205 name=substance_elem.text, 206 registry_number=chemical_elem.find("./RegistryNumber").text, 207 ) 208 ) 209 210 mesh_headings = [] 211 mesh_elems = elem.findall("./MedlineCitation/MeshHeadingList/MeshHeading") 212 for mesh_elem in mesh_elems: 213 descriptor_elem = mesh_elem.find("./DescriptorName") 214 qualifiers = [ 215 MeshQualifier( 216 ui=qualifier_elem.attrib["UI"], 217 name=qualifier_elem.text, 218 major_topic=qualifier_elem.attrib["MajorTopicYN"] == "Y", 219 ) 220 for qualifier_elem in mesh_elem.findall("./QualifierName") 221 ] 222 mesh_headings.append( 223 MeshHeading( 224 ui=descriptor_elem.attrib["UI"], 225 name=descriptor_elem.text, 226 major_topic=descriptor_elem.attrib["MajorTopicYN"] == "Y", 227 qualifiers=qualifiers, 228 ) 229 ) 230 231 supplementary_concepts = [ 232 SupplementaryMeshConcept( 233 ui=concept_elem.attrib["UI"], 234 type=concept_elem.attrib["Type"], 235 name=concept_elem.text, 236 ) 237 for concept_elem in elem.findall("./MedlineCitation/SupplMeshList/SupplMeshName") 238 ] 239 240 doi_elems = elem.findall("./PubmedData/ArticleIdList/ArticleId[@IdType='doi']") 241 dois = [ 242 doi_elem.text 243 for doi_elem in doi_elems 244 if doi_elem.text and _doi_regex.match(doi_elem.text) 245 ] 246 247 doi = None 248 if dois: 249 doi = dois[0] # We'll just use DOI the first one provided 250 251 pmc_elems = elem.findall("./PubmedData/ArticleIdList/ArticleId[@IdType='pmc']") 252 assert len(pmc_elems) <= 1, "Found more than one PMCID with PMID: %s" % pmid 253 pmcid = None 254 if len(pmc_elems) == 1: 255 pmcid = pmc_elems[0].text 256 257 pub_type_elems = elem.findall( 258 "./MedlineCitation/Article/PublicationTypeList/PublicationType" 259 ) 260 publication_types = [ 261 PublicationType(ui=e.attrib["UI"], name=e.text) 262 for e in pub_type_elems 263 if e.text not in _pub_type_skips 264 ] 265 266 # Extract the title of paper - the DTD requires exactly one ArticleTitle per Article 267 title = elem.findall("./MedlineCitation/Article/ArticleTitle") 268 assert len(title) == 1, "Expected exactly one ArticleTitle for PMID=%s" % pmid 269 title_passages = _extract_passages( 270 title, 271 PUBMED_IGNORE_TAGS, 272 PUBMED_SPLIT_TAGS, 273 PUBMED_KEEP_TAGS, 274 return_xml=False, 275 trim_buggy_sentences=True, 276 fix_exponentials=fix_exponentials, 277 ) 278 title_text = _remove_brackets_from_titles(title_passages[0]) 279 title_text = html.unescape(title_text) 280 if clear_empty_brackets: 281 title_text = _remove_brackets_without_words(title_text) 282 283 # Extract the abstract from the paper 284 abstract = elem.findall("./MedlineCitation/Article/Abstract/AbstractText") 285 abstract_passages = _extract_passages( 286 abstract, 287 PUBMED_IGNORE_TAGS, 288 PUBMED_SPLIT_TAGS, 289 PUBMED_KEEP_TAGS, 290 return_xml=False, 291 trim_buggy_sentences=True, 292 fix_exponentials=fix_exponentials, 293 ) 294 abstract_text = [html.unescape(t) for t in abstract_passages] 295 if clear_empty_brackets: 296 abstract_text = [_remove_brackets_without_words(t) for t in abstract_text] 297 298 journal_title_fields = elem.findall("./MedlineCitation/Article/Journal/Title") 299 journal_title_iso_fields = elem.findall( 300 "./MedlineCitation/Article/Journal/ISOAbbreviation" 301 ) 302 303 journal_title, journal_iso_title = "", "" 304 assert len(journal_title_fields) <= 1, "Error with pmid=%s" % pmid 305 assert len(journal_title_iso_fields) <= 1, "Error with pmid=%s" % pmid 306 if journal_title_fields: 307 journal_title = journal_title_fields[0].text 308 if journal_title_iso_fields: 309 journal_iso_title = journal_title_iso_fields[0].text 310 311 yield PubMedArticle( 312 pmid=pmid, 313 pmcid=pmcid, 314 doi=doi, 315 pub_year=pub_year, 316 pub_month=pub_month, 317 pub_day=pub_day, 318 title=title_text, 319 abstract=abstract_text, 320 journal=journal_title, 321 journal_iso=journal_iso_title, 322 authors=authors, 323 chemicals=chemicals, 324 mesh_headings=mesh_headings, 325 supplementary_mesh=supplementary_concepts, 326 publication_types=publication_types, 327 ) 328 329 # Important: clear the current element from memory to keep memory usage low 330 elem.clear()
Arguments:
- source: path to the MEDLINE xml file
- clear_empty_brackets: remove any "(...)"/"[...]"/"{...}" left containing no word characters.
- fix_exponentials: recover a digit-preceded numeric
<sup>as "^N" instead of losing it to plain concatenation, e.g."10<sup>8</sup>"-> "10^8". Same default as pubmedxml2txt/pubmedxml2bioc.
353def pubmedxml2bioc( 354 source: Union[str, TextIO], 355 sections: Iterable[str] = ("title", "abstract"), 356 clear_empty_brackets: bool = True, 357 fix_exponentials: bool = True, 358) -> Iterable[bioc.BioCDocument]: 359 """ 360 Args: 361 source: path to the MEDLINE xml file 362 sections: which of "title"/"abstract" to include, and in what order. 363 clear_empty_brackets: see parse_pubmedxml. 364 fix_exponentials: see parse_pubmedxml. 365 """ 366 for pm_doc in parse_pubmedxml( 367 source, clear_empty_brackets=clear_empty_brackets, fix_exponentials=fix_exponentials 368 ): 369 bioc_doc = bioc.BioCDocument() 370 bioc_doc.id = pm_doc.pmid 371 bioc_doc.infons["title"] = pm_doc.title 372 bioc_doc.infons["pmid"] = pm_doc.pmid 373 bioc_doc.infons["pmcid"] = pm_doc.pmcid 374 bioc_doc.infons["doi"] = pm_doc.doi 375 bioc_doc.infons["year"] = pm_doc.pub_year 376 bioc_doc.infons["month"] = pm_doc.pub_month 377 bioc_doc.infons["day"] = pm_doc.pub_day 378 bioc_doc.infons["journal"] = pm_doc.journal 379 bioc_doc.infons["journal_iso"] = pm_doc.journal_iso 380 bioc_doc.infons["authors"] = ", ".join(pm_doc.authors) 381 bioc_doc.infons["chemicals"] = "\t".join(_format_chemical_for_infons(c) for c in pm_doc.chemicals) 382 bioc_doc.infons["mesh_headings"] = "\t".join( 383 _format_mesh_heading_for_infons(h) for h in pm_doc.mesh_headings 384 ) 385 bioc_doc.infons["supplementary_mesh"] = "\t".join( 386 _format_supplementary_mesh_for_infons(s) for s in pm_doc.supplementary_mesh 387 ) 388 bioc_doc.infons["publication_types"] = "\t".join( 389 _format_publication_type_for_infons(p) for p in pm_doc.publication_types 390 ) 391 392 offset = 0 393 for section in sections: 394 value = getattr(pm_doc, section) 395 texts = (value,) if isinstance(value, str) else value 396 for text_source in texts: 397 if not text_source: 398 continue 399 passage = bioc.BioCPassage() 400 passage.infons["section"] = section 401 passage.text = text_source 402 passage.offset = offset 403 offset += len(text_source) 404 bioc_doc.add_passage(passage) 405 406 yield bioc_doc
Arguments:
- source: path to the MEDLINE xml file
- sections: which of "title"/"abstract" to include, and in what order.
- clear_empty_brackets: see parse_pubmedxml.
- fix_exponentials: see parse_pubmedxml.
409def pubmedxml2txt( 410 source: Union[str, TextIO], 411 sections: Iterable[str] = ("title", "abstract"), 412 include_metadata: bool = False, 413 passage_separator: str = "\n\n", 414 clear_empty_brackets: bool = True, 415 fix_exponentials: bool = True, 416) -> Iterator[str]: 417 """ 418 Convert a MEDLINE XML file into plain text, one string per article. 419 420 Args: 421 source: path to the MEDLINE xml file 422 sections: which of "title"/"abstract" to include, and in what order. 423 include_metadata: prepend a "label: value" header block (pmid, pmcid, doi, year, 424 month, day, journal, authors) before the text, separated by passage_separator 425 like any other passage. Fields that are empty/missing are omitted. 426 passage_separator: string used to join the header (if any), and every extracted 427 passage, into the single returned string. 428 clear_empty_brackets: see parse_pubmedxml. 429 fix_exponentials: see parse_pubmedxml. 430 431 Returns: 432 An iterator over one plain text string per article 433 """ 434 for pm_doc in parse_pubmedxml( 435 source, clear_empty_brackets=clear_empty_brackets, fix_exponentials=fix_exponentials 436 ): 437 parts = [] 438 if include_metadata: 439 header = _format_metadata_header( 440 { 441 "pmid": pm_doc.pmid, 442 "pmcid": pm_doc.pmcid, 443 "doi": pm_doc.doi, 444 "year": pm_doc.pub_year, 445 "month": pm_doc.pub_month, 446 "day": pm_doc.pub_day, 447 "journal": pm_doc.journal, 448 "authors": "; ".join(pm_doc.authors) if pm_doc.authors else None, 449 } 450 ) 451 if header: 452 parts.append(header) 453 454 parts.extend(pm_doc.iter_text(sections)) 455 456 yield passage_separator.join(parts)
Convert a MEDLINE XML file into plain text, one string per article.
Arguments:
- source: path to the MEDLINE xml file
- sections: which of "title"/"abstract" to include, and in what order.
- include_metadata: prepend a "label: value" header block (pmid, pmcid, doi, year, month, day, journal, authors) before the text, separated by passage_separator like any other passage. Fields that are empty/missing are omitted.
- passage_separator: string used to join the header (if any), and every extracted passage, into the single returned string.
- clear_empty_brackets: see parse_pubmedxml.
- fix_exponentials: see parse_pubmedxml.
Returns:
An iterator over one plain text string per article
76@dataclass 77class PubMedArticle: 78 """One MEDLINE/PubMed article, as extracted by `parse_pubmedxml`.""" 79 80 pmid: str 81 """PubMed ID.""" 82 83 pmcid: Optional[str] 84 """PubMed Central ID, or None if not linked.""" 85 86 doi: Optional[str] 87 """DOI, or None if not found.""" 88 89 pub_year: Optional[int] 90 """Publication year, or None if not found.""" 91 92 pub_month: Optional[int] 93 """Publication month, or None if not found.""" 94 95 pub_day: Optional[int] 96 """Publication day, or None if not found.""" 97 98 title: str 99 """Article title.""" 100 101 abstract: Iterable[str] 102 """Abstract passages, one per `<AbstractText>` element.""" 103 104 journal: str 105 """Journal title, or an empty string if not found.""" 106 107 journal_iso: str 108 """ISO abbreviation of the journal title, or an empty string if not found.""" 109 110 authors: Iterable[str] 111 """Author names, in document order.""" 112 113 chemicals: Iterable[Chemical] 114 """Chemical substances, in document order.""" 115 116 mesh_headings: Iterable[MeshHeading] 117 """MeSH headings, each with its own qualifiers nested inside, in document order.""" 118 119 supplementary_mesh: Iterable[SupplementaryMeshConcept] 120 """Supplementary MeSH concepts, in document order.""" 121 122 publication_types: Iterable[PublicationType] 123 """Publication types, in document order (excludes generic NLM support-type labels 124 like "Research Support, N.I.H., Extramural").""" 125 126 def iter_text(self, sections: Iterable[str] = ("title", "abstract")) -> Iterator[str]: 127 """ 128 Yield each non-empty passage of text from the given fields, in order. 129 130 Args: 131 sections: which fields to pull text from, and in what order. Defaults to 132 ("title", "abstract"). 133 """ 134 for section in sections: 135 value = getattr(self, section) 136 texts = (value,) if isinstance(value, str) else value 137 for text in texts: 138 if text: 139 yield text
One MEDLINE/PubMed article, as extracted by parse_pubmedxml.
MeSH headings, each with its own qualifiers nested inside, in document order.
Supplementary MeSH concepts, in document order.
Publication types, in document order (excludes generic NLM support-type labels like "Research Support, N.I.H., Extramural").
126 def iter_text(self, sections: Iterable[str] = ("title", "abstract")) -> Iterator[str]: 127 """ 128 Yield each non-empty passage of text from the given fields, in order. 129 130 Args: 131 sections: which fields to pull text from, and in what order. Defaults to 132 ("title", "abstract"). 133 """ 134 for section in sections: 135 value = getattr(self, section) 136 texts = (value,) if isinstance(value, str) else value 137 for text in texts: 138 if text: 139 yield text
Yield each non-empty passage of text from the given fields, in order.
Arguments:
- sections: which fields to pull text from, and in what order. Defaults to ("title", "abstract").
6@dataclass 7class Chemical: 8 """One `<Chemical>` entry from the article's ChemicalList.""" 9 10 ui: str 11 """MeSH substance ID (e.g. "D000068877").""" 12 13 name: str 14 """Substance name.""" 15 16 registry_number: str 17 """CAS registry number, or "0" if none is assigned."""
One <Chemical> entry from the article's ChemicalList.
34@dataclass 35class MeshHeading: 36 """One `<MeshHeading>` entry: a descriptor plus any qualifiers refining it.""" 37 38 ui: str 39 """MeSH descriptor ID (e.g. "D009369").""" 40 41 name: str 42 """Descriptor name (e.g. "Neoplasms").""" 43 44 major_topic: bool 45 """Whether the descriptor itself is a major topic (MajorTopicYN="Y").""" 46 47 qualifiers: Iterable[MeshQualifier] 48 """Qualifiers refining this descriptor, in document order."""
One <MeshHeading> entry: a descriptor plus any qualifiers refining it.
20@dataclass 21class MeshQualifier: 22 """One `<QualifierName>` attached to a MeSH heading's descriptor.""" 23 24 ui: str 25 """MeSH qualifier ID (e.g. "Q000378").""" 26 27 name: str 28 """Qualifier name (e.g. "genetics").""" 29 30 major_topic: bool 31 """Whether this qualifier is a major topic of the article (MajorTopicYN="Y")."""
One <QualifierName> attached to a MeSH heading's descriptor.
51@dataclass 52class SupplementaryMeshConcept: 53 """One `<SupplMeshName>` entry from the article's SupplMeshList.""" 54 55 ui: str 56 """Supplementary concept ID (e.g. "C000657245").""" 57 58 type: str 59 """One of "Disease", "Protocol", "Organism", "Anatomy", "Population" (DTD-enumerated).""" 60 61 name: str 62 """Concept name."""
One <SupplMeshName> entry from the article's SupplMeshList.
65@dataclass 66class PublicationType: 67 """One `<PublicationType>` entry from the article's PublicationTypeList.""" 68 69 ui: str 70 """MeSH publication-type ID (e.g. "D016428").""" 71 72 name: str 73 """Publication type name (e.g. "Journal Article")."""
One <PublicationType> entry from the article's PublicationTypeList.