yogl commited on
Commit
6735e1e
·
verified ·
1 Parent(s): d227adb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +71 -63
app.py CHANGED
@@ -65,14 +65,14 @@ DISPLAY_COLUMNS_ALL = [
65
 
66
  # ==========================
67
  # UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
 
 
68
  # ==========================
69
 
70
  UI_TABLE_COLUMNS = [
71
  "Сходство",
72
  "ФИО",
73
- "ORCID",
74
  "Название диссертации",
75
- "ВАК",
76
  "Организация",
77
  "Тип",
78
  "Год",
@@ -83,7 +83,6 @@ UI_TABLE_COLUMNS = [
83
  "Работ",
84
  "Цитат",
85
  ]
86
-
87
  DEFAULT_VISIBLE_UI = {c: (c != "Организация") for c in UI_TABLE_COLUMNS}
88
 
89
 
@@ -147,11 +146,6 @@ def _safe_text(x: Any) -> str:
147
  return s
148
 
149
 
150
- def _safe_fragment(s: Any) -> str:
151
- t = _safe_text(s).replace("#", " ").replace("\n", " ").strip()
152
- return t
153
-
154
-
155
  def _norm_openalex_url(x: Any) -> str:
156
  s = _safe_text(x)
157
  if not s:
@@ -168,7 +162,7 @@ def _norm_orcid_url(x: Any) -> str:
168
  return ""
169
  if s.startswith("http://") or s.startswith("https://"):
170
  return s
171
- m = re.search(r"(0000-[0-9X]{4}-[0-9X]{4}-[0-9X]{4})", s)
172
  return f"https://orcid.org/{m.group(1)}" if m else ""
173
 
174
 
@@ -458,7 +452,11 @@ def build_result_df(results):
458
 
459
  # ВАЖНО: используем author_org_short (fallback на author_org_name если вдруг короткого нет)
460
  org_short = meta.get("author_org_short", None)
461
- if org_short is None or (isinstance(org_short, float) and pd.isna(org_short)) or str(org_short).lower() in {"none", "nan"}:
 
 
 
 
462
  org_short = meta.get("author_org_name", None)
463
 
464
  rows.append(
@@ -541,36 +539,26 @@ def run_search(
541
  if only_orcid:
542
  df_raw = df_raw[df_raw["orcid_url"].map(_safe_text) != ""]
543
 
544
- df_raw = df_raw.reset_index(drop=True)
545
-
546
  # Ограничиваем top_k уже после фильтрации
 
547
  if len(df_raw) > int(top_k):
548
  df_raw = df_raw.iloc[: int(top_k)].copy()
549
 
550
- # Перенумеруем "№"
551
- if "№" in df_raw.columns:
552
- df_raw["№"] = np.arange(1, len(df_raw) + 1)
553
-
554
- # Установим индекс как стабильный id
555
  df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
556
  df_raw = df_raw.drop_duplicates(subset=["reg_norm"]).set_index("reg_norm", drop=True)
557
 
558
- # UI-таблица
559
- fio_txt = df_raw["fio"].map(_safe_text)
560
- orcid_url = df_raw["orcid_url"].map(_safe_text)
561
- fio_cell = np.where(orcid_url != "", orcid_url + "#" + fio_txt.map(_safe_fragment), fio_txt)
562
-
563
- title_txt = df_raw["title"].map(_safe_text)
564
- vak_link = df_raw["vak_link"].map(_safe_text)
565
- title_cell = np.where(vak_link != "", vak_link + "#" + title_txt.map(_safe_fragment), title_txt)
566
 
 
 
567
  df_ui = pd.DataFrame(
568
  {
569
  "Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
570
- "ФИО": fio_cell,
571
- "ORCID": df_raw["orcid_url"].map(_safe_text),
572
- "Название диссертации": title_cell,
573
- "ВАК": df_raw["vak_link"].map(_safe_text),
574
  "Организация": df_raw["author_org_short"].map(_safe_text),
575
  "Тип": df_raw["dissertation_type"].map(_safe_text),
576
  "Год": pd.to_numeric(df_raw["protection_year"], errors="coerce").astype("float64"),
@@ -585,8 +573,8 @@ def run_search(
585
  )
586
  df_ui.index.name = "reg_norm"
587
 
588
- # Excel (как раньше)
589
- df_excel_ru = df_raw.reset_index(drop=False).rename(columns=COLUMN_LABELS_RU_EXCEL)
590
  output = io.BytesIO()
591
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
592
  df_excel_ru.to_excel(writer, index=False)
@@ -604,7 +592,22 @@ def format_selected_list_from_raw(df_raw: pd.DataFrame, selected_regnorms: List[
604
  fio = _safe_text(raw.get("fio"))
605
  title = _safe_text(raw.get("title"))
606
  year = _safe_text(raw.get("protection_year"))
607
- lines.append(f"- **{fio}** — {title} ({year})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  return "\n".join(lines)
609
 
610
 
@@ -623,8 +626,12 @@ if "last_df_raw" not in st.session_state:
623
  st.session_state.last_df_raw = None
624
  if "last_excel" not in st.session_state:
625
  st.session_state.last_excel = None
 
 
626
  if "selected_regnorms" not in st.session_state:
627
  st.session_state.selected_regnorms = set()
 
 
628
  if "search_id" not in st.session_state:
629
  st.session_state.search_id = 0
630
 
@@ -717,12 +724,14 @@ if do_search:
717
  st.session_state.last_df_raw = df_raw
718
  st.session_state.last_excel = excel_bytes
719
 
720
- # Важно: НЕ сбрасываем выбор полностью; оставляем только те, что есть в новых результатах
721
  if isinstance(df_ui, pd.DataFrame) and not df_ui.empty:
722
  st.session_state.selected_regnorms = set(st.session_state.selected_regnorms) & set(df_ui.index)
723
  else:
724
  st.session_state.selected_regnorms = set()
725
 
 
 
726
  st.session_state.search_id += 1
727
 
728
  df_ui_saved = st.session_state.last_df_ui
@@ -732,10 +741,10 @@ excel_saved = st.session_state.last_excel
732
  if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
733
  st.success(f"Найдено записей: {len(df_ui_saved)}")
734
 
735
- # Соберём отображаемую таблицу + колонку выбора
736
  selected_set = set(st.session_state.selected_regnorms) & set(df_ui_saved.index)
737
  st.session_state.selected_regnorms = selected_set
738
 
 
739
  df_display = df_ui_saved.copy()
740
  df_display.insert(0, "Выбрать", df_display.index.map(lambda x: x in selected_set))
741
 
@@ -745,30 +754,8 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
745
  full_column_config = {
746
  "Выбрать": st.column_config.CheckboxColumn("Выбрать", width="small"),
747
  "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
748
- "ФИО": st.column_config.LinkColumn(
749
- "ФИО",
750
- display_text=r"(?:.*#)?(.*)$",
751
- width="medium",
752
- help="Если для автора найден ORCID — ФИО кликабельно и ведёт на ORCID.",
753
- ),
754
- "ORCID": st.column_config.LinkColumn(
755
- "ORCID",
756
- display_text=r"https://orcid\.org/(0000-[0-9X]{4}-[0-9X]{4}-[0-9X]{4})",
757
- width="small",
758
- help="Профиль автора в ORCID (если найден).",
759
- ),
760
- "Название диссертации": st.column_config.LinkColumn(
761
- "Название диссертации",
762
- display_text=r"(?:.*#)?(.*)$",
763
- width="large",
764
- help="Если есть ссылка ВАК — название кликабельно и ведёт на ВАК.",
765
- ),
766
- "ВАК": st.column_config.LinkColumn(
767
- "ВАК",
768
- display_text=r".+",
769
- width="small",
770
- help="Прямая ссылка на карточку/страницу ВАК (если есть).",
771
- ),
772
  "Организация": st.column_config.TextColumn("Организация", width="large"),
773
  "Тип": st.column_config.TextColumn("Тип", width="small"),
774
  "Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
@@ -778,13 +765,13 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
778
  display_text=r"https://openalex\.org/(A\d+)",
779
  width="small",
780
  help="Профиль автора в OpenAlex (если найден).",
 
781
  ),
782
  "h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
783
  "i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
784
  "Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
785
  "Цитат": st.column_config.NumberColumn("Цитат", format="%.0f", width="small"),
786
  }
787
-
788
  column_config_filtered = {k: v for k, v in full_column_config.items() if k in df_edit.columns}
789
 
790
  edited = st.data_editor(
@@ -797,21 +784,42 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
797
  key=f"editor_{st.session_state.search_id}",
798
  )
799
 
800
- # Обновим выбранные reg_norm из чекбоксов — это не будет "сбрасываться" при фильтрах
801
  if isinstance(edited, pd.DataFrame) and "Выбрать" in edited.columns:
802
  st.session_state.selected_regnorms = set(edited.index[edited["Выбрать"] == True].tolist())
803
 
804
  selected_regnorms = sorted(list(st.session_state.selected_regnorms))
805
 
806
  if selected_regnorms:
807
- with st.expander("Полный текст (для выбранных строк)", expanded=False):
808
  for reg_norm in selected_regnorms[:50]:
809
  if df_raw_saved is None or reg_norm not in df_raw_saved.index:
810
  continue
811
  raw = df_raw_saved.loc[reg_norm]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  st.markdown(
813
- f"- **{_safe_text(raw.get('fio'))}** — {_safe_text(raw.get('title'))}\n"
814
- f" \n {_safe_text(raw.get('author_org_short'))} ({_safe_text(raw.get('protection_year'))})"
815
  )
816
 
817
  if excel_saved is not None:
 
65
 
66
  # ==========================
67
  # UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
68
+ # ВАЖНО: ВАК и ORCID отдельными столбцами НЕ выводим.
69
+ # Ссылки показываем в блоке "Полный текст" / "Перечень диссертаций".
70
  # ==========================
71
 
72
  UI_TABLE_COLUMNS = [
73
  "Сходство",
74
  "ФИО",
 
75
  "Название диссертации",
 
76
  "Организация",
77
  "Тип",
78
  "Год",
 
83
  "Работ",
84
  "Цитат",
85
  ]
 
86
  DEFAULT_VISIBLE_UI = {c: (c != "Организация") for c in UI_TABLE_COLUMNS}
87
 
88
 
 
146
  return s
147
 
148
 
 
 
 
 
 
149
  def _norm_openalex_url(x: Any) -> str:
150
  s = _safe_text(x)
151
  if not s:
 
162
  return ""
163
  if s.startswith("http://") or s.startswith("https://"):
164
  return s
165
+ m = re.search(r"(0000-[0-9X]{4}-[0-9X]{4}-[0-9X]{4}-[0-9X]{4})", s)
166
  return f"https://orcid.org/{m.group(1)}" if m else ""
167
 
168
 
 
452
 
453
  # ВАЖНО: используем author_org_short (fallback на author_org_name если вдруг короткого нет)
454
  org_short = meta.get("author_org_short", None)
455
+ if (
456
+ org_short is None
457
+ or (isinstance(org_short, float) and pd.isna(org_short))
458
+ or str(org_short).lower() in {"none", "nan"}
459
+ ):
460
  org_short = meta.get("author_org_name", None)
461
 
462
  rows.append(
 
539
  if only_orcid:
540
  df_raw = df_raw[df_raw["orcid_url"].map(_safe_text) != ""]
541
 
 
 
542
  # Ограничиваем top_k уже после фильтрации
543
+ df_raw = df_raw.reset_index(drop=True)
544
  if len(df_raw) > int(top_k):
545
  df_raw = df_raw.iloc[: int(top_k)].copy()
546
 
547
+ # Дедуп и индекс
 
 
 
 
548
  df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
549
  df_raw = df_raw.drop_duplicates(subset=["reg_norm"]).set_index("reg_norm", drop=True)
550
 
551
+ # Перенумеруем "№" (после дедупа)
552
+ if "" in df_raw.columns:
553
+ df_raw[""] = np.arange(1, len(df_raw) + 1)
 
 
 
 
 
554
 
555
+ # UI-таблица: ВАЖНО — "ФИО" и "Название" делаем ТЕКСТОВЫМИ,
556
+ # чтобы Streamlit не создавал "липовые" ссылки для строк без ORCID/ВАК.
557
  df_ui = pd.DataFrame(
558
  {
559
  "Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
560
+ "ФИО": df_raw["fio"].map(_safe_text),
561
+ "Название диссертации": df_raw["title"].map(_safe_text),
 
 
562
  "Организация": df_raw["author_org_short"].map(_safe_text),
563
  "Тип": df_raw["dissertation_type"].map(_safe_text),
564
  "Год": pd.to_numeric(df_raw["protection_year"], errors="coerce").astype("float64"),
 
573
  )
574
  df_ui.index.name = "reg_norm"
575
 
576
+ # Excel
577
+ df_excel_ru = df_raw.reset_index(drop=True).rename(columns=COLUMN_LABELS_RU_EXCEL)
578
  output = io.BytesIO()
579
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
580
  df_excel_ru.to_excel(writer, index=False)
 
592
  fio = _safe_text(raw.get("fio"))
593
  title = _safe_text(raw.get("title"))
594
  year = _safe_text(raw.get("protection_year"))
595
+
596
+ vak = _safe_text(raw.get("vak_link"))
597
+ orcid = _safe_text(raw.get("orcid_url"))
598
+ oa = _safe_text(raw.get("openalex_url"))
599
+
600
+ link_parts = []
601
+ if vak:
602
+ link_parts.append(f"[ВАК]({vak})")
603
+ if orcid:
604
+ link_parts.append(f"[ORCID]({orcid})")
605
+ if oa:
606
+ link_parts.append(f"[OpenAlex]({oa})")
607
+
608
+ links_line = (" \n " + " ".join(link_parts)) if link_parts else ""
609
+
610
+ lines.append(f"- **{fio}** — {title} ({year}){links_line}")
611
  return "\n".join(lines)
612
 
613
 
 
626
  st.session_state.last_df_raw = None
627
  if "last_excel" not in st.session_state:
628
  st.session_state.last_excel = None
629
+
630
+ # выбор диссертаций хранится по reg_norm, чтобы не сбрасывался при фильтрации/перерисовке
631
  if "selected_regnorms" not in st.session_state:
632
  st.session_state.selected_regnorms = set()
633
+
634
+ # счётчик только для управляемого сброса состояния таблицы при НОВОМ поиске
635
  if "search_id" not in st.session_state:
636
  st.session_state.search_id = 0
637
 
 
724
  st.session_state.last_df_raw = df_raw
725
  st.session_state.last_excel = excel_bytes
726
 
727
+ # НЕ сбрасываем выбор: оставляем то, что есть в новых результатах
728
  if isinstance(df_ui, pd.DataFrame) and not df_ui.empty:
729
  st.session_state.selected_regnorms = set(st.session_state.selected_regnorms) & set(df_ui.index)
730
  else:
731
  st.session_state.selected_regnorms = set()
732
 
733
+ # новый поиск -> новый ключ таблицы (чтобы корректно пересобрать data_editor),
734
+ # но выбор переносится через selected_regnorms выше
735
  st.session_state.search_id += 1
736
 
737
  df_ui_saved = st.session_state.last_df_ui
 
741
  if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
742
  st.success(f"Найдено записей: {len(df_ui_saved)}")
743
 
 
744
  selected_set = set(st.session_state.selected_regnorms) & set(df_ui_saved.index)
745
  st.session_state.selected_regnorms = selected_set
746
 
747
+ # Таблица + колонка выбора
748
  df_display = df_ui_saved.copy()
749
  df_display.insert(0, "Выбрать", df_display.index.map(lambda x: x in selected_set))
750
 
 
754
  full_column_config = {
755
  "Выбрать": st.column_config.CheckboxColumn("Выбрать", width="small"),
756
  "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
757
+ "ФИО": st.column_config.TextColumn("ФИО", width="medium"),
758
+ "Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  "Организация": st.column_config.TextColumn("Организация", width="large"),
760
  "Тип": st.column_config.TextColumn("Тип", width="small"),
761
  "Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
 
765
  display_text=r"https://openalex\.org/(A\d+)",
766
  width="small",
767
  help="Профиль автора в OpenAlex (если найден).",
768
+ validate=r"^https://openalex\.org/A\d+$|^$",
769
  ),
770
  "h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
771
  "i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
772
  "Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
773
  "Цитат": st.column_config.NumberColumn("Цитат", format="%.0f", width="small"),
774
  }
 
775
  column_config_filtered = {k: v for k, v in full_column_config.items() if k in df_edit.columns}
776
 
777
  edited = st.data_editor(
 
784
  key=f"editor_{st.session_state.search_id}",
785
  )
786
 
787
+ # Синхронизация выбора
788
  if isinstance(edited, pd.DataFrame) and "Выбрать" in edited.columns:
789
  st.session_state.selected_regnorms = set(edited.index[edited["Выбрать"] == True].tolist())
790
 
791
  selected_regnorms = sorted(list(st.session_state.selected_regnorms))
792
 
793
  if selected_regnorms:
794
+ with st.expander("Полный текст и ссылки (для выбранных строк)", expanded=False):
795
  for reg_norm in selected_regnorms[:50]:
796
  if df_raw_saved is None or reg_norm not in df_raw_saved.index:
797
  continue
798
  raw = df_raw_saved.loc[reg_norm]
799
+
800
+ fio = _safe_text(raw.get("fio"))
801
+ title = _safe_text(raw.get("title"))
802
+ org = _safe_text(raw.get("author_org_short"))
803
+ year = _safe_text(raw.get("protection_year"))
804
+
805
+ vak = _safe_text(raw.get("vak_link"))
806
+ orcid = _safe_text(raw.get("orcid_url"))
807
+ oa = _safe_text(raw.get("openalex_url"))
808
+
809
+ # Ссылки показываем ТОЛЬКО если они реально есть (ничего не "выдумываем")
810
+ links = []
811
+ if vak:
812
+ links.append(f"[ВАК]({vak})")
813
+ if orcid:
814
+ links.append(f"[ORCID]({orcid})")
815
+ if oa:
816
+ links.append(f"[OpenAlex]({oa})")
817
+
818
+ links_md = (" \n " + " ".join(links)) if links else ""
819
+
820
  st.markdown(
821
+ f"- **{fio}** — {title}\n"
822
+ f" \n {org} ({year}){links_md}"
823
  )
824
 
825
  if excel_saved is not None: