Corredor

ウェブ、プログラミングの勉強メモ。

WSL 環境に Java を入れる

WSL に Java (JRE) をインストールする。ただそれだけ。

Ubuntu 系は存在しないコマンドを入力すると、apt で入れられそうなサジェストを出してくれるので、それを見て好きな JRE・JDK を入れるのが手っ取り早い。

# 以下でインストールできる
$ sudo apt install -y openjdk-11-jre-headless

# Java のパスを確認する
$ sudo update-alternatives --list java
/usr/lib/jvm/java-11-openjdk-amd64/bin/java

# 以下のように ~/.bashrc などで指定すれば OK
$ export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64

# 再起動
$ java -version

これだけ〜。

WSL構築と利用―Windows10で利用するLinux環境

WSL構築と利用―Windows10で利用するLinux環境

WSL2 上で起動した Selenium Webdriver や Puppeteer から Windows 側の Chrome ウィンドウを操作したかったが無理

出来たって人もいるみたいだけど、自分は無理だった。

やりたいこと

  1. Windows 側で常用している Chrome ブラウザのプロファイルを利用して、Selenium 的なツールでブラウザの自動操作を行いたい
    • サイトへのログイン情報とかを流用したかったので、既存のプロファイルを使い回したかった
  2. ツールが Chrome を自動操作している様子をリアルタイムに確認したかった
    • つまりヘッドレスで動くのではなく、ヘッドありで動かしたかった

それでいて、

  • Windows 側には Java・Node.js などがインストールされていない
  • WSL 側にのみ、Java や Node.js がインストールされている

という環境だった。最近は開発環境を WSL に移しているので、Windows 側には余計なモノをインストールしたくなかった。

こんな環境で、果たして WSL から Windows へと世界を飛び越えられるのか、試してみた次第。

Selenium Webdriver は出来そうで出来なかった

まずは過去にも使ったことのある、Ndoe.js 製の selenium-webdriver を使ってみた。

neos21.hatenablog.com

  • WSL 側に Node.js プロジェクトを作って、selenium-webdriver をインストールする
  • Windows 側で、Windows 用の chromedriver.exe をダウンロードして適当なところに置いておく
  • selenium-webdriver で chromedriver.exe へのパスを /mnt/c/ で指定する

…というだけで動かせるらしかったが、動かなかった。Builder のインスタンスを生成するところでタイムアウトになった。

#!/bin/sh
/mnt/c/PATH/TO/chromedriver.exe "$@"

あとはパスの指定方法を

  • '/mnt/c/Users/…'
  • 'C:\\Users\\…' (JS の文字列なのでバックスラッシュはエスケープする)
  • 'C:/Users/…'

などの表記パターンでそれぞれ試したが、うまく行かず。コード片残しとく。

const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

(async () => {
  const service = new chrome.ServiceBuilder(CHROME_DRIVER_PATH).build();
  chrome.setDefaultService(service);
  
  const options = new chrome.Options()
    .setChromeBinaryPath(CHROME_BROWSER_PATH)
    .addArguments([
      `--user-data-dir=${CHROME_USER_DATA_DIR}`
    ]);
  
  const capabilities = webdriver.Capabilities.chrome().set('chromeOptions', {
    args: [
      '--no-sandbox'
    ]
  });
  
  const driver = await new webdriver.Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .withCapabilities(capabilities)
    .build();
  // ココでエラーが出て続行不可能
})();

Selenium Server を Windows 側に立ててみたがダメだった

上の記事によると、Windows 側に Java をインストールし、

↑ から Selenium Server (Grid) の JAR ファイル selenium-server-standalone-3.141.59.jar を落としてきて、コレに接続するようにしたら動く、とか書いてあった。

# PowerShell にて。Chocolatey で JRE 8 を入れておく
PS1> choco install jreruntime

# PowerShell で Selenium Server を動かしてみる
PS1> java -jar .\selenium-server-standalone-3.141.59.jar -port 4445

ココまでやると、

  • http://localhost:4445/wd/hub/static/resource/hub.html

なんかにアクセスして Selenium Server が動いているのを確認できる。

そしたら WSL 側に戻る。先程見ていた文献で usingServer() メソッドの存在走っていたので、Builder 部分を次のようにしてみる。

const driver = await new webdriver.Builder()
  .forBrowser('chrome')
  .usingServer('http://172.xx.xx.xx:4445/wd/hub')
  .setChromeOptions(options)
  .withCapabilities(capabilities)
  .build();

localhost ではなく 172.xx.xx.xx なる IP を指定しているのは、後で詳しく説明するが、Windows ホスト側の localhost を参照するため。/etc/resolv.conf 内に記載の nameserver の IP アドレスを書いてある。

コレを実行すると、PowerShell 上で動く Selenium Server がエラーログを出力していて、通信が届いていることは確認できた。でもそのエラーを解消しきれず断念。なんか実行パスかドライバーのパスが違うぐらいのエラーな感じがするんだけど、どうやってもダメだった。

Selenium Webdriver ではなく Puppeteer を使ってみたがダメだった

Selenium Webdriver を諦めて、Puppeteer を使ってみることにした。コイツは Selenium よりもブラウザ周りが扱いやすいっぽい。

await puppeteer.launch({
  executablePath: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
  userDataDir: USER_DATA_DIR
})

こんな感じになるのだが、結局は上手く動かなかった。

launch() メソッドが Windows 側に Chrome ウィンドウを開くところまでは行くのだが、その直後に

connect ECONNREFUSED 127.0.0.1:60081

というようなエラーメッセージで異常終了してしまい、launch() メソッドが完了しない。

そもそも WSL と Windows 間の localhost はどう関係しているのか

Puppeteer のエラーメッセージの中に、

  • ws://127.0.0.1:50887/devtools/browser/

とかいう URL が見えて、ふと「この 127.0.0.1 は、WSL Ubuntu の世界における localhost だよな…」と思った。

ネットワークの基礎知識として、localhost自機を表すホスト名なのは分かっている。で、WSL の世界でいう localhost と、Windows の世界の localhost は別物だろうな、という認識もある。例えば Docker コンテナ内の localhost は Windows ホスト側の localhost とは違うから、Windows 側から Docker コンテナ内で動く Web サーバを見たければポートフォワードが必要だったりするので、その辺が分かっていないワケではない。

でも、WSL 上で $ node server.js のように起動した http://localhost:8080/ な開発サーバなんかは、Windows 側から http://localhost:8080/ で繋げられてるよな?

調べてみると、どうやら WSL 上の localhost は、Windows 側からも localhost として見えるように調整されているが、その逆は透過されないようなのだ。

Windows 側の localhost に、WSL からアクセスしたい場合は、/etc/resolv.conf に記載の IP を使ったりするワケである。

そうすると、Puppeteer のエラーメッセージ中に出てきた 127.0.0.1 は、WSL 内の localhost を見てしまっていて、Windows 側の localhost は覗けていないことになる。Windows 側の Chrome を触りたいのに、WSL 内の localhost に閉じていたら上手く動かないやろな、と…。

未検証の内容

なんだか段々諦めてきてしまって、以降の検証をやる前にこの記事を書いてしまった。WSL2 はまだまだアップデートが盛んで、仕様変更も多いので、過去のやり方が通用しなくなっているのだろうし、それがまたいつか別の方法で出来るようになっているかもしれない。

Puppeteer に関しては、起動済みの Chrome ブラウザに後から接続しにいくという方法があるらしい。

なんか原理的に考えるとコレが上手く行きそう感あるな…。

あとはヘッドありで動かすのを諦めて、Docker に閉じ込めてしまうとか。

やりたいことをとにかく実現したいなら、環境を汚しまくれば不可能ではない。

  • WSL に GUI デスクトップ環境を作り、Ubuntu 版の Chrome を動かす (= 完全に WSL 内で完結させる。Windows 側の要素は一切使わない。デスクトップを見るのは VcXsrv で)
  • 今まで Windows GitBash を使っていたんだし、Windows 側に Java でも Node.js でも入れて環境構築したらええ。そしたら localhost で Chrome を触れる (= WSL を一切使わない)

こうなるともうクソダルゲンナリマンで、「それならブラウザの自動化なんか実現しなくていい」って気持ちになってしまうので、もうやらないことにする。w

子供に教えるコンピュータ ②Web Scraping編

子供に教えるコンピュータ ②Web Scraping編

Node.js でプライベートファイルエクスプローラ CGI を作ってみた

かつて存在した CGI 配布サイト「すえぽんさいと」で、「Xplorer」「Web Editor」という2つの CGI が配布されていた。

Xplorer は、サーバ上の指定のディレクトリ配下のファイルツリーを表示し、Web Editor はさらにファイル編集も出来るシロモノだった。

すえぽんさいとの URL http://CGIScriptMarket.com/ はもう死んでいるようで、当該スクリプトも公式での配布は見当たらない。自分がローカルに保存していたモノを勝手に GitHub にアップしているので、どんなコードだったかは確認していただける。

元々は Perl 製で、実装はまともに読んでいないのだが、今回このスクリプトのアイデアを元に Node.js 製の CGI スクリプトを作ってみた。それが Node.js CGI Explorer

インストール方法

簡単にインストール方法を紹介する。

まず、対象のサーバの Apache や nginx で CGI が扱えるようにしておき、さらにそのサーバに Node.js をインストールしておく (async を使っているので v12 以降)。

Node.js をインストールしたら $ type node とかで Node.js のフルパスを確認しておく。/usr/bin/node などと分かるはず。

そしたら本スクリプト index.js.cgi をサーバに配置する。ファイル名や拡張子は自由に変更して問題ない。

1行目の Shebang を、先程確認した Node.js のフルパスに書き換える。大抵は #!/usr/bin/node とかになるかな。

また、ファイル中に定数を2ヶ所ほど設定する。

  • アクセスパスワードとなる const credential
  • ファイルツリーを表示したいルートディレクトリを const rootDirectory

ルートディレクトリは、この CGI を実行するユーザの権限さえあればどこでも良い。つまり、よくある /var/www/html/ 以外の場所を指定することもできるワケだ。

CGI ファイルを編集したら、最後に実行権限を付与して ($ chmod 755 index.js.cgi) 完了。

使い方

ブラウザで CGI の URL にアクセスすると、最初はパスワード入力欄が表示される。const credential で指定したパスワードを入力してログインする。

ログインできると、const rootDirectory で指定したディレクトリ直下のファイルやディレクトリがツリー表示される。ツリーといっても、リンクの箇条書きだが…。

ディレクトリのリンクを押下すれば、ディレクトリを掘り下げていける。子階層からは1階層上に上がれる ../ リンクがある。

ファイルのリンクを押下すると、そのファイルを表示またはダウンロードできる。

  • テキストファイル系は text/plain 扱いでそのまま表示
  • ブラウザで表示できる画像ファイル系はそのまま表示
  • それ以外はブラウザ上で表示するのがキツそうなので application/octet-stream でダウンロードさせる

この辺は Content-Type ヘッダと Content-Disposition ヘッダを付与して雑に process.stdout.write() しているだけなので、もしかしたら上手く行かないことがあるかも。

Node.js におけるファイル一覧の取得や、CGI でファイルをレスポンスする方法は以下を参考にした。

一応考慮してあること

GET リクエストの場合は常にログインページが表示される。ログイン後の全てのページ遷移は POST で行っていて、ログインページで入力させたパスワードを都度検証している (CGI という古い技術なのに LocalStorage とか組み合わせていてカオス感ある)。

credential の値が平文保存なのはイケてないと思うが、一応はコレを外部に漏らさなければファイルツリーが表示できない作りにしてある。

ディレクトリ移動の際は、rootDirectory で指定したパス配下しか表示させないようにしている。うかつに / まで上れたりしないようにしてはある。

ファイルの編集機能は持たせていない。面倒臭かったのでw.表示だけだからまぁ安心?w

つーワケで以上。

はてなブログ API では予約投稿ができない

はてなブログ API を使ってみたが、クソほど充実していない。

はてなブログ API を使う

はてなブログ API を使ってみようーと思うと、公式の不親切な OAuth がどうしたこうしたみたいなリファレンスが飛んでくる。

なんやねんこの OAuth 1.0 ってのは。結局 curl じゃやれんのかいな。メンドクサ。

と思ってよくよく調べると、もっと簡単なやり方があった。

自分のブログの「設定」→「詳細設定」の下部に「AtomPub」という項目がある。ココの

  • ルートエンドポイント : https://blog.hatena.ne.jp/neos21/neos21.hatenablog.com/atom
  • API キー : xxxxxxxxxx

を控えておく。

ルートエンドポイントは、ユーザ ID とブログ URL から組み立てるだけなのでバレても良いが、API キーはパスワードと同等なのでバラさないように。

curl でやるなら、以下のように -u 【ユーザ ID】:【API キー】 を指定すればアレコレできる。

# 公開済の記事を取得する例
$ curl -X GET -u 'neos21:xxxxxxxxxx' 'https://blog.hatena.ne.jp/neos21/neos21.hatenablog.com/atom/entry'

# 投稿する
$ curl -X POST -u 'neos21:xxxxxxxxxx' -d '<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app">
  <title>エントリタイトル</title>
  <author><name>neos21</name></author>
  <content type="text/x-markdown">__エントリ本文__

# ほげふが</content>
  <updated>2021-01-01T08:00:00+09:00</updated>
  <category term="Scala" />
  <app:control><app:draft>yes</app:draft></app:control>
</entry>' \
  'https://blog.hatena.ne.jp/neos21/neos21.hatenablog.com/atom/entry'

記事取得は GET、記事投稿は POST だ。

下書き投稿はできるが予約投稿はできない

そんで本題。POST で記事を投稿する時、

<app:control><app:draft>yes</app:draft></app:control>

とすれば下書き投稿され、

<app:control><app:draft>no</app:draft></app:control>

app:draft の値を yes から no に変えれば、実際に投稿できる。

一方で、updated 要素では記事の投稿時刻を指定できる。

<updated>2021-01-01T08:00:00+09:00</updated>

ということは、未来日付を指定しつつ下書き保存したら、予約投稿になるのか?と思ったが、予約投稿にはならない。

以下の記事も同じことを検証し、そう結論づけていた。

結局、日付を指定しようがしまいが、「下書き」or「実際に投稿」しか選べないし、日付はどちらにせよ「投稿日時」として利用されるだけなのだ。コレは API で作った下書き投稿をブラウザで開いてみれば分かる。

予約投稿はどう実現されている?

じゃあブラウザで見ている公式の記事投稿画面では、どうやって「予約投稿する」処理をしているのか。

開発者ツールで見てみると、/edit への POST 時に、フォームパラメータとして draft: 予約投稿する という情報を投げているようだった。

それならと思って

<app:control><app:draft>予約投稿する</app:draft></app:control>

というパラメータで POST してみたが、コレは yes 扱いになってしまい即投稿されてしまった。

結論:API からは予約投稿できない

ということで結論。

  • はてなブログの AtomPub API を使うには、面倒臭そうな OAuth とかは不要で、API キーさえあれば GET や POST ができる
  • はてなブログの AtomPub API では、未来日の投稿日時を指定しても、下書きと即投稿しか出来す、予約投稿はできない

いちいちブラウザでポチポチしたくないんだけど、仕方なさそう…。

参考文献

はてなブログ Perfect GuideBook [改訂第2版]

はてなブログ Perfect GuideBook [改訂第2版]

  • 作者:JOE AOTO
  • 発売日: 2020/07/18
  • メディア: 単行本