A Case Study on the Efficacy of Machine Learning Algorithms in Predicting Sepsis Outcomes in Hospitalized Patients

Authors

  • Alex Nelson PhD
  • Morgan Allen MD
  • Dana Martinez Associate Professor

Keywords:

Sepsis prediction, Machine learning, Clinical algorithms, Patient outcomes, Critical care, Data analytics, Healthcare innovation, Predictive modeling, Medical informatics

Abstract

Sepsis remains a leading cause of morbidity and mortality in critically ill patients, demanding innovative strategies for timely intervention. This study aims to evaluate the efficacy of machine learning algorithms in predicting sepsis outcomes, leveraging clinical data from a cohort of hospitalized patients. A total of 1,500 patient records were analyzed using supervised learning models, including Random Forest and Gradient Boosting. Results indicate that the models demonstrated significant predictive accuracy, with an area under the curve (AUC) of 0.85, surpassing traditional scoring systems such as SOFA and qSOFA. These findings suggest that integrating machine learning into clinical practice could enhance early detection and treatment of sepsis, potentially reducing mortality rates. This study highlights the critical role of advanced analytics in modern medicine, advocating for broader implementation across healthcare systems.

Author Biographies

Alex Nelson, PhD

PhD
Harvard Medical School
25 Shattuck Street, Boston, MA 02115, USA

Morgan Allen, MD

MD
Charité – Universitätsmedizin Berlin
Charitéplatz 1, 10117 Berlin, Germany

Dana Martinez, Associate Professor

Associate Professor
University of Toronto
27 King's College Cir, Toronto, ON M5S 1A1, Canada

References

import logging

import time

from urllib.parse import urljoin, urlparse

from datetime import datetime

import xml.etree.ElementTree as ET

import xml.dom.minidom

import re

from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError

from bs4 import BeautifulSoup

# --- КОНФИГУРАЦИЯ ---

SITE_CONFIG = {

'humstudios.com.ua': {

'base_url': 'https://humstudios.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'div.content',

'issue_date_container': 'div.event_date',

'journal_title_fallback': 'h1',

'issn_container': 'div.h_bot_part div.col-xs-12',

'article_item': 'div.section:has(h3.section_title)',

'article_link': 'h3.section_title a',

'article_doi_text': 'li.website:-soup-contains("DOI")',

'article_details_waitFor': 'h2.section_title',

'article_title': 'h2.section_title',

'authors_container': 'h2.section_title',

'author_link': 'a[href*="/en/author/"]',

'abstract_container': 'iframe[class="iframe-responsive"]',

'dates_container': 'div.event_date',

'pages_container': 'div.text_post_section.tags_section div.btn-tags',

'article_doi_link': 'p.button.banner_button',

'article_pdf_iframe_src': 'iframe[src*="/web/uploads/pdf/"]',

},

'patterns': {

'volume_issue_url_part': 't-',

'doi_prefix_separator': '/',

'date_format': '%d.%m.%Y',

'revised_date_keyword': 'Revised',

'accepted_date_keyword': 'Accepted',

'received_date_keyword': 'Received',

'pages_keyword': 'Pages',

'default_doi_prefix': '10.0000',

},

'lang': 'en'

},

'd-governance.com.ua': {

'base_url': 'https://d-governance.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'div.attorney-single',

'issue_title': 'div.info-header h3',

'issue_date': 'ul.contact-info li',

'journal_title': 'header.main-header h1',

'issn_footer': 'footer.main-footer .footer-block .text',

'article_item': 'article.featured-news-column',

'article_link': 'h3 a',

'article_details_waitFor': 'h3.article-title',

'article_title': 'h3.article-title',

'authors_container': 'div.article-authors a.article-author',

'abstract_block': 'div.article-info-block',

'dates_block': 'div.article-info-block.additional-block',

},

'patterns': {

'volume_issue_regex': r'Volumes*(d+),s*No.s*(d+)',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.56318',

'journal_abbrev': 'dg',

},

'lang': 'en'

},

'lawjournal.com.ua': {

'base_url': 'https://lawjournal.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'section.details_section',

'issue_title': 'ul.breadcrumb_nav h2',

'issue_date': 'ul.course_info_list li',

'journal_title': 'h1.banner_big_title',

'issn_banner': 'h2.banner_small_title',

'article_item': 'div.course_card.list_layout',

'article_link': 'h3.item_title a',

'article_details_waitFor': 'section.page_banner',

},

'patterns': {

'volume_issue_regex': r'Volumes*(d+),s*No.s*(d+)',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.63341',

'journal_abbrev': 'naia-chasopis',

'issn_print': '2519-4216',

'eissn': '2519-4313',

},

'lang': 'en'

},

'journal-knu.com.ua': {

'base_url': 'https://journal-knu.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'section.journal-issue-redesign',

'issue_title': 'h3.journal-title',

'issue_date': 'span.journal-date',

'issue_doi': 'span.journal-doi',

'journal_title': 'h1.banner_big_title',

'issn_block': 'div.journal-info-block p',

'article_item': 'article.article-card',

'article_link': 'h3.article-title a',

'article_details_waitFor': 'main.article-main-redesign',

},

'patterns': {

'volume_issue_regex': r'Volumes*(d+),s*No.s*(d+)',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.31721/2306-5451',

'journal_abbrev': 'jkrnu',

'issn_print': '2306-5451',

'eissn': '2519-2736',

},

'lang': 'en'

},

'devma.com.ua': {

'base_url': 'https://devma.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'div.attorney-single',

'issue_title': 'div.info-header h3',

'issue_date': 'ul.contact-info li',

'journal_title': 'div.title',

'issn_container': 'div.issn',

'article_item': 'article.featured-news-column',

'article_link': 'h3 a',

'article_details_waitFor': 'div.details-header.article',

},

'patterns': {

'volume_issue_regex': r'Volumes*(d+),s*No.s*(d+)',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.57111',

'journal_abbrev': 'devt',

'issn_print': '2413-9610',

'eissn': '2663-2365',

},

'lang': 'en'

},

'ue-bulletin.com.ua': {

'base_url': 'https://ue-bulletin.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'div.issue-page',

'issue_title': 'h1.issue-page__title',

'issue_date': 'div.issue-page__date',

'journal_title': 'h1.site-header__brand-title',

'issn_container': 'div.site-header__meta',

'article_item': 'article.issue-articles__item',

'article_link': 'h3.issue-articles__item-title a',

'article_doi': 'a.issue-articles__doi-link',

'article_details_waitFor': 'header.article-page__header',

},

'patterns': {

'volume_issue_regex': r'Vol.s*(d+),s*No.s*(d+)',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.69587',

'journal_abbrev': 'ueb',

'issn_print': '2306-546X',

'eissn': '2414-3774',

'doi_pattern': r'10.d+/w+/d+.d+.d+',

},

'lang': 'en'

},

'profedu.com.ua': {

'base_url': 'https://profedu.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'div.issue-page',

'issue_title': 'h1.issue-page__title',

'issue_date': 'div.issue-page__date',

'journal_title': 'h1.site-header__brand-title',

'issn_container': 'div.site-header__meta',

'article_item': 'article.issue-articles__item',

'article_link': 'h3.issue-articles__item-title a',

'article_doi': 'a.issue-articles__doi-link',

'article_details_waitFor': 'header.article-page__header',

},

'patterns': {

'volume_issue_regex': r'Vol.s*(d+),s*No.s*(d+)',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.69587',

'journal_abbrev': 'pemtt',

'issn_print': '2415-3729',

'eissn': '2415-3737',

'doi_pattern': r'10.d+/w+/d+.d+.d+',

},

'lang': 'en'

},

'arrcjournal.org': {

'base_url': 'https://arrcjournal.org/en/journals/',

'selectors': {

'issue_container_waitFor': 'div.attorney-single',

'issue_title': 'h3.custom-member-title',

'issue_date': 'li.custom-contact-date',

'journal_title': 'div.widget-about p strong',

'issn_container': 'div.site-header__meta',

'article_item': 'article.widget-category.featured-news-column',

'article_link': 'h3.custom-article-title a',

'article_doi': 'p.custom-article-doi a',

'article_details_waitFor': 'div.details-header.default-text-box',

},

'patterns': {

'volume_issue_regex': r'Iss.s*(d+),s*(d{4})',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.33744',

'journal_abbrev': 'arrc',

'issn_print': '0365-8171',

'eissn': '2707-4099',

'doi_pattern': r'10.d+/w+-d+-d+.d+',

},

'lang': 'en'

},

'artnotes.com.ua': {

'base_url': 'https://artnotes.com.ua/en/journals/',

'selectors': {

'issue_container_waitFor': 'section.issue-page',

'issue_title': 'h1.issue-page__title',

'issue_date': 'span.issue-page__meta-item',

'journal_title': 'span.journal-brand__title',

'issn_container': 'div.journal-header__issn',

'article_item': 'article.issue-article',

'article_link': 'h3.issue-article__title a',

'article_doi': 'p.issue-article__doi a',

'article_details_waitFor': 'header.article-hero',

},

'patterns': {

'volume_issue_regex': r'Volumes*(d+),s*No.s*(d+),s*(d{4})',

'date_format': '%d.%m.%Y',

'default_doi_prefix': '10.63009',

'journal_abbrev': 'noac',

'issn_print': '2226-2180',

'eissn': '2226-2180',

'doi_pattern': r'10.d+/w+/d+.d+.d+',

},

'lang': 'en'

},

}

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def sanitize_xml_string(text_to_clean):

"""Удаляет недопустимые в XML 1.0 символы из строки."""

if not isinstance(text_to_clean, str):

return text_to_clean

illegal_xml_chars_re = re.compile(r'[x00-x08x0bx0cx0e-x1fx7f-x84x86-x9f]')

return illegal_xml_chars_re.sub('', text_to_clean)

def get_config_for_url(url):

parsed_url = urlparse(url)

domain = parsed_url.netloc.replace('www.', '')

if domain in SITE_CONFIG:

logging.info(f"Используется конфигурация для: {domain}")

return SITE_CONFIG[domain]

else:

parts = domain.split('.')

for i in range(len(parts) - 1):

parent_domain = '.'.join(parts[i:])

if parent_domain in SITE_CONFIG:

logging.info(f"Используется конфигурация для родительского домена: {parent_domain} (для {domain})")

return SITE_CONFIG[parent_domain]

logging.error(f"Конфигурация для домена {domain} не найдена!")

raise ValueError(f"Конфигурация для домена {domain} не найдена!")

def parse_date(date_str, fmt, default_year):

if date_str is None:

return {'month': '01', 'day': '01', 'year': str(default_year)}

try:

date_obj = datetime.strptime(date_str.strip(), fmt)

return {

'month': date_obj.strftime('%m'),

'day': date_obj.strftime('%d'),

'year': date_obj.strftime('%Y')

}

except (ValueError, TypeError):

return {'month': '01', 'day': '01', 'year': str(default_year)}

def safe_get_text(element, default=""):

return element.get_text(strip=True) if element else default

def find_element(soup, selectors, first_only=True):

if not isinstance(selectors, list):

selectors = [selectors]

for selector in selectors:

try:

if first_only:

element = soup.select_one(selector)

if element: return element

else:

elements = soup.select(selector)

if elements: return elements

except Exception as e:

continue

return None if first_only else []

def extract_issue_data(soup, main_url, config, domain):

issue_data = {}

current_year = datetime.now().strftime('%Y')

# --- Дата выпуска ---

meta_domains = ['d-governance.com.ua', 'lawjournal.com.ua', 'journal-knu.com.ua', 'devma.com.ua', 'ue-bulletin.com.ua', 'profedu.com.ua', 'arrcjournal.org', 'artnotes.com.ua']

if domain in meta_domains:

date_str = None

if domain == 'journal-knu.com.ua':

date_container = soup.select_one(config['selectors']['issue_date'])

if date_container:

full_text = safe_get_text(date_container)

date_match = re.search(r'(d{2}.d{2}.d{4})', full_text)

if date_match:

date_str = date_match.group(1)

elif domain == 'devma.com.ua':

date_container = soup.select_one(config['selectors']['issue_date'])

if date_container:

full_text = safe_get_text(date_container)

date_match = re.search(r'(d{2}.d{2}.d{4})', full_text)

if date_match:

date_str = date_match.group(1)

elif domain in ['ue-bulletin.com.ua', 'profedu.com.ua', 'arrcjournal.org', 'artnotes.com.ua']:

date_container = soup.select_one(config['selectors']['issue_date'])

if date_container:

full_text = safe_get_text(date_container)

date_match = re.search(r'(d{2}.d{2}.d{4})', full_text)

if date_match:

date_str = date_match.group(1)

else:

date_container = soup.select_one(config['selectors']['issue_date'])

if date_container:

full_text = safe_get_text(date_container)

date_match = re.search(r'(d{2}.d{2}.d{4})', full_text)

if date_match:

date_str = date_match.group(1)

year_match = re.search(r'(d{4})', main_url)

default_year = year_match.group(1) if year_match else current_year

issue_data['date'] = parse_date(date_str, config['patterns']['date_format'], default_year)

else:

date_container = find_element(soup, config['selectors']['issue_date_container'])

issue_date_str = safe_get_text(date_container) if date_container else None

url_year_part = main_url.split('-')[-1]

default_year = url_year_part if url_year_part.isdigit() else current_year

issue_data['date'] = parse_date(issue_date_str, config['patterns']['date_format'], default_year)

logging.info(f"Дата выпуска: {issue_data['date']}")

# --- Название журнала ---

journal_title = None

if domain == 'journal-knu.com.ua':

journal_title = "Journal of Kryvyi Rih National University"

desc_elem = soup.select_one('p.journal-description strong')

if desc_elem:

journal_title = safe_get_text(desc_elem, journal_title)

elif domain == 'devma.com.ua':

title_elem = soup.select_one(config['selectors']['journal_title'])

journal_title = safe_get_text(title_elem, "Development Management")

elif domain in ['ue-bulletin.com.ua', 'profedu.com.ua']:

title_elem = soup.select_one(config['selectors']['journal_title'])

journal_title = safe_get_text(title_elem, "University Economic Bulletin")

elif domain == 'arrcjournal.org':

title_elem = soup.select_one(config['selectors']['journal_title'])

journal_title = safe_get_text(title_elem, "Automobile Roads and Road Construction")

if journal_title == "Automobile Roads and Road Construction":

title_tag = soup.find('title')

if title_tag:

title_parts = title_tag.get_text(strip=True).split('|')

if title_parts:

journal_title = title_parts[0].strip()

elif domain == 'artnotes.com.ua':

title_elem = soup.select_one(config['selectors']['journal_title'])

journal_title = safe_get_text(title_elem, "Notes on Art Criticism")

elif domain in ['lawjournal.com.ua', 'd-governance.com.ua']:

journal_title = safe_get_text(soup.select_one(config['selectors']['journal_title']), "Unknown Journal")

else:

title_tag = soup.find('title')

journal_title_raw = safe_get_text(title_tag, "Unknown Journal")

journal_title = journal_title_raw.split('|')[0].strip()

issue_data['journal_title'] = journal_title

# --- Сокращение журнала ---

if domain in meta_domains:

issue_data['journal_abbrev'] = config['patterns'].get('journal_abbrev', 'unknown')

else:

issue_data['journal_abbrev'] = ''.join(word[0].lower() for word in journal_title.split() if word)

logging.info(f"Название журнала: {journal_title}, Сокращение: {issue_data['journal_abbrev']}")

# --- ISSN и e-ISSN ---

if domain in ['lawjournal.com.ua', 'journal-knu.com.ua', 'devma.com.ua', 'ue-bulletin.com.ua', 'profedu.com.ua', 'arrcjournal.org', 'artnotes.com.ua']:

issue_data['issn'] = config['patterns'].get('issn_print', 'Unknown ISSN')

issue_data['eissn'] = config['patterns'].get('eissn', 'Unknown e-ISSN')

elif domain == 'd-governance.com.ua':

issn, eissn = "Unknown ISSN", "Unknown e-ISSN"

footer_blocks = soup.select(config['selectors']['issn_footer'])

for block in footer_blocks:

text = safe_get_text(block)

if 'e-ISSN' in text:

match = re.search(r'e-ISSNs*([d-]+)', text)

if match: eissn = match.group(1)

elif 'ISSN' in text and 'e-ISSN' not in text:

match = re.search(r'ISSNs*([d-]+)', text)

if match: issn = match.group(1)

issue_data['issn'], issue_data['eissn'] = issn, eissn

else:

issn, eissn = "Unknown ISSN", "Unknown e-ISSN"

issn_container = find_element(soup, config['selectors']['issn_container'])

if issn_container:

text = issn_container.text.strip().lower()

issn_match = re.search(r'issns*([d-]+)', text)

eissn_match = re.search(r'e-issns*([d-]+)', text)

if issn_match: issn = issn_match.group(1)

if eissn_match: eissn = eissn_match.group(1)

issue_data['issn'], issue_data['eissn'] = issn, eissn

logging.info(f"Найдены: ISSN='{issue_data['issn']}', e-ISSN='{issue_data['eissn']}'")

# --- Том, номер, год ---

volume, issue, year = "unknown", "unknown", issue_data['date']['year']

if domain in meta_domains:

issue_title = safe_get_text(soup.select_one(config['selectors']['issue_title']))

match = re.search(config['patterns']['volume_issue_regex'], issue_title)

if match:

if domain == 'arrcjournal.org':

issue = match.group(1)

year = match.group(2)

volume = "1"

elif domain == 'artnotes.com.ua':

volume = match.group(1)

issue = match.group(2)

year = match.group(3)

else:

volume, issue = match.group(1), match.group(2)

if domain in ['ue-bulletin.com.ua', 'profedu.com.ua']:

year_match = re.search(r'(d{4})', main_url)

if year_match:

year = year_match.group(1)

else:

url_parts = main_url.strip('/').split('/')

volume_issue_part_indicator = config['patterns']['volume_issue_url_part']

volume_issue_url_part = next((part for part in url_parts if volume_issue_part_indicator in part), None)

if volume_issue_url_part:

parts = volume_issue_url_part.replace(volume_issue_part_indicator, '').split('-')

if len(parts) >= 2:

volume, issue = parts[0], parts[1]

issue_data['volume'], issue_data['issue'], issue_data['year'] = volume, issue, year

logging.info(f"Том: {volume}, Выпуск: {issue}, Год: {year}")

# --- DOI Выпуска ---

doi_prefix = config['patterns']['default_doi_prefix']

if domain == 'arrcjournal.org':

issue_data['doi'] = f"{doi_prefix}/arrc/{issue}.{year}"

elif domain == 'artnotes.com.ua':

issue_data['doi'] = f"{doi_prefix}/noac/{issue}.{year}"

else:

issue_data['doi'] = f"{doi_prefix}/ueb/{issue}.{year}"

if domain == 'journal-knu.com.ua':

doi_elem = soup.select_one(config['selectors'].get('issue_doi'))

if doi_elem:

raw_doi = safe_get_text(doi_elem)

if 'doi.org/' in raw_doi:

issue_data['doi'] = raw_doi.split('doi.org/')[-1]

else:

issue_data['doi'] = raw_doi

elif domain not in ['devma.com.ua', 'ue-bulletin.com.ua', 'profedu.com.ua', 'arrcjournal.org', 'artnotes.com.ua']:

issue_data['doi'] = f"{doi_prefix}-{year}-{issue}-{volume}"

issue_data['resource'] = main_url

logging.info(f"DOI выпуска: {issue_data['doi']}")

return issue_data

def extract_article_links_and_dois(soup, main_url, config, domain):

"""Извлекает ссылки и DOI статей со страницы выпуска."""

article_elements = soup.select(config['selectors']['article_item'])

article_links = []

article_dois = []

base_url = config.get('base_url') or f"{urlparse(main_url).scheme}://{urlparse(main_url).netloc}"

logging.info(f"Найдено {len(article_elements)} блоков статей.")

for article_el in article_elements:

link_elem = article_el.select_one(config['selectors']['article_link'])

if link_elem and link_elem.get('href'):

full_link = urljoin(base_url, link_elem['href'])

article_links.append(full_link)

else:

article_links.append(None)

logging.warning("Не найдена ссылка для статьи в блоке.")

doi_elem = article_el.select_one(config['selectors'].get('article_doi', ''))

if doi_elem:

doi_text = safe_get_text(doi_elem)

doi_match = re.search(r'(10.d+/S+)', doi_text)

if doi_match:

article_dois.append(doi_match.group(1))

else:

article_dois.append(None)

else:

article_dois.append(None)

valid_links = []

valid_dois = []

for i, link in enumerate(article_links):

if link:

valid_links.append(link)

valid_dois.append(article_dois[i] if i < len(article_dois) else None)

logging.info(f"Извлечено уникальных ссылок на статьи: {len(valid_links)}")

return valid_links, valid_dois

def extract_article_details(page, article_url, config, domain):

"""Извлекает данные о статье."""

article_data = {}

try:

logging.info(f"Открываем статью: {article_url}")

page.goto(article_url, timeout=60000)

page.wait_for_selector(config['selectors']['article_details_waitFor'], timeout=30000)

page.wait_for_timeout(1000)

soup = BeautifulSoup(page.content(), 'html.parser')

# --- ДЛЯ САЙТОВ, ИСПОЛЬЗУЮЩИХ МЕТА-ТЕГИ ---

if domain in ['ue-bulletin.com.ua', 'profedu.com.ua', 'arrcjournal.org', 'artnotes.com.ua']:

# Заголовок

title_meta = soup.find('meta', {'name': 'citation_title'})

article_data['title'] = title_meta['content'] if title_meta else "Title not found"

# Авторы

authors = []

for meta_author in soup.find_all('meta', {'name': 'citation_author'}):

author_name = meta_author['content'].strip()

if author_name:

authors.append(author_name)

article_data['authors'] = authors

logging.info(f"Авторы ({len(authors)}): {authors}")

# Аннотация

abstract = "Abstract not found"

desc_meta = soup.find('meta', {'name': 'DC.Description'})

if desc_meta and desc_meta.get('content'):

abstract = desc_meta['content'].strip()

article_data['abstract'] = abstract

# Даты (для arrcjournal формат DD.MM.YYYY, для остальных YYYY-MM-DD)

received_meta = soup.find('meta', {'name': 'DC.Date.dateSubmitted'})

accepted_meta = soup.find('meta', {'name': 'DC.Date.issued'})

received_date_str = received_meta['content'] if received_meta else None

accepted_date_str = accepted_meta['content'] if accepted_meta else None

default_year = datetime.now().year

if domain == 'arrcjournal.org':

article_data['print_date'] = parse_date(received_date_str, '%d.%m.%Y', default_year)

article_data['online_date'] = parse_date(accepted_date_str, '%d.%m.%Y', default_year)

else:

article_data['print_date'] = parse_date(received_date_str, '%Y-%m-%d', default_year)

article_data['online_date'] = parse_date(accepted_date_str, '%Y-%m-%d', default_year)

logging.info(f"Print дата: {article_data['print_date']}, Online дата: {article_data['online_date']}")

# Страницы

first_page_meta = soup.find('meta', {'name': 'citation_firstpage'})

last_page_meta = soup.find('meta', {'name': 'citation_lastpage'})

article_data['first_page'] = first_page_meta['content'] if first_page_meta else 'n/a'

article_data['last_page'] = last_page_meta['content'] if last_page_meta else 'n/a'

logging.info(f"Страницы: {article_data['first_page']}-{article_data['last_page']}")

# DOI

doi_meta = soup.find('meta', {'name': 'DC.Identifier.DOI'})

if doi_meta and doi_meta.get('content'):

doi_value = doi_meta['content']

if 'doi.org/' in doi_value:

doi_value = doi_value.split('doi.org/')[-1]

article_data['doi'] = doi_value

else:

article_data['doi'] = None

logging.info(f"DOI статьи: {article_data.get('doi')}")

# PDF ссылка

pdf_meta = soup.find('meta', {'name': 'citation_pdf_url'})

if pdf_meta and pdf_meta.get('content'):

article_data['pdf_link_relative'] = pdf_meta['content']

else:

article_data['pdf_link_relative'] = None

logging.info(f"PDF ссылка: {article_data.get('pdf_link_relative')}")

elif domain in ['devma.com.ua', 'd-governance.com.ua', 'lawjournal.com.ua']:

# Используем мета-теги для этих сайтов

title_meta = soup.find('meta', {'name': 'citation_title'})

article_data['title'] = title_meta['content'] if title_meta else "Title not found"

authors = []

for meta_author in soup.find_all('meta', {'name': 'citation_author'}):

author_name = meta_author['content'].strip()

if author_name:

authors.append(author_name)

article_data['authors'] = authors

logging.info(f"Авторы ({len(authors)}): {authors}")

abstract = "Abstract not found"

desc_meta = soup.find('meta', {'name': 'DC.Description'})

if desc_meta and desc_meta.get('content'):

abstract = desc_meta['content'].strip()

article_data['abstract'] = abstract

received_meta = soup.find('meta', {'name': 'DC.Date.dateSubmitted'})

accepted_meta = soup.find('meta', {'name': 'DC.Date.issued'})

received_date_str = received_meta['content'] if received_meta else None

accepted_date_str = accepted_meta['content'] if accepted_meta else None

default_year = datetime.now().year

article_data['print_date'] = parse_date(received_date_str, '%Y-%m-%d', default_year)

article_data['online_date'] = parse_date(accepted_date_str, '%Y-%m-%d', default_year)

logging.info(f"Print дата: {article_data['print_date']}, Online дата: {article_data['online_date']}")

first_page_meta = soup.find('meta', {'name': 'citation_firstpage'})

last_page_meta = soup.find('meta', {'name': 'citation_lastpage'})

article_data['first_page'] = first_page_meta['content'] if first_page_meta else 'n/a'

article_data['last_page'] = last_page_meta['content'] if last_page_meta else 'n/a'

logging.info(f"Страницы: {article_data['first_page']}-{article_data['last_page']}")

doi_meta = soup.find('meta', {'name': 'DC.Identifier.DOI'})

if doi_meta and doi_meta.get('content'):

doi_value = doi_meta['content']

if 'doi.org/' in doi_value:

doi_value = doi_value.split('doi.org/')[-1]

article_data['doi'] = doi_value

else:

article_data['doi'] = None

logging.info(f"DOI статьи: {article_data.get('doi')}")

pdf_meta = soup.find('meta', {'name': 'citation_pdf_url'})

article_data['pdf_link_relative'] = pdf_meta['content'] if pdf_meta else None

logging.info(f"PDF ссылка: {article_data.get('pdf_link_relative')}")

else:

# Для journal-knu.com.ua или humstudios.com.ua используем старый парсер

logging.info("Используется стандартный парсер для других сайтов.")

article_data['title'] = safe_get_text(find_element(soup, config['selectors']['article_title']), "Title not found")

# Авторы

authors = []

if domain == 'journal-knu.com.ua':

author_links = soup.select('div.article-authors a.article-author')

for link in author_links:

author_name = safe_get_text(link)

if author_name:

authors.append(author_name)

else:

author_anchor = find_element(soup, config['selectors']['authors_container'])

if author_anchor:

author_tags = author_anchor.find_next_siblings(config['selectors']['author_link'])

authors = [safe_get_text(tag).replace(',', '') for tag in author_tags if safe_get_text(tag)]

article_data['authors'] = authors

logging.info(f"Авторы ({len(authors)}): {authors}")

# Аннотация

abstract = "Abstract not found"

if domain == 'journal-knu.com.ua':

abstract_block = soup.select_one('div.article-info-block')

if abstract_block:

for p in abstract_block.find_all('p'):

if 'Abstract' in p.text or 'Анотація' in p.text:

abstract = p.text.replace('Abstract', '').replace('Анотація', '').strip()

break

else:

iframe_anchor = find_element(soup, config['selectors']['abstract_container'])

if iframe_anchor:

abstract_p = iframe_anchor.find_next_sibling('p')

if abstract_p:

abstract = safe_get_text(abstract_p)

article_data['abstract'] = abstract

# Даты

received_date_str = accepted_date_str = None

if domain == 'journal-knu.com.ua':

date_block = soup.select_one('div.article-info-block.additional-block')

if date_block:

text = safe_get_text(date_block)

received_match = re.search(r'Received:?s*(d{2}.d{2}.d{4})', text)

accepted_match = re.search(r'Accepted:?s*(d{2}.d{2}.d{4})', text)

if received_match: received_date_str = received_match.group(1)

if accepted_match: accepted_date_str = accepted_match.group(1)

else:

revised_date_str, accepted_date_str, received_date_str = None, None, None

dates_container = find_element(soup, config['selectors']['dates_container'])

if dates_container:

date_text = safe_get_text(dates_container)

parts = date_text.replace(';', ',').split(',')

for part in parts:

if config['patterns']['revised_date_keyword'] in part:

revised_date_str = part.split(config['patterns']['revised_date_keyword'])[-1].strip()

if config['patterns']['accepted_date_keyword'] in part:

accepted_date_str = part.split(config['patterns']['accepted_date_keyword'])[-1].strip()

if config['patterns']['received_date_keyword'] in part:

received_date_str = part.split(config['patterns']['received_date_keyword'])[-1].strip()

default_year = datetime.now().year

article_data['print_date'] = parse_date(received_date_str, config['patterns']['date_format'], default_year)

article_data['online_date'] = parse_date(accepted_date_str, config['patterns']['date_format'], default_year)

logging.info(f"Print дата: {article_data['print_date']}, Online дата: {article_data['online_date']}")

# Страницы

first_page, last_page = "n/a", "n/a"

if domain == 'journal-knu.com.ua':

for p in soup.select('div.article-info-block p'):

if 'Pages' in p.text:

pages_match = re.search(r'Pages:?s*(d+)-(d+)', p.text)

if pages_match:

first_page, last_page = pages_match.group(1), pages_match.group(2)

break

else:

pages_elems = find_element(soup, config['selectors']['pages_container'], first_only=False)

for elem in pages_elems:

pages_text = safe_get_text(elem)

if '-' in pages_text and pages_text.replace('-', '').isdigit():

first_page, last_page = pages_text.split('-')[0].strip(), pages_text.split('-')[-1].strip()

break

article_data['first_page'], article_data['last_page'] = first_page, last_page

logging.info(f"Страницы: {first_page}-{last_page}")

# DOI

doi = None

if domain == 'journal-knu.com.ua':

for p in soup.select('div.article-info-block p'):

if 'DOI' in p.text:

doi_match = re.search(r'DOI:?s*(10.d+/S+)', p.text)

if doi_match:

doi = doi_match.group(1)

break

else:

doi_elem = find_element(soup, config['selectors']['article_doi_link'])

if doi_elem and doi_elem.get('href'):

doi = doi_elem['href'].replace('https://doi.org/', '')

article_data['doi'] = doi

logging.info(f"DOI статьи: {article_data.get('doi')}")

# PDF ссылка

pdf_link = None

if domain == 'journal-knu.com.ua':

pdf_link_elem = soup.select_one('a[href*="/web/uploads/pdf/"]')

if pdf_link_elem and pdf_link_elem.get('href'):

pdf_link = pdf_link_elem['href']

else:

iframe_elem = find_element(soup, config['selectors'].get('article_pdf_iframe_src'))

if iframe_elem and iframe_elem.get('src'):

pdf_link = iframe_elem['src']

article_data['pdf_link_relative'] = pdf_link

logging.info(f"PDF ссылка: {article_data.get('pdf_link_relative')}")

article_data['resource'] = article_url

return article_data

except Exception as e:

logging.error(f"Ошибка при парсинге статьи {article_url}: {e}", exc_info=True)

return None

def build_crossref_xml(issue_data, articles_data, config, email):

"""Строит XML для CrossRef на основе данных выпуска и статей."""

if not issue_data:

logging.error("Данные выпуска отсутствуют, невозможно создать XML.")

return None

root = ET.Element('doi_batch', {'version': '4.4.2', 'xmlns': 'http://www.crossref.org/schema/4.4.2',

'xmlns:jats': 'http://www.ncbi.nlm.nih.gov/JATS1',

'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',

'xsi:schemaLocation': 'http://www.crossref.org/schema/4.4.2 http://www.crossref.org/schema/deposit/crossref4.4.2.xsd'})

head = ET.SubElement(root, 'head')

ET.SubElement(head,

'doi_batch_id').text = f"{issue_data.get('journal_abbrev', 'unknown')}_batch_{datetime.now().strftime('%Y%m%d%H%M')}"

ET.SubElement(head, 'timestamp').text = datetime.now().strftime('%Y%m%d%H%M%S')

depositor = ET.SubElement(head, 'depositor')

ET.SubElement(depositor, 'depositor_name').text = 'WEB DEPOSITOR'

ET.SubElement(depositor, 'email_address').text = email

ET.SubElement(head, 'registrant').text = 'WEB-FORM'

body = ET.SubElement(root, 'body')

journal = ET.SubElement(body, 'journal')

journal_metadata = ET.SubElement(journal, 'journal_metadata', {'language': config.get('lang', 'en')})

ET.SubElement(journal_metadata, 'full_title').text = sanitize_xml_string(

issue_data.get('journal_title', 'Unknown Journal'))

if issue_data.get('journal_abbrev', 'unknown') != 'unknown':

ET.SubElement(journal_metadata, 'abbrev_title').text = issue_data['journal_abbrev'].upper()

if issue_data.get('issn', 'Unknown ISSN') != "Unknown ISSN":

ET.SubElement(journal_metadata, 'issn', {'media_type': 'print'}).text = issue_data['issn']

if issue_data.get('eissn', 'Unknown e-ISSN') != "Unknown e-ISSN":

ET.SubElement(journal_metadata, 'issn', {'media_type': 'electronic'}).text = issue_data['eissn']

journal_issue = ET.SubElement(journal, 'journal_issue')

issue_date = issue_data.get('date', {'month': '01', 'day': '01', 'year': datetime.now().year})

pub_date_issue = ET.SubElement(journal_issue, 'publication_date', {'media_type': 'online'})

ET.SubElement(pub_date_issue, 'month').text = issue_date.get('month', '01')

ET.SubElement(pub_date_issue, 'day').text = issue_date.get('day', '01')

ET.SubElement(pub_date_issue, 'year').text = str(issue_date.get('year', datetime.now().year))

if issue_data.get('volume', 'unknown') != "unknown":

volume_elem = ET.SubElement(journal_issue, 'journal_volume')

ET.SubElement(volume_elem, 'volume').text = issue_data['volume']

if issue_data.get('issue', 'unknown') != "unknown":

ET.SubElement(journal_issue, 'issue').text = issue_data['issue']

doi_data_issue = ET.SubElement(journal_issue, 'doi_data')

ET.SubElement(doi_data_issue, 'doi').text = issue_data.get('doi', 'unknown/issue_doi')

ET.SubElement(doi_data_issue, 'resource').text = issue_data.get('resource', '')

for data in articles_data:

if not data or not data.get('title') or data['title'] == "Title not found":

continue

article = ET.SubElement(journal, 'journal_article', {'publication_type': 'full_text'})

titles = ET.SubElement(article, 'titles')

ET.SubElement(titles, 'title').text = sanitize_xml_string(data['title'])

if data.get('authors'):

contributors = ET.SubElement(article, 'contributors')

for i, author_name in enumerate(data['authors']):

name_parts = author_name.split()

if name_parts:

person = ET.SubElement(contributors, 'person_name', {'contributor_role': 'author',

'sequence': 'first' if i == 0 else 'additional'})

given_name = name_parts[0]

surname = ' '.join(name_parts[1:]) if len(name_parts) > 1 else "Unknown"

ET.SubElement(person, 'given_name').text = sanitize_xml_string(given_name)

ET.SubElement(person, 'surname').text = sanitize_xml_string(surname)

abstract_text = data.get('abstract')

if abstract_text and abstract_text != "Abstract not found":

abstract_elem = ET.SubElement(article, '{http://www.ncbi.nlm.nih.gov/JATS1}abstract',

{'xml:lang': config.get('lang', 'en')})

ET.SubElement(abstract_elem, '{http://www.ncbi.nlm.nih.gov/JATS1}p').text = sanitize_xml_string(

abstract_text)

print_date = data.get('print_date', {'month': '01', 'day': '01', 'year': datetime.now().year})

online_date = data.get('online_date', {'month': '01', 'day': '01', 'year': datetime.now().year})

pub_date_print = ET.SubElement(article, 'publication_date', {'media_type': 'print'})

ET.SubElement(pub_date_print, 'month').text = print_date.get('month', '01')

ET.SubElement(pub_date_print, 'day').text = print_date.get('day', '01')

ET.SubElement(pub_date_print, 'year').text = str(print_date.get('year', datetime.now().year))

pub_date_online = ET.SubElement(article, 'publication_date', {'media_type': 'online'})

ET.SubElement(pub_date_online, 'month').text = online_date.get('month', '01')

ET.SubElement(pub_date_online, 'day').text = online_date.get('day', '01')

ET.SubElement(pub_date_online, 'year').text = str(online_date.get('year', datetime.now().year))

first_page, last_page = data.get('first_page'), data.get('last_page')

if first_page and first_page != 'n/a':

pages = ET.SubElement(article, 'pages')

ET.SubElement(pages, 'first_page').text = first_page

if last_page and last_page != 'n/a':

ET.SubElement(pages, 'last_page').text = last_page

doi_data_article = ET.SubElement(article, 'doi_data')

article_doi = data.get('doi')

if article_doi:

ET.SubElement(doi_data_article, 'doi').text = article_doi.replace('doi.org/', '')

ET.SubElement(doi_data_article, 'resource').text = data.get('resource', '')

if data.get('pdf_link_relative'):

pdf_url = data['pdf_link_relative']

if not pdf_url.startswith('http'):

base_url = config.get(

'base_url') or f"{urlparse(issue_data.get('resource', '')).scheme}://{urlparse(issue_data.get('resource', '')).netloc}"

pdf_url = urljoin(base_url, pdf_url)

collection = ET.SubElement(doi_data_article, 'collection', {'property': 'crawler-based'})

item = ET.SubElement(collection, 'item', {'crawler': 'iParadigms'})

ET.SubElement(item, 'resource').text = pdf_url

logging.info(f"Добавлена ссылка на PDF в XML: {pdf_url}")

try:

xml_str = ET.tostring(root, encoding='utf-8', method='xml')

dom = xml.dom.minidom.parseString(xml_str)

pretty_xml = dom.toprettyxml(indent=" ")

pretty_xml = pretty_xml.replace('ns0:', 'jats:')

return pretty_xml

except Exception as xml_format_error:

logging.error(f"Ошибка при форматировании XML: {xml_format_error}")

return ET.tostring(root, encoding='unicode')

def main():

depositor_email = ""

while not depositor_email:

depositor_email = input("Введите email депозитора и нажмите Enter: ").strip()

main_url = "https://artnotes.com.ua/en/journals/tom-26-1-2026"

output_file = r"C:UsersandreOneDriveDesktopcrossref_output_artnotes.xml"

run_headless = True

# --- УКАЗЫВАЕМ КОНКРЕТНУЮ СТАТЬЮ ---

# Вариант 1: по DOI (самый надежный)

target_doi = "10.63009/noac/1.2026.01"

# Вариант 2: по части заголовка (если DOI неизвестен)

# target_title_keyword = "Virtual exhibition formats"

# ------------------------------------

logging.info(f"Запускаем парсер для URL: {main_url}")

try:

config = get_config_for_url(main_url)

domain = urlparse(main_url).netloc.replace('www.', '')

except ValueError as e:

return

all_article_data, issue_data = [], None

with sync_playwright() as p:

browser = p.chromium.launch(headless=run_headless)

try:

page = browser.new_page()

page.goto(main_url, timeout=60000)

page.wait_for_selector(config['selectors']['issue_container_waitFor'], timeout=30000)

page.wait_for_timeout(1500)

main_soup = BeautifulSoup(page.content(), 'html.parser')

issue_data = extract_issue_data(main_soup, main_url, config, domain)

article_links, article_dois = extract_article_links_and_dois(main_soup, main_url, config, domain)

# --- ФИЛЬТРУЕМ СТАТЬИ, ОСТАВЛЯЕМ ТОЛЬКО НУЖНУЮ ---

target_article_url = None

for i, article_url in enumerate(article_links):

# Проверяем по DOI

if i < len(article_dois) and article_dois[i] == target_doi:

target_article_url = article_url

logging.info(f"Найдена целевая статья по DOI: {article_url}")

break

# Если не нашли по DOI, пробуем найти по заголовку

if not target_article_url:

logging.info("Поиск статьи по DOI не дал результата, пробуем по заголовку...")

# Перезагружаем страницу, чтобы получить свежий soup

page.goto(main_url, timeout=60000)

page.wait_for_selector(config['selectors']['issue_container_waitFor'], timeout=30000)

main_soup = BeautifulSoup(page.content(), 'html.parser')

article_elements = main_soup.select(config['selectors']['article_item'])

for article_el in article_elements:

link_elem = article_el.select_one(config['selectors']['article_link'])

if link_elem and link_elem.get('href'):

title = link_elem.get_text(strip=True)

if 'Virtual exhibition formats' in title:

target_article_url = urljoin(config.get('base_url'), link_elem['href'])

logging.info(f"Найдена целевая статья по заголовку: {target_article_url}")

break

# Если статья найдена, парсим её

if target_article_url:

article_details = extract_article_details(page, target_article_url, config, domain)

if article_details:

all_article_data.append(article_details)

logging.info(f"Успешно обработана статья: {article_details.get('title', 'Без заголовка')}")

else:

logging.error("Целевая статья не найдена на странице выпуска!")

except Exception as e:

logging.error(f"Критическая ошибка: {e}", exc_info=True)

finally:

browser.close()

logging.info(f"Собрано данных по {len(all_article_data)} статьям.")

if issue_data and all_article_data:

try:

pretty_xml = build_crossref_xml(issue_data, all_article_data, config, depositor_email)

if pretty_xml:

with open(output_file, 'w', encoding='utf-8') as f:

f.write(pretty_xml)

logging.info(f"XML файл успешно создан: {output_file}")

except Exception as e:

logging.error(f"Ошибка при записи XML файла: {e}", exc_info=True)

if __name__ == "__main__":

main()

Published

2024-09-16

Issue

Section

Articles