スクレイピング

UrlFetchApp.fetchメソッド

URLを指定してWebページのHTMLを取得するには組み込みオブジェクトUrlFetchAppのfetchメソッドを使用する。GETリクエストでHTMLデータを取得するサンプルプログラムを以下に示す。

function myFunction() {
  const URL = 'https://www.matsue.jrc.or.jp/publics/index/741/';
  const response = UrlFetchApp.fetch(URL);
  const html = response.getContentText('UTF-8');
  Logger.log(html);
}

リスト1. HTMLデータの取得

松江日赤病院の病院指標をスクレイピングする

以下の手順で松江日赤病院の病院指標をスクレイピングする。

  1. GAS(Google Apps Script)のUrlFetchApp.fetchを使用して松江日赤病院の病院指標のHTMLを取得
  2. 取得したHTMLから正規表現を利用して診断群分類別患者数等の表を読みとる
  3. 読みとった表をスプレッドシートへ書き込む

下図は、松江日赤病院の病院指標のWebサイトをGoogle Chromeのデベロッパーツール(メニューから[その他のツール]→[デベロッパーツール]を選択)で表示したものである。

 

図1.松江日赤病院の病院指標

図1には糖尿病・内分泌内科の上位5位までの診断群分類別患者数、平均在院日数、転院率、平均年齢等のデータが表形式で表示されており、対応するHTMLが右側のElementsタブに表示されている。 

このElementsタブを見ながら、正規表現を利用して該当テーブルのtableタグを切り出して、次にその中から行データ(trタグ)を切り出して、最後に列データ(tdタグ)を切り出す。

下記はtableタグを切り出すスクリプトで、その結果切り出されたtableタグのHTMLデータが図2に示すHTMLである。

  // <table>タグ
  const re_table = /<table class="type004Table" id="type004Table".*?<\/table>/s;
  const table_html = html.match(re_table);
  Logger.log(table_html[0]);

リスト2. tableタグの切り出し

図2.リスト2のスクリプトで切り出されたtableタグのHTML

なお、リスト2のスクリプトはリスト1の関数の末尾に追記する。

次いで、切り出されたtableタグのHTMLからtrタグを切り出すスクリプトを以下に示す。

  // <tr>タグ
  const re_tr = /<tr class="type001Tr".*?<\/tr>/sg;
  const tr_array = table_html[0].match(re_tr);
  Logger.log(tr_array.length);

リスト3. trタグの切り出し

正規表現にはオプションgが設定されているので、複数のtrタグが抽出され、結果は各々のtrタグが配列tr_arrayに格納される。

そこで、配列に格納されたtrタグをfor文を使って順番に取り出して、tdタグを抽出するのが次のスクリプトである。

  for (let tr_html of tr_array) {
    Logger.log(tr_html);
    // <td>タグ
    const re_td = /<td class="type004Td".*?<\/td>/sg;
    const td_array = tr_html.match(re_td);
    Logger.log(td_array.length);
    for (let td_html of td_array) {
      Logger.log(td_html);
      Logger.log(strip_tag(td_html));
    }
  }

リスト4. tdタグの切り出し

ここで、strip_tag()関数は、引数に指定された文字列からタグを削除する関数で、以下のようになっている。

function strip_tag(html) {
  const regex = /<.*?>/g;
  const result = html.replace(regex, '');
  return result;
}

リスト5. タグ除去関数

リスト1~5をまとめたものをリスト6に示す。

function myFunction() {
  const URL = 'https://www.matsue.jrc.or.jp/publics/index/741/';
  const response = UrlFetchApp.fetch(URL);
  const html = response.getContentText('UTF-8');

  // <table>タグ
  const re_table = /<table class="type004Table" id="type004Table".*?<\/table>/s;
  const table_html = html.match(re_table);

  // <tr>タグ
  const re_tr = /<tr class="type001Tr".*?<\/tr>/sg;
  const tr_array = table_html[0].match(re_tr);

  for (let tr_html of tr_array) {
    // <td>タグ
    const re_td = /<td class="type004Td".*?<\/td>/sg;
    const td_array = tr_html.match(re_td);
    for (let td_html of td_array) {
      Logger.log(td_html);
      Logger.log(strip_tag(td_html));
    }
  }
}

リスト6. 一連の処理を一つにまとめた関数

上記プログラムの実行結果(ログ)の一部を図3, 4に示す。

図3.リスト6の実行結果

図4.リスト6の実行結果(続き)

これを見ると、確かに図1の「■糖尿病・内分泌内科」の診断群分類表の項目が抽出できていることがわかる。

モジュール化

リスト6はtableタグの先頭要素table_html[0]の処理しかしていない。そこで、次のステップとしてtableタグの全要素を処理できるようにtableタグのHTMLを処理する部分のモジュール化を行う。

作成する関数は、入力引数にtableタグのHTMLを受け取ってtrタグを切り出し、ついでtdタグを切り出して表のセルの値を抽出する処理を行う。これをparseTableという関数で実装したのが下記のリスト7である。

function parseTable(table_html) {
  // <tr>タグ
  const re_tr = /<tr class="type001Tr".*?<\/tr>/sg;
  const tr_array = table_html.match(re_tr);

  for (let tr_html of tr_array) {
    // <td>タグ
    const re_td = /<td class="type004Td".*?<\/td>/sg;
    const td_array = tr_html.match(re_td);
    for (let td_html of td_array) {
      Logger.log(strip_tag(td_html));
    }
  }
}

リスト7. tableタグのHTMLを処理するparseTable関数

そして、もとのウェブサイトからtableタグをグローバルに抽出して、その各々をリスト7の関数parseTableへ引き渡して全tableタグを処理するプログラムをリスト8に示す。

function myFunction() {
  const URL = 'https://www.matsue.jrc.or.jp/publics/index/741/';
  const response = UrlFetchApp.fetch(URL);
  const html = response.getContentText('UTF-8');

  // <table>タグ
  const re_table = /<table class="type004Table" id="type004Table".*?<\/table>/gs;
  const table_html = html.match(re_table);
  
  // 全<table>の処理
  for (let table of table_html) {
    parseTable(table);
  }
}

リスト8. 全tableタグを処理するプログラム

parseTableの戻り値の設計

リスト7のparseTable関数は、抽出した表のセルの値をログに出力しているだけである。しかし、折角抽出した値を出力するだけでなく、後利用したい。そこで、parseTableがリスト9に示すようなオブジェクトを戻り値として返すように修正する。

{
 "deptName": "■糖尿病・内分泌内科",
 "dpc": [
  {
   "DPCコード": "100071xx99x100",
   "DPC名称": "2型糖尿病(糖尿病性ケトアシドーシスを除く。)(末梢循環不全あり。) 手術なし 手術・処置等2 1あり 定義副傷病 なし 85歳未満",
   "患者数": "27",
   "平均在院日数(自院)": "14.96",
   "平均在院日数(全国)": "14.10",
   "転院率": "0.00",
   "平均年齢": "62.48",
   "患者用パス": ""
  },
  {
   "DPCコード": "040081xx99x00x",
   "DPC名称": "誤嚥性肺炎 手術なし 手術・処置等2 なし 定義副傷病 なし",
   "患者数": "26",
   "平均在院日数(自院)": "20.04",
   "平均在院日数(全国)": "20.84",
   "転院率": "23.08",
   "平均年齢": "83.19",
   "患者用パス": ""
  },
  {
   "DPCコード": "100070xx99x000",
   "DPC名称": "2型糖尿病(糖尿病性ケトアシドーシスを除く。)(末梢循環不全なし。) 手術なし 手術・処置等2 なし 定義副傷病 なし 85歳未満",
   "患者数": "22",
   "平均在院日数(自院)": "11.59",
   "平均在院日数(全国)": "10.84",
   "転院率": "0.00",
   "平均年齢": "56.86",
   "患者用パス": ""
  },
  {
   "DPCコード": "100071xx99x000",
   "DPC名称": "2型糖尿病(糖尿病性ケトアシドーシスを除く。)(末梢循環不全あり。) 手術なし 手術・処置等2 なし 定義副傷病 なし 85歳未満",
   "患者数": "19",
   "平均在院日数(自院)": "12.26",
   "平均在院日数(全国)": "11.51",
   "転院率": "0.00",
   "平均年齢": "65.74",
   "患者用パス": ""
  },
  {
   "DPCコード": "100070xx99x100",
   "DPC名称": "2型糖尿病(糖尿病性ケトアシドーシスを除く。)(末梢循環不全なし。) 手術なし 手術・処置等2 1あり 定義副傷病 なし 85歳未満",
   "患者数": "18",
   "平均在院日数(自院)": "14.78",
   "平均在院日数(全国)": "13.72",
   "転院率": "0.00",
   "平均年齢": "65.17",
   "患者用パス": ""
  }
 ]
}

リスト9. 関数parseTableの戻り値

リスト9に示すのは、deptNameという診療科名称とdpcという診断群分類データ配列からなるオブジェクトで、診断群分類データは図5に示す表の各行の値を格納する連想配列の形式になっている。

図5.診断群分類表

 アルゴリズム

関数parseTableをリスト9に示すオブジェクトを返すようなアルゴリズムを設計する。

図5に示すようにtableタグのHTMLは1行目に診療科名称(この例では「■糖尿病・内分泌内科」)、2行目に項目名(「DPCコード」、「DPC名称」、・・・)、3行目以降に診断群分類データが格納されている。

そこで、リスト7のparseTableのプログラムにおいて、行を表す変数rowと列を表す変数colを導入して、各ループ(行ループ、列ループ)の都度、該当する変数(row, col)をインクリメントして、現在、どのセルを処理しているのかをアルゴリズムが把握できるようにする。

そして、rowが0(1行目)、colが0(1列目)の場合はセル値を診療科名称へ代入し、rowが1(2行目)ならセル値を項目名称配列の要素に追加し、rowが2以上なら、colで示される項目名称をキーとする連想配列の値にセル値を代入する。これによってリスト9のオブジェクトを作成する。

以上のアルゴリズムを実装したのがリスト10である。

function parseTable(table_html) {
  // <tr>タグ
  const re_tr = /<tr class="type001Tr".*?<\/tr>/sg;
  const tr_array = table_html.match(re_tr);

  const data = {
    'deptName': null,
    'dpc': []
  };
  const itemNames = [];
  let row = 0;
  for (let tr_html of tr_array) {
    // <td>タグ
    const re_td = /<td class="type004Td".*?<\/td>/sg;
    const td_array = tr_html.match(re_td);

    let col = 0;
    const dpc_row = {};
    for (let td_html of td_array) {
      const value = strip_tag(td_html);
      if (row == 0) {
        if (col == 0) {
          data.deptName = value;
        }
      } else if (row == 1) {
        itemNames.push(value);
      } else {
        dpc_row[itemNames[col]] = value;
      }
      col = col + 1;
    }
    if (row > 1) {
      data.dpc.push(dpc_row);
    }
    row = row + 1;
  }
  return data;
}

リスト10. 診断群分類オブジェクトを返すparseTable関数

最後にリスト8を修正してparseTableの戻り値を格納して診療科別の診断群分類ベスト5を出力するプログラムをリスト11に示す。

function test_parseTables() {
  const URL = 'https://www.matsue.jrc.or.jp/publics/index/741/';
  const response = UrlFetchApp.fetch(URL);
  const html = response.getContentText('UTF-8');

  // <table>タグ
  const re_table = /<table class="type004Table" id="type004Table".*?<\/table>/gs;
  const table_html = html.match(re_table);
  const dpc_data = [];
  for (let table of table_html) {
    const result = table.match(/DPCコード<\/td>/);
    if (result) {
      dpc_data.push(parseTable(table));
    }
  }
  Logger.log(JSON.stringify(dpc_data, null, ' '));
  return dpc_data;
}

リスト11. 診断群分類オブジェクトを返すparseTable関数

リスト11に示すように、Webサイトから抽出したtableタグのforループで、診断群分類データのみを処理対象とするために"DPCコード</td>"という文字列を正規表現でパターンマッチし、マッチしたものだけを処理対象にしている。

doGet

リスト11の関数test_parseTables()を呼び出して取得した診断群分類オブジェクトをJSONデータとして返す関数を以下に示す。

function doGet(e) {
  const out = ContentService.createTextOutput();
  
  //Mime TypeをJSONに設定
  out.setMimeType(ContentService.MimeType.JSON);
  
  result = test_parseTable();
  out.setContent(JSON.stringify(result));
  
  return out;
}

リスト11.1. 診断群分類オブジェクトをJSONデータで取得するdoGetエントリ

スプレッドシート

次は、スクレイピングしたデータをスプレッドシートに出力するように関数parseTableを作り変える。リスト10はごちゃごちゃしているので、リスト7から出発した方がよいだろう。

まず、バインドしているスプレッドシートオブジェクトを取得するには次のようにする。

const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

リスト12. バインドしているスプレッドシートの取得

もし、IDが分かっている別のスプレッドシートを取得したいのであれば、つぎのように書く。

const spreadsheet = SpreadsheetApp.openById(SPREAD_SHEET_ID);

リスト13. IDが分かっている別のスプレッドシートの取得

ここで、SPREAD_SHEET_IDは取得したいスプレッドシートのIDである。

次に、スプレッドシートオブジェクトからシート名を指定してシートオブジェクトを取得するには次のように書く。

const sheet = spreadsheet.getSheetByName('DPC');

リスト14. シート名を指定してシートオブジェクトを取得

これは、シート名'DPC'のシートオブジェクトを取得しているところである。

もし、新規にシート名を指定してシートオブジェクトを作成したい場合は次のように書く。

const sheet = spreadsheet.insertSheet('■泌尿器科・内分泌科');

リスト15. シート名を指定してシートオブジェクトを作成

これは、シート名が'■泌尿器科・内分泌科'のシートオブジェクトを作成しているところである。シートオブジェクトはinsertSheetメソッドの戻り値として取得できる。

シートにデータを行単位で追加するにはシートオブジェクトのappendRowメソッドを使って行う。

const values = ['00071xx99x100', '2型糖尿病', 27, 14.96];
sheet.appendRow(values);

リスト16. シートにデータを行単位で追加

これは、'00071xx99x100', '2型糖尿病', 27, 14.96の4つの項目からなる行をシートに挿入しているところである。配列には文字列、整数、実数など異なるデータ型が混在できる。

以上述べたことを使って、スクレイピングしたデータをスプレッドシートの既存のシート(シート名は'DPC')に書き込むプログラムを以下に示す。

function parseTable(table_html, spreadsheet) {
  const re_tr = /<tr.*?<\/tr>/sg;
  const tr_html = table_html.match(re_tr);
  const sheet = spreadsheet.getSheetByName('DPC');
  for(let s of tr_html) {
    const re_td= /<td.*?<\/td>/sg;
    const td_html = s.match(re_td);
    let values = [];
    for(let d of td_html) {
      const value = strip_tag(d);
      values.push(value);
    }
    sheet.appendRow(values);
  }
}

リスト17. スクレイピングしたデータをスプレッドシートに書き込む(既存シート)

関数parseTableは第2引数にスプレッドシートオブジェクトspreadsheetを取るので、呼び出すときはリスト12または13を使って書き込むスプレッドシートを取得してから呼び出すことになる。

次に、既存シートではなく、診療科ごとに診療科名をシート名とするシートを作成してスクレイピングしたデータを記入するプログラムを以下に示す。

function parseTable(table_html, spreadsheet) {
  const re_tr = /<tr.*?<\/tr>/sg;
  const tr_html = table_html.match(re_tr);
  let row = 0;
  let sheet = null;
  for(let s of tr_html) {
    const re_td= /<td.*?<\/td>/sg;
    const td_html = s.match(re_td);
    let col = 0;
    let values = [];
    for(let d of td_html) {
      const value = strip_tag(d);
      if (row == 0) {
        if (col == 0) {
          sheet = spreadsheet.insertSheet(value);
        }
      } else {
        values.push(value);
      }
      col++;
    }
    if (row > 0) {
      sheet.appendRow(values);
    }
    row++;
  }
}

リスト18. スクレイピングしたデータをスプレッドシートに書き込む(新規シート)

診療科名は先頭行(row=0)の先頭列(col=0)にあるので、inserSheetメソッドを使ってシートを作成してシートオブジェクトを取得する。行の処理が終わると2行目以降(row > 0)について、行単位でシートにデータをappendRowメソッドを使って追記する。

0 件のコメント:

コメントを投稿

退院サマリーの標準化

 頼んでおいた「退院サマリー標準化の試み」 1) が届いたので読んでみた。この中に「 病院での診療録の質を向上させるために最も有効な方法の1つは、退院サマリーを監査することである 」という記述がある。その理由として「 日常的な診療記録(経過記録;progress note)は、入...