メインコンテンツへスキップ

Appleのヘルスケアデータから体重を抜き出してCSVファイルを作成する

Appleのヘルスケアアプリのデータを使って、他のアプリでグラフを描こうと思ったが、エクスポートされるデータはXMLのため、そのままでは扱いにくい。

このため、XMLファイルから体重データを抜き出してCSVを出力するRubyスクリプトを書いた。

require 'nokogiri'
require 'date'

reader = Nokogiri::XML::Reader File.read('export.xml')
data = []

reader.each do |node|
  if node.name == 'Record' && node.attributes['type'] == 'HKQuantityTypeIdentifierBodyMass'
    d = Date.parse(node.attributes['startDate']).strftime("%Y-%m-%d")
    data << [d, node.attributes['value']]
  end   
end
data.sort.each {|d|
  puts d.join(',')
}

=begin
<Record type="HKQuantityTypeIdentifierBodyMass" sourceName="1byone Health" sourceVersion="63" device="&lt;&lt;HKDevice: 0x3013e9b30&gt;, name:iPhone, manufacturer:Apple, model:iPhone, hardware:iPhone, firmware:iPhone, software:17.5.1, localIdentifier:ja_JP, UDIDeviceIdentifier:ja_JP, creation date:2024-06-02 22:17:36 +0000&gt;" unit="kg" creationDate="2024-06-17 07:17:17 +0900" startDate="2024-06-17 07:17:17 +0900" endDate="2024-06-17 07:17:17 +0900" value="71.9"/>
=end

以下、ChatGPTにヘルスケアデータの構造を聞いた回答

Appleのヘルスケアアプリでエクスポートされるデータは、XML (Extensible Markup Language) フォーマットで提供されます。このフォーマットは、ヘルスデータを構造化して格納するための標準的な形式です。

データの構造と内容
#

  1. XMLヘッダー エクスポートされたファイルは、XMLヘッダーから始まります。

    <?xml version="1.0" encoding="UTF-8"?>
  2. 全体のルート要素 データ全体は <HealthData> というルート要素でラップされています。

    <HealthData locale="en_US">
        ...
    </HealthData>
  3. データの各エントリー ヘルスケアのデータは、それぞれの記録を <Record> 要素として格納します。主な属性は以下の通りです:

    • type: データの種類(例: 歩数、心拍数など)。
    • sourceName: データのソース名(iPhone、Apple Watchなど)。
    • sourceVersion: ソースのバージョン情報。
    • unit: データの単位(例: count, bpm, kcal など)。
    • creationDate: データが作成された日時。
    • startDate: データの開始日時(測定期間の開始)。
    • endDate: データの終了日時(測定期間の終了)。
    • value: データの値(例: 歩数の数値、体重の値など)。

    例:

    <Record type="HKQuantityTypeIdentifierStepCount" 
            sourceName="iPhone" 
            sourceVersion="16.2" 
            unit="count" 
            creationDate="2024-12-10 07:30:00 +0000" 
            startDate="2024-12-10 07:00:00 +0000" 
            endDate="2024-12-10 07:05:00 +0000" 
            value="1200"/>
  4. ワークアウトデータ ワークアウトデータは <Workout> 要素で表されます。これには以下の属性が含まれます:

    • workoutActivityType: ワークアウトの種類(例: ランニング、サイクリングなど)。
    • duration: ワークアウトの継続時間。
    • totalDistance: 総移動距離。
    • totalEnergyBurned: 総消費カロリー。
    • startDate: ワークアウト開始日時。
    • endDate: ワークアウト終了日時。

    例:

    <Workout workoutActivityType="HKWorkoutActivityTypeRunning" 
             duration="1800" 
             totalDistance="5.0" 
             totalEnergyBurned="300" 
             startDate="2024-12-10 06:30:00 +0000" 
             endDate="2024-12-10 07:00:00 +0000" />
  5. メタデータ 各エントリーには <MetadataEntry> 要素が付属している場合があります。この要素には、追加情報がキー・バリュー形式で格納されます。

    例: 1

    <MetadataEntry key="HKMetadataKeyHeartRateMotionContext" value="1"/>
  6. ファイルの最後 エクスポートされたデータの最後には、ヘルスデータの概要を示す <ExportDate> 要素が含まれます。

    <ExportDate value="2024-12-11 08:00:00 +0000"/>

主なデータ種類と識別子
#

  • 歩数: HKQuantityTypeIdentifierStepCount
  • 心拍数: HKQuantityTypeIdentifierHeartRate
  • 体重: HKQuantityTypeIdentifierBodyMass
  • 睡眠分析: HKCategoryTypeIdentifierSleepAnalysis

使用例と注意点
#

  • データ解析: このXMLファイルを解析するには、プログラミング言語(例: Python, JavaScript, R)や専用ツールを使用します。
  • プライバシー: エクスポートされたデータには個人情報が含まれるため、取り扱いには注意が必要です。
  • データ量: データが大量になる場合があるので、解析時にはパフォーマンスを考慮してください。

これがAppleのヘルスケアアプリでエクスポートされるデータのフォーマットの概要です。

関連記事

〰️ USB Type-Cの複雑さ

iPhone 15でLightningからUSB Type-Cへ変更される記念?で、なんとなくしか理解していないUSB Type-Cの仕様についてまとめてみた。 USB Type-Cとは # USB Type-Cは、物理的なコネクタの形状を指すものである。その背後にあるデータ転送や電力供給の仕様は、USB Type-Cの形状だけでは特定できない。 USBの仕様 # 仕様 データ転送速度 注意点 USB 2.0 最大480 Mbps 新しいデバイスとの互換性が必要な場合には注意が必要 USB 3.2 Gen 1 最大5 Gbps USB 3.2 Gen 2 最大10 Gbps USB 3.2 Gen 2x2 最大20 Gbps USB4 最大40 Gbps Thunderbolt 3との互換性があるが、すべてのUSB4デバイスやケーブルがThunderbolt 3と互換性があるわけではない USB Power Delivery (USB PD) # USB Type-Cコネクタを使用するデバイス間での高出力の電力供給を可能にするもので最大100W (20V@5A)の電力供給が可能。

Webページから本文らしき部分を抽出する

さまざまなパターンの日本語文章をデータとして欲しいケースがあったので、指定したURLから本文らしき内容を抽出するスクリプトを書いた。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 require 'playwright' require 'readability' require 'html2text' # 指定したURLから本文らしき内容を抽出して返却する def html2text(url) Playwright.create(playwright_cli_executable_path: 'npx playwright') do |playwright| playwright.chromium.launch(headless: true) do |browser| begin page = browser.new_page page.goto(url, waitUntil: 'load') doc = Readability::Document.new(page.content) sleep 1 return {:title => page.title, :content => Html2Text.convert(doc.content) } rescue return {:title => nil, :content => nil } end end end end url = ARGV.shift doc = html2text(url) puts "#{doc[:title]}\n#{doc[:content]}" Javascriptでコンテンツを生成するページに対応するためPlaywrightを使用。 使用したライブラリ # cantino/ruby-readability: Port of arc90’s readability project to Ruby YusukeIwaki/playwright-ruby-client: Playwright client for Ruby soundasleep/html2text_ruby: A Ruby component to convert HTML into a plain text format.

Textwell

Textwell - The Modeless Textbox for iPhone, iPad, iPod touch, Mac, and Watch. メモ、メッセージ、ブログ、検索、投稿など、あらゆる文章作成タスクに使用できる多目的テキストエディタ ファイリングやスタイリングのための機能はないが、JavaScriptベースのカスタマイズ可能なアクション、自動履歴、クラウド同期などをサポートしており、シンプルで拡張性が高い Mac版、iOS版、AppleWatch版がある Textwell | URL Schemes 自作のアクションなど # ソートして重複行を削除するTextwellのアクション Obsidianのデイリーノートへ追記するTextwellのアクション Amazonから書誌情報をTextwellへ取り込むブックマークレット

crisp-game-lib

crisp-game-libはブラウザゲームを開発するための軽量なゲームライブラリ。JavaScriptでゲームを開発し簡単に公開することができる abagames/crisp-game-lib: Minimal JavaScript library for creating classic arcade-like mini-games running in the browser

ソートして重複行を削除するTextwellのアクション

ファイルの内容を昇順にソートして重複した行を1行にするTextwellのアクションを書いた。 いわゆる$ sort foobar | uniqするアクションです。 https://gist.github.com/htakeuchi/fd9e36227ad1688b31e9b84eafbf17a9 const { text, range } = T; const selectionStart = range.len > 0 ? range.loc : 0; const selectionEnd = range.len > 0 ? selectionStart + range.len : text.length; const lines = text.split('\n'); let pointerStart = 0; let replacingRangeLoc = 0; const hitLines = []; for (const line of lines) { const pointerEnd = pointerStart + line.length; if (pointerStart > selectionEnd) break; if ( (pointerStart <= selectionStart && selectionStart <= pointerEnd) || (pointerStart <= selectionEnd && selectionEnd <= pointerEnd) || (selectionStart < pointerStart && pointerEnd < selectionEnd) ) { if (hitLines.length === 0) replacingRangeLoc = pointerStart; hitLines.push(line); } pointerStart = pointerEnd + 1; // 1 means a line break. } const blankLines = []; const numLines = []; const strLines = []; hitLines.forEach((content) => { const intContent = parseInt(content); if (!content.match(/\S/)) { blankLines.push(content); // Ignore blank line } else if (isNaN(intContent)) { strLines.push(content); } else { numLines.push({ num: intContent, str: content }); } }); numLines.sort((a, b) => a.num - b.num); const sortedNumLines = numLines.map(({ str }) => str); strLines.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); const sortedLines = [...blankLines, ...sortedNumLines, ...strLines]; // Remove duplicates const uniqueLines = [...new Set(sortedLines)]; // Join lines into text const replacingText = uniqueLines.join('\n'); T('replaceRange', { text: replacingText, replacingRange: { loc: replacingRangeLoc, len: selectionEnd - replacingRangeLoc }, selectingRange: { loc: replacingRangeLoc + replacingText.length, len: 0 }, });

Amazonの商品紹介Markdownをつくるブックマークレット

このサイトはObsidianで管理しているノートをQuartzで公開しており、全てのテキストはMarkdownで書いている。 このため、Amazonの商品紹介をするのがなかなか面倒であり、これを改善するためのブックマークレットを書いた。 使い方 # Amazonの商品ページに行ってこのブックマークレットを起動すると、以下のようなMarkdownが生成され、クリップボードへコピーされる。 ![[オン] ランニングシューズ Cloudmonster メンズ](https://a.media-amazon.com/images/I/51lG1xvL7nL._AC_SY200_.jpg) [[オン] ランニングシューズ Cloudmonster メンズ](https://www.amazon.co.jp/gp/product/B0CN337TNH/?tag=namaraiicom-22) 出力はこんな感じ。殺風景だけどMarkdownだけで書いていて、専用のCSSを当てていないのでやむをえない。 [オン] ランニングシューズ Cloudmonster メンズ 実装 # 実装はこんな感じ。Amazon(JP)で複数ジャンルの商品ページのHTMLを確認し、チェックした範囲では動作しているが、うまく動かないページもあると思う。 もし、動かないページをみつけたらこちらまでご連絡いただければ幸い。ブックマークレットへの変換は以下のページが便利です。 Bookmarklet スクリプト変換 (function() { function copyToClipboard(text) { navigator.clipboard.writeText(text).then(function() { alert('クリップボードにコピーされました'); }).catch(function(error) { console.error('クリップボードへのコピーに失敗しました', error); }); } function getElement(selector) { return document.querySelector(selector); } try { var size = 200; var asinElement = getElement('input#ASIN'); if (!asinElement) throw new Error('ASINが見つかりませんでした'); var asin = asinElement.value; var titleElement = getElement('span#productTitle'); if (!titleElement) throw new Error('製品名が見つかりませんでした'); var title = titleElement.textContent.trim(); var thumbnailUrl = getElement('img#landingImage')?.src || getElement('img[src*="_SY"]')?.src || getElement('input#productImageUrl')?.value || getElement('img[src*="_SX"]')?.src; if (!thumbnailUrl) { throw new Error('サムネイル画像が見つかりませんでした'); } var productUrl = 'https://www.amazon.co.jp/gp/product/' + asin + '/?tag=namaraiicom-22'; var modifiedUrl; var sizeMatch = thumbnailUrl.match(/_(_SY|_SX)(\d+)_/); if (sizeMatch && sizeMatch[2]) { modifiedUrl = thumbnailUrl.replace(/_(_SY|_SX)\d+_/, `_${sizeMatch[1]}${size}_`); } else if (thumbnailUrl.match(/_(SY|SX)(\d+)_/)) { modifiedUrl = thumbnailUrl.replace(/_(SY|SX)\d+_/, `_SY${size}_`); } else { modifiedUrl = thumbnailUrl; } var markdownContent = `![${title}](${modifiedUrl})\n\n[${title}](${productUrl})`; copyToClipboard(markdownContent); } catch (error) { console.error(error); } })();