URLからhtmlのリンクタグを作成するpythonスクリプト

11月になりようやく肌寒くなってまいりましたね。
個人的にはもう少し暖かい気候が続いてほしかったです。
暑くも寒くもない方が過ごしやすいので。
(先週末記事を書いたのですが、今週は暖かそうですね)

それはさておき今回は題の通り、
URLを与えてhtmlのリンクタグを作成するプログラムを作成した話となります。
ただそれだけのプログラムなのですが、
毎度参考のリンクのタグを手作業で作るのが面倒だったのと、
検索してみたところurlのタイトルを自動で取得してリンクタグを作成してくれるサービスは
特に見当たらなかったため、自前で作成した次第です。

今回は実行のしやすさを考えてpythonのスクリプトで作成しました。

必要なpip

pip install requests beautifulsoup4 pyperclip

作成したコード

import requests
from bs4 import BeautifulSoup
import pyperclip
import sys

# URLからhtmlのタイトルを取得
def get_html_title_from_url(url: str) -> str:
    try:
        response = requests.get(url)
        response.raise_for_status()  # HTTPエラーが発生した場合は例外を発生させる
        # バイナリのcontentを渡してエンコードをよしなにしてもらう
        soup = BeautifulSoup(response.content, 'html.parser') 
        # ページのタイトルを取得
        title = soup.title.string if soup.title else 'No Title' 
        return title
    except requests.RequestException as e:
        print(f"HTTPリクエスト中にエラーが発生しました: {e}")
        return ""

# URLとタイトルからhtmlのリンクタグを作成
def create_html_link(url: str, title: str) -> str:
    return f'<a href="{url}" title="{title}">{title}</a>'

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使用法: python create_link_tag.py URL")
    else:
        url = sys.argv[1]
        title = get_html_title_from_url(url)
        html_link = create_html_link(url, title)
        print(html_link)
        pyperclip.copy(html_link) # クリップボードにコピー
        print("リンクがクリップボードにコピーされました。")

urlをコマンドライン引数で与えることでリンクタグが出力されます。
無精のためクリップボードにコピーまで行われます。

試しに前回の私の記事のURLを与えてみると、、、

Microsoft IMEの不具合によるSendKeysの動作不良 | 計測制御ソフト受託開発 インフォテック ブログ

<a href="https://www.infortec.co.jp/blog/archives/Item_3080" title="Microsoft IMEの不具合によるSendKeysの動作不良 | 計測制御ソフト受託開発 インフォテック ブログ">Microsoft IMEの不具合によるSendKeysの動作不良 | 計測制御ソフト受託開発 インフォテック ブログ</a>

と出力され、問題なく動作します。
これでチョット楽できます✌

ちなみに今回のコードはほとんどMicrosoft Copilotくんに指示を与えて書いてもらいました。
普段使っておらず不慣れな言語の小規模なコードを作成してもらうのに、
チャットAIは本当に便利ですね。

おかげで、面倒なちょっとした手作業をpythonやbatで自動化するのも楽に行えます。
便利な時代になりました。もっと便利になって欲しいですね!以上です。

Microsoft IMEの不具合によるSendKeysの動作不良

1. はじめに
つい先日、Windows10の特定モデルのPCにて、
プログラムからキー送信を行うのAPIであるSendKeysを使用した機能が動作しない、
という不具合が報告されてきました。
その原因及び回避方法が特定できましたので、その共有となります。

2. 原因
原因はタイトルに記載していますが、おそらくMicrosoft IMEの不具合で、
IMEが半角の場合にSendKeysが動作しないことがあるようです。
(IMEが全角の場合は動作することに気付き、原因に辿り着けました)
Windows 10 version 2004 以降の新しいIMEにはどうも不具合があり、
PCによっては入力時などに問題が発生することがあるそうです。

3. 回避方法
問題を回避するには、
IMEの設定にて「以前のバージョンの Microsoft IME を使う」をオンにすることで回避可能となります。
これによりIMEが半角時でもSendKeysが動作しました。

このIMEの設定は以下のレジストリを変更することでも設定可能です。
> レジストリキー:HKCU¥SOFTWARE¥Microsoft¥Input¥TSF¥Tsf3Override¥{03b5835f-f03c-411b-9ce2-aa23e1171e36}
> 名前:NoTsf3Override2
> 種別:DWORD値
> 値:1
次の記事から引用させていただいております。ありがとうございます。
「以前のバージョンのMicrosoft IMEを使う」をコマンドで変更する方法を検証してみた : ITインフラに悩まされてる日常

上記設定のレジストリ値は、
・管理者権限なしでプログラムから編集可能
・PCの再起動なしで反映可能(IMEのプロセスの再起動により反映)
のため、プログラムからも容易に設定変更可能です。
なのでこの回避方法は、人手による設定変更なしでプログラムだけで完結できます。

4. おわりに
最初は全く見当が付かなかったのですが無事解決して良かったです。
調べている過程で知ったのですが、
どうもSendKeysはWindows10以降まともにサポートされていないようです。
PCによっては動作しないことがまれにあり使用は非推奨である旨の書き込みをちらほら見かけました。
そのため、今回の件もMicrosoft IMEだけが原因でなくSendKeys側にも問題があるのかもしれません。
今後はSendKeysの使用には慎重になろうと思います。

参考:
「以前のバージョンの Microsoft IME のご使用について | Microsoft Japan Windows Technology Support Blog
Windows10の「Microsoft IME」を以前のバージョンに戻したら、ソフトの挙動が安定した話。 | メモノローグ

C#のインタフェースでテンプレートメソッドパターン

9月ももう末ですね。
ようやく涼しくなって来てありがたいです。今夏は暑すぎました。。。

最近今更のように少しずつデザインパターンを学習(再学習含む)しています。
学習中にJavaのコードをC#で書き換える写経をしています。

そんな中でAbstractクラスで実装されがちなテンプレートメソッドパターンを
C#の場合はインタフェースのみで実現できることに気が付きました。
(Javaはおそらく完全な実現は無理そうです、
 インタフェースでfinal defaultなメソッドは宣言できないみたいなので)

実現方法は
「C#8.0でインタフェースにデフォルト実装メソッドを定義し、sealedで修飾」
です。

実現例:

/// <summary> 表示インタフェース </summary>
public interface IDisplay {
    /// <summary> オープンします。 </summary>
    void Open();
    /// <summary> プリントします。 </summary>
    void Print();
    /// <summary> クローズします。 </summary>
    void Close();

    /// <summary> 表示します。 </summary>
    sealed void Display() {
        Open();
        Print();
        Close();
    }
}

「C#8.0以降なら似たようなことをできるものの
 C#にはfinal修飾子がないため実現できない」と思っていたのですが、
デフォルト実装したメソッドをsealedで修飾可能なことに今更気付きました。
(というかそもそもメソッドにsealed修飾できることを知りませんでした)

これならデフォルト実装したテンプレートメソッドを
継承先のクラスにより上書きされてしまうことを防げますね。

実際にテンプレートメソッドパターンを実装することはあまりないかもしれませんが、
インタフェースにsealedなデフォルト実装メソッドを宣言可能なことは
知っているといずれ役立ちそうな気がします。

※調べてみると2019年に既に気付いていた方がおりますね。
 もしかして皆さん知っておられるのでしょうか。

Template Method Pattern C# 8.0風味 #デザインパターン – Qiita

以上です。

VSとMacのペアリング時のメモリ不足をキャッシュ削除により解消

MAUIでiPhoneアプリを開発する場合、ビルドのためにVisualStudioとMacをペアリングする必要があります。
アプリ開発の終盤、そのペアリング時やビルド時にMacのメモリが不足している旨の警告が出るないしエラーが出て失敗することが頻発しました。
その解決方法の共有となります。

タイトルの通り、キャッシュの削除により解決できました。
Windows・Macそれぞれで削除が必要です。

・Windowsでは以下の2フォルダがキャッシュなので削除
%localappdata%\Temp\Xamarin\XMA
%localappdata%\Xamarin\MonoTouch(←こちらはMacの接続先情報なので不要かもですが念のため)

・Macでは以下のフォルダがキャッシュなので削除
$HOME/Library/Caches/Xamarin/XMA

これで警告やエラー出ずにペアリング・ビルドできるようになりました。

以下はキャッシュが溜まる要因の予想です。
・ペアリング時にVisualStudioのバージョンないしMAUIのバージョンごとにキャッシュが溜まる
・異なるPCや異なるプロジェクトでペアリングを行うと、バージョンが異なりがちでその度にキャッシュが溜まる
くらいにふんわり考えています。

若干面倒なのであまりキャッシュ溜まらないことを願っています。
今後は1プロジェクトを改造・保守するのが主なので、上記予想が正しければ大丈夫なのではないかと思うのですが。。。

以上です。

[参考]
Clearing the Broker, IDB, Build, and Designer Agents on the Mac – Microsoft Learn
c# – Can't connect VS2022 .Net Maui Project with Mac – Stack Overflow
macos – I can't connect to Mac with Xamarin Mac Agent from Visual Studio 2015 – Stack Overflow

GooglePlayストアページからアプリのバージョンを取得する方法【令和最新版】

はじめに
Mauiのアプリ開発が完了しました。

開発終盤に開発した機能に、
Androidアプリにて現在のアプリのバージョンがストアのバージョンよりも古い場合、
古いバージョンは使用して欲しくないのでアプリを終了し、ユーザに更新を促す処理があります。

処理は以下のような流れで、
0. アプリを起動
1. ストアのアプリバージョンを取得
2. 現在のバージョンと比較
3. 現在のバージョンが古ければ、ストアページに遷移してアプリを終了

この内の1の工程におけるGooglePlayストアからアプリのバージョンを取得する処理についての共有記事となります。
重要なことなので先に言っておきますと、
デバイスによりバージョンが異なるアプリ(大手のアプリの多く、YouTubeやGoogleChromeなどなど)の場合は取得できません。

先行事例
iPhoneのAppStoreアプリの場合、iTunesAPIによりバージョンが取得できるので、
GooglePlayストアにも該当するAPIがないか調べてみたのですが見つからず、
代わりにストアページのhtmlを取得してパースする方法が見つかりました。
Google Playにあるアプリのバージョンを取得したい – すなばいじり

これで解決かと思い、ストアページのhtmlを覗いてみたところ、
どうも記事の投稿時点(6年前)と今とでは形式が異なるようで、
パースする正規表現に関しては考え直す必要がありました。

手法
考え直してC#にて実装した方法が以下のやりかたです。

    /// <summary> ストア上のアプリバージョンを取得します。 </summary>
    public async Task GetStoreAppVersion(string appPackageId) {
        // 毎回HttpClientのインスタンスを作り直すのは良くないそうですがサンプルコードなので
        using var httpClient = new HttpClient(); 

        // ストアページのhtmlをhttpで取得してバージョンをパースする
        var playStoreUrl = $"https://play.google.com/store/apps/details?id={appPackageId}";

        // Getリクエストを投げて、結果のContentをstringで取得する処理(実装省略
        var content = await GetContent(httpClient, playStoreUrl);

        // バージョン部 (?\d+\.\d[\d\.]*): 数字1個以上 ドット1個 数字ないしドット0個以上
        // 一致例:<script class="ds:0" nonce="EssQcMMS9rrE4BPTbLD7PA">AF_initDataCallback( ...... ],[[[1.0.0]],[ ...... );</script>
        var match = Regex.Match(content, @"<script class=""ds:\d+"" nonce=""[^""]+"">AF_initDataCallback\(.+?\[\[\[""(?\d+\.\d[\d\.]*)""\]\].+?\);</script>");

        if (match.Success.Not()) return null;

        var ver = match.Groups["ver"].Value;
        return new Version(ver);
    }

この実装により、数字とドットからなる一般的な記法のバージョンをパース可能です。
※バージョンが特殊な記法の場合はバージョン部の正規表現を変える必要があります。
 また、バージョンがデバイスにより異なる場合も取得できません。
 一般向けの大手のアプリはデバイスにより異なる場合が多く取得できないので、
 残念ながら汎用性はないです。
※正規表現文字列は変数化しようかと思ったのですが、
 VSがいい感じに見やすく色分けしてくれたのでそのままにしました

取得例
例として先行事例での対象アプリ、Nintendo Switch Onlineから取得してみます。
引数appPackageIdに”com.nintendo.znca”を与えてGetStoreAppVersionを呼ぶと、
html内の以下の記述にマッチし、バージョン2.10.1が取得できます。

<script class=”ds:0″ nonce=”EssQcMMS9rrE4BPTbLD7PA”>AF_initDataCallback( …… ],[[["2.10.1"]],[ …… );</script>

ストアのページでバージョンを確認してみると実際に2.10.1と表示されているので、
正しくバージョンを取得できていることがわかります。
(この表示から取得するので、ここに”デバイスにより異なります”と記載されている場合は取得できません。)

おわりに
アプリの更新を促す処理の実装のために、
GooglePlayストアページのhtmlからアプリのバージョンをパースして取得する方法の
令和最新版(24/8/16現在)についての共有でした。

この方法はデバイスによりバージョンの異なるアプリは取得できませんが、
自社で開発するアプリならバージョンを統一できるので問題なく使用できます。

今後は工場向けのモバイルアプリも増えていくかと思いますので、
その開発の際にも役に立つ機能です。

ちなみに更新を促す機能を実装する手段としては他に、
Google Play CoreライブラリのIn-app updates機能を使用して
バージョンチェックから更新処理までの一連の流れを
ライブラリの機能で実装する方法もあるようでした。
おそらくそちらが主流だと思われたのですが、
以下の理由などにより泣く泣く不採用(バージョン取得の方を採用)としました。

・ライブラリの調査にかかる時間が不透明
・デバッグしながらの開発ができず開発コストが増える
(GooglePlayConsoleにリリースビルドしてアップロードしないとおそらくテストできないため)
・iPhoneアプリにて既にitunesAPIでバージョン取得・自前で比較・ストアに遷移まで行っており、
 バージョン取得の方法であれば実装の共通化が可能でその分開発コストが下がる

今の方法だとストアのhtmlの形式が変更される度に
バージョンをパースする部分をメンテしないといけないので、
あまり変更されないと、、、良いな、、、とGoogle本社の方角に向けてお祈りしております。

以上です。

夏バテ

夏が始まってしまい暑くなって参りましたが、皆さんいかがお過ごしでしょうか。

自分は悲しきかな既に夏バテ気味でおります。
暑さで体力を奪われると体調を崩したり活動に支障を来したりで困るので、
早く涼しくなって欲しいものです。

皆さんは夏バテに負けずに元気に過ごされることを祈っております。

MAUIにてFirebase使用にあたり困ったこと ~VSのビルドアクションの選択肢にGoogleServicesJsonが出て来ない~

先日、MAUIのAndroidアプリにてプッシュ通知を実現するため、
Firebase Cloud Messagingを用いる方法を調査していました。
その際に詰まったことがあったので共有します。

Firebaseから取得した設定ファイル google-services.json をプロジェクトに含める必要があるのですが、
含めた後にプロパティからビルドアクションをGoogleServicesJsonに変更しないと
Firebaseの処理が動作しません。

しかしビルドアクションの選択肢にGoogleServicesJsonが出てきませんでした。

これは定番の困りごとのようで、調べてみると解決策がいくつか出てきたのですが
自分の環境ではどれも上手く行かず、ようやく上手く行ったのは以下の手順による方法でした。

1. nugetで以下のパッケージをインストール
※1つずつ試してはないので不要なもの含まれている可能性有

Xamarin.Firebase.Common
Xamarin.Firebase.Config
Xamarin.Firebase.Iid
Xamarin.GooglePlayServices.Base
Xamarin.GooglePlayServices.Basement
Xamarin.GooglePlayServices.Tasks

2. .csprojに以下を直接記述して、対象のパッケージの.targetファイルをインポート


<Import Project="C:\Users\<my user>\;.nuget\packages\xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets" Condition="Exists('C:\Users\<my user>\.nuget\packages\xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets')" />

3. 2の記述にてnugetフォルダパスが個人のローカルパスになっているため、マクロに置き換え
※個人プロジェクトなら不要です

C:\Users\<my user>\.nuget\packages\ → $(NuGetPackageRoot)


<Import Project="$(NuGetPackageRoot)xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets" Condition="Exists('$(NuGetPackageRoot)xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets')" />

これで無事ビルドアクションの選択肢にGoogleServicesJsonが出て来て、
Firebaseのプッシュ通知を動作させることができました。

以下のページにより解決に辿り着けました。感謝します。
xamarin – Can't set Build Action to GoogleServiceJson – Stack Overflow

.NET MAUIへの移行

今月とうとうXamarinのサポートが終了しましたね。
後継のクロスプラットフレームワークである.NET MAUI[1]に移行するため、
調査や準備を進めています。

現行のアプリのUIを1から新規プラットフォームで構築し直すのは大変です。
しかし、今回は以下の理由などにより、カメラなどネイティブの機能が必要ない箇所を除いて、
WebViewにhtmlを表示するWebアプリ化を行うことになりました。
・サーバーと通信するアプリである
・Webアプリの構築基盤が既に存在する
このWebアプリ化を移行前に行うことにより、MAUIへの移行もスムーズに行えると思われます。

MAUIに関しては、3月に手が空いていたので業務時間内に調査・勉強して、
実機でアプリを実行できるところまで進んでいます。
WPFと同様xamlでUIを記述する形式なため、WPF開発者ならスムーズに開発できる印象を受けました。
VisualStudioをMacとペアリングすることで、開発まではWindows上で行えるのも便利で良いですね。

MAUIの開発が進みましたらまた何か書こうと思います。
以上です。

[1].NET Multi-platform App UI. 2022年に正式リリース。

VisualStudioのGitでブランチの変更内容を確認

Gitでコミットをする場合、都度ざっくりコミット内容のチェックは行うのですが、
とは言え見落としなどしてしまうことがあります。

なのでブランチをmasterにマージする前に全ての変更を最終確認したいと思っていたのですが、
VisualStudioのGitなら以下の手順で簡単に確認できることに今更気が付きました。

1. VS上部のGitからブランチの管理を開く
2. ウィンドウ左部のブランチにてmasterを右クリックして対象のブランチにmasterをマージ

3. ウィンドウ右部のローカル履歴にてブランチの最新のコミット(masterをマージしたコミット)と
    masterの最新のコミットを同時に選択し、右クリックしてコミットの比較

小規模かつ短期間のブランチなら都度確認だけでも問題ないかもしれませんが、
開発が長期に渡った場合や、ブランチ内で修正を何度も行った場合などは、
ゴミとなるコードが残っていないかや意図せず破壊的変更を行ってしまっていないかなど
最終確認するのが良いのかなと思っています。
以上です。

C#で扱える環境変数の種類とその取得/設定に関して

案件で環境変数への値の取得/設定に関して少し調査を行ったので、
ブログ用に少し内容増やして調査結果以下に記載します。

C#において扱える環境変数の種類(EnvironmentVariableTarget)は以下の3つです。

Process: プロセスごとの環境変数

User: ユーザごとの環境変数
(システムの詳細設定から確認可能、レジストリ値 HKEY_CURRENT_USER\Environment内)

Machine: PC共通のシステム環境変数
(システムの詳細設定から確認可能。レジストリ値
 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment内)

取得、設定、削除(nullを設定)は以下のメソッドにより可能です。

string? Environment.GetEnvironmentVariable (string variable, EnvironmentVariableTarget target);
void Environment.SetEnvironmentVariable (string variable, string? value, EnvironmentVariableTarget target);

以下の表に、取得/設定時の種類ごとの主な違いをまとめてみました。

以下知見や所感です。
・ユーザ及びマシンは、プロセスの再起動など不要で、
 同時に起動している異なるプロセスにて共有可能でした。

・管理者権限必要なのはマシンだけでした。
 ユーザも内部的にはレジストリ値なので権限必要に思えたのですが、
 HKEY_CURRENT_USER下のキーの変更は権限不要らしいです。

・ユーザとマシンは他のアプリとも共有で影響及ぼすかもなので取得や設定、削除に注意が必要です。
 また、プロセスとは異なり、システム詳細設定やレジストリエディターから
 値が容易に確認可能なため、プロキシ設定などの認証情報の書き込みを行うと
 ユーザIDとパスワードが平文で見えてしまう点も注意です。

・ユーザやマシンへのSetは2秒程度かかり、実行時暫し動作が固まったので、
 場合によってはプログレス表示必要かもしれません。
 内部でレジストリへの書き込みを行う関係か少し時間がかかるようですね。
(参考PCスペック:CPU=intel core i9-13900, RAM=32GB)

・どれを使うかは用途次第ですが、扱いやすいのは管理者権限不要なプロセスとユーザで、
 プロセス間で共有するかどうかで使い分けるのが良いかと思いました。

以上です。