はてなブログ内の画像を一括ダウンロード (Linux/Macコマンド使用)

こちらの記事を参考にLinuxコマンドラインでやってみた。たぶんMacでもできるはず。

(1) はてなブログのデータをエクスポート

はてなブログの「設定(スパナのアイコン)」→「詳細設定」の下のほうの「エクスポート / 記事のバックアップと製本サービス」でエクスポートしてダウンロードする。(MovableType形式のテキストファイルで書きだされる。)

(2) はてなフォトライフの画像のURLを抽出

はてなフォトライフの画像のURLは例えば下記のようなものである。
fotolife/」以降は「頭文字/ユーザ名/数字/数字.拡張子」の形式のようだ。

https://cdn-ak.f.st-hatena.com/images/fotolife/l/licheng/20211111/20211111230348.png

このような文字列を抽出してファイルに出力するには下記のコマンド(1行)を実行する。
ここで export.txt はエクスポートしたテキストファイルとする。
抽出したURLは list.txt に書き出される。

cat export.txt | grep https://cdn-ak.f.st-hatena.com/images/fotolife | sed -r "s/^.*(https:\/\/cdn-ak\.f\.st-hatena.com\/images\/fotolife\/l\/licheng\/[0-9]*\/[0-9]*\.[a-z]{3}).*$/\1/" > list.txt

(3) 一括ダウンロード

list.txt に書き出されたURLのファイルをダウンロードするには下記のコマンドを実行する。
ここで dl_folder はダウンロード先のフォルダ名とする。

wget -nc -P dl_folder -i list.txt

または、URLのディレクトリ構造どおりに保存したいなら下記のコマンドを実行する。

wget -nc -x -i list.txt

ダウンロードされるファイルの数は list.txt の行数よりかなり少ない。これはダブりを除外しているためである。記事ごとにアイキャッチ画像が指定されているため、必ずダブりが発生する。

参考

なんかよく分からんけどWebアプリをAzureにデプロイするまで (前編)

Web系ぜんぜん分からん組込み系のオッサンがなんかよく分からんけどやってみたメモ。
下記の公式サイトのチュートリアルをもとにやってみた。

ローカルにNode.jsをインストールする

なんかよく分からんけど、とりあえずローカルのPC上で動くNode.js環境を作る。僕はドザー(死語)なので、Windows版のインストーラNode.jsの公式サイトからダウンロードしてインストールしてもよいのだけど、この先DOS窓で作業するのはツラそうなので、WSLのUbuntu(詳しくはこちらの記事)にインストールすることにする。(PowerShellなにそれおいしいの?)

sudo apt update
sudo apt install nodejs
sudo apt install npm
node --version

しかし、node --version でバージョンを確認してみるとv10と、ちょっと古かった。そこで n をインストールして特定バージョン(今回はv14)のnode.jsをインストールし直し、最初にインストールしたnode.jsとnpmはアンインストールし、ログインし直す。改めてバージョンを確認するとv14になっていた。(詳しくはこちらの記事)

sudo npm install n -g
sudo n v14
sudo apt purge -y nodejs npm
exec $SHELL -l
node --version

あと、ファイルはWindows側に置いておいたほうが便利が良さそうなので、例えばドキュメントフォルダへのシンボリックリンクを張っておく。

ln -s /mnt/c/Users/ユーザ名/Documents ~/documents

ああ、でもファイルをWindows側(NTFS)に置いちゃうとパーミッションが全部777になっちゃうし、シンボリックリンクを作られるとWindowsからは空のファイルに見えちゃうのが難点。悩ましいところで正解が分からんけども、とりあえずこれで進める。

Express + Node.js でローカルにアプリのひな型を作る

なんかよく分からんけど、ExpressとかいうNode.jsのフレームワークを使うと簡単にWebアプリができるらしい。Webアプリのひな型を生成するために、テキトーなディレクトリに移動して次のコマンドを実行する。npx を使うと、よかろうで必要なものをインストールしてくれるらしい。ここで myExpressApp は任意のアプリ名とする。

npx express-generator myExpressApp --view pug

すると、myExpressApp というディレクトリができ、ここにWebアプリのひな型が生成されるようだ。このディレクトリに移動して npm install を実行すると、なんかよく分からんけどアプリがインストールされる?

cd myExpressApp 
npm install

ここで「found n vulnerabilities (n個の脆弱性が見つかった)」というメッセージが出た場合は npm audit を実行して指示されたコマンドを実行する。(例えば「Run npm install pug@3.0.2 to resolve 2 vulnerabilities」のようなメッセージが出る。) そしてnpm install を実行し直す。

npm audit
sudo npm install pug@3.0.2    # 脆弱性対策の一例
npm install

インストールできたら npm start でアプリを起動する。(停止はふつうに Ctrl+C で。)

npm start

ブラウザで http://localhost:3000 にアクセスして「Express Welcome to Express」と表示されたら成功。

VS CodeのAzure App Service拡張機能をインストール

さすがにAzureへのデプロイの操作をコマンドライン(Azure CLI)でやるほどストイックな人間ではないので、VS Code拡張機能を利用することにする。(コマンドラインでやりたい硬派な人はこちら)

「Azure」で検索すれば「Azure App Service」という拡張機能がすぐ見つかるのでこれをインストールする。

f:id:licheng:20211109214252p:plain

Azure へのデプロイ

さっきローカルに作ったアプリのディレクトリ(例では myExpressApp)で code . を実行するとVS Codeが起動してアプリのディレクトリが開く。または、VS Codeのメニューの File → Open Folder で アプリのディレクトリを開いても良い。「Do you trust the authors of the files in this folder?」とか聞かれたらYesで。

code .

なんかよく分からんけど、さっきAzure App Service拡張機能をインストールしたので、VS Codeの左端にAzureのアイコンが追加されている。ここをクリックして、「Sign in to Azure」を選択。(組込み系のオッサンはメニューの日本語化などせぬのである。必要ないし、トラブったときに英語の方がググラビリティが良い。)

f:id:licheng:20211109232647p:plain

するとブラウザに飛んでアカウント選択画面になる。ここでサインイン済みのアカウントを選択すると「You are signed in now and can close this page.」と表示されるので、この画面は閉じてVS Codeに戻る。すると作成済みのサブスクリプションが表示される。(なんの手違いかサブスクリプションが2個できてしまっているが、よく分からん。)ここで雲のアイコンをクリックしてWebアプリのデプロイに進む。

f:id:licheng:20211109231856p:plain

(そもそもデプロイってどういう意味かよく分からんけど、まあアプリをAzureにアップロードして実行するってことやろ、知らんけど。)

デプロイまでの手順
  • フォルダの選択: 今開いているフォルダを選択。
  • サブスクリプションの選択: 作成済みのサブスクリプションを選択。(※1)
  • Webアプリの選択:「Create new Web App... Advanced」を選択。
  • Webアプリの名前を入力: グローバルにユニークな名前を付ける。
  • リソースグループの選択: なんかよく分からんけど新しいリソースグループを作成する。(※2)
  • ランタイムの選択: 今回はNode.js v14のアプリなので「Node 14 LTS」を選択。
  • OSの選択: とりあえずまあ「Linux」を選択。
  • ロケーションの選択: わいは「Japan West」やで。
  • Linux Appサービスプランの選択: なんかよく分からんけど新しく作成する。
  • 価格レベルの選択: とりあえずまあ「Free(F1)」か「Basic(B1)」で。
  • Application Insightsリソースの選択: なんかよく分からんけど、今回はスキップする。

すると、Webアプリのデプロイが始まる。「Always deploy the workspace "フォルダ名" to "アプリ名" ?」と聞かれたら Yes で。Webアプリのデプロイが完了したらポップアップ表示されるので、「Browse Website」をクリックする。ブラウザが開いて、ローカル環境と同様に「Express Welcome to Express」と表示されたら成功。 URLは https://アプリ名.azurewebsites.net/ となる。

※1 ※2:サブスクリプションとかリソースグループとか、公式のドキュメント読んでもなんか分からんけど、こちらの記事にざっくりした解説があった。

アプリを変更して再デプロイする

アプリを試しにちょこっと変更してみる。myExpressApp/views/index.pug を開いて「p Welcome to #{title}」と書かれてあるのを「p Welcome to Azure!」に書き換えてみる。この index.pug ていうファイル、謎言語の謎ファイルだけども、どうやら index.html を生成するテンプレートらしい。

ローカルでアプリを起動してブラウザで再び http://localhost:3000 にアクセスすると今度は「Express Welcome to Azure!」と表示される。

VS Codeで再びAzure App Service拡張機能の雲のアイコンをクリックして再デプロイする。完了通知のポップアップで「Browse Website」をクリックし、ローカルと同様に表示が変わっていたら成功。

ひとまずこれで、「Webアプリをローカルで作成→動作確認→Azureにデプロイ→動作確認→ローカルに戻って変更→以降繰り返し」の流れができた。よく分からんけど。

所感

  • なんかよく分からんけど、頼りになるのはLinuxコマンドの教養である。
  • マイクロソフトのドキュメントは、かなり行間を読まないといけない。


後編につづく

ノート:ESP32(Arduino core)でWebサーバ&WebSocketサーバ

(1) Webサーバ

(1.1) ライブラリのインクルードとオブジェクトの定義

Arduino core for the ESP32に含まれるライブラリを使用する。
ポート番号を指定してオブジェクトを定義する。

#include <WebServer.h>

WebServer webServer(80);
(1.2) 初期化 (setup()での処理)

URLごとのコールバック関数を設定し、Webサーバを開始する。

  webServer.on("/", func_handleRoot);
  webServer.on("/index.html", func_handleRoot);
  webServer.onNotFound(func_notFound);
  webServer.begin();
(1.3) ループ処理 (loop()での処理)

loop()内で必ずhandleClientメソッドを呼ぶこと。
すなわち、loop()を他の同期処理などで止めてはならない。

  webServer.handleClient();
(1.4) リクエスト受信

リクエスト受信があると、初期化時に設定したコールバック関数が呼ばれる。
そこでクエリパラメータを取得できる。

  // パラメータの個数をチェック
  if(webServer.args() > 0){
    // 特定のパラメータがあるかチェック
    if (webServer.hasArg("hoge")){
      // パラメータの値を取得
      String val = webServer.arg("hoge");
    }
  }

また、リクエストのURLやメソッド、パラメータ名は下記のようにして取得できる。

  String uri = webServer.uri();
  HTTPMethod method = webServer.method(); // HTTP_GET, HTTP_POST など
  String argName = webServer.argName(0); // 最初のパラメータ名
(1.5) レスポンス送信

必要ならばHTTPヘッダを設定する。
HTTPステータスコード、コンテンツタイプ、コンテンツを指定してレスポンスを送信する。

  String message = "ほげほげ";
  webServer.sendHeader("Cache-Control", "no-cache");
  webServer.send(200, "text/plain", message);

ファイルを送信する場合にはSPIFFSライブラリを併用する。
ファイルを開き、コンテンツタイプを指定して送信し、最後にファイルを閉じる。

#include <FS.h>
#include <SPIFFS.h>
…
  File file = SPIFFS.open("/index.html", FILE_READ);
  size_t sent = webServer.streamFile(file, "text/html");
  file.close();

ファイルはアップローダーを使ってESP32に書き込む。アップローダーは下記ページからダウンロードできる。ZIPを解凍してArduoinoのtoolsフォルダに配置しておく。スケッチフォルダ内にdataフォルダを作成してそこに書き込みたいファイルを配置し、ツール → ESP32 Sketch Data Upload で書き込む。

【参考】

(2) WebSocketサーバ

(2.1) ライブラリのインクルードとオブジェクトの定義

WebSocketサーバはarduinoWebSocketsライブラリをGitHubからダウンロードして使用する。
ポート番号を指定してオブジェクトを定義する。

#include <WebSocketsServer.h>

static WebSocketsServer webSocket(81);


(2.2) 初期化 (setup()での処理)

WebSocketサーバを開始し、イベントハンドラ関数を設定する。

  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
(2.3) ループ処理 (loop()での処理)

loop()内で必ずloopメソッドを呼ぶこと。
すなわち、loop()を他の同期処理などで止めてはならない。

  webSocket.loop();
(2.4) 受信

受信データは初期化時に設定したコールバック関数で取得する。また、接続時、切断時、エラー時にもコールバック関数が呼ばれるのでイベント種別を判定して各々を処理する。

  // num: クライアント番号
  // type: イベント種別
  // payload, length: 受信データとそのサイズ
  void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length)
  {
    switch(type) {
      case WStype_DISCONNECTED:
        切断時の処理;
        break;
      case WStype_CONNECTED:
        {
          IPAddress ip = webSocket.remoteIP(num); // クライアントのIPアドレスを取得
          接続時の処理
        }
        break;
      case WStype_TEXT:
        テキストデータ受信時の処理;
        break;
      case WStype_BIN:
        バイナリデータ受信時の処理;
        break;
      case WStype_ERROR:
        エラー時の処理;
        break;
    }
  }
(2.5) 送信

ブロードキャストであれば broadcastTXT メソッドを用いる。

  webSocket.broadcastTXT(tx_str); // 文字列を渡す場合 (文字数を指定しない場合)
  webSocket.broadcastTXT(tx_buff, tx_len); // バイト数を指定する場合

特定のクライアントに送信するのであれば sendTXT メソッドを用いる。

  webSocket.sendTXT(client_num, tx_str); // 文字列を渡す場合 (文字数を指定しない場合)
  webSocket.sendTXT(client_num, tx_buff, tx_len); // バイト数を指定する場合

また、バイナリデータを送信する場合は broadcastTXT に代えて broadcastBIN を、sendTXT に代えて sendBIN を用いる。

ノート:AjaxとWebSocket

Webページで、画面遷移せずにサーバと非同期通信をして画面を部分的に書き換えたりする方法としてAjaxとWebSocketがある。Ajaxは必ずブラウザからのリクエストに応じてサーバがレスポンスを返す。そのため、サーバ側の変化をモニタしたい場合には周期的にポーリングするしかない。これに対してWebSocketはブラウザ側からもサーバ側からもいつでもデータの送信ができる。

(1) Ajax

AjaxにはXMLHttpRequestオブジェクトを用いる。XMLという名前がついているがデータはJSON形式でもかまわない。最近ではXMLHttpRequestよりもモダンなAPIとしてfetchというものもあるが、ここではふれないでおく。

使い方は、まずXMLHttpRequestオブジェクトを作り、イベントハンドラを定義し、メソッド(GET/POST)とURLを指定し、リクエストヘッダを設定し、ボディを渡して送信するという流れである。

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    イベントハンドラ処理;
};
xhr.open('POST', 'https://hogehoge.hoge/piyopiyo');
xhr.setRequestHeader( 'Content-Type', 'application/json' );
xhr.send(json_text); // GETの場合は引数なしにする

受信データはonreadystatechangeハンドラで状態とレスポンスコードをチェックしてからresponseTextを取得する。XMLの場合にはresponseTextに代えてresponseXMLを取得する。

xhr.onreadystatechange = function() {
  if (this.readyState == 4){ // 4=DONE
    if(this.status == 200) { // 200=OK
      var json_text = this.responseText; // JSONの場合
      var domDoc = this.responseXML; // XMLの場合
      受信データの処理;
    }
  }
};
JSONデータの扱い

JavaScriptオブジェクトをJSON形式の文字列に変換するにはJSON.stringify()を用いる。

var json_text = JSON.stringify(js_obj);

JSON形式の文字列をJavaScriptオブジェクトに変換するにはJSON.parse()を用いる。

var js_obj = JSON.parse(json_text);
XMLデータの扱い

XMLデータの扱いは少しめんどくさい。
XMLドキュメントから要素の値を取得するにはgetElementsByTagName()を用いる。

var hoge = domDoc.getElementsByTagName('hoge')[0].childNodes[0].nodeValue;

(2) WebSocket

WebSocketにはWebSocketオブジェクトを用いる。

まず、window.onloadなどでURLを指定してWebSocketオブジェクトを生成し、ハンドラを定義する。

var ws = new WebSocket('wss://hogehoge.hoge');
ws.onmessage = function(evt){
  受信時の処理;
};
ws.onopen = function (evt) {
  接続時の処理;
};
ws.onclose = function (evt) {
  切断時の処理;
};
ws.onerror = function (err) {
  エラー時の処理;
};

受信したデータはonmessageハンドラの引数として取得する。

ws.onmessage = function(evt){
  const json_text = evt.data;
  受信データの処理;
};

データを送信する場合はsend()メソッドを用いる。

ws.send(json_text);

もしも一定時間で接続が切られるような場合は、定期的にデータを送信することで接続を維持できる。

var aliveTimer = setInterval( function () {
  ws.send("keep alive");
}, 30*1000);

ノート:Javascriptの書き方

ふだんC言語しか書かないマイコンおじさんによる備忘録。

script要素

そもそも、JavaScriptscript要素内に書く。script要素は、head要素内とbody要素内のどちらにでも書けるが、body要素の最下部に書くのが無難。

外部のJavaScriptファイルを読み込むには、scriptタグにsrc属性で指定する。

<script src='hoge.js' />

window.onload

ページの読み込みが完了したときに実行したい処理(初期化など)は、window.loadに定義する。

window.onload = function(){
  処理;
};

setInterval

周期タイマ処理はsetIntervalを用いてイベントハンドラを設定する。時間の単位はミリ秒である。また、周期タイマ処理の停止にはclearIntervalを用いる。

var timer1 = null;
...
timer1 = setInterval(func_timer1, 10*1000);
...
clearInterval(timer1)

HTML要素の操作

JavaScriptで操作したいHTML要素にidを付けておく。

<span id='hoge'></span>

JavaScriptからは document.getElementById で参照して操作する。

document.getElementById('hoge').innerHTML= 'あいうえお';

jQueryを使えばもっと簡潔に記述できるが、ここではふれない。

input要素

テキストボックスやボタンなどはinput要素を用いる。<input type="submit">タグを用いて送信するのであればinput要素はform要素内に書く。そうでないならformタグは必須ではない。

<input type='text' id='text1' value='初期値' />
<input type='password' id='password1' />
<input type='number' id='number1' min='0' max='100' step = '1' value='50' />
<input type='range' id='range1' min='0' max='100' step = '1' value=50 />
<input type='checkbox' id='checkbox1' /> checkbox1

JavaScriptからは document.getElementById で参照して値を取得する。formタグとname属性を用いれば、document.form名.input名 の形でinput要素を参照でき、記述が少し簡便にはなるが、ここではふれない。

const text1 = document.getElementById('text1').value;
const number1= parseInt(document.getElementById('number1').value);

ボタンなどに対するイベントハンドラは、タグ内に呼び出しを書く。ここでthisを引数に渡すとどのinput要素のイベントかをイベントハンドラ側で判別できる。

<input type='button' id='button1' value='ボタン1' onclick='clickButton(this)' />
<input type='range' id='range1' min='0' max='100' oninput='changeSlider(this)' />
function clickButton(element) {
  if(element.id=='button1') {
    button1をクリックしたときの処理;
  }
}
function changeSlider(element) {
  if(element.id=='range1') {
    range1の値が変化したときの処理;
  }
}

canvas要素

グラフィックを描画するにはcanvas要素を用いる。

<canvas id='canvas1' width='400' height='300' />

JavaScript側ではcanvas要素を参照してgetContextでコンテキストを取得し、グラフィックを描画する。

var canvas1 = document.getElementById('canvas1');
var context1 = canvas1.getContext('2d');
context1.fillStyle = "blue";
context1.fillRect(10, 10, 100, 100);

canvas要素上でのマウスイベントは下記のようにイベントハンドラを定義する。

canvas1.addEventListener('mousedown', onDown);
canvas1.addEventListener('mouseup', onUp);
canvas1.addEventListener('click', onClick);

マウスの座標はイベントハンドラの引数から取得できる。

function onClick(e) {
  var rect = e.target.getBoundingClientRect();
  var x = e.clientX - rect.left;
  var y = e.clientY - rect.top;
  処理;
}

また、ウィンドウ全体のサイズはwindow.innerWidth, window.innerHeightで取得できる。キャンバスのサイズはCanvas.width, Canvas.heightで取得・設定できる。

APIの詳細は下記を参照。

AjaxとWebSocket

次の記事にまとめた。

メモ:CSSの文法

すぐ忘れるのでCSSの書き方をメモ

(1) タグのスタイルを指定

p { font-size: 16px;}
/* プロパティを複数指定する */
p {
    font-size: 16px;
    font-weight: bold;
}
/* 複数のタグにスタイルを指定する */
p, div { font-size: 16px;}
/* 全てのタグにスタイルを指定する */
* { font-size: 16px;}

(2) クラスのスタイルを指定

.class_name { font-size: 16px;}
/* 特定のタグのクラスにスタイルを指定 */
p.class_name { font-size: 16px;}

(3) IDのスタイルを指定

#id_name { font-size: 16px;}
/* 特定のタグのIDにスタイルを指定 */
p#id_name { font-size: 16px;}