2023年11月27日月曜日

【定期】家計の見直し(2023年11月)

久しぶりの家計の見直しです。

経営と家計が一体化した家族経営では、家計の最適化が心強い生存基盤になります。翻って、家族や自身にとって本当に価値あるものが何なのか、確認する良い機会になります。


・家電を買うのは難しい


最近、洗濯機と冷蔵庫の調子が悪くなって買い直したのですが、いやはや、どの商品にするのか、どこで買うのか、どういう割引やキャンペーンが使えるのか、選択肢があり過ぎて考えるのが大変ですね。

ていうか、そもそもなんですが、家電て高いんですね……。頓着がなさ過ぎて、昔ひとり暮らしを始めた頃の家電一式10万円!みたいなイメージで止まっていました(笑)。

ひとまず、複数店舗とサイトを見比べて、予算内で必要な機能を満たしたものは買えた気がしますが……まぁ、欲はかかずに、問題なく使えるものが買えたならよしとしましょう。

ちなみに、Haier の洗濯機と AQUA の冷蔵庫を買いました。


・いよいよ新NISAが始まる


いよいよ新NISAが始まりますね。少しずつでもやらないよりはマシなので、みちみちと積み立てていく予定です。

SBI証券での積立設定もすでにできるようになっていたので、忘れないうちにやっておきました。

農業という波のある職業であればこそ、やはりこうした有利な制度は積極的に利用して、いざという時のために備えていきたいですね。

一応、iDeCoも最低掛金でやっています。


・考えても仕方ないけど円の先行きとか


最近、米ドル/円が150円あたりをうろうろしていますが、この先どうなるのですかね。

ユーロ/円も160円を超えていて、以前の感覚でこの相場を見てしまうと海外旅行もちょっと尻込みしてしまいます。

このまま円が弱くなるなら何か輸出とかしたくなりますね。乾椎茸とか。

物価高で厳しいからもっと補助を出してほしいという話も時々聞きますが、そうするとよりインフレ、より円安という方向に圧力がかかるわけで(実際はもっと複雑なのでしょうけど)、構造を変えない限りはより大変になっていくんじゃないかという気がします。経済って難しいですね。


・昨今の雑感


そんなこんな日々ですが、心穏やかに過ごすためには、(生計を共にする)家族での価値観の共有はとても大切だなと思うところです。

もちろん、完全一致なんてことはまず無理ですが、お互いが協力すればそれぞれ納得できる形は作れるんじゃないかと思います。

また、その合意形成(と言うと大げさですが)に向けた行動自体に、けっこう大きな意味があるように思います。


犬と猫(コケモモときな粉)

2023年11月24日金曜日

Google フォームで決済機能付き注文フォームを実現する

Google フォームと GAS(Google Apps Script)で、クレジットカードによる決済機能付きの注文フォームを無料&サーバーレスで実現します。

データベース代わりとしてスプレッドシート、確認メール等の送信に Gmail、オンライン決済には PAY.JP を使用します。

(システムの構築・運用は無料ですが、決済手数料はかかります)


注文を受けた際の流れは以下のようになります。丸数字付きの太字が注文者の動き、その下がシステムの動きの簡単な説明になります。

-----
① Google フォームから注文
→ Google フォームの回答をスプレッドシートに記録。決済ページのリンクを記載した確認メールを生成・送信する。

② 決済ページにアクセス
→ メールアドレス、注文合計金額が表示された決済ページを生成する。決済ページには有効期限を設定。

③ 決済
→ PAY.JP から Webhook を受け取り、スプレッドシートに決済完了を記録。決済完了のメールを生成・送信する。

④ 注文完了
-----

今回、在庫管理の部分については省いて作成しています。在庫管理については、「【GAS】Google フォームとスプレッドシートを連携して選択肢を動的に入力する。そして注文フォームを作ってみる。」を参考にしてみてください。

また、GAS によるシンプルな PAY.JP の導入については「GAS でオンライン決済 PAY.JP を導入する」をご参照ください。


***

それでは、以下 Google フォームを使用した決済機能付き注文フォームの作り方になります。PAY.JP のアカウントは作成済みの想定です(テストモードを使用)。

(決済に関するものですので、一応こうやると動くよという参考程度のものとしてください。ご利用の際は自己責任でお願いします)


1)注文フォームの作成


まずは、Google フォームで簡単な注文フォームを作成します。とりあえず、今回は以下のようにしました。

1) 商品 [プルダウン] ※1~複数個
2) お名前 [記述式](必須)
3) メールアドレス [記述式](必須)
4) お電話番号 [記述式]
5) 住所(続き) [記述式](必須)
6) お支払い方法 [ラジオボタン](必須)



2)回答を記録するスプレッドシートの加工


フォームの回答先となる新しいスプレッドシートを作成します。

「フォームの回答 1」シートの末列に「決済金額」「固有番号」「有効期限」「顧客番号」「決済状況」の5つの列を追加します。

「商品情報」という名前のシートを追加、A列に「商品名」、B列に「価格」を入力しておきます。その際、1行目は項目名として使用し、2行目から追記します。フォームの商品数、順番とそろえてください。



3)注文フォームから注文受付処理用の GAS を作成


フォームから GAS を作成します。フォームから作成することで、トリガー作成時に、イベントのソースとしてフォームを選択することができるようになります。

「注文受付.gs」
const sp = SpreadsheetApp.openById("スプレッドシート ID");
const sh = sp.getSheetByName("フォームの回答 1");
const lastRow = sh.getLastRow();
const lastCol = sh.getLastColumn();
const item_sh = sp.getSheetByName("商品情報");

function processOrder(e) {
  Utilities.sleep(5000);

  FormApp.getActiveForm(); // フォームへのパーミッションを与えるためのおまじない
  const res = e.response.getItemResponses();

  // 商品情報の取得
  const itemCount = item_sh.getLastRow() - 1;
  const item = item_sh.getRange(2, 1, itemCount, 2).getValues();

  // 金額合計を計算
  let amount = 0;
  for(let i = 0; i < itemCount; i++) {
    if(res[i].getResponse() != "") {
      amount += res[i].getResponse() * item[i][1];
    }
  }
  sh.getRange(lastRow, lastCol - 4).setValue(amount); // 合計金額を書き込み

  const id = generateId(); // 固有番号を生成
  sh.getRange(lastRow, lastCol - 3).setValue(id); // 固有番号を書き込み

  // 固有番号の有効期限を計算
  const timeStamp = new Date(sh.getRange(lastRow, 1).getValue());
  let expirationTimestamp = new Date(timeStamp.setHours(timeStamp.getHours() + 1));
  expirationTimestamp = Utilities.formatDate(expirationTimestamp, "JST", "yyyy/MM/dd HH:mm:ss");
  sh.getRange(lastRow, lastCol -2).setValue(expirationTimestamp); // 有効期限を書き込み

  sendConfirmationEmail(res, itemCount, item, amount, id); // 確認メールの送信
}
1行目の「スプレッドシート ID」を書き換えてください。

processOrder 関数を、フォーム送信時に起動するようにトリガー設定をしておきます。


「固有番号の生成.gs」
function generateId() {
  const previousIdSet = sh.getRange(2, lastCol - 3, lastRow - 1, 1).getValues(); // 生成済みの固有番号を取得

  let id;
  let isDuplicate;

  do {
    id = generateRandomString(); // ランダムな文字列を生成
    isDuplicate = checkForDuplicate(id, previousIdSet); // 重複をチェック
  } while (isDuplicate);

  return id;
}

function checkForDuplicate(value, previousValue) {
  for (var i = 0; i < previousValue.length; i++) {
    if (previousValue[i][0] === value) {
      return true; // 重複が見つかった場合
    }
  }
  return false; // 重複がない場合
}

function generateRandomString() {
  const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let randomString = "";
  for (var i = 0; i < 30; i++) {
    let randomIndex = Math.floor(Math.random() * characters.length);
    randomString += characters.charAt(randomIndex);
  }
  return randomString;
}


「確認メールの送信.gs」
function sendConfirmationEmail(res, itemCount, item, amount, id) {
  // メール本文の作成
  const name = res[itemCount].getResponse(); // お名前
  const email = res[itemCount + 1].getResponse(); // メールアドレス
  const phone = res[itemCount + 2].getResponse(); // お電話番号
  const address = res[itemCount + 3].getResponse(); // 住所
  const payment = res[itemCount + 4].getResponse(); // お支払方法

  let order = [];
  for(let i = 0; i < itemCount; i++) {
    order.push(item[i][0] + " " + res[i].getResponse() + "個");
  }
  let body = name + " 様\n\nご注文ありがとうございます。\n\n以下の内容でご注文を承りました。ご確認ください。\n\n-----\n" + order.join("\n") + "\n\n合計 ¥" + amount + "\n-----\n\nお届け先住所:" + address + "\n電話番号:" + phone + "\n\nお支払い方法:" + payment;

  if(payment == "クレジットカード") {
    const url = "ウェブアプリの URL";
    body += "\n\nご注文内容を確認の上、以下リンク先よりお支払いを完了してください:\n" + url + "?id=" + id;
  } else if(payment == "銀行振込") {
    body += "\n\nご注文内容を確認の上、以下振込先へのお支払いを完了してください:" + "\n〇〇銀行\n△△支店\n普通 ×××××××";
  }
 
  // 確認メールの送信
  GmailApp.sendEmail(
    email,
    "ご注文内容の確認と決済ページのご案内",
    body,
    {
      from: "送信元メールアドレス",
      name: "送信者名"      
    }
  );
}
28行目の「送信元メールアドレス」、29行目の「送信者名」を書き換えてください。

16行目の「ウェブアプリの URL」は後ほど書き換えます。


「決済ページの生成.gs」
function doGet(e) {
  const PUBLISH_KEY = "公開鍵";
  const html = HtmlService.createTemplateFromFile('index');

  // URL から固有番号を取得
  const id = e.parameter["id"];
  if(id == null) {
    return HtmlService.createHtmlOutput("<center>無効な URL です。(1)</center>"); // 固有番号がない場合はエラーページを表示
  }

  // 対象の固有番号の行を特定
  const idSet = sh.getRange(2, lastCol - 3, sh.getLastRow(), 1).getValues();
  let rowNum = 0;
  for(let i = 0; i < idSet.length; i++) {
    if(id == idSet[i]) {
      rowNum = 2 + rowNum + i;
      break;
    }
  }
  if(rowNum == 0) {
    return HtmlService.createHtmlOutput("<center>無効な URL です。(2)</center>"); // 有効な固有番号がない場合はエラーページを表示
  } else if(sh.getRange(rowNum, lastCol).getValues() != "") {
    return HtmlService.createHtmlOutput("<center>無効な URL です。(3)</center>"); // 既に決済完了している場合はエラーページを表示
  }

  // 該当の注文を取得
  const order = sh.getRange(rowNum, 1, 1, lastCol - 1).getValues();

  // 有効期限のチェック
  const expirationTimestamp = order[0][lastCol - 1]; // 有効期限
  const currentTime = new Date(); // 現在時刻
  if(expirationTimestamp < currentTime) {
    return HtmlService.createHtmlOutput("<center>有効期限を過ぎた決済ページです。(4)</center>"); // 有効期限を過ぎた場合はエラーページを表示
  }

  // 決済情報
  const email = order[0][lastCol - 9]; // メールアドレス
  const amount = order[0][lastCol - 5]; // 決済金額

  // 決済情報を index.html へ
  html.rowNum = rowNum;
  html.email = email;
  html.amount = amount;
  html.PUBLISH_KEY = PUBLISH_KEY;

  return html.evaluate();
}

// PAY.JPでの決済処理
function doPost(e) {  
  const card = e.parameter["payjp-token"];
  const rowNum = e.parameter["rowNum"];
  const amount = e.parameter["amount"];
  const SECRET_KEY = PropertiesService.getScriptProperties().getProperty("SECRET_KEY");

  let customer =  UrlFetchApp.fetch("https://api.pay.jp/v1/customers", {
    "method" : "post",
    "payload" : {
      "card": card
    },
    "headers": {'Authorization': "Basic " + Utilities.base64Encode(SECRET_KEY + ":")}
  });
  customer = JSON.parse(customer);

  sh.getRange(rowNum, lastCol - 1).setValue(customer.id); // 顧客番号を書き込み

  UrlFetchApp.fetch("https://api.pay.jp/v1/charges", {
    "method" : "post",
    "payload" : {
      "amount": amount,
      "currency": "JPY",
      "customer": customer.id
    },
    'headers' : {'Authorization': "Basic " + Utilities.base64Encode(SECRET_KEY + ":")}
  });

  return HtmlService.createHtmlOutput("<center>" + amount + "円のお支払いが完了しました。<br /><br />ブラウザを閉じてください。</center>");
}
2行目の「公開鍵」を書き換えてください。公開鍵は、PAY.JP のテスト公開鍵になります。

54行目の「SECRET_KEY」は、「プロジェクトの設定」の「スクリプト プロパティ」に保存してください。値は、PAY.JP のテスト秘密鍵になります。


「index.html」
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <base target="_top">
    <title>注文フォーム 決済ページ(テスト)</title>
  </head>
  <body>
    <center>
    <h1>注文フォーム 決済ページ(テスト)</h1>
    <p>メールアドレスと決済金額をご確認の上、お支払いを完了させてください。</p><br />
    <form action="ウェブアプリの URL" method="post">
      <input type="hidden" id="rowNum" name="rowNum" value=<?= rowNum?> />
      <label>メールアドレス: <?= email?><input type="hidden" id="email" name="email" value=<?= email?> /></label><br />
      <label>決済金額: <?= amount?><input type="hidden" id="amount" name="amount" value=<?= amount?> /></label><br /><br />
      <script src="https://checkout.pay.jp/" class="payjp-button" data-key=<?= PUBLISH_KEY?>></script>
    </form>
    </center>
  </body>
</html>
12行目の「ウェブアプリの URL」は後ほど書き換えます。


コードが一通りできたら、デプロイをします。

GAS の「デプロイ」→「新しいデプロイ」から「種類の選択」で「ウェブアプリ」を選択。

・説明:任意
・ウェブアプリ / 次のユーザーとして実行:自分
・ウェブアプリ / アクセスできるユーザー:全員

として、「デプロイ」を実行。

「確認メールの送信.gs(16行目)」と「index.html(12行目)」の該当箇所を表示されたウェブアプリの URL に書き換えます。

「デプロイ」→「デプロイの管理」から編集で「新バージョン」を再度デプロイします。


4)Webhook 受取用の GAS の作成


PAY.JP からの Webhook 受取用に、決済ページとは別の URL を持たせたウェブアプリが必要となるため、新たな GAS を作成します。こちらの GAS はスプレッドシートから作成しても、紐づけなしで作成しても構いません。

「webhook受取.gs」
const sp = SpreadsheetApp.openById("スプレッドシート ID");
const sh = sp.getSheetByName("フォームの回答 1");
const lastRow = sh.getLastRow();
const lastCol = sh.getLastColumn();

function doPost(e) {
  const contents = JSON.parse(e.postData.contents);

  // Webhook の確認
  const cusSet = sh.getRange(2, lastCol - 1, lastRow - 1, 2).getValues(); // 顧客番号一覧の取得

  // 顧客番号が存在するかの確認
  const cus = contents.data.customer; // 顧客番号
  for(let i = 0; i < cusSet.length; i++) {
    if(cusSet[i][0] == cus) {
      if(contents.type == "charge.succeeded") {
        sh.getRange(i + 2, lastCol).setValue("完了"); // 決済完了を書き込み
        paymentConfirmationEmail(i + 2); // 決済完了メールの送信
      } else {
        sh.getRange(i + 2, lastCol).setValue(contents.type); // 決済失敗
      }
    }
  }
}
1行目の「スプレッドシート ID」を書き換えてください。

※ Webhook のヘッダーに含まれている X-Payjp-Webhook-Token を、GAS の doPost 関数で取得する方法がわからなかった(できない?)ので、顧客番号で Webhook の正当性を判断する形にしています。


「決済完了メールの送信.gs」
function paymentConfirmationEmail(rowNum) {
  const name = sh.getRange(rowNum, lastCol - 9).getValue();
  const email = sh.getRange(rowNum, lastCol - 8).getValue();

  let body = name + " 様\n\n決済が完了しました。\n\n発送まで今しばらくお待ちください。\n\nどうぞ宜しくお願い致します。";

  // 決済完了メールの送信
  GmailApp.sendEmail(
    email,
    "決済が完了しました",
    body,
    {
      from: "送信元メールアドレス",
      name: "送信者名"      
    }
  );
}
13行目の「送信元メールアドレス」、14行目の「送信者名」を書き換えてください。


こちらもデプロイをします。

GAS の「デプロイ」→「新しいデプロイ」から「種類の選択」で「ウェブアプリ」を選択。

・説明:任意
・ウェブアプリ / 次のユーザーとして実行:自分
・ウェブアプリ / アクセスできるユーザー:全員

として、「デプロイ」を実行。

ウェブアプリの URL をメモしておきます。

こちらの URL は知られないように管理してください。


5)PAY.JP で Webhook の設定

PAY.JP の「API設定」で、Webhookの追加をします。URL は、先ほどメモした Webhook 受取用の GAS のウェブアプリの URL となります。


これで完成となります。


***

思ったよりも手順が多くなりました。実際に運用する際には、もう少しエラーハンドリング等しっかりやる必要がありそうですが、とりあえず、一通り動くものができてよかったです。

2023年11月14日火曜日

GAS でオンライン決済 PAY.JP を導入する

サービスに不可欠な決済。

現状、Web サービスへのオンライン決済導入というと、Stripe がダントツに強いそうですが、何かあった時には国内のサービスの方が安心な気もします(実際のところは分かりませんが)。

ということで、今回は、国産の PAY.JP によるオンライン決済(クレジットカードによる決済)のテスト導入を試してみました。

PAY.JP を運営している PAY 株式会社は、ネットショップ作成サービスを行っている BASE 株式会社の100%子会社みたいです。


最終的に、決済付きの Google フォームと連携した注文フォームをサーバーレスで運用できたらいいなと思っているので、実装には GAS(Google Apps Script)を使用しました。

-----
・追記(2023-11-24)

-----


以下、簡単な導入方法になります。

(決済に関するものですので、一応こうやると動くよという参考程度のものとしてください。ご利用の際は自己責任でお願いします)


① PAY.JP のアカウントを作成


まず、PAY.JP のアカウントを作成、「API設定」の「APIキーの情報」から「テスト秘密鍵」と「テスト公開鍵」をメモしておきます。


② GAS で「コード.gs」と「index.html」を作成


・「コード.gs」
const price = "3000";

function doGet(e) {
  const PUBLISH_KEY = "公開鍵";
  const html = HtmlService.createTemplateFromFile('index');
  
  html.price = price;
  html.PUBLISH_KEY = PUBLISH_KEY;

  return html.evaluate();
}

// PAY.JPでの決済処理
function doPost(e) {
  const card = e.parameter["payjp-token"];
  const SECRET_KEY = PropertiesService.getScriptProperties().getProperty("SECRET_KEY");

  let customer =  UrlFetchApp.fetch("https://api.pay.jp/v1/customers", {
    "method" : "post",
    "payload" : {
      "card": card
    },
    "headers": {'Authorization': "Basic " + Utilities.base64Encode(SECRET_KEY + ":")}
  });
  customer = JSON.parse(customer);

  UrlFetchApp.fetch("https://api.pay.jp/v1/charges", {
    "method" : "post",
    "payload" : {
      "amount": price,
      "currency": "JPY",
      "customer": customer.id
    },
    'headers' : {'Authorization': "Basic " + Utilities.base64Encode(SECRET_KEY + ":")}
  });

  return HtmlService.createHtmlOutput(price + "円の決済が完了しました。");
}
5行目の公開鍵は、先ほどメモした「テスト公開鍵」に書き換えてください。

「テスト秘密鍵」は、「プロジェクトの設定」の「スクリプト プロパティ」に保存してください。プロパティ名は「SECRET_KEY」としています。


・「index.html」
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <base target="_top">
    <title>PAY.JP Checkout Test</title>
  </head>
  <body>
    <p>PAY.JP 決済テスト</p>
    <form action="ウェブアプリのURL" method="post">
      <script src="https://checkout.pay.jp/" class="payjp-button" data-key=<?= PUBLISH_KEY?>></script>
    </form>
  </body>
</html>
10行目の「ウェブアプリのURL」は後ほど書き換えます。


③ ウェブアプリのデプロイ


GAS の「デプロイ」→「新しいデプロイ」から「種類の選択」で「ウェブアプリ」を選択。

・説明:任意
・ウェブアプリ / 次のユーザーとして実行:自分
・ウェブアプリ / アクセスできるユーザー:全員

として、「デプロイ」を実行。

表示されたウェブアプリのURLを index.html の10行目(action=)に入力。

「デプロイ」→「デプロイの管理」から編集で「新バージョン」を再度デプロイ。


これで、完成です。


***

ウェブアプリのURLにアクセスして、動かしてみるとこんな感じになります。




うまくいっていれば、PAY.JP アカウントの売上データに反映されているはずです。

今回、エラーハンドリング等は省いて最低限の構成ですが、実際の運用ではもちろん必要になります。


2023年11月11日土曜日

Google One VPN の IP アドレスがどのように変化しているのか確認する

最近、Google One VPN を使っているのですが、ふと IP アドレスって変化しているのかなと気になって調べてみました。


【実験方法】

 NGINX でサーバーを立てて、1分間隔で60回アクセス(計1時間)アクセスする。


【結果】

IP アドレスに変化なし。


定期的に変化しているものかと思ったら、少なくても1時間の中では変化していませんでした。2回ほど試しましたが、同様の結果でした。

(以前ちらっと確認したときに、分単位程度の間隔で変化していた気がしたのですが、何を見たのか……)

ちなみに、VPN への接続を一旦切って再度接続した場合ですが、これはやはり IP アドレスが変わるようです。



これからの農業経営を考える(2023・秋)

秋はこれからの農業経営について考えたくなる季節です。ブログを遡っていたら昨年も同じような時期に同じようなことを書いていますね。若干のずれを考慮すると、気温に反応しているのでしょうか(笑)。

そして、1年ほど経ったものの、具体的な数値目標には全くもって到達しておりませんね(笑……)。今後も継続して励みます。


さて最近、身の回りで経営について話し合うような機会が多々あり、とても勉強になっています。

中でも面白いなと思ったのが、これまで家族経営でやっていた同一品目の農園を合併して法人化するアイデアです。

単体では資金力、その他諸々、法人化してやっていくだけの体制を整えるのが困難。しかし、このままではジリ貧。

そうした時に、複数の農園が合わさり組織化することで、資本力や労働力に関する課題解決を目指すということですね。

集落単位で組織化する集落営農や、規模を求めるメガファーム化等とは異なる文脈で、あくまで農園と農園の合併ですね。後から揉めないように、諸々の調整は必須かと思いますが、1つの選択肢としてはありな気がします。

隣り合った農園の必要はなく、むしろ災害の激甚化、頻発化著しい昨今、地理的にも、適度に分散させた方が災害耐性は上がりそうです。


秋の庭

2023年10月29日日曜日

徒然じゃない日々(26)

農家として目の前の物理と向き合っていると、PC1台でどこでも働くことができるリモートワークみたいなものにちょっと憧れます。

……というわけで、せっかくのやりたいことはやってみよう!と思いまして、最近、隙間の時間を活用してフリーのプログラマーとして働き始めました。

幸いなことにご依頼もいただくことができ、自作スマート農業でも使っている GAS や Python でいろいろと作っています。強制的にスキルアップもしていくので、とても楽しいです。

これをまた自作スマート農業の開発に還元していければと思います。

***


話は変わりまして、先日、農業や農村に関する会議に出席する機会がありました。

若干思うところがあったので、ちょっと記録しておこうと思います。

人口減少・高齢化が進む中で、今ある農地を全部守っていくのは不可能。これは、おそらく多くの関係者が感じているところではないかと思います。

農林水産省が公開している「地域計画策定マニュアル(Ver.3.2)」でも協議事項「農業上の利⽤が⾏われる農⽤地等の区域」として言及がされており、どうしても農業利用できない土地は、より省力的で簡易な方法で保全等行っていきましょう、ということになっています。

市の基本計画では、担い手への農地の集積や遊休農地の削減が項目としてありますが、こことのすり合わせが、今後、非常に重要になっていきそうです。

また、いざ農地利用をやめた際に、「省力的で簡易な方法」による管理にスムーズに移行できるものなのか、というのは個人的にとても疑問で、また次の課題になっていきそうな気がしています。

***


そんなこんなで、日々あれこれありますが、地道にコツコツやっていくしかないですね。頑張っていきましょう。



2023年10月28日土曜日

Pyinstaller で Python スクリプトを実行ファイルに変換(exe 化)する

Pyinstaller を用いて Python スクリプトを実行ファイルに変換(exe 化)する機会があったので、いつものごとく備忘録です。Python を実行するための環境を整えなくてもいいというのは、本当に便利ですね。

今回は、venv モジュールで仮想環境の構築を行い、その上で PyInstaller を用いて Python スクリプト を実行ファイルへ変換(exe 化)します。

PyInstaller のデメリットとして、ファイルサイズが大きくなることが挙げられます。必要なパッケージのみをインストールした仮想環境上で実行ファイルを作成することで、ファイルの肥大化を抑えます。

尚、PyInstaller の実行環境は Windows 11 Pro 22H2、Python 3.11.0 です。PyInstaller で作成した実行ファイルの互換性は同一 OS に限られるため、Windows で作成したものは、Windows 上でしか動きません。


▼ venv モジュールによる仮想環境の構築・有効化


プロジェクト用のフォルダを作成・移動。
$ mkdir project
$ cd project

仮想環境の作成。
$ python -m venv 仮想環境名

プロジェクト用フォルダ内に、仮想環境名のフォルダが作成されます。仮想環境名には一般的に venv や env が使われるそうです。

pip が最新の状態でインストールする場合は以下のコマンド。(たぶん、基本的にはこっちで作っておいた方が便利)
$ python -m venv --upgrade-deps 仮想環境名

仮想環境の有効化。
$ venv\Scripts\activate


ちなみに、仮想環境は初期化(インストールしたパッケージ等のクリア)も可能。以下の通り deactivate で仮想環境を終了した上で、クリアを実行。
$ deactivate
$ python -m venv --clear 仮想環境名

また、仮想環境名のフォルダをまるごと削除することで、仮想環境自体の削除ができます。


▼ PyInstaller で実行ファイルへ変換(exe 化)


以下、仮想環境下での操作となります。

PyInstallar をインストール。
$ pip install pyinstaller

Python ファイルを実行ファイルに変換(exe 化)。
$ pyinstaller sample.py --onefile --noconsole

関連ファイルをひとつにまとめる「--onefile」と、実行時にコンソール画面を表示させない「--noconsole」は付けておいた方がいいみたいです。


プロジェクト用のフォルダ内に「build」「dist」という2つのフォルダと spec ファイルが作成されます。実行ファイル(exe)は「dist」フォルダ内にあります。


ということで、とりあえず動くものが作れました。


・追記(2023-10-30)

PyInstaller で作成した実行ファイルは、Windows Defender 等のセキュリティソフトによってウィルス判定されることが多々あるようです。(「ファイルにウイルスまたは望ましくない可能性のあるソフトウェアが含まれているため、操作は正常に完了しませんでした。」等のメッセージが出る)

その際は、以下のページで紹介されているような対策があるようです。



これでもダメな場合は、ひとまずセキュリティソフト側で除外設定等するしかないんですかね、たぶん。


大あくび