#はじめに
以前、Windows上のpythonでselenium+webdriverを扱った事があるのですが、なかなか便利なものだと感じておりました。最近またWebの自動化を調査していたところ、seleniumのコンテナがある事を知り、我が家のRaspberry Pi kubernetesでも動かせないか調べて、なんとか動かす事ができました。
自動化のテスト内容は、私の住んでいる地域では、2021年度からTEPCOの電気料金を通知してくれる検針票が紙からWebでの確認に切り替わりましたので、試しに電気代の取得を自動化してみました。
#環境
- 我が家のkubernetes v1.20.x (ARM64) on Raspberry Pi 4B
% ssh chino kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
chino Ready control-plane 254d v1.20.5 10.0.0.1 <none> Debian GNU/Linux 10 (buster) 5.4.83-v8+ docker://20.10.5
chiya Ready worker 254d v1.20.0 10.0.0.5 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://20.10.1
cocoa Ready worker 248d v1.20.0 10.0.0.2 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://20.10.1
maya Ready worker 111d v1.20.1 10.0.0.7 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ cri-o://1.20.0
megu Ready worker 146d v1.20.1 10.0.0.8 <none> Debian GNU/Linux 10 (buster) 5.4.83-v8+ cri-o://1.20.0
rize Ready worker 248d v1.20.0 10.0.0.3 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://20.10.1
syaro Ready worker 248d v1.20.0 10.0.0.4 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://20.10.1
Seleniumのデプロイ
SeleniumHQにkubernetes用のyamlがありますが、そのままデプロイしてもRaspberry PiのARM64アーキテクチャ環境では動作しません。
ARM用のイメージについて色々調べたところ、issuesにAMD64/ARM64のイメージを作ってる方がおりました。
「seleniarm」とは洒落てますね!
その方が作成したkubernetes用のyamlは、以下にあります。
ですが、私はfull-gridではなく、1台だけ使えればいいので「seleniarm/standalone-chromium」のコンテナイメージだけを使い、hubのポート(4444)とvncのポート(5900→6900)をexternalIP(192.168.1.98)経由でアクセスできるようにpodをデプロイしました。
まずは、Serviceリソースです。
apiVersion: v1
kind: Service
metadata:
name: seleniarm-chromium
labels:
app: seleniarm-chromium
spec:
type: ClusterIP
externalIPs:
- 192.168.1.98
selector:
app: seleniarm-chromium
ports:
- name: vnc
port: 6900
targetPort: 5900
- name: hub
port: 4444
targetPort: 4444
kubectlで適用します。
# kubectl apply -f seleniarm-chromiun-svc.yaml
service/seleniarm-chromium created
次にPodリソースです。
apiVersion: v1
kind: Pod
metadata:
name: seleniarm-chromium
labels:
app: seleniarm-chromium
spec:
containers:
- name: seleniarm-chromium
image: seleniarm/standalone-chromium:4.0.0-beta-1-20210215
resources:
requests:
cpu: 1
memory: 1Gi
limits:
cpu: 1
memory: 1Gi
ports:
- name: vnc
containerPort: 5900
protocol: TCP
- name: hub
containerPort: 4444
protocol: TCP
volumeMounts:
- name: dshm
mountPath: /dev/shm
volumes:
- name: dshm
emptyDir: { "medium": "Memory" }
kubectlで適用します。
# kubectl apply -f seleniarm-chromiun-pod.yaml
pod/seleniarm-chromium created
アプリケーション毎の各リソースに同じラベルを付与しておくと、リソース確認時のラベルでのフィルタ指定が楽になります。アプリケーションが多くなるようであれば、手間でも付与しておいた方が良いと思います。
# kubectl get all -l app=seleniarm-chromium
NAME READY STATUS RESTARTS AGE
pod/seleniarm-chromium 1/1 Running 0 12m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/seleniarm-chromium ClusterIP 10.96.12.179 192.168.1.98 6900/TCP,4444/TCP 12m
Selenium Serverへのアクセス
selenium hubのコンソールポートとも言われているようですが、ブラウザでアクセスすると、以下のような画面が表示されます。
tuxペンギンの横にある「i」のアイコンをクリックすると、ノードの詳細が表示されます。
ちゃんと「aarch64」で稼働できているようです。
VNCポートへの接続(MacOS X)
MacOS Xの場合は、Finderのメニューから「移動」-「サーバへ接続…」を選択します。
以下のような形式のアドレスでVNC接続できます。(IPアドレスはご自身の環境のものを指定してください)
VNC接続用のパスワードは「secret」です。
以下のようなロゴが確認できればChromiumノードは正常かと思います。
Node-REDでフローの作成
Node-REDの「パレットの管理」の「ノードを追加」タブで"selenium"を検索語にして、「node-red-contrib-selenium-webdriver」ノードを追加します。
作成したサブフローは以下のようなものです。
入力が必要なものは、全て環境変数で設定しています。
サブフローを書き出したJSONは以下になります。
(seleniumサーバへの接続URLは、サブフローを編集してご自身の環境に合ったものを指定してください)
2021/10/13記載:電話認証が必要になったようなので、スクレイピングできなくなりました。
TEPCOに会員登録してもAPIでさくっと取得できるような仕組みは提供されていないようです。
人の手作業でPull型の情報取得しかできないのはレガシーシステムの考え方なのかなと。
↓三角をクリックでJSONデータが展開されます。
~~絶対会員登録せずに確認する電気代取得サブフロー~~
[
{
"id": "dc9d610d.db821",
"type": "subflow",
"name": "電気代取得",
"info": "",
"category": "",
"in": [
{
"x": 30,
"y": 80,
"wires": [
{
"id": "333b08dd.0bdd58"
}
]
}
],
"out": [
{
"x": 570,
"y": 460,
"wires": [
{
"id": "a384a647.e68528",
"port": 0
}
]
}
],
"env": [
{
"name": "OFFICECODE",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "事業所コード"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "VISITNUM1",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "お客様番号1"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "VISITNUM2",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "お客様番号2"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "VISITNUM3",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "お客様番号3"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "VISITNUM4",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "お客様番号4"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "ZIPCODE1",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "郵便番号1"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "ZIPCODE2",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "郵便番号2"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "FIRSTNAME",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "セイ"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "LASTNAME",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "メイ"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "PHONENUM1",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "電話番号1"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "PHONENUM2",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "電話番号2"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "PHONENUM3",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "電話番号3"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "BANCHI",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "番地"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "GOU",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "号"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
}
],
"meta": {},
"color": "#FFAAAA",
"icon": "font-awesome/fa-flash"
},
{
"id": "333b08dd.0bdd58",
"type": "open-web",
"z": "dc9d610d.db821",
"name": "TEPCO Web検針票",
"browser": "chrome",
"weburl": "https://www.kenshin.tepco.co.jp/",
"width": 1024,
"height": 768,
"webtitle": "Google",
"timeout": 3000,
"maximized": true,
"server": "c6ee5be2.9cbec8",
"x": 200,
"y": 80,
"wires": [
[
"0c1928376142fec3"
]
]
},
{
"id": "a384a647.e68528",
"type": "close-web",
"z": "dc9d610d.db821",
"name": "",
"waitfor": 500,
"x": 470,
"y": 460,
"wires": [
[]
]
},
{
"id": "7bb1c1d6.a09cb",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "事業所コード",
"text": "",
"selector": "id",
"target": "officeCode",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 890,
"y": 80,
"wires": [
[
"77348357.8acdac"
]
]
},
{
"id": "578491ab.632b7",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "お客様番号1",
"text": "",
"selector": "id",
"target": "visitNumber1",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 390,
"y": 140,
"wires": [
[
"46bf4be5.dd4b54"
]
]
},
{
"id": "e6a1ba49.8b01f8",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "お客様番号2",
"text": "",
"selector": "id",
"target": "visitNumber2",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 750,
"y": 140,
"wires": [
[
"a4f546a3.c2bf58"
]
]
},
{
"id": "94b6af32.8c5e7",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "お客様番号3",
"text": "",
"selector": "id",
"target": "visitNumber3",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 1110,
"y": 140,
"wires": [
[
"fa76224d.19f81"
]
]
},
{
"id": "d207bc31.15e9d",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "お客様番号4",
"text": "",
"selector": "id",
"target": "visitNumber4",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 1470,
"y": 140,
"wires": [
[
"a342b4bb.a189d8"
]
]
},
{
"id": "f6a70e22.d581e",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "郵便番号1",
"text": "",
"selector": "id",
"target": "zipCode1",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 390,
"y": 210,
"wires": [
[
"6ea2fc8e.678544"
]
]
},
{
"id": "9e1a02dc.651f4",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "郵便番号2",
"text": "",
"selector": "id",
"target": "zipCode2",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 750,
"y": 210,
"wires": [
[
"7f1b1ac8.44a814"
]
]
},
{
"id": "7f1b1ac8.44a814",
"type": "click-on",
"z": "dc9d610d.db821",
"name": "住所検索",
"selector": "id",
"target": "searchAddress",
"timeout": 1000,
"waitfor": 500,
"clickon": false,
"x": 920,
"y": 210,
"wires": [
[
"571f0a78.bef2d4"
]
]
},
{
"id": "80d1c5a2.083a88",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "契約名義(セイ)",
"text": "",
"selector": "id",
"target": "first_name",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 400,
"y": 360,
"wires": [
[
"e5e08208.c6ea5"
]
]
},
{
"id": "262a8670.52c6ea",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "契約名義(メイ)",
"text": "",
"selector": "id",
"target": "last_name",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 780,
"y": 360,
"wires": [
[
"81aba5bf.f99688"
]
]
},
{
"id": "5fc18ee8.704e6",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "電話番号1",
"text": "",
"selector": "id",
"target": "phoneNumber1",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 390,
"y": 670,
"wires": [
[
"21b8833f.795a8c"
]
]
},
{
"id": "72999e09.363cb",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "電話番号2",
"text": "",
"selector": "id",
"target": "phoneNumber2",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 770,
"y": 670,
"wires": [
[
"a0a555b7.cfdaa8"
]
]
},
{
"id": "16ffc28.fdfa03e",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "電話番号3",
"text": "",
"selector": "id",
"target": "phoneNumber3",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 1130,
"y": 670,
"wires": [
[]
]
},
{
"id": "81aba5bf.f99688",
"type": "click-on",
"z": "dc9d610d.db821",
"name": "照会",
"selector": "id",
"target": "inquiry",
"timeout": 1000,
"waitfor": 500,
"clickon": false,
"x": 190,
"y": 460,
"wires": [
[
"4d792456.f4a91c"
]
]
},
{
"id": "4d792456.f4a91c",
"type": "get-text",
"z": "dc9d610d.db821",
"name": "",
"expected": "",
"selector": "className",
"target": "info-price",
"timeout": 1000,
"waitfor": 500,
"savetofile": false,
"x": 330,
"y": 460,
"wires": [
[
"a384a647.e68528"
]
]
},
{
"id": "8a09a506.aed678",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "OFFICECODE",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 700,
"y": 80,
"wires": [
[
"7bb1c1d6.a09cb"
]
]
},
{
"id": "77348357.8acdac",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "VISITNUM1",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 210,
"y": 140,
"wires": [
[
"578491ab.632b7"
]
]
},
{
"id": "46bf4be5.dd4b54",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "VISITNUM2",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 570,
"y": 140,
"wires": [
[
"e6a1ba49.8b01f8"
]
]
},
{
"id": "a4f546a3.c2bf58",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "VISITNUM3",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 930,
"y": 140,
"wires": [
[
"94b6af32.8c5e7"
]
]
},
{
"id": "fa76224d.19f81",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "VISITNUM4",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1290,
"y": 140,
"wires": [
[
"d207bc31.15e9d"
]
]
},
{
"id": "a342b4bb.a189d8",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "ZIPCODE1",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 210,
"y": 210,
"wires": [
[
"f6a70e22.d581e"
]
]
},
{
"id": "6ea2fc8e.678544",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "ZIPCODE2",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 570,
"y": 210,
"wires": [
[
"9e1a02dc.651f4"
]
]
},
{
"id": "7a5d7e5b.fc01d",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "FIRSTNAME",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 210,
"y": 360,
"wires": [
[
"80d1c5a2.083a88"
]
]
},
{
"id": "e5e08208.c6ea5",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "LASTNAME",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 590,
"y": 360,
"wires": [
[
"262a8670.52c6ea"
]
]
},
{
"id": "d86ff679.729248",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "PHONENUM1",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 210,
"y": 670,
"wires": [
[
"5fc18ee8.704e6"
]
]
},
{
"id": "21b8833f.795a8c",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "PHONENUM2",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 590,
"y": 670,
"wires": [
[
"72999e09.363cb"
]
]
},
{
"id": "a0a555b7.cfdaa8",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "PHONENUM3",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 950,
"y": 670,
"wires": [
[
"16ffc28.fdfa03e"
]
]
},
{
"id": "6f807932.4dac58",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "番地",
"text": "",
"selector": "id",
"target": "address_banchi",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 370,
"y": 270,
"wires": [
[
"94aaa7a1.8f5da8"
]
]
},
{
"id": "571f0a78.bef2d4",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "BANCHI",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 210,
"y": 270,
"wires": [
[
"6f807932.4dac58"
]
]
},
{
"id": "618ae571.e8087c",
"type": "send-keys",
"z": "dc9d610d.db821",
"name": "号",
"text": "",
"selector": "id",
"target": "address_gou",
"timeout": 1000,
"waitfor": 500,
"clearval": false,
"x": 700,
"y": 270,
"wires": [
[
"7a5d7e5b.fc01d"
]
]
},
{
"id": "94aaa7a1.8f5da8",
"type": "change",
"z": "dc9d610d.db821",
"name": "",
"rules": [
{
"t": "set",
"p": "value",
"pt": "msg",
"to": "GOU",
"tot": "env"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 540,
"y": 270,
"wires": [
[
"618ae571.e8087c"
]
]
},
{
"id": "0c1928376142fec3",
"type": "click-on",
"z": "dc9d610d.db821",
"name": "照会",
"selector": "className",
"target": "btn-bg-red notes-open-top",
"timeout": 1000,
"waitfor": 500,
"clickon": false,
"x": 380,
"y": 80,
"wires": [
[
"ddbe05e91f23c0a8"
]
]
},
{
"id": "ddbe05e91f23c0a8",
"type": "click-on",
"z": "dc9d610d.db821",
"name": "照会",
"selector": "className",
"target": "btn btn-s modal-button-upper",
"timeout": 1000,
"waitfor": 500,
"clickon": false,
"x": 530,
"y": 80,
"wires": [
[
"8a09a506.aed678"
]
]
},
{
"id": "c6ee5be2.9cbec8",
"type": "selenium-server",
"remoteurl": "http://192.168.1.9:30444"
},
{
"id": "8be0fab6cfc13f9e",
"type": "subflow:dc9d610d.db821",
"z": "4c288cd6.7479c4",
"name": "",
"x": 810,
"y": 1580,
"wires": [
[]
]
}
]
サブフローをダブルクリックして環境変数を設定してインジェクトノードから実行すれば、「msg.payload」に直近の期間の電気代が返ります。
VNCの画面でブラウザの動作が確認できます。いつ見ても自動でブラウザが動いているのを見るのは楽しいです。(4倍速にしてます)
おわりに
今回のテストはcurlでもできそうなくらいの内容なのですが、Node-REDからseleniumが利用できると何でも自動化できそうな気がして夢が膨らみますね。
注意点としては、「node-red-contrib-selenium-webdriver」のノードはサブフローで組まないと、すぐに「RangeError: Maximum call stack size exceeded」になります。
あと、流石にRaspberry Pi kubernetesでは重いですね...full gridをデプロイしてWeb画面のテストとかはキツそうです。