当院の診療も明日大晦日のの発熱外来を残すのみとなりました。年末の空き時間を利用して「大掃除」を行いました。 といっても、窓拭きや床磨きではなく、診療システムの「デジタル大掃除(プログラムの改修)」です。当院では、診療をスムーズに行い、患者様をお待たせしないために「Text Blaze」という業務効率化ツールを導入しています。 これは、複雑なカルテ入力や書類作成を、あらかじめ作ったプログラム(スニペット)で一瞬で完了させる便利なツールです。当院のブログなどで紹介したこともあり他施設でも使用される先生方が少しづつ増えてきています。「Text Blaze」の運用において、電子カルテの仕様変更に強い堅牢なシステムを構築するための「共通コンポーネント化」を導入しましたので紹介します。
課題:クラウド型電子カルテの「CSSセレクタ変更」問題
当院ではText Blazeを活用し、療養計画書やカルテ作成を半自動化しています。しかし、クラウド型電子カルテ(SaaS)を利用していると避けて通れないのが、「UIアップデートによるDOM構造の変更」です。主にカルテ動作の高速化を目的に電子カルテメーカーはCSSセレクタを変更することがあります。当院で使用しているクリニクスという電子カルテでは入力フィールでのデータ取得は以前は
{chart={site: text; selector=.css-8u2jis; page=https://karte.medley.life/*; select=ifneeded}}
で読み込めていたのですが、12月からの改修で
{chart={site: text; selector=.css-gbblo5; page=https://karte.medley.life/*; select=ifneeded}}(
とCSSセレクタのアドレスが変更になっています。
これまでは各スニペット(計画書A、計画書B、紹介状C…)の中にスクレイピング処理を直接記述(ハードコーディング)していました。そのため、カルテ側のクラス名が一つ変わるたびに、数十個ある全てのスニペットを開き、正規表現やセレクタを修正するという、非生産的な作業が発生していました。
解決策:import コマンドによる共通ライブラリ化
この保守コストを削減するために、プログラミングにおけるDRY原則(Don’t Repeat Yourself)を適用しました。具体的には、「電子カルテから情報を読み取る機能」だけを切り出した専用のスニペット(当院ではコマンド名を /imf と命名)を作成し、各帳票スニペットからはそれを import する構成に変更しました。
1. 共通コンポーネント(/imf)の実装
このスニペットは出力(ペースト)を行わず、データの取得と変数の格納のみを担当させます。 以下は /imf のコードです。このコードで電子カルテ情報(年齢、氏名、性別、主病、検査データ)を読み取ります。このコードを使いまわすことにより他のコードをシンプルにします。
{note: preview=no}{chart={site: text; selector=.css-gbblo5; page=https://karte.medley.life/*}}{clinic_name={site: text; page=https://karte.medley.life/*; selector=.header-logo-name}}{patient_name={site: text; page=https://karte.medley.life/*; selector=.css-1npeww9}}{doctor_name={site: text; page=https://karte.medley.life/*; selector=[data-testid="consultation-staff-name"]}}{birthday_age={site: text; page=https://karte.medley.life/*; selector=[data-testid="patient-birthdate"]}}
{run: age=extractregex({site: text; selector=[data-testid="patient-birthdate"]; page=https://karte.medley.life/*},"(..)歳")
sei=extractregex({site: text; selector=.names-age},"(.)性")
sex={=sei}&"性"
`高血圧`= testregex(chart,"(#|#|♯)\s*高血圧|HT|若年性高血圧")
`脂質異常症`=testregex(chart,"(#|#|♯)\\s*(高コレステロール血症|脂質異常|脂質代謝異常|高トリグリセライド血症|高TG血症|家族性高コレステロール血症|高脂血症)")
`糖尿病`=testregex(chart,"(#|#|♯)\\s*(糖尿病|1型糖尿病|2型糖尿病|境界型糖尿病|DM|Diabetes)")
`冠動脈疾患`=testregex(chart,"(#|#|♯)\\s*(冠動脈疾患|狭心症|心筋梗塞|冠攣縮性狭心症|冠動脈狭窄|(不安定)?\\s*狭心症|(陳旧性)?\\s*心筋梗塞|冠動脈硬化)")
`脳血管疾患`=testregex(chart, "(#|#|♯)\\s*(脳血管疾患|脳梗塞|一過性脳虚血発作|脳卒中|陳旧性脳梗塞|脳梗塞後遺症)")
`慢性腎臓病`=testregex(chart, "(#|#|♯)\\s*(慢性腎臓病|CKD|慢性腎不全|腎機能低下|糖尿病性腎症|慢性腎障害)")
`末梢血管疾患`=testregex(chart, "(#|#|♯)\\s*(末梢血管疾患|ASO|閉塞性動脈硬化症|重症下肢虚血|下肢閉塞性動脈硬化症|バージャ病)")
`虚血性心疾患/脳梗塞`=testregex(chart,"(#|#|♯)\\s*(冠動脈疾患|狭心症|心筋梗塞|冠攣縮性狭心症|冠動脈狭窄|(不安定)?\\s*狭心症|(陳旧性)?\\s*心筋梗塞|冠動脈硬化|脳血管疾患|脳梗塞|一過性脳虚血発作|脳卒中|陳旧性脳梗塞|脳梗塞後遺症)")
`喫煙`= testregex(chart,"(#|#|♯)\s*喫煙|タバコ|禁煙できない")
`飲酒`= testregex(chart,"(#|#|♯)\s*飲酒|お酒|アルコール")
`height`=extractregex(chart, "身長\\D{0,8}([0-9]{1,5}(?:\\.[0-9]{1,2})?)", "i")
`weight`=extractregex(chart, "体重\\D{0,8}([0-9]{1,5}(?:\\.[0-9]{1,2})?)", "i")}
{sei=extractregex({site: text; selector=[data-testid="patient-sex"]; page=https://karte.medley.life/*},"(.)性")}{sex={=sei}&"性"}
{run: height=catch(extractregex({=chart},"身長:\s*([\d.]+)\s*cm"), "")
weight=catch(extractregex({=chart},"体重:\s*([\d.]+)\s*kg"), "")
wbc=catch(extractregex({=chart},"白血球数\s*[HL]?\s*([\d.]+)\s*/MCL"), "")
rbc=catch(extractregex({=chart},"赤血球数\s*[HL]?\s*([\d.]+)\s*X10000/MCL"), "")
hgb=catch(extractregex({=chart},"血色素量\s*[HL]?\s*([\d.]+)\s*G/DL"), "")
hct=catch(extractregex({=chart},"ヘマトクリット\s*[HL]?\s*([\d.]+)\s*%"), "")
mcv=catch(extractregex({=chart},"MCV\s*[HL]?\s*([\d.]+)\s*FL"), "")
mch=catch(extractregex({=chart},"MCH\s*[HL]?\s*([\d.]+)\s*PG"), "")
mchc=catch(extractregex({=chart},"MCHC\s*[HL]?\s*([\d.]+)\s*%"), "")
plt=catch(extractregex({=chart},"血小板数\s*[HL]?\s*([\d.]+)\s*X10000/MCL"), "")
ast=catch(extractregex({=chart},"AST\(GOT\)\s*[HL]?\s*([\d.]+)\s*U/L"), "")
alt=catch(extractregex({=chart},"ALT\(GPT\)\s*[HL]?\s*([\d.]+)\s*U/L"), "")
ggtp=catch(extractregex({=chart},"γ-GT\s*[HL]?\s*([\d.]+)\s*U/L"), "")
ck=catch(extractregex({=chart},"CK\(CPK\)\s*[HL]?\s*([\d.]+)\s*U/L"), "")
amylase=catch(extractregex({=chart},"アミラーゼ\s*[HL]?\s*([\d.]+)\s*U/L"), "")
bilirubin=catch(extractregex({=chart},"総ビリルビン\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
tp=catch(extractregex({=chart},"総蛋白\s*[HL]?\s*([\d.]+)\s*G/DL"), "")
alb=catch(extractregex({=chart},"アルブミン\s*[HL]?\s*([\d.]+)\s*G/DL"), "")
ag_ratio=catch(extractregex({=chart},"A/G比\s*[HL]?\s*([\d.]+)"), "")
tg=catch(extractregex({=chart},"中性脂肪\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
hdl=catch(extractregex({=chart},"HDLコレステロール\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
bun=catch(extractregex({=chart},"尿素窒素\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
ua=catch(extractregex({=chart},"尿酸\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
cre=catch(extractregex({=chart},"クレアチニン\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
egfr=catch(extractregex({=chart},"eGFRcreat\s*[HL]?\s*([\d.]+)\s*ML/MIN/1.73"), "")
na=catch(extractregex({=chart},"ナトリウム\s*[HL]?\s*([\d.]+)\s*MEQ/L"), "")
k=catch(extractregex({=chart},"カリウム\s*[HL]?\s*([\d.]+)\s*MEQ/L"), "")
cl=catch(extractregex({=chart},"クロール\s*[HL]?\s*([\d.]+)\s*MEQ/L"), "")
glucose=catch(extractregex({=chart},"血糖\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
hba1c=catch(extractregex({=chart},"HbA1c\s*NGSP\s*[HL]?\s*([\d.]+)\s*%"), "")
ldl=catch(extractregex({=chart},"LDLコレステロール\s*[HL]?\s*([\d.]+)\s*MG/DL"), "")
}{endnote}
{bp_measure=contains({=chart}, "mmHg")}{if: bp_measure}{p=extractregex({=chart},"\d\d+\/\d\d+")}{pressure=splitregex({=p}, "\/") }{sbp=pressure[1]}{dbp=pressure[2]}{ps=splitregex({=extractregex({=chart},"\d\d+\/\d\d+")}, "\/") }{endif}
2. 各帳票スニペット側(呼び出し側)の実装
利用する側のスニペットでは、冒頭で 下記のように/imf をインポートするだけです。電子カルテの使用変更の際もすべてのスニペットを書き直すのではなく/imfのみを修正することですべてのスニペットが作動するようになります。 また、これまでの正規表現やセレクタの記述が一切不要になり、ビジネスロジック(表示内容の制御)に集中できるようになりスニペットの可読性も向上しました。
{import: /imf}
***それまでコードを記載
図解すると下記のようになります。

実装の成果
この改修により、電子カルテのCSSセレクタに変更があった場合でも、共通コンポーネントである /imf ひとつを修正するだけで、全ての帳票スニペットが正常に動作するようになりました。
いわゆる「ページオブジェクトモデル(Page Object Model)」に近い設計です。 View(帳票の見た目)と Logic/Data Access(カルテ情報の取得)を分離することで、メンテナンス性が劇的に向上しました。
まとめ
医療現場における自作ツールの運用は、「作った人しか直せない(属人化)」や「メンテナンスの手間が業務を圧迫する」という問題に直面しがちです。
年末の空き時間を活用してコードをモジュール化・構造化しておくことは、来年の診療業務を円滑にするための非常にリターンの大きい改修でした。

コメント