В поисковой выдаче нашлись три публикации об одном уголовном деле. В каждой — одинаковые числа, похожее описание событий и название одной организации. Если просто сложить ссылки в карточку, получится убедительная подборка. Если посчитать их независимыми подтверждениями, можно получить уверенность, которой исходные данные не дают.
Для проверки этой проблемы достаточно небольшого примера. Возьмём российские публикации о деле «ХимПрома», разложим несколько утверждений на поля и напишем проверку на Python. Она покажет, кому приписываются сведения и какие уточнения меняются при пересказе. Заодно станет понятно, почему строить граф людей и денег иногда стоит начинать с графа источников.
Разбор подготовлен для тематического архива KARTEL KHIMPROM. Связь материала с проектом обозначена открыто; сам архив не используется здесь как независимое подтверждение сведений. Источники примера — МВД Медиа, РБК и Интерфакс.
Что именно будем проверять
7 октября 2022 года МВД Медиа опубликовало сообщение о приговоре по делу «ХимПрома». В тот же день вышел материал Интерфакса, на следующий — публикация РБК.
Это архивный кейс. Он подходит для разбора обработки информации, но не описывает состояние дела в 2026 году. Полный судебный акт в этот эксперимент не включён.
В первом абзаце РБК есть ссылка на сообщение МВД. Интерфакс прямо приписывает сведения представителю ведомства. Поэтому для выбранных утверждений можно выделить общую группу происхождения: заявление МВД от 7 октября.
Это не обвинение редакций в копировании. Агентство могло получить комментарий непосредственно, редакция — самостоятельно уточнить отдельные детали. Но там, где обе публикации прямо пересказывают заявление одного представителя, три URL сами по себе не дают трёх независимых подтверждений.
Первое полезное разделение: публикация — это документ, а источник утверждения — тот, кому в документе приписаны сведения. Они не всегда совпадают.
Число без своего описания становится другим показателем
Второй вопрос возникает при сравнении чисел. В сообщении МВД изъятая масса объединяет наркотические вещества и прекурсоры. В основном тексте РБК оба компонента сохранены, а в лиде описание сокращено до наркотиков. Кроме того, нижняя граница в исходном сообщении превращается в число без такого уточнения.
Это существенная разница для базы данных. Масса веществ вместе с прекурсорами не позволяет определить массу одних наркотиков: соотношение компонентов неизвестно. Если при извлечении оставить только число и единицу измерения, эта неопределённость исчезнет из записи, хотя никуда не исчезла из реальности.
Есть и более тонкий пример: в сообщении МВД количество электронных кошельков задано как нижняя граница, в РБК — как приблизительная оценка. Эти формулировки могут оказаться совместимыми. Тем не менее они не тождественны, и при переносе в таблицу различие стоит сохранить.
Ниже — нормализованная разметка, а не дословные цитаты:
Место в публикации | Значение | Что входит в показатель |
|---|---|---|
МВД, основной текст | Более 9,5 т | Вещества вместе с прекурсорами |
Интерфакс, основной текст | Более 9,5 т | Вещества вместе с прекурсорами |
РБК, основной текст | 9,5 т | Вещества вместе с прекурсорами |
РБК, лид | 9,5 т | Наркотики; прекурсоры не упомянуты |
МВД, финансовый эпизод | Более 1000 | Электронные кошельки |
РБК, финансовый эпизод | Около 1000 | Электронные кошельки |
Из такой таблицы не следует, что какая-либо публикация намеренно вводит читателя в заблуждение. Зато видно, какие фрагменты нужно перечитать перед дальнейшим использованием. Причём основной текст РБК нельзя игнорировать ради более эффектного сравнения только с лидом.
Минимальная модель: документ, утверждение, наблюдение
Вместо таблицы «факт — ссылка» удобно хранить четыре типа записей:
source— конкретная публикация с URL, датой и редакцией;origin— источник, которому приписываются сведения, с основанием такой атрибуции;claim— показатель, который сравнивается в пределах определённого события и даты;observation— изложение этого показателя в конкретном месте публикации.
Последний пункт особенно полезен. Лид и основной текст одной страницы могут описывать показатель по-разному. Если оставить одну запись на URL, придётся потерять либо различие, либо один из фрагментов.
У числового наблюдения должны сохраняться как минимум значение, единицы, объект подсчёта и квалификатор. В примере используются gt для нижней границы, approx для приблизительной оценки и eq для числа без явно указанного квалификатора. Последнее обозначение не означает, что редакция гарантировала абсолютную точность измерения.
Отдельно нужны область события и дата состояния сведений. Фраза о процессуальном статусе в старом сообщении должна оставаться утверждением на соответствующую дату. Дата, когда исследователь открыл страницу, не превращает её содержание в актуальную сводку.
Происхождение тоже следует хранить на уровне наблюдения. В одной статье первая цифра может происходить из пресс-релиза, вторая — из судебного документа, третья — из интервью. Общая метка на всю страницу окажется слишком грубой.
Рабочий пример без внешних библиотек
Оставим в коде два показателя из таблицы: массу и количество кошельков. Чтобы пример помещался в статье, метаданные страниц и пояснения к атрибуции останутся в тексте выше. В полном наборе их нужно хранить рядом с наблюдениями.
Скрипт использует стандартную библиотеку Python. Его можно сохранить как check_sources.py и запустить командой python3 check_sources.py. В Windows с установленным Python обычно подходит py -3 check_sources.py.
from collections import defaultdict from copy import deepcopy # Ручная разметка: claim, source, место, значение, оператор, объект. raw = [ ('mass', 'mvd', 'body', 9.5, 'gt', 'drugs_and_precursors'), ('mass', 'interfax', 'body', 9.5, 'gt', 'drugs_and_precursors'), ('mass', 'rbc', 'body', 9.5, 'eq', 'drugs_and_precursors'), ('mass', 'rbc', 'lead', 9.5, 'eq', 'drugs_unspecified'), ('wallets', 'mvd', 'body', 1000, 'gt', 'electronic_wallets'), ('wallets', 'rbc', 'body', 1000, 'approx', 'electronic_wallets'), ] fields = ('claim', 'source', 'location', 'value', 'operator', 'object') observations = [dict(zip(fields, row)) for row in raw] for row in observations: row.update( origin='mvd-statement-2022-10-07', scope='khimprom-rf-2022', as_of='2022-10-07', unit='t' if row['claim'] == 'mass' else 'count', ) def summarize(rows): if not rows: raise ValueError('Нет наблюдений') contexts = {(r['claim'], r['scope'], r['as_of']) for r in rows} if len(contexts) != 1: raise ValueError('Смешаны показатели, события или даты') origins = {r['origin'] for r in rows if r['origin'] is not None} unknown = {r['source'] for r in rows if r['origin'] is None} flags = [] if len({r['object'] for r in rows}) > 1: flags.append('OBJECT_CHANGE') if len({r['unit'] for r in rows}) > 1: flags.append('UNIT_CHANGE') buckets = defaultdict(list) for row in rows: buckets[(row['object'], row['unit'])].append(row) if any(len({r['operator'] for r in b}) > 1 for b in buckets.values()): flags.append('PRECISION_CHANGE') if any(len({r['value'] for r in b}) > 1 for b in buckets.values()): flags.append('VALUE_CHANGE') if unknown: flags.append('UNKNOWN_ORIGIN') return { 'publications': len({r['source'] for r in rows}), 'origins': len(origins), # Не число независимых подтверждений! 'unknown': len(unknown), 'flags': flags, } if __name__ == '__main__': for claim in ('mass', 'wallets'): rows = [r for r in observations if r['claim'] == claim] print(claim, summarize(rows)) # Искусственный тест: ещё один пересказ прежнего заявления. mass = [r for r in observations if r['claim'] == 'mass'] copy = dict(mass[0], source='another_publication') result = summarize(mass + [copy]) assert result['publications'] == 4 and result['origins'] == 1 # Неизвестное происхождение не превращается в независимое. changed = deepcopy(mass) changed[0]['origin'] = None assert 'UNKNOWN_ORIGIN' in summarize(changed)['flags'] # Разные даты нельзя объединять в один результат. changed = deepcopy(mass) changed[0]['as_of'] = '2026-10-08' try: summarize(changed) except ValueError: pass else: raise AssertionError('Смешение дат не обнаружено')
На этих шести наблюдениях результат выглядит так:
mass {'publications': 3, 'origins': 1, 'unknown': 0, 'flags': ['OBJECT_CHANGE', 'PRECISION_CHANGE']} wallets {'publications': 2, 'origins': 1, 'unknown': 0, 'flags': ['PRECISION_CHANGE']}
У показателя массы три публикации и одна известная группа происхождения. Четыре наблюдения возникают потому, что РБК представлен отдельно основным текстом и лидом. Для кошельков в этом примере размечены две публикации, относящие сведения к тому же заявлению.
OBJECT_CHANGE отмечает изменение состава показателя. PRECISION_CHANGE — различие квалификаторов при одинаковых объекте и единицах. В сумме получаются три замечания, но ни одно из них не является автоматическим вердиктом о ложности сообщения.
В коде намеренно нет оценки достоверности в процентах. Неизвестно, на основании чего присваивать такой процент. Счётчик документов может быть точным, даже когда интерпретация его значения ошибочна.
Самая важная работа осталась до запуска Python
Программа не догадалась, что публикации ссылаются на одно заявление. Группа mvd-statement-2022-10-07 задана вручную после чтения источников. Объекты подсчёта и квалификаторы тоже заданы при подготовке набора.
Поэтому эксперимент состоит из двух разных операций. Сначала исследователь устанавливает, что именно сказано в каждом фрагменте. Затем код последовательно проверяет уже структурированные записи. Вторую операцию легко повторить; качество первой требует отдельного контроля.
Например, если один и тот же объект записать двумя разными строками, появится ложное замечание. Если разные объекты ошибочно назвать одинаково, программа пропустит различие. Для расширения набора понадобятся согласованный словарь и проверка разметки другим человеком.
Группировать источники только по домену тоже нельзя. Одно ведомство может выпустить несколько сообщений с разными основаниями, а разные сайты — воспроизвести одно сообщение. Число групп происхождения описывает нашу разметку, но само по себе не измеряет независимость.
Если установить происхождение не удалось, корректное значение — null. Оно означает «неизвестно». Превращать его в «независимый источник» было бы необоснованным повышением уверенности.
У небольшого примера есть и техническое ограничение: он сравнивает единицы буквально. Тонны и килограммы получат отдельное замечание, а не будут автоматически приведены к одной шкале. Для первой версии лучше явно показать такое ограничение, чем незаметно сопоставить несопоставимые величины.
Как проверить, что инструмент не обманывает исследователя
Для подобных инструментов полезны тесты на ошибки интерпретации, а не только на правильность арифметики. Например, добавление нового пересказа с прежним происхождением должно увеличивать число документов, но не число известных групп атрибуции.
Второй сценарий — неизвестное происхождение. После замены одного origin на None должен появляться сигнал о неполноте разметки. Самостоятельность этого документа не установлена.
Третий — разные даты и события. Если в группу случайно попала запись по другому эпизоду, проверка должна остановиться, а не выдать привлекательный общий итог. Аналогично различающиеся единицы нельзя игнорировать при сравнении значений.
Полный прототип, подготовленный для этого разбора, проверен на Python 3.12.14: пройдены 11 тестов, включая эти ограничения и безопасное включение текста в HTML. Его набор содержит три публикации, четыре показателя и 11 наблюдений. Код выше — сокращённый самостоятельный пример; он также выполнен отдельно. Это проверка поведения программы, а не аудит достоверности исходных сообщений.
Если результаты выводятся в браузере, заголовки и заметки нужно считать внешним текстом. В прототипе они вставляются через textContent, а встроенный JSON экранируется так, чтобы содержимое строки не могло завершить HTML-элемент script. Для наглядного отчёта не понадобились внешние библиотеки или отправка данных на сервер.
Почему это важно для схем людей, организаций и денег
На красивой схеме связь легко начинает выглядеть установленным фактом. Но у линии между двумя людьми может оказаться совсем другое основание: одна публикация, которая пересказывает заявление третьего лица. Если происхождение не видно, граф скрывает неопределённость вместо того, чтобы помогать с ней работать.
Поэтому до построения схемы участников полезно иметь схему доказательных оснований. Для каждого будущего ребра нужны описание связи, источник, место в документе, дата и статус утверждения. Контакт, деловое взаимодействие, владение и предполагаемое участие в преступлении — разные отношения; одно не следует автоматически из другого.
В выбранных сообщениях есть сведения о количестве электронных кошельков, но нет открытого набора их идентификаторов и переводов. Этого недостаточно для восстановления реального графа платежей. Рисовать цепочки переводов на основании одной численности означало бы добавлять данные, которых источник не предоставил.
Такой же контроль нужен названиям. Совпадение слова «Химпром» не устанавливает тождество разных организаций, а сходство имени не доказывает, что две записи относятся к одному человеку. В этом прототипе автоматического объединения людей и организаций нет.
Для тематического архива эти различия должны быть видны и в карточках, и в тексте. Контекст работы проекта с источниками описан в его редакционной политике; конкретное утверждение всё равно требует собственного основания. Общие правила не заменяют документ по отдельному эпизоду.
Практический результат небольшого скрипта скромный: он помогает не потерять происхождение, состав и дату показателя между чтением новости и заполнением базы. Но именно на этом переходе может возникнуть лишнее «подтверждение», исчезнуть важное уточнение или появиться связь, которую ни один источник не устанавливал.
Хороший граф позволяет ответить не только на вопрос «что с чем связано», но и на вопрос «откуда мы это взяли».
Материалы источников проверены 8 октября 2026 года. Пример относится к публикациям 2022 года. Текст и прототип подготовлены с использованием ИИ; чтение и структурирование источников, а также запуск кода выполнены в ходе подготовки. Выводы ограничены приведёнными публикациями и не заменяют изучение материалов дела.

