病名抽出アプリケーション

形態素解析標準病名マスターを用いて入力テキストから病名を抽出してICDコードのリンクを貼るアプリケーションを作成した。

まず、形態素解析で作成した関数queryUniDicMeCabでテキストを形態素解析し、病名候補の複合合成語を抽出する。次に、標準病名マスターで作成したICDコードを取得するWebAPIを用いて複合合成語のICDコードを取得し、もとの形態素解析情報にICDコード表のURLとともに追加する。これをやっているのが以下のプログラムにある関数queryUniDicMeCab_ICD_encodeである。

/**
 * 形態素解析を行ってICDICDコーディング
 */
function queryUniDicMeCab_ICD_encode(text) {
  const morphemes = queryUniDicMeCab(text);
  const disease_names = [];
  const disease_indexes = [];
  for(let i = 0; i < morphemes.length; i++) {
    const morpheme = morphemes[i];
    if(morpheme['品詞'] === '名詞-複合名詞') {
      disease_indexes.push(i);
      disease_names.push(morpheme['書字形']);
    }
  }
  const disease_info = get_disease_info_by_name(disease_names);

  for(let i = 0; i < disease_indexes.length; i++) {
    const index = disease_indexes[i]; 
    const info = disease_info[i];
    if(info) {
      const morpheme = morphemes[index];
      info['url'] = 'http://www.byomei.org/scripts/search/ICD10_searchw.asp?searchstring=' + formatICD(info['icd']);
      morpheme['info'] = info;
    }
  }
  return morphemes;
}
/**
 * ICDコードを成型する(I109 -> I10.9)
 */
function formatICD(icd) {
  if(icd.length > 3) {
    return icd.substr(0, 3) + '.' + icd.substr(3);
  } else {
    return icd;
  }
}
/**
 * 病名配列からICDICDコード情報を取得する
 */
function get_disease_info_by_name(disease_names) {
  const url = 'https://script.google.com/macros/s/{id}/exec';

  const data = {
    "diseases": disease_names,
  };
  var options =
  {
    "method"  : "post",
    "contentType": "application/json",
    "payload" : JSON.stringify(data),
  };

  const response = UrlFetchApp.fetch(url, options);
  return JSON.parse(response);
}

これによって、たとえば形態素「本態性高血圧症」は次のような情報を追加される。

{
 "書字形": "本態性高血圧症",
 "発音形": "ホンタイセーコーケツアツショー",
 "語彙素読み": "ホンタイセイコウケツアツショウ",
 "語彙素": "本態性高血圧症",
 "品詞": "名詞-複合名詞",
 "活用型": "",
 "活用形": "",
 "語形": "ホンタイセーコーケツアツショー",
 "書字形基本形": "本態性高血圧症",
 "語種": "漢漢漢漢漢",
 "info": {
  "id": "20076157",
  "code": "URSQ",
  "icd": "I10",
  "name": "本態性高血圧症",
  "url": "http://www.byomei.org/scripts/search/ICD10_searchw.asp?searchstring=I10"
 }
}

作成した関数queryUniDicMeCab_ICD_encodeを使って、入力テキストから病名を抽出し、リンクを貼るアプリケーションが以下のプログラムである。

/**
 * テキストにICDICDコードの埋め込みを行う
 */
function test_queryUniDicMeCab_ICD_encode() {
  const text = '交感神経の興奮を心臓に伝えるβ1受容体を遮断し、心臓の過剰な働きを抑えることにより、降圧作用、抗狭心症作用、抗不整脈作用、抗心不全作用を示します。通常、本態性高血圧症(軽症?中等症)、狭心症、心室性期外収縮および慢性心不全(虚血性心疾患または拡張型心筋症に基づく)、頻脈性心房細動の治療に用いられます。';
  const morphemes = queryUniDicMeCab_ICD_encode(text);
  const texts = [];
  for(let morpheme of morphemes) {
    let url = null;
    if(morpheme['info']) {
      if(morpheme['info']['url']) {
        url = morpheme['info']['url'];
      }
    }
    if(url) {
      texts.push('<a href="' + url + '">');
    }
    texts.push(morpheme['書字形']);
    if(url) {
      texts.push('</a>');
    }
  }
  const html = texts.join('');
  Logger.log(html);
}

これは

交感神経の興奮を心臓に伝えるβ1受容体を遮断し、心臓の過剰な働きを抑えることにより、降圧作用、抗狭心症作用、抗不整脈作用、抗心不全作用を示します。通常、本態性高血圧症(軽症?中等症)、狭心症、心室性期外収縮および慢性心不全(虚血性心疾患または拡張型心筋症に基づく)、頻脈性心房細動の治療に用いられます。

というテキストを以下のようなハイパーリンク付きの文書に変換する。

交感神経の興奮を心臓に伝えるβ1受容体を遮断し、心臓の過剰な働きを抑えることにより、降圧作用、抗狭心症作用、抗不整脈作用、抗心不全作用を示します。通常、本態性高血圧症(軽症?中等症)、狭心症、心室性期外収縮および慢性心不全虚血性心疾患または拡張型心筋症に基づく)、頻脈性心房細動の治療に用いられます。

この結果で「心室性期外収縮」にリンクが張られていないのは、標準病名マスターには「心室期外収縮」はあっても「心室性期外収縮」という病名がないからである。また、「特発性拡張型心筋症」はあるが「拡張型心筋症」はないのでこれも抽出に失敗している。

病名抽出は、字面が標準病名マスター収載の病名に完璧に一致していないと抽出できないのが課題である。


デモシステム

下記のテキストエリアに標準病名を含む文章を入力して[送信]ボタンを押してください。



上記のでもシステムはGoogle BloggerのHTMLビューで作成した。HTMLを以下に示す。

<script crossorigin="anonymous" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<textarea cols="80" id="text" rows="10">交感神経の興奮を心臓に伝えるβ1受容体を遮断し、心臓の過剰な働きを抑えることにより、降圧作用、抗狭心症作用、抗不整脈作用、抗心不全作用を示します。通常、本態性高血圧症(軽症?中等症)、狭心症、心室性期外収縮および慢性心不全(虚血性心疾患または拡張型心筋症に基づく)、頻脈性心房細動の治療に用いられます。
</textarea>
<br />
<button id="send-button" type="button" value="送信">送信</button>
<div id="data"></div>
  
<script>
$(document).on('click', '#send-button', function(){
  const text = $("#text").val();
  get_disease_info_by_name(text);
});
  
/**
 * ICDコードの埋め込みWebサービス(GETメソッド)→動く!
 * GAS doGetはCORS対応
 * Access-Control-Allow-Origin
 */
function get_disease_info_by_name(text) {
  const url = 'https://script.google.com/macros/s/{id}/exec?text=' + encodeURIComponent(text);
  $.ajax({
    type: "GET",
    url: url,
    dataType : "json"
  }).done(function(morphemes){
    var html = embedICDtoText(morphemes);
    $("#data").html('<p>' + html + '</p>');
  }).fail(function(XMLHttpRequest, status, e){
    alert('Error:' + JSON.stringify(XMLHttpRequest));
  });
}
/**
 * 形態素から元のテキストを復元する関数
 */
function embedICDtoText(morphemes) {
  const texts = [];
  for(let morpheme of morphemes) {
    let url = null;
    if(morpheme['info']) {
      if(morpheme['info']['url']) {
        url = morpheme['info']['url'];
      }
    }
    if(url) {
      texts.push('<a target="_blank" href="' + url + '">');
    }
    texts.push(morpheme['書字形']);
    if(url) {
      texts.push('</a>');
    }
  }
  const html = texts.join('');
  return html;
}
</script>

まず、jQueryを利用するためにCDN経由で読み込んでいる。[送信]ボタンがクリックされたらテキストエリアタグに入力されたテキストを取得し、関数get_disease_info_by_nameを呼び出している。この関数はGASで作成した自作の標準病名検知付き形態素解析WebAPIをAjaxで叩いて、得られた形態素解析結果を関数embedICDtoText関数に渡してICDコードを埋め込んだHTMLを生成し、結果をdivタグに挿入する。この例でわかるようにGASで作成したWebAPIはCORS問題に対応している(ただし、CORSに対応しているのはdoGetのみで、doPostで作成したWebAPIはCORSによってリクエストが拒絶される)。

なお、WebAPIに渡すテキストはurlエンコード(パーセントエンコード)する必要がある。ただし、なぜかBloggerのHTMLビューで作成するscriptタグのencodeURIComponent関数は句読点(、や。)などを正しくエンコードしない。にもかかわらずこのプログラムがうまくいくのはテキストエリアから読み込んだテキストにおいて、句読点や他の特殊文字が「適切に」エスケープされているようで、encodeURIComponent関数の動作に影響されないからである。


病名検索WebAPI

病名を入力として、その解説サイトのURLを検索するWebAPIを開発した。 検索エンジンにはYahoo(https://search.yahoo.co.jp/)を用いた。 また、病名を検索するWebサイトとして「ドクターズファイル」「家庭の医学」を指定できるようにした。 指定しない場合は検索結果の先頭のURLを取得できるようにした。 さらに、1画面分(先頭の10件)を取得するオプションも指定できるようにした。 この病名検索WebAPIはGASで作成っした。プログラムは以下のとおりである。

/**
 * 病名検索Webサービス
 * 
 * 【GETメソッド】CORS対応
 * 
 * @input (1)
 *  name: 病名(例:一過性脳虚血発作)
 *  site: 検索するサイト名(例:ドクターズファイル|家庭の医学|全件)
 * @output (1)
 *  {"name":"一過性脳虚血発作","site":"ドクターズファイル","urls":["https://doctorsfile.jp/medication/18/"]}
 * 例 https://script.google.com/macros/s/{id}/exec?name=一過性脳虚血発作&site=ドクターズファイル
 * 
 * ※linuxのコンソールやAjaxで利用する場合は日本語はURLエンコードが必要
 * ※このプログラムはGoogle検索では"Our systems have detected unusual traffic from your computer network"というエラーが発生して動かない!
 *  →Yahoo検索だと動く
 */
function doGet(e) {
  const results = {
    "name": e.parameter.name,
    "site": e.parameter.site,
    "urls": queryDiseaseNameByYahoo(e.parameter.name, e.parameter.site)
  };
  return ContentService.createTextOutput(JSON.stringify(results)).setMimeType(ContentService.MimeType.JSON);
}
/**
 * YahooYahooで病名を検索
 * @input
 *  name: 病名(例:一過性脳虚血発作)
 *  site: 検索するサイト名(例:ドクターズファイル|家庭の医学|全件)
 *   site='全件'の場合は検索結果を配列にしてすべて返す
 *   site=''の場合は先頭の1件を返す
 *   site='ドクターズファイル'または'家庭の医学'で当該サイトが見つからなかったら先頭の1件を返す
 * @output
 *  検索結果のURLの配列(1件の場合でも配列になっている)
 */
function queryDiseaseNameByYahoo(name, site) {
  const url = 'https://search.yahoo.co.jp/search?p=' + encodeURIComponent(name + ' ' + site);
  const response = UrlFetchApp.fetch(url,{muteHttpExceptions:true});
  const html = response.getContentText('UTF-8');

  let re = /<li>\s*?<a href="(.*?)"/;

  if(site == 'ドクターズファイル') {
    re = /<li>\s*?<a href="(https:\/\/doctorsfile.*?)"/;
  } else if(site == '家庭の医学') {
    re = /<li>\s*?<a href="(https:\/\/www.msdmanuals.*?)"/;
  } else if(site == '全件') {
    const re = /<li>\s*?<a href="(.*?)"/g;
    let arr = null;
    const results = [];
    while(arr = re.exec(html)) {
      results.push(arr[1].replace(/%25/g,'%'));
    }
    return results;
  }

  let arr = html.match(re);
  if(!arr) {
    re = /<li>\s*?<a href="(.*?)"/;
    arr = html.match(re);
  }
  return [arr[1].replace(/%25/g,'%')];

}

このWebAPIを利用して、病名検索デモシステムを開発した。

病名検索デモ

病名:  サイト:

[サイト]には「ドクターズファイル」、「家庭の医学」(MSDマニュアル家庭版)を選択できます。サイトに空欄を指定するとYahoo検索結果の先頭を、「全件」を指定すると1ページの全件(10件)を返します。「家庭の医学」を指定するとたいていの病名はヒットするようです。


開発した病名検索機能をLINE公式アカウントアプリに導入した。「@2」と入力すると「一過性脳虚血発作」を「ドクターズファイル」から検索してURLを出力するようにしたのが下図である。

図1.「@2」を入力


図2.ドクターズファイルの画面
「家庭の医学」で病名を検索すると次のように表示される。
図3.家庭の医学の画面


0 件のコメント:

コメントを投稿