From 7ee55057e03f8f430be4008eb329be3f088802d8 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 1 Aug 2026 14:20:54 +0300 Subject: [PATCH] =?UTF-8?q?local-research:=20=D1=80=D0=B0=D0=B7=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=BD=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20Apple=20?= =?UTF-8?q?=D0=B7=D0=B0=205.5=20=D0=BB=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - девять экспортов, версии 11→13→14; ни один тип записи не исчез, только добавлялись — разбор можно писать аддитивно - Correlation (давление) появился только в 2026 и ломает парсер по одним Record; имя файла локализовано, DTD расходится с данными - коды HealthKit переименовываются: те же записи сна приезжают как Asleep из экспорта 2021 и как AsleepUnspecified из свежего --- docs/backlog/import-eksporta-apple.md | 36 ++++-- docs/backlog/proverka-novyh-sekcij.md | 16 ++- .../backlog/slovar-kategorialnyh-znachenij.md | 18 +++ docs/local-research.md | 109 ++++++++++++++++++ 4 files changed, 169 insertions(+), 10 deletions(-) diff --git a/docs/backlog/import-eksporta-apple.md b/docs/backlog/import-eksporta-apple.md index 1b52e89..6272683 100644 --- a/docs/backlog/import-eksporta-apple.md +++ b/docs/backlog/import-eksporta-apple.md @@ -9,14 +9,36 @@ Это же основание для устаревания нижнего слоя и единственный способ поднять историю глубже недели: дыра старше недели проходами синхронизации не чинится. +Формат разобран на девяти экспортах за 5.5 лет (находки 42–45), гадать не +придётся: + +- **версии 11 → 13 → 14**, на 14 стоит больше года; за всё время **ни один тип + записи не исчез**, только добавлялись. Значит незнакомый тип — новый тип, а + не сломанный парсер: падать на нём нельзя; +- **`Correlation`** — обёртка из двух записей, ею приезжает давление. Появилась + только в 2026 году. Парсер по одним `` разберёт давление как две + несвязанные метрики и потеряет их парность; +- **`WorkoutStatistics`** внутри тренировки — с 2024 года; +- **имя файла локализовано**: `экспорт.xml`, не `export.xml` — так во всех + девяти архивах; +- **DTD расходится с данными** (в v11 у `` на атрибут больше объявленного) + — валидировать документ его же DTD нельзя; +- объём: 1.6 ГБ XML и 3.6 млн записей в свежем экспорте — только потоковый + разбор, документ целиком в память не влезет. + Шаги: -- разбор `экспорт.xml` (HealthKit Export Version 14, `` с - `startDate`/`endDate`/`value`/`sourceName`/`device`) в слой `sample`; -- маршруты GPX и ЭКГ отдельными CSV — они не в XML; -- заливка кусками по годам: файл измеряется сотнями мегабайт. +- потоковый разбор `экспорт.xml` в слой `sample`, включая `Correlation`; +- `HeartRateVariabilityMetadataList` с `InstantaneousBeatsPerMinute` — это + тот же `heartbeatSeries`, что в HAE (находка 39), 1.25 млн ударов; +- маршруты GPX и ЭКГ отдельными файлами — их в XML нет; +- заливка кусками по годам, идемпотентно: повторный импорт того же архива не + должен ничего менять; +- `export_cda.xml` игнорируем — это клинический формат тех же данных. -Готово, когда история за несколько лет лежит в слое `sample`, а суммы по нему -сходятся с часовым слоем HAE на пересечении периодов. +Готово, когда история за несколько лет лежит в слое `sample`, повторный импорт +не меняет ничего, а суммы по слою сходятся с часовым слоем HAE на пересечении +периодов. -Есть готовый файл для проверки: `/home/av/MediaEverything/HealthData/apple_health/`. +Архивы: `/home/av/MediaEverything/HealthData/apple_health/` — девять штук, +2021-12 … 2026-08. Старые версии формата годятся как регрессионный набор. diff --git a/docs/backlog/proverka-novyh-sekcij.md b/docs/backlog/proverka-novyh-sekcij.md index 92f40d3..22b8eaa 100644 --- a/docs/backlog/proverka-novyh-sekcij.md +++ b/docs/backlog/proverka-novyh-sekcij.md @@ -11,9 +11,19 @@ появятся сами. Задача — не пропустить момент: убедиться, что новые секции разбираются, а не молча падают в `parse_status`. -Отдельный вопрос, на который ответят эти же данные: есть ли эти секции в родном -экспорте Apple. Если нет — экспорт им не источник истины, и устаревание -нижнего слоя к ним неприменимо, держим всегда. +Часть вопроса закрыта разбором экспортов (находка 42): в Health эти данные +**есть** и в экспорте они присутствуют — `BodyMass` (1127 записей), +`BloodPressureSystolic`/`Diastolic` (по 18), `BodyTemperature` (11), `Headache` +(36), `SexualActivity` (46), `Dietary*` (по 88). Значит вопрос не «есть ли +данные», а «доедут ли они через HAE и в какой форме». + +Остаётся непроверенным `stateOfMind`: в экспорте его нет ни одним типом. Если +подтвердится, что Apple его не выгружает, то экспорт ему не источник истины — +устаревание нижнего слоя к нему неприменимо, держим всегда. + +Давление приезжает обёрткой `Correlation` из двух записей (находка 44) — в +экспорте точно, а вот как его отдаёт HAE, неизвестно. Это первое, на что +смотреть, когда данные появятся. Готово, когда каждая новая секция либо разобрана, либо явно описана в `docs/local-research.md` как не пришедшая, и ни одна не числится в ошибках diff --git a/docs/backlog/slovar-kategorialnyh-znachenij.md b/docs/backlog/slovar-kategorialnyh-znachenij.md index c61dae6..9bbdd16 100644 --- a/docs/backlog/slovar-kategorialnyh-znachenij.md +++ b/docs/backlog/slovar-kategorialnyh-znachenij.md @@ -15,6 +15,24 @@ HAE отдаёт перечислимые значения строками ло код. Словарь ключуется парой `(локаль, строка)`, локаль берётся из `Accept-Language`. Незнакомая строка → пустой код, а не догадка. +**Словарь фаз сна уже выведен** сопоставлением потока с экспортом за тот же +период (находка 43) — составлять руками не нужно: + +``` +Основная → AsleepCore Бодрствование → Awake БДГ → AsleepREM +Глубокий → AsleepDeep В кровати → InBed Во сне → AsleepUnspecified +``` + +Тем же способом добираются `heart_rate.context` и типы тренировок. + +Осложнение, всплывшее на истории экспортов: **коды тоже не вечны.** Одни и те +же записи сна приезжают как `…Asleep` в экспорте 2021 года и как +`…AsleepUnspecified` в экспорте 2026-го: Apple переименовала значение и +переписывает историю при выгрузке (находка 43). Значит словарь должен +переживать переименование самих кодов, иначе после обновления iOS история +расколется вторично — уже на «стабильной» стороне. Простейшее решение: хранить код как есть, а +эквивалентность старых и новых имён держать отдельной таблицей синонимов. + Готово, когда фазы сна из потока и из экспорта Apple сравниваются напрямую, а `/stats` показывает строки, для которых кода ещё нет. diff --git a/docs/local-research.md b/docs/local-research.md index 4904e0c..619b9e9 100644 --- a/docs/local-research.md +++ b/docs/local-research.md @@ -1318,6 +1318,115 @@ RFC3339 Z 20 data.stateOfMind[].end = 2026-07-31T18:03:51 более полная точка, а не последняя пришедшая, — иначе бедная доставка стирает `start`/`end` у богатой. +## 42. Формат экспорта за 5.5 лет: типы только добавляются + +Девять экспортов из `~/MediaEverything/HealthData/apple_health`, с декабря +2021 по август 2026. Версия формата растёт медленно и давно стоит на месте: + +``` +2021-12 Export Version 11 +2024-06 Export Version 13 +2025-06 Export Version 14 +… 14 ← пять экспортов подряд, больше года без изменений +2026-08 Export Version 14 +``` + +Раскладка архива одинакова во всех девяти: `экспорт.xml`, `export_cda.xml` +(клинический формат, нам не нужен), `workout-routes/` с GPX, `electrocardiograms/` +с CSV. Объём вырос вчетверо — 369 МБ XML и 944 887 записей в 2021 против +1610 МБ и 3 616 171 записи в 2026. + +**Главное для импорта: ни один тип не исчез.** Сверка четырёх экспортов +(v11, v13, v14 первый, v14 последний) по всем типам записей не нашла ни одного +случая пропажи — только появление новых: + +| появился | типы | +|---|---| +| к 2024 (v13) | `PhysicalEffort`, `TimeInDaylight`, `DistanceCycling`, `HeartRateRecoveryOneMinute`, `AudioExposureEvent`, `LowCardioFitnessEvent` | +| к 2025 (v14) | `AppleSleepingWristTemperature`, `BodyTemperature`, `SexualActivity` | +| к 2026 | `DietaryFiber`/`FatTotal`/`Protein`/`Carbohydrates`, `BloodPressureSystolic`/`Diastolic`, `HighHeartRateEvent` | + +Значит разбор экспорта можно писать «аддитивно»: незнакомый тип — это новый +тип, а не сломанный парсер, и падать на нём нельзя. + +Появлялись и **структурные** элементы, а это уже опаснее: +`WorkoutStatistics` внутри тренировки (с 2024) и `Correlation` (см. находку 44). + +## 43. Коды HealthKit не вечны — Apple переписывает историю при экспорте + +Те же самые записи сна, экспортированные с разницей в пять лет, несут **разные +коды**: + +``` +экспорт 2021-12 338 × HKCategoryValueSleepAnalysisAsleep +экспорт 2026-08 338 × HKCategoryValueSleepAnalysisAsleepUnspecified + 62 × HKCategoryValueSleepAnalysisInBed ← в обоих одинаково +``` + +Совпадение счётчиков до единицы означает, что это одни и те же исторические +записи: `Asleep` переименован в `AsleepUnspecified`, и старые данные при +экспорте переписываются новым именем. + +**Следствие:** код HealthKit устойчивее локализованной строки, но не абсолютен. +Словарь категориальных значений обязан переживать переименование самих кодов — +иначе после очередного обновления iOS история расколется вторично, теперь уже +на «стабильной» стороне. + +### Словарь фаз сна выводится из данных + +В свежих записях экспорта фазы полные, и они однозначно ложатся на локализованные +строки HAE из находки 37: + +``` +Основная 692 → HKCategoryValueSleepAnalysisAsleepCore +Бодрствование 568 → HKCategoryValueSleepAnalysisAwake +БДГ 206 → HKCategoryValueSleepAnalysisAsleepREM +Во сне 171 → HKCategoryValueSleepAnalysisAsleepUnspecified +Глубокий 94 → HKCategoryValueSleepAnalysisAsleepDeep +В кровати 38 → HKCategoryValueSleepAnalysisInBed +``` + +То есть первую и главную часть словаря не надо составлять вручную — она +выводится сопоставлением потока с экспортом за тот же период. + +## 44. `Correlation` — структурный элемент, и он появился только что + +Давление приезжает не записью, а обёрткой из двух записей: + +```xml + + + + +``` + +Арифметика сходится: 9 элементов `Correlation` и по 18 записей систолического и +диастолического давления — ровно две записи на обёртку. + +`Correlation` объявлен в DTD наравне с `Record` и `Workout`, но в данных до +2026 года не встречался ни разу. Парсер, написанный по одним лишь `Record`, +давление разберёт как две несвязанные метрики и потеряет то, что делает его +измерением — их **парность**. + +## 45. DTD экспорта врёт, а имя файла локализовано + +Две мелочи, каждая из которых ломает разбор на ровном месте. + +**DTD расходится с данными.** В экспорте 2021 года (v11) `` +объявляет четыре атрибута, а сам элемент `` несёт пять — лишний +`HKCharacteristicTypeIdentifierCardioFitnessMedicationsUse`. Валидировать +документ по его собственному DTD нельзя; разбираем то, что есть. + +**Имя файла переведено.** Внутри архива лежит `экспорт.xml`, а не `export.xml` +— и так во всех девяти архивах начиная с 2021 года. Имя зависит от языка +телефона (`locale="ru_RU"` в корневом элементе). Захардкоженное `export.xml` +не найдёт ничего. + +Там же, в ``, значение локализовано: `CardioFitnessMedicationsUse="Нет"`. +То есть правило «экспорт говорит кодами» верно для типов записей и категориальных +значений, но не для всего документа. + ## Инструмент Разбор ведётся скриптом `tmp/research/hl.py` (Python 3, только стандартная