Photo by Victor Dunn on Pexels.com

AIで記事を学習して新たな記事を生み出すにはお金が必要だと思っていたがそうでも.

2025.06.22

Logging

おはようございます.AIで記事を学習して新たな記事を生み出すにはお金が必要だと思っていたがそうでもなくローカルPCでそこら辺に落ちているLlamaモデルを持ってきてチューニングすれば何とかなるじゃねぇという思いに至った.

実はあなたの手元にあるPCと、そこら中に「落ちている」オープンソースのAIモデル、特にLlama 3があれば、十分記事が生成できるんです。


ローカルAI記事生成は、もはや夢物語じゃない

「AIで記事生成」と聞くと、SFのような世界や、大企業だけが使える特権のように感じるかもしれません。しかし、今は違います。オープンソースの強力な言語モデル、特にMetaが公開したLlama 3の登場は、この常識を大きく覆しました。

Llama 3は、その性能の高さにもかかわらず、誰でも無料で利用できるという点が最大の魅力です。さらに、80億パラメータの8Bモデルであれば、最新のゲーミングPCとまではいかなくとも、ある程度の性能を持つPCであれば十分に動作します。これにより、高額なクラウドサービスを利用せずとも、自分のPCでAI記事生成の環境を構築することが現実的になりました。


なぜLlama 3があなたのPCと相性抜群なのか?

Llama 3がローカルPCでの記事生成に適している理由はいくつかあります。

  • 完全無料のオープンソース: 利用に費用がかからないため、予算を気にせずAIを試したり、本格的に導入したりできます。
  • 選べるモデルサイズ: Llama 3には様々なサイズのモデルがあり、PCのスペックに合わせて選べます。特に8Bモデルは、個人利用に最適なバランスを持っています。
  • 活発な開発者コミュニティ: 世界中の開発者がLlama 3を使った新しいツールや効率的なチューニング方法を日々共有しています。困ったときには助けを借りられる心強い味方です。
  • 「量子化」でさらに軽量に: モデルのサイズを大幅に小さくする「量子化」という技術を使えば、より少ないメモリでLlama 3を動かせるようになります。これにより、より多くのPCで利用の道が開けます。

あなたのPCを「記事生成マシン」に変える秘訣

もちろん、いきなりプロのライター並みの記事をAIに書かせるのは難しいかもしれません。しかし、ちょっとした工夫で「何とかなる」レベルの記事生成は十分に可能です。

  1. 少量のデータでファインチューニング: 大量の記事データは不要です。あなたが書きたい記事のテーマやスタイルに合った、質の良い記事を数十〜数百程度集めてLlama 3を学習(ファインチューニング)させれば、その分野に特化した記事生成能力が格段に向上します。
  2. プロンプト(指示文)の工夫: AIへの「指示の出し方」は非常に重要です。具体的で明確なプロンプトを与えることで、チューニングが完璧でなくても、驚くほど質の高い記事が生成できます。これはまるで、優秀なアシスタントに的確な指示を出すようなものです。
  3. 効率的な学習方法の活用: 「LoRA(Low-Rank Adaptation)」のような効率的なファインチューニング手法を使えば、少ないGPUメモリでも短時間でモデルを特定のタスクに最適化できます。

あなたの創造性が、今、AIで加速する

かつては一部の専門家や企業にしか手の届かなかったAIによる記事生成が、今やあなたのPCで実現できる時代になりました。これはまさにAI技術の「民主化」です。

とまぁそういう訳なので何とかしてみますが、ファインチューニングにどれぐらい時間がかかるのかが未知数だったりする.

ファインチューニングPythonコード

以下のPythonコードは、Llama 3モデルをロードし、提供されたテキスト記事でファインチューニング(LoRA使用)を実行し、結果を保存します。 上記の入力値は、このコードに自動的に反映されます。 このコードをPythonファイル(例: `finetune_llama.py`)として保存し、実行してください。

import os
import torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskType

# GPUの利用可能性を確認
print("GPUが利用可能か確認中...")
if not torch.cuda.is_available():
    print("GPUが見つかりません。Fine-tuningにはGPUが強く推奨されます。")
    # GPUがない場合は、ここでスクリプトを終了するか、CPUモードで続行するか選択できます。
    # exit("GPUがないため終了します。")
else:
    print(f"GPUが利用可能です: {torch.cuda.get_device_name(0)}")

# --- 1. モデルとトークナイザーのロード ---
# Llama 3モデルのパスを指定します。Hugging Faceのモデル名(例: "meta-llama/Llama-3-8B")
# またはローカルにダウンロードしたモデルのパスを指定してください。
MODEL_NAME = "meta-llama/Llama-3-8B" # ユーザーが入力したパスがここに挿入されます

print(f"モデルとトークナイザーをロード中: {MODEL_NAME}")

# 4bit量子化設定 (GPUメモリの節約に役立ちます)
# bnb_4bit_compute_dtypeは、Ampere以降のNVIDIA GPUに推奨されるbfloat16を使用しています。
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4", # NF4 (NormalFloat4) 量子化タイプ
    bnb_4bit_compute_dtype=torch.bfloat16 
)

# トークナイザーをロード
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
# Llama 3はデフォルトでbos_tokenを付与しないことがあるため、明示的に追加。
# また、padding_side='right'はLlamaモデルに推奨される設定です。
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

# モデルをロードし、量子化設定を適用し、自動的にGPUにマッピングします。
model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    quantization_config=bnb_config,
    device_map="auto", # 利用可能なデバイス(GPU)に自動的にモデルを分散
    trust_remote_code=True # リモートコードの実行を許可
)
print("モデルロード完了。")

# k-bit学習用にモデルを準備 (PEFTライブラリのため)
# gradient_checkpointingを有効にすることで、メモリ使用量をさらに削減できます。
model.gradient_checkpointing_enable()
model = prepare_model_for_kbit_training(model)
print("k-bit学習用にモデルを準備しました。")

# --- 2. データセットの準備 ---
# あなたのテキスト記事ファイルが格納されているディレクトリを指定します。
# 例: 'your_article_data/' の中に 'article1.txt', 'article2.txt', ... と置かれている場合
DATA_DIR = "./your_article_data/" # ユーザーが入力したパスがここに挿入されます

print(f"データセットをロード中: {DATA_DIR}")

# 'text'形式でデータセットをロードします。指定されたディレクトリ内のすべての.txtファイルを読み込みます。
# 各ファイルが1つのエントリとして扱われます。
try:
    dataset = load_dataset('text', data_files={'train': os.path.join(DATA_DIR, '*.txt')})
    print(f"データセットのサンプル数: {len(dataset['train'])}")
except Exception as e:
    print(f"データセットのロード中にエラーが発生しました。ディレクトリとファイル形式を確認してください: {e}")
    exit("データセットロード失敗。")

# データセットをトークン化する関数
# 長い記事をモデルの最大入力長に分割します。
def tokenize_function(examples):
    # Llama 3の最大入力長は通常8192ですが、お使いのGPUのVRAMに合わせて調整してください。
    # ここでは一般的な値として2048を設定しています。
    max_length = 2048 
    # truncate=Trueで最大長を超えるテキストを切り捨てます。
    return tokenizer(examples["text"], truncation=True, max_length=max_length)

# データセットをトークン化します。
# num_procはCPUコア数に応じて並列処理を行い、処理を高速化します。
tokenized_dataset = dataset.map(
    tokenize_function,
    batched=True,
    num_proc=os.cpu_count(),
    remove_columns=["text"] # 元のテキスト列は学習に不要になるため削除します。
)
print("データセットのトークン化が完了しました。")

# --- 3. PEFT (LoRA) の設定 ---
# LoRA (Low-Rank Adaptation) は、元のモデルの重みをフリーズし、
# 小さなアダプター層を追加して学習させることで、効率的にファインチューニングを行います。
# これにより、GPUメモリの使用量を抑えつつ、高い性能を実現できます。
lora_config = LoraConfig(
    r=16, # LoRAのランク。値を大きくすると表現力が増すが、メモリ消費も増える。
    lora_alpha=32, # LoRAのスケーリング係数。rの2倍程度が推奨されることが多いです。
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # LoRAを適用する層。Llamaモデルで一般的な層。
    bias="none", # バイアスを学習しない設定。
    lora_dropout=0.05, # ドロップアウト率。過学習を防ぐために設定します。
    task_type=TaskType.CAUSAL_LM, # タスクタイプを因果言語モデルに設定。
)

# モデルにLoRAアダプターを追加します。
model = get_peft_model(model, lora_config)
print("モデルにLoRAアダプターを適用しました。")
model.print_trainable_parameters() # 学習可能なパラメータ数を確認します。

# --- 4. 学習の実行 ---
# 学習済みモデルを保存するディレクトリ
OUTPUT_DIR = "./llama3_finetuned_model/" # ユーザーが入力したパスがここに挿入されます

# 学習の設定
training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    num_train_epochs=3, # エポック数。データセットのサイズと希望する精度に応じて調整してください。
    per_device_train_batch_size=1, # GPUあたりのバッチサイズ。VRAMが少ない場合は1に設定。
    gradient_accumulation_steps=4, # 勾配を蓄積するステップ数。実質的なバッチサイズは per_device_train_batch_size * gradient_accumulation_steps になります。
    optim="paged_adamw_8bit", # 8bit AdamWオプティマイザを使用し、メモリ効率を向上させます。
    save_steps=500, # 500ステップごとにモデルを保存します。
    logging_steps=100, # 100ステップごとにログを出力します。
    learning_rate=2e-4, # 学習率。
    fp16=True, # 混合精度学習を有効化 (GPUが対応している場合)。VRAM削減と高速化に寄与します。
    max_steps=-1, # num_train_epochsに基づいて学習します。
    group_by_length=True, # 同じ長さのシーケンスをグループ化し、パディングを削減します。
    lr_scheduler_type="cosine", # 学習率スケジューラーのタイプ。
    warmup_ratio=0.03, # ウォームアップ比率。
    report_to="none", # レポート先を指定しない (wandbなどを使用しない場合)。
)

# トレーナーの初期化
# data_collatorは、モデルの入力形式に合わせてデータを整形します。
trainer = Trainer(
    model=model,
    train_dataset=tokenized_dataset["train"],
    args=training_args,
    data_collator=lambda data: {
        'input_ids': torch.stack([f['input_ids'] for f in data]),
        'attention_mask': torch.stack([f['attention_mask'] for f in data]),
        'labels': torch.stack([f['input_ids'] for f in data]), # 因果言語モデルでは、入力自体がラベルとなります。
    },
)

# 学習の開始
print("Fine-tuningを開始します...")
trainer.train()
print("Fine-tuningが完了しました。")

# --- 5. 学習済みモデルの保存 ---
# LoRAアダプターのみを保存します。これにより、ファイルサイズが小さく、効率的に管理できます。
trainer.save_model(OUTPUT_DIR)
print(f"学習済みLoRAアダプターが '{OUTPUT_DIR}' に保存されました。")

# 保存したアダプターを使って推論を行う方法の例 (コメントアウトされています):
# このコードは、ファインチューニング後にモデルをロードして推論を行うための参考例です。
# from peft import PeftModel
#
# # 元のモデルをロード (学習時と同じ量子化設定を使用します)
# base_model = AutoModelForCausalLM.from_pretrained(
#     MODEL_NAME,
#     quantization_config=bnb_config,
#     device_map="auto",
#     trust_remote_code=True
# )
#
# # 保存したLoRAアダプターを元のモデルに結合します。
# peft_model = PeftModel.from_pretrained(base_model, OUTPUT_DIR)
#
# # 推論モードに設定します。
# peft_model.eval()
#
# # テキスト生成の例
# prompt = "ローカルPCでのLlama 3ファインチューニングの利点とは"
# inputs = tokenizer(prompt, return_tensors="pt").to("cuda") # 入力をGPUに移動
#
# with torch.no_grad(): # 勾配計算を無効化し、メモリ使用量を削減
#     outputs = peft_model.generate(
#         **inputs,
#         max_new_tokens=200, # 生成する新しいトークンの最大数
#         do_sample=True, # サンプリングによる生成を有効化
#         top_p=0.9, # Nucleusサンプリングの閾値
#         temperature=0.7, # 生成の多様性を制御する温度
#         eos_token_id=tokenizer.eos_token_id # 終了トークンID
#     )
# print("\n--- 生成されたテキスト ---")
# print(tokenizer.decode(outputs[0], skip_special_tokens=True))

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, ;;), )。, アウト, アシスタント, アダプター, あたり, アップ, あなた, いくつ, ウォーム, エポック, エラー, エントリ, オープン, オプティマイザ, お金, クラウドサービス, グループ, クン, ゲーミング, コード, コア, ここ, こと, コミュニティ, コメント, これ, サイズ, サンプリング, サンプル, シーケンス, スクリプト, スケーリング, スケジューラー, スタイル, ステップ, スペック, すべて, ソース, そこら, タイプ, ダウンロード, タスク, ため, チューニング, ツール, データ, データセット, テーマ, ディレクトリ, テキスト, デバイス, デフォルト, トー, トークナイザー, とき, どれ, トレーナー, ドロップ, バイアス, パス, バッチ, パディング, パラメータ, バランス, ファイル, ファイルサイズ, ファインチューニング, ファインチューニングコード, フリーズ, プロ, プロンプト, マシン, マッピング, メモリ, モード, モデル, もの, ユーザー, よう, ライター, ライブラリ, ラベル, ランク, リモート, レベル, レポート, ローカル, ロード, ログ, 一般, 一部, 上記, 不要, 世界, 世界中, 並み, 並列, 予算, 付与, 以下, 以降, 企業, 使い, 使用, 係数, 保存, 個人, 優秀, 入力, 公開, 共有, 具体, 処理, 出力, 分割, 分散, 分野, 初期, 利点, 利用, 制御, 削減, 削除, 創造, 加速, 助け, 効率, 動作, 勾配, 十分, 参考, 反映, 可能, 向上, 味方, 因果, 場合, 多様, 夢物語, 大幅, 大量, 失敗, 学習, 完了, 完全, 完璧, 実現, 実行, 実質, 寄与, 対応, 専門, 導入, 少量, 工夫, 希望, 常識, 強力, 形式, 必要, 思い, 性能, 手元, 手法, 技術, 抜群, 指定, 指示, 挿入, 推奨, 推論, 提供, 整形, 新た, 方法, 日々, 明日, 明確, 明示, 時代, 時間, 最大, 最新, 最適, 有効, 未知数, 本格, 格段, 格納, 構築, 様々, 比率, 民主, 活用, 活発, 消費, 混合, 済み, 温度, 準備, 無効, 無料, 特定, 特権, 現実, 理由, 環境, 生成, 発生, 登場, 的確, 相性, 短時間, 確認, 秘訣, 移動, 程度, 管理, 節約, 精度, 終了, 結合, 結果, 続行, 能力, 自体, 自分, 自動的, 蓄積, 表現, 言語, 計算, 記事, 設定, 許可, 調整, 費用, 軽量, 追加, 通常, 適用, 選択, 重み, 重要, 量子, 開始, 開発, 関数, 閾値, 非常, 高速, 高額, 魅力,

x.gdのURL短縮機能を利用するためのコードを作りましたので、お裾分け.

2025.06.08

Logging

おはようございます.x.gdのURL短縮機能を利用するためのコードを作りましたので、お裾分けです、コードは下記になります.ドキュメントをぱぱっと見て作った感じなので不具合などが潜んでいるかもしれないけど恐らく大丈夫そうという事で木曜日の早朝にXでコードをシェアした形になります.

なお、x.gdからAPIの発行を行う作業をお忘れなく(こちらからAPIを発行).

因みに自分はt2t.jpというドメインを私有していて、こちらで自分専用の短縮URLを発行出来るようにしています.この頃は自分が作った短縮URLをあまり使ってないので、こちらをサービス化しようかな等と考えていたりします.

因みにどうするかは未定ですけどね.もしみんなが使用出来るようにしたら、このサイトでも告知します.

<?php
require 'config.php';

class ShortUrl
{

    public $apiKey = null;
    public $endPoint = 'https://xgd.io/V1/shorten?';

    public function __construct($api_key = null)
    {
        $this->apiKey = $api_key;
        return $this;
    }

    public function getShortUrl($url = null, $shortid = null, $analytics = true, $filterbots = false)
    {
        if (empty($this->apiKey)) {
            return json_encode(['status' => 0, 'message' => 'api-keyがありません']);
        }
        if (empty($url)) {
            return json_encode(['status' => 0, 'message' => 'URLがありません']);
        }

        if (isset($url) && !preg_match('/^http[s]?:\/\/(www\.)?/', $url)) {
            return json_encode(['status' => 0, 'message' => 'URLが無効です']);
        }

        if (isset($shortid) && !preg_match('/^([0-9a-zA-Z_]{6,15})$/', $shortid)) {
            return json_encode(['status' => 0, 'message' => '短縮コードが無効です']);
        }

        if (!is_bool($analytics)) {
            $analytics = true;
        }

        if (!is_bool($filterbots)) {
            $filterbots = false;
        }

        $parameter = [
            'key' => $this->apiKey,
            'url' => $url,
            'shortid' => $shortid,
            'analytics' => $analytics ? 'true' : 'false',
            'filterbots' => $filterbots ? 'true' : 'false'
        ];

        $response = @file_get_contents($this->endPoint . http_build_query($parameter));
        return $response;
    }
}

if($argv[1]){
    $url = $argv[1];
    $shortid = isset($argv[2]) ? $argv[2] : null;
    $analytics = isset($argv[3]) && $argv[3] === 'true' ? true : false;
    $filterbots = isset($argv[4]) && $argv[4] === 'true' ? true : false;   
    echo (new ShortUrl(APIKEY))->getShortUrl($url, $shortid, $analytics, $filterbots);
}

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, お裾分け, コード, こちら, サービス, サイト, シェア, そう, ため, ドキュメント, ドメイン, みんな, よう, 下記, 不具合, 作業, 使用, 利用, 告知, 大丈夫, 専用, 感じ, 早朝, 明日, 木曜日, 未定, 機能, 無効, 発行, 短縮, 私有, 自分,

万博の来場者数と5月23日以降の予測をバイブコーディングで作ってみました

2025.05.31

Logging

おはようございます.万博の来場者数と5月23日以降の予測をバイブコーディングで作ってみましたのでコードをシェアします.大阪万博の来場者数はYahooニュースの記事のデータを元にしています、其処から今後の予測を立ててもらいました.

https://zip358.com/tool/EXPO2025-OSAKA-KANSAI-JAPAN

因みに予測は簡易的なものなので、恐らく大きくハズレるとは思いますがそれなりの表が生成されるのが凄いなと思います.生成AIはOpenAIの無料枠を使用してデータと次のデータを元に10月13日までの予測を出力してという指示とJavaScriptやCSSを使用してグラフ化してという指示を出しただけで、コードを書いてくれます.

    const labels = [];
    const allVisitors = [];
    const generalVisitors = [];
    const staffVisitors = [];

    const rawData = [
      ["4/13", 146426, 22000], ["4/14", 70488, 17000], ["4/15", 63719, 16000], ["4/16", 73869, 15000],
      ["4/17", 82692, 15000], ["4/18", 93908, 15000], ["4/19", 108773, 15000], ["4/20", 95524, 16142],
      ["4/21", 99638, 17352], ["4/22", 103729, 17099], ["4/23", 99140, 17795], ["4/24", 108888, 17376],
      ["4/25", 110759, 17591], ["4/26", 122102, 17299], ["4/27", 102015, 16985], ["4/28", 121282, 17465],
      ["4/29", 97559, 17813], ["4/30", 101397, 17621], ["5/1", 105945, 17548], ["5/2", 104805, 17741],
      ["5/3", 120696, 17886], ["5/4", 136805, 16289], ["5/5", 126371, 16804], ["5/6", 76517, 16935],
      ["5/7", 91688, 16932], ["5/8", 105449, 18578], ["5/9", 105782, 19813], ["5/10", 128918, 21373],
      ["5/11", 121667, 18268], ["5/12", 117658, 17856], ["5/13", 123640, 18087], ["5/14", 129527, 17668],
      ["5/15", 129456, 17762], ["5/16", 132817, 17700], ["5/17", 123974, 16464], ["5/18", 117000, 16000],
      ["5/19", 129000, 17000], ["5/20", 125000, 18000], ["5/21", 129000, 19000], ["5/22", 134000, 18000],
      ["5/23", 157000, 18000]
    ];

    rawData.forEach(([date, total, staff]) => {
      labels.push(date);
      allVisitors.push(total);
      staffVisitors.push(staff);
      generalVisitors.push(total - staff);
    });

    const futureDays = 143;
    for (let i = 1; i <= futureDays; i++) {
      const futureDate = new Date(2025, 4, 13 + i); // 4月13日からスタート
      const label = `${futureDate.getMonth() + 1}/${futureDate.getDate()}`;
      labels.push(label);

      // 予測ロジック: 最近5日間の平均にランダムなゆらぎを加える
      const recentGeneral = generalVisitors.slice(-5);
      const avgGeneral = recentGeneral.reduce((a, b) => a + b, 0) / recentGeneral.length;
      const fluctuation = Math.sin(i / 5) * 5000 + (Math.random() - 0.5) * 8000;
      const predictedGeneral = Math.round(avgGeneral + fluctuation);

      const predictedStaff = 18000 + Math.round(Math.sin(i / 7) * 1000);
      const predictedTotal = predictedGeneral + predictedStaff;

      generalVisitors.push(predictedGeneral);
      staffVisitors.push(predictedStaff);
      allVisitors.push(predictedTotal);
    }

    const ctx = document.getElementById('visitorChart').getContext('2d');
    const chart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: labels,
        datasets: [
          {
            label: '来場者数(合計)',
            data: allVisitors,
            borderColor: 'rgba(75, 192, 192, 1)',
            backgroundColor: 'rgba(75, 192, 192, 0.1)',
            borderWidth: 2,
            tension: 0.3,
            fill: true
          },
          {
            label: '一般来場者数',
            data: generalVisitors,
            borderColor: 'rgba(255, 159, 64, 1)',
            backgroundColor: 'rgba(255, 159, 64, 0.1)',
            borderWidth: 2,
            tension: 0.4,
            fill: true
          },
          {
            label: '関係者数',
            data: staffVisitors,
            borderColor: 'rgba(153, 102, 255, 1)',
            backgroundColor: 'rgba(153, 102, 255, 0.1)',
            borderWidth: 2,
            tension: 0.4,
            fill: true
          }
        ]
      },
      options: {
        animation: {
          duration: 1500,
          easing: 'easeInOutQuart'
        },
        responsive: true,
        scales: {
          y: {
            title: {
              display: true,
              text: '人数'
            }
          },
          x: {
            title: {
              display: true,
              text: '日付'
            },
            ticks: {
              maxRotation: 90,
              minRotation: 45,
              maxTicksLimit: 50
            }
          }
        },
        plugins: {
          legend: {
            position: 'top',
          },
          title: {
            display: false
          }
        }
      }
    });

これは脅威だなって思う人もいると思いますが、自分は便利だなって思う方です.今後、数年でエンジニア職を奪われかねないという懸念もありますが、コード書きは無くならないじゃないかなって思う方です.理由は修正しないといけない事が必ず起きる事.動作はするが予期しない動作が発生した場合、素人には対応できないため.

未来は人が描いている斜め上の未来になる事が多く、どう転ぶかは「神のみぞ知る」だと思います.なのであまり脅威に思わずそうなった時に対応するというスタンスで良いのかなと.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エンジニア職, スタンス, バイブコーディング, ミゾ, ランダム, 万博, 予測, 予測ロジック, 人数, 其処, 動作, 大阪万博, 懸念, 指示, 日付, 月日, 月日以降, 生成, 脅威,

デジタルアドレスAPIのコードを書いてみました.書いたのは

2025.05.30

Logging

おはようございます.デジタルアドレスAPIのコードを書いてみました.書いたのは生成AIだけど一度で上手く正しいコードが生成出来たわけではなくて二、三回の指示出しを行って下記のコードが生成されました.

デジタルアドレスのAPIを使用するには企業もしくは個人事業者で屋号を登録されている方でないとAPIのアカウント登録は今のところ出来ないので、自分はリファレンスと生成AIが出力したコードを見て恐らく正しく処理されるだろうと思ったのでgist.githubに公開しました.

尚、引数にIPアドレスを渡さないといけない所があるけれど、これはサーバーのIPアドレスになります.


<?php

class JapanPostAPIClient
{
    private string $clientId;
    private string $secretKey;
    private string $clientIp;
    private ?string $accessToken = null;
    private ?array $lastResponse = null;

    public function __construct(string $clientId, string $secretKey, string $clientIp)
    {
        $this->clientId = $clientId;
        $this->secretKey = $secretKey;
        $this->clientIp = $clientIp;
    }

    public function authenticate(): self
    {
        $url = 'https://api.da.pf.japanpost.jp/api/v1/j/token';
        $data = json_encode([
            'grant_type' => 'client_credentials',
            'client_id' => $this->clientId,
            'secret_key' => $this->secretKey
        ]);

        $headers = [
            "Content-Type: application/json",
            "x-forwarded-for: {$this->clientIp}"
        ];

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $response = curl_exec($ch);
        $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpcode !== 200) {
            throw new Exception("Token request failed with status {$httpcode}: {$response}");
        }

        $responseData = json_decode($response, true);
        $this->accessToken = $responseData['token'] ?? null;
        $this->lastResponse = $responseData;

        return $this;
    }

    public function searchCode(string $searchCode, array $params = []): self
    {
        if (!$this->accessToken) {
            throw new Exception("Access token is not set. Please call authenticate() first.");
        }

        $defaultParams = [
            'page' => 1,
            'limit' => 10,
            'choikitype' => 1,
            'searchtype' => 1
        ];

        $queryParams = http_build_query(array_merge($defaultParams, $params));
        $url = "https://api.da.pf.japanpost.jp/api/v1/searchcode/{$searchCode}?{$queryParams}";

        $headers = [
            "Authorization: Bearer {$this->accessToken}",
            "Accept: application/json"
        ];

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception("Search request failed with status {$httpCode}: {$response}");
        }

        $this->lastResponse = json_decode($response, true);
        return $this;
    }

    public function getJson(): string
    {
        return json_encode($this->lastResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    }

    public function getArray(): ?array
    {
        return $this->lastResponse;
    }
}

// 使い方の例:
// $client = new JapanPostAPIClient('YOUR_CLIENT_ID', 'YOUR_SECRET_KEY', 'IPアドレス.xxx.xxx.xxx');
// echo $client->authenticate()->searchCode('1000001')->getJson();

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, アカウント登録, アドレス, コード, サーバー, デジタルアドレス, リファレンス, 下記, 企業, 使い方, , 個人事業者, 屋号, 引数, , 指示出し, 正しいコード, 生成, 自分,

トランプ大統領の2025年からの任期をカウントするJSコードです.

2025.05.24

Logging

おはようございます.トランプ大統領の2025年からの任期をカウントするコードです.まだまだトランプ政権は始まったばかりなんですが、もう自分としてはあんま嬉しい気持ちはしていません.いま儲かっているのはデイトレードを行っている人達かも知れないです.

// トランプ大統領の2025年からの任期をカウント
const termStart = new Date('2025-01-20');
const termEnd = new Date('2029-01-20');
const today = new Date();

const totalTermDays = Math.floor((termEnd - termStart) / (1000 * 60 * 60 * 24));

if (today < termStart) {
  console.log('任期はまだ始まっていません。');
} else if (today > termEnd) {
  const servedDays = totalTermDays;
  console.log('任期は終了しています。');
  console.log(`トランプ大統領は ${servedDays} 日間、在任しました。`);
} else {
  const servedDays = Math.floor((today - termStart) / (1000 * 60 * 60 * 24));
  const remainingDays = Math.floor((termEnd - today) / (1000 * 60 * 60 * 24));
  console.log(`任期中です。(${today.toISOString().split('T')[0]} 現在)`);
  console.log(`在任日数: ${servedDays} 日`);
  console.log(`残り日数: ${remainingDays} 日`);
  console.log(`任期全体の日数: ${totalTermDays} 日`);
}

秒でウクライナとロシアの戦争を止めると言っていたのに、泥沼化しています.トランプさんになってからアメリカは前より酷くなったのではとか思っていますが、アメリカ国民は満足しているのでしょうか🤔.

知らないけれど、結構たいへんそうな気がしています.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, アメリカ, アメリカ国民, あんま嬉しい気持ち, ウクライナ, カウント, コード, ディトレード, トランプ大統領, トランプ政権, ロシア, 任期, 任期中, 任期全体, 在任日数, 戦争, 日数, , 泥沼化, ,

配列で返して変数で受け取る事を分割代入というであってる.

2025.05.16

Logging

おはようございます.配列で返して変数で受け取る事を分割代入という…どんなコードか具体的に書いたのが下記です.

<?php

function getArray(): array {
  return [10, 20];
}

[$x, $y] = getArray();

echo "x: " . $x . "\n";
echo "y: " . $y . "\n";

// 要素数が合わない場合
function getMoreArray(): array {
  return [100, 200, 300];
}

[$p, $q] = getMoreArray(); // $q には 200 が代入され、300 は無視される
echo "p: " . $p . "\n";
echo "q: " . $q . "\n";

[$m, , $n] = getMoreArray(); // カンマで要素をスキップ
echo "m: " . $m . "\n";
echo "n: " . $n . "\n";

// 連想配列のようなキー指定も可能 (PHP 7.1以降)
function getAssocArray(): array {
  return ['name' => '太郎', 'age' => 30];
}

['name' => $userName, 'age' => $userAge] = getAssocArray();

echo "名前: " . $userName . "\n";
echo "年齢: " . $userAge . "\n";

?>

最初、凄くこれに違和感を覚えたのを覚えています.理由はカッコカッコで括ってあるから、なんだか配列を連想するのですよね.とても便利ですけどね、因みにPHP言語でも比較的に新しいバージョンだったら使用できます.

尚、PHPバージョン7.1以下だった場合はlistを使用すると同じようなことになります.

<?php

function getArray(): array {
  return [10, 20];
}

list($x, $y) = getArray();

echo "x: " . $x . "\n";
echo "y: " . $y . "\n";

// 要素数が合わない場合
function getMoreArray(): array {
  return [100, 200, 300];
}

list($p, $q) = getMoreArray(); // $q には 200 が代入され、300 は無視される
echo "p: " . $p . "\n";
echo "q: " . $q . "\n";

list($m, , $n) = getMoreArray(); // カンマで要素をスキップ
echo "m: " . $m . "\n";
echo "n: " . $n . "\n";

?>

それにしても分割代入って呼び名、直ぐに忘れそう・・・.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, $代, カッコカッコ, カンマ, キー指定, コード, スキップ, バージョン以下, 分割代, 分割代入, 呼び名, 変数, 太郎, 新しいバージョン, 最初, 要素, 言語, 連想配列, 違和感, 配列,

Photo by Amy Chandra on Pexels.com

tMDbのAPIを使用して今まで観た映画をリスト化してみましたよ.

2025.04.27

Logging

おはようございます.tMDbのAPIを使用して今まで観た映画をリスト化してみましたよ.sqliteの構造は下記になります.idはbigintと記載していますが、AUTO_INCREMENTの間違いです🐼.

Xにも呟きましたがAPIを使用して何かするのはこの頃飽きてきて触らなかったのですが、たまには触れてみようとコードを書いているうちに簡単に作れるようになってしまって少々物足りなさを感じています.

次回はAPIを使う側ではなくて作る側になろうと思っています.GW期間になると思いますが600円のレンタルサーバーのフリードメインを使ってツマラナイAPIみたいな失笑系を作ってみます.

なお、ソースコードの解説がなくてすみません、このソースコードを生成AIに投げてどのような処理か説明して言えば恐らく正解を教えてくれます.

明日へ続く

#真相をお話します.
少年と犬
Flow
curl -sS https://getcomposer.org/installer | php
composer require guzzlehttp/guzzle
composer require illuminate/database
<?php
require_once 'config.php';
require_once 'vendor/autoload.php';
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Capsule\Manager as DB;

class MovieList
{
    public $result = '';
    public $imgBasicUrl = 'https://image.tmdb.org/t/p/w500';
    public $filePath = FILEPATH_DIR_;
    public function __construct($filePath='')
    {
        $filePath = $filePath?$filePath:$this->filePath;
        if (!file_exists($filePath.'/movie-list.db')) {
            exit;
        }        
        $capsule = new Capsule();

        $capsule->addConnection([
            'driver'   => 'sqlite',
            'database' => $filePath.'/movie-list.db',
            'prefix'   => '',
        ]);

        $capsule->setAsGlobal();
        $capsule->bootEloquent();
    }

    public function getMovieList(string $q): void
    {
        $client = new \GuzzleHttp\Client();

        $response = $client->request('GET', 'https://api.themoviedb.org/3/search/multi?query=' . $q . '&include_adult=false&language=ja-JP&page=1', [
            'headers' => [
                'Authorization' => 'Bearer ' . API_KEY,
                'accept' => 'application/json',
            ],
        ]);
        $this->result = $response->getBody();
    }

    public function getResult(): void
    {
        $data = DB::table('list')->get();
        $lists = [];
        foreach ($data as $key => $value) {
           $result = unserialize($value->value);
            if(isset($result?->media_type) && $result->media_type == 'movie'){
                $lists[] = [
                    'id'=>$data[$key]->id,
                    'title'=>$result->title,
                    'img'=>$this->imgBasicUrl . $result->poster_path,
                ];
            }
        }
        print json_encode($lists);
    }

    public function saveCsvlist(): void
    {
        $fileData = explode("\n",file_get_contents('movie-list.csv'));
        $fileData = array_reverse($fileData);
        foreach($fileData as $line)
        {
            foreach(explode(',',$line) as $data){
                $q = urlencode($data);
                $movieList = new MovieList();
                $movieList->getMovieList($q);
                foreach((json_decode($movieList->result))?->results as $result){
                    DB::table('list')->insert([
                        'value' => serialize($result)
                    ]);
                }
            }
        }        
    }

    public function delete($id): void
    {
        
        DB::table('list')->where('id', '=', $id)->delete();
        print json_encode(['res'=>'true']);
    }

}
//(new MovieList())->saveCsvlist();
if($_SERVER['REQUEST_METHOD'] == 'POST')
{
    if(!isset($_POST['id'])){
        (new MovieList())->getResult();
    }else{
        //(new MovieList())->delete($_POST['id']);
    }   
}

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, コード, ソースコード, ツマラナイ, フリードメイン, レンタルサーバー, , , 処理, 失笑系, 少年, 構造, 次回, 正解, , 生成, 真相,

404画像をそのまま残しておくとSEOに影響があるのでどうするか?#php

2025.04.23

Logging

おはようございます.404画像をそのまま残しておくとSEOに影響があるのでどうするか?php の場合、下記のコードで404ページに飛んでいるかどうかの確認は出来る.でも毎回、確認しているとページを表示している時に遅延が発生してとても良い処理とは言えないなのでDBに画像あるなしが確認できるテーブルを作ります.

<?php
class HttpCodeChecker
{

    public function getHttpStatusCode(string $url): int|false
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_exec($ch);

        if (curl_errno($ch)) {
            curl_close($ch);
            return false; // エラーがあった場合
        }

        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return $httpCode;
    }
}

画像のステータス確認できるコードで定期的にページをクロールしてステータスをテーブルに保存(更新)することにより404画像によってSEOに影響を与えない作りになります.

他にも方法はあるけど、レンタルサーバーではこの方法がベストプラクティスな解なのかもしれない.クロールするのが出来ない場合は一度、画像URLをJsonデータで出力してフロントエンド側で画像有無を判断し存在するものだけを表示するという案もあるのだけども、今のSEO的にはあまりオススメはしない.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エラー, コード, ステータス, データ, テーブル, フロントエンド側, ベストプラクティス, レンタルサーバー, 下記, 保存, 影響, 方法, , 毎回, 画像, 画像有無, 確認, 良い処理, 遅延,

FF14のエオルゼア時間を表示するツールのソースコードを作りましたのでお裾分け.

2025.04.10

Logging

おはようございます.先人の知恵を拝借して人工知能にぶん投げてソースコードを作っていただきました.尚、ツワモノさんがいて天候などもいつ起きるのか把握できるそうです.どういうアルゴリズムなのかは見ていないので分からないけど自分でも可能そうな気もします.

因みにソースコードをこちらに貼り付けておきますのでご自由にご使用ください.それにしてもオンラインゲームなのに自分の狩り場には人っ子一人いないのが悲しいです.

function lt2et(date) {
  // UNIX時間(秒)を取得
  const ut = date.getTime() / 1000;

  // エオルゼア時間1/1/1 00:00:00から経過した秒数
  const et_seconds = (ut * 60.0) / 175.0;

  // 各エオルゼア単位を計算
  const et_hours = ut / 175.0;
  const et_days = et_hours / 24.0;
  const et_months = et_days / 32.0;

  const et_year = et_months / 12.0 + 1.0;
  const et_month = (et_months % 12.0) + 1.0;
  const et_date = (et_days % 32.0) + 1.0;
  const et_hour = et_hours % 24.0;
  const et_minute = et_seconds % 60.0;

  return {
    year: Math.floor(et_year),
    month: Math.floor(et_month),
    date: Math.floor(et_date),
    hour: Math.floor(et_hour),
    minute: Math.floor(et_minute),
  };
}
setInterval(() => {
  let date;

  date = new Date(); // 現在時刻

  const et = lt2et(date);

  // 出力(例: 0001-01-01 00:00)
  document.getElementById("output").textContent = `
  ${et.month.toString().padStart(2, "0")}-${et.date
    .toString()
    .padStart(2, "0")} ${et.hour.toString().padStart(2, "0")}:${et.minute
    .toString()
    .padStart(2, "0")}
`;
}, 0);

Lv30なったらチョコボに乗れるらしいのでいまはそれを楽しみにしてソロで狩り続けています.

最後にZen foxで活動しています.
※Hades【Mana】で活動しています.
フォロー等よろしくお願いいたします🙇.
https://jp.finalfantasyxiv.com/lodestone/character/34913141/

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, アルゴリズム, エオルゼア時間, オンラインゲーム, ソースコード, ソロ, チョコボ, ツワモノさん, 人工知能, , 出力, 各エオルゼア単位, 天候, 時刻, 最後, 狩り場, 知恵, , 秒数, 計算,

何もわからない??デフォルト. null合体演算子

2025.03.30

Logging

おはようございます.??(クエスチョンマーク二つ)ってのはnull合体演算子と言います.PHP以外にもあります.三項演算子などは知っている人いると思いますが、null合体演算子は知らない人もちらほらいるかもです.

因みにいま関わっているシステムには出てきますね.自分が開発にジョインしたときに自分もnull合体演算子を使用したことはあります.

三項演算子はまぁ適度に使用しますが深い階層にはしません.昔もあまり深い階層にはしなかったと思いますがしていたら後釜に迷惑が行ったかもです、そんな感じで深い階層にするのはあまり可読性が宜しく無い気がします.

null合体演算子は三項演算子よりもわかりやすいかもです.nullだったら別の値に変えてしまう処理だと思ってください.

サンプルコードリンクを貼っときます.
https://www.php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.coalesce

尚、null合体演算子は優先順位が低いそうですね.そのため下記のようなことする時はカッコで囲んであげないと正しい結果が得られないそうです.算術とかでもそうみたい.

<?php
// $name が未定義の場合、警告が発生します。
print 'Mr. ' . $name ?? 'Anonymous';
// "Mr. Anonymous" と出力
print 'Mr. ' . ($name ?? 'Anonymous');
?>

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, かっこ, クエスチョンマーク二つ, サンプルコードリンク, す算術, 三項演算子, , 優先順位, 処理, 出力, 可読性, 合体演算子, 後釜, , 未定義, 正しい結果, 深い階層, 警告, 迷惑, 開発,

アナログ時計をTOPページに追加しました.ソースコードはこちら.

2025.03.26

Logging

おはようございます.アナログ時計をTOPページに追加しました.ソースコードはこちらに記載していきます.尚、ソースコードは生成AI、Grokを使用して制作しました.

<div class="clock">
    <div class="hand hour-hand"></div>
    <div class="hand minute-hand"></div>
    <div class="hand second-hand"></div>
    <div class="center-clock"></div>
</div>
.clock {
  /* width: 200px;
  height: 200px;
  border: 2px solid rgb(226, 226, 226); */
  border-radius: 50%;
}

.hand {
  position: absolute;
  bottom: 50%;
  left: 50%;
  transform-origin: bottom;
  background: #333!important;
}

.hour-hand {
  width: 4px;
  height: 60px;
  background: #333!important;;
}

.minute-hand {
  width: 3px;
  height: 80px;
  background: #666!important;;
}

.second-hand {
  width: 2px;
  height: 90px;
  background: rgb(94, 93, 93)!important;;
}

.center-clock {
  width: 10px;
  height: 10px;
  background: rgb(95, 95, 95)!important;;
  border-radius: 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 4;
}
        function updateClock() {
            const now = new Date();
            const hours = now.getHours();
            const minutes = now.getMinutes();
            const seconds = now.getSeconds();

            // 角度の計算
            const hourDeg = (hours % 12 + minutes / 60) * 30; // 12時間で360度
            const minuteDeg = (minutes + seconds / 60) * 6; // 60分で360度
            const secondDeg = seconds * 6; // 60秒で360度

            // 針の回転
            document.querySelector('.hour-hand').style.transform = `translateX(-50%) rotate(${hourDeg}deg)`;
            document.querySelector('.minute-hand').style.transform = `translateX(-50%) rotate(${minuteDeg}deg)`;
            document.querySelector('.second-hand').style.transform = `translateX(-50%) rotate(${secondDeg}deg)`;
        }

        // 初回実行
        updateClock();
        // 1秒ごとに更新
        setInterval(updateClock, 1000);

生成AIが最初登場した時はここまで出来なかったので一回目の指示でちゃんとしたものを制作していけるようになったというのは正直なところ驚きです.やっぱり指数関数的に人工知能は成長するのかなとたまに思うことがありますが、それと相反する思いも持っています.

相反するとは、あと5年ぐらいで人工知能も頭打ちになるじゃないかなとも思っています.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, アナログ時計, ソースコード, ところ, 人工知能, , 初回実行, 回転, , , 指数, 指示, 最初, 生成, , 秒ごと, 角度, 計算, , 頭打ち,

クラシックAPIを使用してGitHub APIから草データを取得.

2025.03.12

Logging

おはようございます.クラシックAPIを使用してGitHub APIから草データを取得するコードをAPI使用しつつ手直してモノの数分で完成しました.こういうのを思うと少人数の開発でも希望を持てるなって思います.ちなみに自分は未だに無料枠でコードを生成してもらっています.

正直なところ、無料枠で事が足りるという印象ですね.コードの改善や命名は生成AIにやってもらった方が優秀です、自分の無能がよくわかります.

草データの使用方法はタイトル通りなので特に問題ないかなって思います.コンフィグファイルは書かずしても変数に代入してあげれば良いわけですからね.一応、github上にソースコードを掲載しています.

https://github.com/taoka3/GitHubGrass

良かったらいいね👍️(⭐️)宜しくお願い致します.

<?php
class GitHubGrass
{
    private string $token;
    private string $username;
    private array $weeks = [];
    private int $cellSize = 12;
    private int $padding = 2;
    private array $colors = [];

    public function __construct(string $username, string $token)
    {
        $this->username = $username;
        $this->token = $token;

        // 色の設定(GitHub風)
        $this->colors = [
            'level0' => [235, 237, 240],
            'level1' => [155, 233, 168],
            'level2' => [64, 196, 99],
            'level3' => [48, 161, 78],
            'level4' => [33, 110, 57]
        ];
    }

    /**
     * GitHub APIから草データを取得
     */
    public function fetchContributions(): bool
    {
        $url = 'https://api.github.com/graphql';
        $query = <<<'JSON'
        {
          user(login: "USERNAME") {
            contributionsCollection {
              contributionCalendar {
                weeks {
                  contributionDays {
                    contributionCount
                  }
                }
              }
            }
          }
        }
        JSON;

        // ユーザー名を埋め込む
        $query = str_replace("USERNAME", $this->username, $query);

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $this->token,
            'User-Agent: ContributionsApp'
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['query' => $query]));

        $response = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($response, true);
        if (!isset($data['data']['user']['contributionsCollection']['contributionCalendar']['weeks'])) {
            $this->weeks = []; // 空の配列を設定
            return false;
        }

        $this->weeks = $data['data']['user']['contributionsCollection']['contributionCalendar']['weeks'];
        return true;
    }

    /**
     * 草画像を生成
     */
    public function generateImage(): string
    {
        if (count($this->weeks)) {
            die('No data available.');
        }

        // 画像サイズ設定
        $width = (count($this->weeks) * ($this->cellSize + $this->padding)) + $this->padding;
        $height = (7 * ($this->cellSize + $this->padding)) + $this->padding;

        // 画像作成
        $image = imagecreatetruecolor($width, $height);
        $bgColor = imagecolorallocate($image, 255, 255, 255);
        imagefill($image, 0, 0, $bgColor);

        // 色の作成
        $colorPalette = [];
        foreach ($this->colors as $key => $rgb) {
            $colorPalette[$key] = imagecolorallocate($image, ...$rgb);
        }

        // セル描画
        foreach ($this->weeks as $x => $week) {
            foreach ($week['contributionDays'] as $y => $day) {
                $count = $day['contributionCount'];
                $color = $this->getColor($count, $colorPalette);

                // 四角形を描画
                imagefilledrectangle(
                    $image,
                    $x * ($this->cellSize + $this->padding) + $this->padding,
                    $y * ($this->cellSize + $this->padding) + $this->padding,
                    ($x + 1) * ($this->cellSize + $this->padding),
                    ($y + 1) * ($this->cellSize + $this->padding),
                    $color
                );
            }
        }

        // 出力
        ob_start();
        imagepng($image);
        $imageData = ob_get_contents();
        ob_end_clean();
        imagedestroy($image);

        $base64 = base64_encode($imageData);

        return $base64;
    }

    /**
     * コントリビューション数に応じた色を取得
     */
    private function getColor(int $count, array $colorPalette)
    {
        if ($count == 0) {
            return $colorPalette['level0'];
        } elseif ($count < 5) {
            return $colorPalette['level1'];
        } elseif ($count < 10) {
            return $colorPalette['level2'];
        } elseif ($count < 20) {
            return $colorPalette['level3'];
        } else {
            return $colorPalette['level4'];
        }
    }
}

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, $代, から草データ, クラシック, コード, コントリビューション数, コンフィグファイル, セル描画, ソースコード, 出力, 命名, 四角形, 変数, 少人数, 手直し, 描画, 未だ, 生成, 配列,

画像の存在有無確認用.NodeJSで動く一式を共有します😌.

2025.02.21

Logging

おはようございます.画像の存在有無確認用.NodeJSで動く一式を共有します😌.先日の朝ポストしたものの一式のファイルになります、URLはダミーです.また、このプログラムコードはあるKサイトの画像の存在有無確認のために作ったものになります.

https://github.com/taoka3/check-images-app

このプログラムコードをカスタマイズすることによって、404ページへリダイレクトするようなページでも画像の存在有無を確認することが可能になります.

因みにこのコードでは大量に画像存在有無を確認する場合には適していない為、修正が必要になります.どこを修正しないといけないかは、ご自身でコードを修正してお試しください.ヒントは同期処理を非同期処理にという事です.また、このコードでコンソールに結果を出力していますが、業務などで使用する場合はCSVなどに出力するのが適切なのかも知れません.

const { loadImage } = require("@napi-rs/canvas");

function checkImages(name,imageUrls)
{
  imageUrls.forEach(async (imgUrl) => {
    try {
      // canvasを使って画像が存在するか確認
      await loadImage(imgUrl); // 画像をロードして存在確認
      console.log(`name: ${name}, url: ${imgUrl}, exists: true`);
    } catch (error) {
      console.log(`name: ${name}, url: ${imgUrl} exists: false `);
    }
  });
}

プログラムコードは、朝にちゃちゃっと作ったものになります.なのでエレメントが無かったら落ちたりします.そういう欠陥はあるものの簡易的に画像の存在有無を確認したい場合などには役立つかと思います.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エレメント, コード, コンソール, ヒント, ファイル, プログラムコード, 一式, 修正, 同期処理, 存在有無, 存在有無確認, 存在有無確認用, 存在確認, , 業務, 欠陥, 画像存在有無, 結果, 非同期処理,

Vue.jsでコードを書き直してみたけどバニラJSが良い時もある技術的な話といえばそんな感じ.

2025.02.09

Logging

おはようございます.何もしていないわけではないけど毎日小銭が入ってきた頃を思うといまは前に戻った感じがあります、やっぱり無いよりある方が良いですね.さてHxH-moji.comサービスのバニラJSコードをVue.jsへ置き換えてみたのだけどもレンダリングが遅くなってこれは駄目だなと思ったので元の状態に戻しました.

たまにバックのソースコードがどんな感じに動いているのかを調べている人がいるのだけど、そんなに大したソースコードが動いているわけではないのでお恥ずかしいですね.最近やっと名前付けを変更してソースコードを見たらわかる感じにしたつもりでいます.

因みに下記がレンダリングが遅くてボツにしたコードです.本当ならVue.jsでサクサクと動くコードを作りたいのですがVue.jsはまだまだ初心者さんレベルです..あまりVue.jsもReactもあまり分からないのが現状です、業務で使用することはあるものの一から作るとなるとあんまり上手く作れないです.

トホホ・・・.

この頃思うのは広告掲載だけで何とか回るものを作りたいなとサブスクは個人ではちょっと荷が重い.重いもありますが何せ審査が通りそうにないECサービスはまだ手を付けていないのはそんな理由です.自分のテンション上がるのを待って作りたいと思います.

const { createApp, ref, onMounted, nextTick } = Vue;

const app = createApp({
    setup() {
        const canvas = ref(null);
        const languageInput = ref('');
        const fontsize = ref(16);
        const alertMessage = ref('');
        const mobileText = ref('');
        const isMobile = ref(window.innerWidth <= 768);

        const fetchText = async (flg) => {
            try {
                const body = new FormData();
                body.append('txt', languageInput.value);

                const response = await fetch("/common/jpcvn", {
                    method: "POST",
                    headers: { "X-CSRF-TOKEN": document.querySelector("[name='token']").value },
                    body: body
                });
                const resultdata = await response.json();
                drawCanvas(resultdata.txt);
                mobileHxH(resultdata.txt);

                canvas.value.toBlob((blob) => {
                    const formData = new FormData();
                    formData.append("upload_data", blob, "canvas_image.png");
                    formData.append("name", document.getElementsByName("name")[0].value);
                    formData.append("sns_name", document.getElementById("sns").value);
                    formData.append("text_data", resultdata.txt);
                    if (!flg) formSave(formData);
                }, "image/png");
            } catch (error) {
                console.error(error);
            }
        };

        const drawCanvas = async (text) => {
            await nextTick();
            if (!canvas.value) return;  // canvasがnullの場合は処理を中止
            const ctx = canvas.value.getContext("2d");
            ctx.clearRect(0, 0, canvas.value.clientWidth, canvas.value.clientHeight);

            const background = new Image();
            background.src = "/images/paper_00107.jpg";
            background.onload = () => {
                ctx.drawImage(background, 0, 0, canvas.value.clientWidth, canvas.value.clientHeight);
                ctx.fillStyle = "rgba(0, 0, 0)";
                ctx.font = `${fontsize.value}px HUNTERxHUNTER`;
                const lines = textChange(text, fontsize.value);
                lines.forEach((line, i) => {
                    ctx.fillText(line, 3, 10 + fontsize.value * (1.1618 * i + 1));
                });
            };
        };


        const textChange = (t, f) => {
            let n = parseInt(350 / f);
            let s = t.split("");
            let LLine = 0;
            for (let i = 0; i < s.length; i++) {
                if (LLine && (LLine + 1) % n === 0) {
                    if (s[i] !== "\n") s[i] += "\n";
                    LLine = 0;
                } else {
                    LLine++;
                    if (s[i] === "\n") LLine = 0;
                }
            }
            return s.join("").split("\n");
        };

        const formSave = async (formData) => {
            alertMessage.value = '';
            try {
                const response = await fetch("/common/image_save", {
                    method: "POST",
                    headers: { "X-CSRF-TOKEN": document.querySelector("[name='token']").value },
                    body: formData,
                });
                const result = await response.json();
                if (result.ret === "ok" && result.url) {
                    window.location.href = result.url;
                } else {
                    alertMessage.value = result.txt;
                }
            } catch (error) {
                console.error(error);
            }
        };

        const mobileHxH = (tt) => {
            if (isMobile.value) {
                mobileText.value = tt;
            }
        };

        const adjustTextareaRows = () => {
            isMobile.value = window.innerWidth <= 768;
        };

        onMounted(() => {
            adjustTextareaRows();
            window.addEventListener("resize", adjustTextareaRows);
        });

        return {
            canvas,
            languageInput,
            fetchText,
            alertMessage,
            mobileText,
            isMobile,
            fontsize
        };
    }
});

app.mount('#app');

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, コード, サブスク, ソースコード, テンション, トホホ, バニラコード, ボツ, ももあまり, レンダリング, 下記, 中止, 何せ審査, , 処理, 名前付け, 小銭, 業務, 現状, ,

Photo by FOX on Pexels.com

8割のドメインをVPSサーバーで運用に切り替えました.あとの2割

2025.01.25

Logging

おはようございます.8割のドメインをVPSサーバーで運用に切り替えました.あとの2割も切り替える予定です.正直な所、もう一ランク上のVPSサーバーで運用するべきだったと後悔しています.メモリ2Gあれば十分かと思っていたけどそうでもない.

因みに移行していないドメインはここのドメインとHドメインとTドメインになります.こちらのドメインも来週中に移行する予定です.ただメモリの関係上、移行することができない場合もあります.たぶんギリ大丈夫そうなんですけどねぇ.

サーバー移行の際にエンジンエックスプロキシ(リバースプロキシ)のコンテナ起てて、ドメインをそれぞれのコンテナに振り分けて運用しているわけですが、WordPressのサーバーがドメインを叩くと無限ループに陥るのですね.この解決策を探るのにかなり時間を費やしました.

解決策はwp-config.phpにこれを記載すれば無限ループから開放されます.

define('FORCE_SSL_ADMIN', true);
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ) {
       $_SERVER['HTTPS']='on';
}

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エンジンエックスプロキシ, かなり時間, キリ, こんてな, サーバー, サーバー移行, それぞれ, ドメイン, メモリ, リバースプロキシ, , , 無限ループ, 解決策, 運用, 関係上, ,

グーグルカレンダーの予定を一部表示させたい時のPHPコード.

2025.01.08

Logging

おはようございます.グーグルカレンダーの予定を一部表示させたい時のPHPコードは下記になります、参考にしたサイトはこちらですが参考にしただけでコードは違うものになります.APIを毎日叩いていると課金しなくてはならないかもなのでデータベースに保存する形にしました.これも良い方法かと言えばどうだろうと言う思いもあります.

<?php
ini_set('display_errors', 0);
require_once './vendor/autoload.php';
require_once './config.php';

use Carbon\Carbon;
use Google\Client;
use Google\Service\Calendar;
use Illuminate\Database\Capsule\Manager as DB;

class openSchedule
{
    public $DB = null;
    public function __construct()
    {
        $database = new DB();
        $database->addConnection([
            'driver' => 'mysql',
            'host' => DB_HOST,
            'port' => PORT,
            'database' => DB_DATABASENAME,
            'username' =>  DB_USER,
            'password' => DB_PASSWORD,
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_general_ci',
        ]);

        $database->setAsGlobal();
        $database->bootEloquent();
        $this->DB = $database;
    }

    public function saveSchedule($dateTimeString):void
    {
        //$this->DB::table('schedules')->delete();
        try {
            $client = new Client();
            $client->useApplicationDefaultCredentials();
            $client->addScope(Calendar::CALENDAR_READONLY);
            $client->setAuthConfig(JSONPATH);
            $service = new Calendar($client);
           // カレンダーID
            $calendarId = GOOGLECALENDARID;

            // 取得時の詳細設定
            $optParams = [
                'maxResults' => 10,
                'orderBy' => 'startTime',
                'singleEvents' => TRUE,
                'timeMin' => (new \DateTime($dateTimeString))->format(DATE_RFC3339),
                'timeZone' => 'Asia/Tokyo',
            ];
            $results = $service->events->listEvents($calendarId, $optParams);
            if (empty($results->getItems())) {
                echo "イベントが見つかりませんでした。";
            } else {
                foreach ($results->getItems() as $event) {
                    $this->DB::table('schedules')->insert([
                        'start' => Carbon::parse($event->getStart()->dateTime)->format('Y-m-d H:i:s'),
                        'end' => Carbon::parse($event->getEnd()->dateTime)->format('Y-m-d H:i:s'),
                        'title' => $event->getSummary(),
                        'detail'=>$event->getDescription()
                    ]);
                }
            }

        } catch (Google\Service\Exception $e) {
            echo 'Google Service Exception: ' . $e->getMessage();
        } catch (Exception $e) {
            echo 'General Exception: ' . $e->getMessage();
        }
    }

    public function getWeekDays($dateTimeString):string
    {
        // Carbonインスタンスに変換してフォーマット
        $carbon = Carbon::parse($dateTimeString);

        // 日本語の曜日名配列
        $japaneseWeekdays = ['日', '月', '火', '水', '木', '金', '土'];

        // 曜日の数値を取得し、日本語の曜日名に変換
        $weekdayNum = $carbon->dayOfWeek;
        $japaneseWeekday = $japaneseWeekdays[$weekdayNum];
        return $japaneseWeekday;
    }

    public function getSchedule($dateTimeString):string
    {   
        $str = '';
        $events = $this->DB::table('schedules')->select(['*'])->where('start','>=',$dateTimeString)->limit(1)->get();
        foreach($events as $event){
            
            $str.= sprintf("%s <br>",$event->title);
            $str.= sprintf("%s %s曜日 <br>",Carbon::parse($event->start)->format('Y年m月d日 H時i分'),$this->getWeekDays($event->start));
            $str.= sprintf("%s<br>",Carbon::parse($event->end)->format('Y年m月d日 H時i分'));
        }
        return $str;
    }

    public function getLastDate():string|null
    {
        $lastRecord = $this->DB::table('schedules')
                   ->orderBy('id', 'desc')
                   ->first();
        return $lastRecord?$lastRecord->start:null;
    }
}

//print (new openSchedule)->getSchedule((new \DateTime())->format('Y-m-d 00:00:00'));

if(isset($argv) && $argv[0]){
    $openSchedule = new openSchedule();
    $dateTimeString = $openSchedule->getLastDate();
    if($dateTimeString){
        $date = new DateTime($dateTimeString);
        $date->modify('+1 day');
        $openSchedule->saveSchedule($date->format('Y-m-d 00:00:00'));
    }else{
        $openSchedule->saveSchedule((new \DateTime())->format('Y-m-d H:i:s'));
    }   
}

因みに近日中にブログに仕事の予定などを記載するようにします.近日中なのでいつになるかは未定です.

そうそうCalendarIDという物がグーグルデベロッパサイトにあると思い込んでいて馬鹿な事をしていました.皆さんは間違わないように(笑).CalendarIDはグーグルカレンダーの設定の中にあります.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, インスタンス, カレンダー, グーグルカレンダー, グーグルデベロッパサイト, コード, データベース, フォーマット, 参考, 変換, 年月日, 数値, 時分, 曜日, 曜日名, 曜日名配列, , 皆さん, 近日中,

blueskyAPIが処理されなくなったので、対応を行った話.

2025.01.06

Logging

おはようございます.今日から仕事始めの方も多いはず自分もそんな感じです.さてblueskyAPIが処理されなくなったので対応を行った話を書いていきます.ブルースカイの独自処理が上手く処理されなくなったのでその対応を行っていました.今まで公式に落ちているPHP言語のライブラリーを使用していたんだけど、レンタルサーバーの環境が変わった関係により処理がされなくなったので、自前のAPI処理を他のエンジニアが公開されているコードを参考にしてカード板自動投稿を作りました.

ソースコードは下記になります.

    public function cardPost($text, $imagePath = null, $link = null)
    {
        $imageUri = $imagePath ? $this->uploadImage($imagePath) : null;

        $record = [
            "\$type" => "app.bsky.feed.post",
            "text" => $text,
            "createdAt" => Carbon::now()->format('c'),
        ];

        if ($imageUri && $link) {
            $record['embed'] = [
                "\$type" => "app.bsky.embed.external",
                "external" => [
                    "uri" => $link,
                    "title" => $text,
                    "description" => $text,
                    "thumb" => $imageUri
                ]
            ];
        }

        $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.createRecord");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: application/json",
                "Authorization: Bearer {$this->jwt}",
            ],
            CURLOPT_POSTFIELDS => json_encode([
                "repo" => $this->handle,
                "collection" => "app.bsky.feed.post",
                "record" => $record,
            ]),
        ]);

        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true);
    }

これでカード型のポストが出来ます.参考にしたサイトはこちらの@ma7ma7pipipiさんのソースコードになります.そのコードにカード型のコードを追加した形になります.

https://qiita.com/ma7ma7pipipi/items/bf7fda65ee71c873c70a

一からコードを書かないで良かったのでとても助かりました.感謝ですね😌、ありがとうございます.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エンジニア, カード型, カード板自動投稿, コード, ソースコード, ライブラリー, レンタルサーバー, 下記, 仕事始め, 公式, 処理, 参考, 多いはず自分, , 感謝, 環境, 自前, 言語,

ビットフライヤーはビットコインのビットコインをリアルタイムで受信するphpコード

2024.12.30

Logging

おはようございます.ビットフライヤーはビットコインのビットコインをリアルタイムで受信するphpコードを書きましたのでお裾分けです.以前書いていたものとは違う感じになります.ビットフライヤーさんのAPIを参照するとルビやJSの例コードはあるものの、何故かPHP言語のコードが無いので書いてみた形になります.ベースは生成AIに出力してもらったの物になります.それを自分の方がクラス化して使いやすくした形になります.

<?php
require 'vendor/autoload.php';

use WebSocket\Client;

class bitflyer
{
    public $client = null;
    public $channelName = "lightning_board_snapshot_BTC_JPY";
    public $wsUrl = 'wss://ws.lightstream.bitflyer.com/json-rpc';

    public function connecting()
    {
        try {
            // WebSocketクライアントを初期化
            echo "Connecting to WebSocket server...\n";
            $this->client = new Client($this->wsUrl);

            echo "Connection established. Subscribing to channel...\n";

            // サブスクライブメッセージを送信
            $subscribeMessage = json_encode([
                'method' => 'subscribe',
                'params' => ['channel' => $this->channelName],
                'id' => null,
            ]);
            $this->client->send($subscribeMessage);
        } catch (\WebSocket\ConnectionException $e) {
            echo "WebSocket connection error: " . $e->getMessage() . "\n";
            sleep(1); // 1秒待機して再接続
            $this->client = null;
            $this->connecting()->receive();
        } catch (Exception $e) {
            echo "General error: " . $e->getMessage() . "\n";
            $this->client = null;
            $this->connecting()->receive();
        }
        return $this;
    }

    public function receive()
    {
        try {
            // メッセージ受信処理
            while (true) {
                $message = $this->client->receive();

                // 受信データが空でないか確認
                if (!empty($message)) {
                    $data = json_decode($message, true);

                    // デコードが成功したか確認
                    if (json_last_error() === JSON_ERROR_NONE) {
                        if (isset($data['method']) && $data['method'] === 'channelMessage') {
                            print_r($data['params']);
                        }
                    } else {
                        echo "JSON デコード エラー: " . json_last_error_msg() . "\n";
                    }
                } else {
                    echo "返却値 空.\n";
                }
            }
        } catch (Exception $e) {
            echo $e->getMessage();
            $this->client = null;
            $this->connecting()->receive();
        }

        echo "Ctrl+C にて終了\n";
        return $this;
    }
}

(new bitflyer)->connecting()->receive();

尚、レンタルサーバーなんかでこのコードを動かすのは禁止されていますので、必ずローカルマシンで動かしてください.もし何かトラブルになっても保証出来ないので注意してください.因みに自分はDocker内で動かしています.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エラー, お裾分け, クライアント, コード, サブスクライブメッセージ, デコード, ビットコイン, ビットフライヤー, ビットフライヤーさん, ベース, リアルタイム, ルビやの例コード, レンタルサーバー, ローカルマシン, , 接続, 秒待機, 言語, 返却値,

ガチで知らなかった.もう何年もPHP言語使っているのに恥

2024.12.28

Logging

おはようございます.ガチで知らなかった.もう何年もPHP言語使っているのに恥…下記のコードで動くことを知らなかった.そもそもそんな返却の仕方が出来ないと思い込んでいたのでlist関数を使用していた.

function abc(){
    return ['a','b','c'];
}
[$a,$b,$c] = abc();
echo "a={$a} b={$b} c={$c}";

改修とか機能追加とか、新規開発で他の人のコードとかを見るけど、そういう使い方をしている人を見たことがなかったので、知る由もなく今の今まで来てしまった.これphpのサンドボックスでバージョンの違うものを実行してみたらphp7.4系でも動いたので絶句してしまった.因みに7.0系は動かない.

自分はphp5系のからPHP言語を主とした仕事をしてきたわけです.仕事で使用するコードってある程度似ているので同じような関数などを使う反面、バージョンアップで追加された機能を使うことはほぼ無い.非対応になった関数を置き換えることはあっても.そういう事もあり今の今まで知らずにいた.

知るきっかけになったのは、プルリクエストしマージ後に自動でコードの正規化が行われた時にコードが置き換わった事によることで知ることに…

知るは一時の恥、知らぬは一生の恥.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, ガチ, きっかけ, コード, サンドボックス, バージョン, バージョンアップ, プルリクエストしマージ後, 一生, , 仕方, 反面, , 恥下記, 改修, 正規化, , 言語, 返却, 関数,

クリスマスなのでブラウザに雪を降らすJSコードを生成AIで生成.

2024.12.25

Logging

おはようございます.クリスマスなのでブラウザに雪を降らすJSコードを生成AIで生成.今年も彼女がいないクリスマスを過ごしました.彼女になってくれる人お待ちしております、この姿勢が彼女が出来ない要素なんだということも分かっていますが.

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>雪</title>
    <style>
        #snowContainer {
            position: absolute;
            /* 親要素に対する絶対配置 */
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            /* 雪が画面外に出ないようにする */
            pointer-events: none;
            /* 雪がマウスイベントを邪魔しないようにする */
            z-index: 9999;
            /* 他の要素より前面に表示 */
        }

        .snowflake {
            position: absolute;
            width: 10px;
            height: 10px;
            background-color: white;
            border-radius: 50%;
            /* 丸くする */
            opacity: 0.7;
            /* 少し透明にする */
            animation: snowfall linear infinite;
            /* アニメーションを指定 */
        }

        @keyframes snowfall {
            0% {
                transform: translateY(-10px);
            }

            /* 開始位置 */
            100% {
                transform: translateY(100vh);
            }

            /* 終了位置(画面下部) */
        }
        html{
            background-color: black;
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>
<body>
    <div id="snowContainer"></div>
    <script>
        const snowContainer = document.getElementById('snowContainer');
        const numSnowflakes = 50; // 雪片の数

        for (let i = 0; i < numSnowflakes; i++) {
            const snowflake = document.createElement('div');
            snowflake.classList.add('snowflake');
            snowflake.style.left = `${Math.random() * 100}vw`; // 横方向のランダムな位置
            snowflake.style.animationDuration = `${Math.random() * 3 + 2}s`; // アニメーション速度をランダムに
            snowflake.style.animationDelay = `${Math.random()}s`; // アニメーション開始時間をランダムに
            snowflake.style.fontSize = `${Math.random() * 10 + 5}px`;
            snowContainer.appendChild(snowflake);
        }
    </script>
</body>
</html>

そんな自分でも好きですという奇特な人はいないものかな…(受け身(笑)

そんな自分が生成AIに雪を降らすJSコードを頼んだところ、俊足の解を出してくれました.なんて時代なんだと思いながら出力されたコードをhtmlファイルにペーストしてhtml背景を黒にしてあとは一応、確認.

最後に雪が降っているデモサイトを貼っときます.

https://zip358.com/tool/snow

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, アニメーション, アニメーション速度, クリスマス, マウスイベント, ランダム, 俊足, 前面, , 姿勢, 最後, 生成, 生成今年, 画面下部, 背景, 要素, 親要素, , 配置, 雪片,

大晦日までカウントダウンしてまたカウントダウンするJSコード #永遠

2024.12.18

Logging

おはようございます.大晦日までカウントダウンしてまたカウントダウンするJSコードだけではツマラナイので全て漢字に変換して表示するコードを書きました.これでもツマラナイと思う人もいると思います.

そんなに難しいコードでもないのでコードを添付します💁.

        function updateCountdown() {
            const now = new Date();
            const nextNewYear = new Date(now.getFullYear() + 1, 0, 1, 0, 0, 0); // 次の年の1月1日0時0分0秒
            const diff = nextNewYear - now; // ミリ秒差

            if (diff > 0) {
                const days = Math.floor(diff / (1000 * 60 * 60 * 24));
                const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
                const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
                const seconds = Math.floor((diff % (1000 * 60)) / 1000);

                // 表示を更新
                document.getElementById("countdown").innerText =
                    kanji(`${days}日 ${hours}時間 ${minutes}分 ${seconds}秒`);
            } else {
                document.getElementById("countdown").innerText = "明けましておめでとうございます!";
            }
        }

        // 初回呼び出しと1秒ごとの更新
        updateCountdown();
        setInterval(updateCountdown, 1000);
        function kanji(str){
            let oo = [{'kanji':'零'},{'kanji':'壱'},{'kanji':'弐'},{'kanji':'参'},{'kanji':'肆'},{'kanji':'伍'},{'kanji':'陸'},{'kanji':'漆'},{'kanji':'捌'},{'kanji':'玖'}];
            oo.forEach((o,index)=>{
                str = str.replace(new RegExp(index, "g"),o.kanji);
            });
            return str;
        }

もっと芸のあるカウントダウンを作ろうとするとp5jsなどのライブラリが必要になると思います.WEBサイトでパーティカルなどを行っているサイトは大体こういうライブラリを使用しています.p5jsなどを使用して昔作ってみようかななどと思ったことが有りますが、実はp5jsは一度も触ったことがないです.

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, カウント, カウントダウン, コード, ツマラナイ, パーティカル, ライブラリ, , 全て漢字, 初回呼び出し, , 大晦日, 月日時分秒, 漆#, 玖#, , 秒ごと, 肆#, , 陸#,

webpushのLaravelを使用しないパターンを箇条書き、フロントエンド側は

2024.11.14

Logging

おはようございます.webpushのLaravelを使用しないパターンを箇条書き、フロントエンドは過去記事を参照ください.まずwebpushを使用するにはパブリックキーとシークレットキーが必要になります.下記のURLより発行してみてください.

https://web-push-codelab.glitch.me

フロントエンド側でサービスワーカーの登録された識別データをバックエンドに送信.

// db.php: データベース接続の設定
function getDbConnection() {
    $host = 'localhost';
    $dbname = 'your_database';
    $username = 'your_username';
    $password = 'your_password';

    try {
        $pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $pdo;
    } catch (PDOException $e) {
        die("DB接続エラー: " . $e->getMessage());
    }
}

// endpoint_register.php: エンドポイントをデータベースに登録
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $endpoint = $_POST['endpoint'];
    $auth = $_POST['auth'];
    $p256dh = $_POST['p256dh'];

    if ($endpoint && $auth && $p256dh) {
        $pdo = getDbConnection();
        $stmt = $pdo->prepare("INSERT INTO push_subscriptions (endpoint, auth, p256dh) VALUES (?, ?, ?)");
        $stmt->execute([$endpoint, $auth, $p256dh]);
        echo "登録成功";
    } else {
        echo "エンドポイントデータが不完全です";
    }
}

バックエンド側で下記のようなコードでデータを保存します.

次にminishlink/web-pushというライブラリを使用し登録されたデータを元に送信処理を行います.

composer require minishlink/web-push
// message_send.php: データベースからエンドポイントを取得し、プッシュ通知を送信
require 'db.php';
require 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

// VAPIDキーの設定
$auth = [
    'VAPID' => [
        'subject' => 'https://example.com',
        'publicKey' => 'YOUR_PUBLIC_VAPID_KEY',
        'privateKey' => 'YOUR_PRIVATE_VAPID_KEY',
    ],
];

$webPush = new WebPush($auth);

// データベースからサブスクリプション情報を取得
$pdo = getDbConnection();
$stmt = $pdo->query("SELECT endpoint, auth, p256dh FROM push_subscriptions");
$subscriptions = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($subscriptions as $sub) {
    $subscription = Subscription::create([
        'endpoint' => $sub['endpoint'],
        'publicKey' => $sub['p256dh'],
        'authToken' => $sub['auth'],
    ]);

    // 送信するメッセージ
    $message = json_encode(['title' => '通知タイトル', 'body' => 'メッセージ内容']);

    // プッシュ通知の送信
    $webPush->sendOneNotification($subscription, $message);
}

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, エンドポイント, キー, コード, サービスワーカー, サブスクリプション情報, シークレットキー, データベース, データベース接続, パターン, バックエンド, バックエンド側, パブリックキー, プッシュ通知, フロントエンド, フロントエンド側, ライブラリ, 接続エラー, 識別データ,

Push通知ってブラウザ閉じても通知出来る様に出来るのか?

2024.11.04

Logging

おはようございます.Push通知ってブラウザ閉じても通知出来る様に出来るのか?答えは出来るのですが無料でその機能を実装できるのか.こたえはYesに近い?.有料のサービス機能push7を使用すればもっと簡単に可能です.

サービスワーカーとかいう機能を使えば良いみたいですね.知らないは一時の恥ですね.サービスワーカーとGCPやララベルの拡張Webpushなどを使えば出来そうですがまだ試していません.

因みにPusherサービスを使用して実装しました.当分、無料枠で対応可能な感じですね💁.

下記はリアルタイムPush通知の動作とソースコードの一部になります.

<?php
namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class NotificationEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $title;
    public $message;
    public $userId;

    public function __construct($title, $message,$userId='')
    {
        $this->title = $title;
        $this->message = $message;
        $this->userId = $userId;
    }

    public function broadcastOn()
    {
        return new Channel('notifications.' . $this->userId);
    }

    public function broadcastAs()
    {
        return 'notification-event';
    }
}

明日へ続く

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, サービス, サービスワーカー, サービス機能, ソースコード, ブラウザ, ララベル, リアルタイム通知, 一部, 下記, 動作, , 拡張, 有料, 機能, 無料, 無料枠, 答え, 通知,

高知県のダム貯水率グラフ化2

2020.03.26

Logging

以前、高知県のダム貯水率のグラフ化してみた事があるのですがいつの間にかデータが取得できなくなっていたようです。なので取得できるようにPHPコードを変更しました。高知県のダム貯水率をJSON形式で吐き出してほしいなと思う人は少数ながらいると思います。そんな方は私がJSON形式で吐き出したデータを密かに使っていたりするのかな?因みにプログラム出来るよというひとは自身で貯水率のデータが転がっているURLを掲載しますので解析して頂ければ有り難いと思います。

https://suibo-kouho.suibou.bousai.pref.kochi.lg.jp/suibou/main.html?no=4&no2=0&fnm=openTable

<?php
$html = file_get_contents("高知県のダム貯水率のURL");
$html = mb_convert_encoding($html,"UTF-8","SJIS");
$dom = new DOMDocument();
$html = mb_convert_encoding($html, "HTML-ENTITIES", 'UTF-8');
@$dom->loadHTML($html,LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$xpath = new DOMXPath($dom);
$ary=array(
    "永瀬ダム"=>"nagase",
    "鎌井谷ダム"=>"kamaidani",
    "鏡ダム"=>"kagami",
    "早明浦ダム"=>"sameura",
    "桐見ダム"=>"kirimi",
    "坂本ダム"=>"sakamoto",
    "大渡ダム"=>"oodo",
    "中筋川ダム"=>"nakasugawa",
    "以布利川ダム"=>"iburigawa"
);
for($i=1;$i<=9;$i++){
    $r[$xpath->query("/html/body/form/div[2]/table/tbody/tr[$i]/td[3]")->item(0)->textContent] = preg_replace("/[\x{00a0}|?]/u","",$xpath->query("/html/body/form/div[2]/table/tbody/tr[$i]/td[7]")->item(0)->textContent);
}
foreach ($r as $key => $value) {
    $obj[$ary[$key]] = $value;
}
//echo '{"nagase":"91.16","kamaidani":"77.57","kagami":"39.32","sameura":"100.00","kirimi":"46.09","sakamoto":"99.38","oodo":"98.70","nakasugawa":"77.90","iburigawa":"99.12"}';
print json_encode($obj);

https://zip358.com/tool/demo6/index.html

著者名  @taoka_toshiaki

※この記事は著者が40代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, グラフ化, コード, ダム貯水率, ひと, 中筋川ダム, 以布利川ダム, 坂本ダム, 大渡ダム, 少数, 形式, 明浦ダム, 桐見ダム, 永瀬ダム, 自身, 貯水率, 鎌井谷ダム, 鏡ダム, 高知県,

MYSQLのif文みたいなものを使用する機会が物凄く少ない気がする。

2019.05.18

Logging

### mysql case = if文みたいなもの
```sql
select namae,tensu
case when (tensu >= 75 and tensu <= 80)  then '可'
when (tensu >= 25 and tensu < 75)  then '否' else '???' end as kahi
from tbl;
table name tbl
|namae|tensu|
|---|---|
|hirose|77|
|non|76|
|arimura|78|
|aragaki|80|

mysql case = if文みたいなものを使用する機会が物凄く少ない気がする。
プログラム言語って使用しなければ抜け落ちるです。
自分の場合、たぶん長期的な記憶に障害があって抜け落ちるですw🙄。
メモしたことすら忘れてしまう…これは痛い老害?。

でも、好きな事は大体覚えていますね、概念的な事を覚えていたら大丈夫です😌。
 
 

著者名  @taoka_toshiaki

※この記事は著者が30代前半に書いたものです.

Profile
高知県在住の@taoka_toshiakiです、記事を読んで頂きありがとうございます.
数十年前から息を吸うように日々記事を書いてます.たまに休んだりする日もありますがほぼ毎日投稿を心掛けています😅.
SNSも使っています、フォロー、いいね、シェア宜しくお願い致します🙇.
SNS::@taoka_toshiaki

OFUSEで応援を送る

タグ

```, 25, 39, 75, 76, 77, 78, 80, and, aragaki, arimura, as, case, else, end, from, gt, hirose, if, kahi, lt, MYSQL, namae, name, non, select, SQL, TABLE, tbl, tensu, then, when, こと, プログラム, メモ, もの, 使用, , , 場合, , 機会, , 自分, 言語, 記憶, 障害,