local-research: разобран формат экспорта Apple за 5.5 лет
- девять экспортов, версии 11→13→14; ни один тип записи не исчез, только добавлялись — разбор можно писать аддитивно - Correlation (давление) появился только в 2026 и ломает парсер по одним Record; имя файла локализовано, DTD расходится с данными - коды HealthKit переименовываются: те же записи сна приезжают как Asleep из экспорта 2021 и как AsleepUnspecified из свежего
This commit is contained in:
@@ -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
|
||||
<Correlation type="HKCorrelationTypeIdentifierBloodPressure" sourceName="PICOOC"
|
||||
startDate="2026-01-01 09:41:49 +0300" endDate="2026-01-01 09:41:49 +0300">
|
||||
<Record type="HKQuantityTypeIdentifierBloodPressureSystolic" … />
|
||||
<Record type="HKQuantityTypeIdentifierBloodPressureDiastolic" … />
|
||||
</Correlation>
|
||||
```
|
||||
|
||||
Арифметика сходится: 9 элементов `Correlation` и по 18 записей систолического и
|
||||
диастолического давления — ровно две записи на обёртку.
|
||||
|
||||
`Correlation` объявлен в DTD наравне с `Record` и `Workout`, но в данных до
|
||||
2026 года не встречался ни разу. Парсер, написанный по одним лишь `Record`,
|
||||
давление разберёт как две несвязанные метрики и потеряет то, что делает его
|
||||
измерением — их **парность**.
|
||||
|
||||
## 45. DTD экспорта врёт, а имя файла локализовано
|
||||
|
||||
Две мелочи, каждая из которых ломает разбор на ровном месте.
|
||||
|
||||
**DTD расходится с данными.** В экспорте 2021 года (v11) `<!ATTLIST Me>`
|
||||
объявляет четыре атрибута, а сам элемент `<Me>` несёт пять — лишний
|
||||
`HKCharacteristicTypeIdentifierCardioFitnessMedicationsUse`. Валидировать
|
||||
документ по его собственному DTD нельзя; разбираем то, что есть.
|
||||
|
||||
**Имя файла переведено.** Внутри архива лежит `экспорт.xml`, а не `export.xml`
|
||||
— и так во всех девяти архивах начиная с 2021 года. Имя зависит от языка
|
||||
телефона (`locale="ru_RU"` в корневом элементе). Захардкоженное `export.xml`
|
||||
не найдёт ничего.
|
||||
|
||||
Там же, в `<Me>`, значение локализовано: `CardioFitnessMedicationsUse="Нет"`.
|
||||
То есть правило «экспорт говорит кодами» верно для типов записей и категориальных
|
||||
значений, но не для всего документа.
|
||||
|
||||
## Инструмент
|
||||
|
||||
Разбор ведётся скриптом `tmp/research/hl.py` (Python 3, только стандартная
|
||||
|
||||
Reference in New Issue
Block a user