OpenCV で手本の文字と自分が書いた文字をマッチングしてみた(その1:特徴点マッチング)

OpenCV は初めて触った上、画像処理の知識がないので雑なまとめです。

はじめに

書道で使う手本は文字が小さく、手本は何十ページもあるため、後で自分が書いたものと手本を見返そうとしても探すのが大変だったりする。

f:id:nyamadori:20180603003552j:plainf:id:nyamadori:20180603003604j:plain
左: 手本をもとに自分が書いたもの、右: 手本の一部。手本の真ん中の行が書いた場所

自分が書いた文字と予めスキャンしておいた手本の中の文字とを自動でマッチングし、手本の文字と自分の文字をあわせた合成画像を生成できれば、あとで見返すのに便利そうである*1。例えば以下のような感じ。このように並べてみればニュアンスの違いが一目瞭然だし、一枚の画像に合成しておけば、スマホでいつでも確認できる。

f:id:nyamadori:20180603021624p:plain

OpenCV の特徴点マッチング

上のような合成画像を作るには、いろいろやることがありそうだが、ひとまず OpenCV の特徴点マッチングを用いて、手本の文字と自分の文字のマッチングを試みた。特徴点マッチングについては、以下を参照してほしい。雰囲気がつかめると思う。

特徴点マッチングはこのように実装した。このプログラムを実行すると、左側は自分が書いたもの、右側は手本に合成された一枚の画像が GUI で表示される。結果画像の左側と右側の間には、マッチした特徴点同士が線で結ばれる。

import numpy as np
import cv2 as cv
import matplotlib
from matplotlib import pyplot as plt

# 手本をもとに自分が書いたもの
origin1 = cv.blur(cv.imread('a_reproduce.jpg', cv.IMREAD_GRAYSCALE), (10, 10))

# 手本
origin2 = cv.blur(cv.imread('a_model.jpg', cv.IMREAD_GRAYSCALE), (10, 10))

# リサイズしないと処理速度的に厳しかった
resized_image_1 = cv.resize(origin1, None, fx=0.2, fy=0.2, interpolation=cv.INTER_CUBIC)
resized_image_2 = cv.resize(origin2, None, fx=0.2, fy=0.2, interpolation=cv.INTER_CUBIC)

# 二値画像に変換
ret1, preprocessed_image_1 = cv.threshold(resized_image_1, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
ret2, preprocessed_image_2 = cv.threshold(resized_image_2, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)

# 特徴点検出器をインスタンス化
detector = cv.AKAZE_create()

# 特徴点を検出
kp1, des1 = detector.detectAndCompute(preprocessed_image_1, None)
kp2, des2 = detector.detectAndCompute(preprocessed_image_2, None)

bf = cv.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
good = []
for m,n in matches:
    if m.distance < 0.75 * n.distance:
        good.append([m])

img3 = cv.drawMatchesKnn(preprocessed_image_1, kp1, preprocessed_image_2, kp2, good, None, flags=2)

plt.imshow(img3) # 結果画像を表示
plt.show()

結果は次のようになった。ひと目で見て分かる通り、うまくマッチングできていない。うまくマッチングできていれば文字を繋ぐ線が分散せず、一つの対応する文字に集中する。

f:id:nyamadori:20180603010004p:plain

以下は正しくマッチできている箇所。周辺の空間に特徴があれば、精度高くマッチングできそう。

f:id:nyamadori:20180603010258p:plain

以下はミスマッチの箇所。エッジの角度やコーナーの形が類似する部分でミスマッチが多発しているっぽい。

f:id:nyamadori:20180603010308p:plain

考察

  • 特徴点マッチングは、エッジの角度や空間の形状をもとにしていると思われる
  • 文字は単純な線や空間で構成されているため、エッジの角度や空間の形状をもとにした特徴点マッチングでは、文字同士 をマッチングするのが難しい
  • 文字同士をマッチングするには、エッジの角度や空間の形状という細かい粒度ではなく、「文字」を塊として手本とマッチングする必要があると考えられる

おわりに

画像処理をちゃんと分かってなくてもそれっぽい結果が得られる OpenCV すごい。

次は、「文字」を塊として扱いやすいテンプレートマッチングを試してみたい。

*1:半紙一枚書くたびに写真を撮ればいいと思うかもしれないが、集中力が切れてしまう

Docker Compose で MySQL コンテナを立てるときに MYSQL_ROOT_PASSWORD 環境変数の指定でハマった

結論

  1. Docker Compose のサービスの環境変数は Hash 形式で設定するのがおすすめ
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_ROOT_PASSWORD: '' # **空文字列** がパスワードとして設定される
  1. docker-compose down db だけだと、db コンテナに紐づく volume は削除されない。MYSQL_ROOT_PASSWORD 周りの環境設定を書き換えた後は、ボリュームを含めて削除する必要がある

背景

罠1. 配列形式で環境変数を指定したときの解釈

配列形式の環境変数指定の場合、""空文字列ではなく、ダブルクオーテーション 2 文字("")の文字列として解釈されて、MySQL ユーザが作成される。

environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_ROOT_PASSWORD="" # 「""」がパスワードとして設定される

この挙動はパット見ではすぐ気づけないので、以下のような Hash 形式で指定するのがおすすめ。

environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_ROOT_PASSWORD: '' # **空文字列** がパスワードとして設定される

そもそもこのケースの場合、パスワードが空なので MYSQL_ROOT_PASSWORD 環境変数の指定は不要である。

environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'

罠2. docker-compose down db だと volume は削除されない

  • docker-compose down db しても、オプション無しではサービスがマウントしているボリュームが削除されない
    • MySQL コンテナのデータは、MySQL ユーザも含めボリュームに永続化されるため、 docker-compose down しても作成した MySQL ユーザは削除されない
    • MYSQL_ROOT_PASSWORD 周りの環境設定を書き換えた後は、ボリュームを含めて削除する必要がある

docker-compose down docker-compose up して試行錯誤中の環境変数を試してみるも、docker-compose down だけだと MySQL ユーザが削除されない。環境変数の設定値を試行錯誤するうちに、MYSQL_ALLOW_EMPTY_PASSWORD が正しく設定された MySQL ユーザが知らないうちに作られてしまっていたため、以下の記述をすればまっさらな環境でも空パスワードの MySQL ユーザができると勘違いしていた。

# 誤った記述
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_ROOT_PASSWORD="" # 「""」がパスワードとして設定される

Sidekiq を使うとき、ActiveRecord のコネクションプール数 >= Sidekiq のスレッド数 + 1 にするのが良さそう

以下の記事に書かれていたことそのままだが備忘としてまとめた。

repl.info

  • ActiveRecord のコネクションプールは、各 Sidekiq ワーカスレッドと そのスレッドが動作するプロセス(Sidekiq プロセス) に一つずつ割り当てられる
  • Sidekiq の並列数(ワーカスレッド数)と、コネクションプール上限数が等しいとき、ワーカスレッドに割り当てられるはずのプールが一つ不足する(Sidekiq プロセスに一つ割り当てられるため)
  • この状態でかつ、database.yml の checkout_timeout に設定した秒数(デフォルトは 5 秒)以上かかるジョブを実行する場合
    • 2 つのワーカスレッドに処理を割り当てると、片方のワーカスレッドはコネクションを取得できるので処理が実行できる
    • しかし、もう片方のワーカスレッドは、はじめのワーカスレッドと Sidekiq プロセスによってコネクションプールに空きがないため、checkout_timeout 秒以内にコネクションを取得できず、ジョブの実行に失敗する
  • したがって、ActiveRecord のコネクションプール数は、ワーカスレッド数 + 1 以上にするのが適切である

以上!

開発用データ入り MySQL Docker イメージを作成する仕組みを AWS CodeBuild で構築したよ

流れをぶった切ってしまってすみません! Speee Advent Calendar 2017 の 20 日目の記事です。

19 日目: スポンサー活動を本気でやってみた1年でした

概要

開発用のデータは、Web アプリケーションを作る上で必要不可欠です。しかし、手動で Jenkins から日時更新のダンプデータをローカルに落としてきて MySQL にインポートするのはダサいです。はじめから開発データが入った MySQL Docker イメージを配布しておけば、手動でダンプデータをインポートする必要がなく便利です。そこで AWS CodeBuild で開発データ入り MySQL イメージを作る仕組みを構築しました。運用はまだできていませんが、CodeBuild を使う上でハマったことがいくつかあったので書こうと思います。

きっかけ

ある日、開発環境の MySQL コンテナのデータがぶっ壊れました。データを残しつつ復旧するのが面倒だったので、MySQL コンテナを再ビルド、開発データをインポートし復旧させました。復旧に数十分かかったのですが、仮に開発データが Docker Volume ではなく、本体コンテナのイメージレイヤーに含まれていれば、データが壊れたとしても、コンテナを再起動することで破損したファイルを持つイメージレイヤーが消え、容易に復旧できるのではと思ったのです。

実際、それほど頻繁にデータは壊れませんが、はじめから開発データが入った MySQL イメージを配布しておけば、手動でダンプデータをインポートする必要がなくなり便利になるはずです。

仕組み

以下の図で説明します。まず、Jenkins が本番環境のマスク済みデータ(開発データ)を S3 バケットにアップロードします。Lambda は、S3 バケットへのアップロードを検知し、CodeBuild をキックします。それを受け CodeBuild は、最新の開発データを S3 から取得し、Docker コンテナ上で動作する MySQL に開発データをインポートします。インポート後、Docker コンテナの状態をイメージとして保存、ECR に push します。ECR に push されたイメージは、docker-compose 等から利用できます。

f:id:nyamadori:20171222010826p:plain

CodeBuild

以下がこの仕組みで作った CodeBuild の YAML ファイルです。この YAML ファイルをビルドスペックといい、buildspec.yml というファイル名をつけます。CircleCI のような感じの設定ファイルでビルド手順を記述できますが、いくつかハマリポイントがありました。環境変数は、AWS コンソール画面から与えています。

version: 0.2 # ハマリポイント 1

phases:
  pre_build:
    commands:
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email) # ハマリポイント 2
      - aws s3 cp s3://${DUMP_BUCKET}/${DUMP_PATH} .
      - docker build -t mysql_without_volume docker/mysql_without_volume
      - docker run -d --name mysql_with_seed -e 'MYSQL_ALLOW_EMPTY_PASSWORD=yes' -p 3306:3306 mysql_without_volume
      - sudo apt update && sudo apt-get -y install mysql-client
  build:
    commands:
      - DOCKER_IMAGE_TAG=$(date '+%Y%m%d')
      - DOCKER_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}"
      - echo $DOCKER_REPO
      - echo $DOCKER_IMAGE_TAG
      - |
          mysql -u root -h 127.0.0.1 <<-SQL
            create database ${DUMP_DATABASE} CHARACTER SET utf8mb4;
          SQL
      - gzip -dc $(basename $DUMP_PATH) | mysql -u root -h 127.0.0.1 -f ${DUMP_DATABASE}
      - docker commit mysql_with_seed "${DOCKER_REPO}:latest"
      - docker commit mysql_with_seed "${DOCKER_REPO}:${DOCKER_IMAGE_TAG}"
  post_build:
    commands:
      - docker push "${DOCKER_REPO}:latest"
      - docker push "${DOCKER_REPO}:${DOCKER_IMAGE_TAG}"

MySQL Docker イメージ

この仕組みは、開発用データと一緒に MySQL イメージを配布できるようにするため、Docker 公式の MySQL 5.6 イメージ から Volume を取り除いた独自のイメージを使っています。といっても、公式 Dockerfile から VOLUME の記述をコメントアウト しただけです。これで DB のデータをコンテナのイメージレイヤーに含めることができます。

成果物

github.com

ハマりどころ

環境変数

ビルドスペックのバージョン 0.1 は、1 コマンドごとにシェルのインスタンスが独立していて、カレントディレクトリや環境変数の状態を共有することができません。 例えば、以下のようなビルドスペックにおいて、最初に定義した環境変数 DOCKER_IMAGE_TAG DOCKER_REPO の値を、後ろの echo している行で取得することができません。

version: 0.1

phases:
  build:
    commands:
      - DOCKER_IMAGE_TAG=$(date '+%Y%m%d')
      - DOCKER_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}"
      - echo $DOCKER_REPO       # => 何も表示されない
      - echo $DOCKER_IMAGE_TAG  # => 何も表示されない

対策は、バージョン 0.2 のビルドスペックを使うことです。バージョン 0.2 から前のコマンドで定義した環境変数を、後ろのコマンドに引き継いでくれます。phase が分かれていても問題ありません。 どうしてもバージョン 0.1 を使う必要がある場合は、シェルスクリプトとして切り出すか、環境変数の定義と参照を 1 コマンドにまとめることになると思います。

docs.aws.amazon.com

CodeBuild のサンプル解説ページで想定している Docker バージョンが古く、ECR ログインがコマンドオプションでこける

今回の仕組みを作るにあたり、以下の AWS 公式が提供しているサンプルを参考にしましたが、ECR のログインコマンドでコケてしまいました。

docs.aws.amazon.com

原因としては、Docker 17.06 以降 docker login コマンドから --email オプションが削除されたものの、aws ecr get-login コマンドが使う docker login コマンドに --email オプションが含まれていて、コマンドエラーになったようです。対策は、以下のように aws ecr get-login コマンドに ----no-include-email を付与することです。これで正常に動作するはずです。

- $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)

docs.docker.com github.com

感想

今回、初めて CodeBuild を触りましたが、一言で表すと CircleCI のような感じです。AWS のサービスなので、すでに利用している EC2 や S3 等と密に連携できたり、サーバレスでリソースを気にせずジョブを実行できるし、CircleCI に置くのが憚られるセンシティブなデータを扱ったり、多数のビルドジョブによるテスト実行だったりに使えそうだと思いました。

21日目は、「朝起きれない問題に本気で向き合う目覚ましサービスを作った」です!

kohtaro24.hatenablog.jp

よくコケる不安定な feature spec への対処療法 rspec-retry gem

背景

業務で開発している Web サービスの feature spec が非常に不安定で、feature spec が CI 上でコケるたびに、ビルドジョブをよく手動で再実行していました。1 回の再実行でパスすることもあれば、まれに 10 回以上の再実行が必要なこともありました。

その Web サービスでは、CI のジョブ終了までに約 8 分(中央値)かかります。つまり、リトライの回数が増えるたび、大体 8 の倍数分リリースに要する時間が増加してしまいます。

このような状態だと、価値をユーザに早く届けられなくなってしまうし、個人的にもフラストレーションがたまっていたので、 2 ヶ月前に rspec-retry gem を導入しました。

rspec-retry gem

rspec-retry gem は、RSpec の example に :retry オプションを追加し、spec が成功するまでオプションで指定した回数だけリトライしてくれる gem です。

今回は、fail しやすい feature spec 全体に対して、retry オプションを付与するようにしました。

RSpec.configure do |config|
  # run retry only on features
  config.around :each, type: :feature do |ex|
    ex.run_with_retry retry: 3
  end

  config.verbose_retry = true
  config.display_try_failure_messages = true
end

導入した結果

以下の画像は、その Web サービスの CircleCI の Insights から見られるグラフで、縦軸が完了までにかかった時間、横軸がビルドの実行日時です。縦棒は、すべてのジョブではなく、ある時点でサンプルしたジョブで、緑が成功したジョブ、赤が失敗したジョブを表しています。

結果はグラフの通りで、rspec-retry 導入以降はジョブ実行中のリトライが効いたことで、手動でリトライを行うことがほとんどなくなりました。素晴らしい!

f:id:nyamadori:20171119192821p:plain

とはいえ、あくまでこの方法は対処療法で、本来ならリトライが必要にならない spec に修正するのが根本対策です。同じように困っている方がいたらお試しあれ。

リンク

ElastiCache + Redis に出てくる概念と、クラスタモードごとの違い

はじめに

Web サイト表示速度向上の一環として、仕事で、ElastiCache + Redis によるキャッシュ層を導入する。 導入にあたり、ElastiCache + Redis で利用するノードタイプ(インスタンスタイプ)や、制限などの事前調査が必要になった。

ElastiCache + Redis は、ノードタイプやクラスタの種類によって、機能サポートが異なり、混乱するところが多々あったため、AWS のドキュメントを参考に、分かりにくいところをまとめた。

これから導入する段階なので、AWS のドキュメントの内容以上のことは書いていません。 運用についてとか、ハマったところなどの記述はありません。

内容におかしいところがあれば教えてください

概念

実際に使うかどうかに関わらず、シャードやレプリカという用語がバンバンでてくるので、まとめる。

f:id:nyamadori:20170912101609p:plain

参考: http://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/WhatIs.Components.html#WhatIs.Components.Nodes

クラスタ (Cluster)

シャードをまとめる論理グループ。複数のシャードを作ることで、シャーディングができる。データはシャード間で分割される。

クラスタごとにキャッシュエンジン(Redis, Memcached)を設定できる。ElastiCache のクラスタには、クラスタモード無効と、クラスタモード有効の 2 種類ある(厳密にはキャッシュエンジンの種類)。

Redis (クラスタモード有効) は、クラスタ 1 つにシャード 1〜15 個持つことができる。 Redis (クラスタモード無効) は、クラスタ 1 つにシャード 1 個持つことができる。

シャード (Shard)

ノードをまとめるグループ。データはノード間で同期され、2つ以上のノードを用いることで、レプリケーションできる。

1 シャードは、読み書きができるプライマリノード 1 個と、読み込み専用のセカンダリノードリードレプリカ)0 〜 5 個を持つ。

ノード (Node)

ElastiCache の最小単位で、保存領域(RAM)を持つ。 設定したノードタイプによって CPU 性能や保存領域のサイズが異なる。

キャッシュエンジンは、クラスタごとに設定できるため、クラスタ以下のノードすべては、同じキャッシュエンジンで動作する。

レプリケーション

ノード間で同じデータを共有すること。 プライマリノードへの書き込みを、プライマリノードに紐づくすべてのリードレプリカへ非同期的に反映する。

以下のメリットがある。

  • 耐障害性の向上
  • 読み込みの負荷分散

シャーディング

データをシャード間で分割すること。

以下のメリットがある(っぽい)。

  • 読み書きの負荷分散
  • コストパフォーマンス向上
    • 小さめのノードを、複数のシャードに分割することで、データ容量を増やしつつ、クラスタ全体のコストを抑えることができる

自動フェイルオーバ

ノードの障害を検知し、障害が発生したノードを新しいノードに置き換える機能。 (フェイルオーバの一般的な意味は: https://www.idcf.jp/words/failover.html

障害時の挙動

  • 障害元がプライマリノードの場合
    • 自動で選択したリードレプリカをプライマリノードに昇格させ、障害が発生したプライマリノードと置き換える。
    • アプリケーションで利用しているプライマリのエンドポイントは、変更する必要がない
    • 昇格にかかる数分間、プライマリノードへの書き込みの一部が失われる
      • シャーディングしている場合は書き込みの一部、していない場合は全ての書き込み(多分)
  • 障害元がレプリカの場合
    • 障害が発生したノードと新しいノードを置き換える
    • フェイルオーバ中、レプリカが本来担う読み込み処理を、プライマリノードが引き受けるので、プライマリノードの負荷が増える
    • アプリケーションで利用しているリードレプリカエンドポイントを、変更する必要がある

参考: http://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/AutoFailover.html#AutoFailover.Overview

用語の違い

API/CLI 、コンソールの間で、用語が異なる場合があるので注意。 ユーザガイドの中でも、用語が揺れているので把握しておく必要がある。

基本的に

クラスタ (cluster) = レプリケーショングループ (replication group)
シャード (shard)   = ノードグループ (node group)

レプリカノードがない場合は、ノードとクラスタが 1 対 1 で対応するので、

クラスタ = ノード

となる。図で示すと AWS マネジメントコンソールが

f:id:nyamadori:20170912101727p:plain

で、API / CLI での用語が

f:id:nyamadori:20170912101737p:plain

という感じ。

参考: http://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/WhatIs.Terms.html

クラスタモードごとの比較

クラスタモードごとに機能サポートに一長一短があるので、慎重に選ぶ必要がある。

Redis (クラスタモード無効)

  • シャーディング:
    • 未対応
  • ノードタイプ等の変更:
    • クラスタ作成後も、ノードタイプ、エンジンのバージョンを変更できる
    • ただし変更中は、読み書き処理がブロックされ、インスタンスが利用できない(試しに、空の t2.micro ノードを t2.small に変更してみると、約 11 分かかった)
    • レプリカの追加は、クラスタ全体を停止することなくできる(っぽい)
    • キャッシュクラスタがあまり利用されない時間帯に行う必要がある
    • 変更処理は、即時実行または、定期用設定したメンテナンス時間帯行われる
    • ノードタイプの変更は、スケールアップしか対応していない(スケールダウンは、新しくクラスタを作る必要がある)
  • バックアップ・復元:
    • S3 へのバックアップ、S3からの復元に対応
    • cache.t1.micro または cache.t2.* ノードではサポートされない
  • 自動フェイルオーバ付マルチ AZ:
    • t1, t2ノードタイプ は未対応

参考

Redis (クラスタモード有効)

  • シャーディング:
    • 1 〜 15 個のシャードを作成可能
  • ノードタイプ等の変更:
    • クラスタ作成後は、ノードタイプ、シャード数、レプリカ数、エンジンのバージョンを変更できない
    • 変更するには、新たなクラスタを作って、データを移行する必要がある
  • バックアップ・復元:
    • 全ノードタイプで S3 へのバックアップ、S3からの復元に対応
    • ただしクラスタレベルでのバックアップのみ対応
  • 自動フェイルオーバ付マルチ AZ:
    • 利用必須
    • Redis バージョンが 3.2.4 以降なら t2 ノードタイプでも対応。
    • 手動でリードレプリカをプライマリに昇格できない

参考

まとめ

  • 雰囲気、自由に使えそうなクラスタ有効モードだが、意外と制約が多い
    • クラスタ作成後に、ノードタイプ、シャード数、レプリカ数、エンジンのバージョンを変更できない
    • 小さいインスタンスを組み合わせてうまく構築できるのなら、コスパがいい(かも)
    • t2 ノードタイプ等の小さいインスタンスの機能サポートが手厚いのは、クラスタ有効モード
  • クラスタモード無効は、ノードタイプ等を変更できるが、読み書き処理がブロックされる
    • ダウンタイムをなくしたいなら、クラスタを複製して、エンドポイントを切り替える必要がある
      • だったら、クラスタモード有効を使ってもあまり関係ない気がする
      • このあたり、簡単にやれる方法があれば教えて欲しいです
  • クラスタモード無効の t2 ノードタイプは、バックアップ・復元ができないので、本番ではあまり使いたくない気がする

次に調べること

Redis クライアントについて

  • 障害時の挙動
    • 接続をリトライすることで生じる、レスポンス遅延とか
    • レプリカのフェールオーバ時に、アプリケーションで指定しているエンドポイントの切り替え
      • 自動でやってくれる方法はないのか

(メモ)フォーム要素に対してはelement.idよりもelement.getAttribute('id')を使うほうがいい

以下、メモ

  • フォーム要素 (HTMLFormElement) は、自身が持つ <input name="x"> に対して formElement[x] のような添字でアクセスできる。
  • <input name="id"> な要素を持つフォームの場合
    • form["id"] には <input name="id"> な要素が入っている
    • なので、フォーム要素に指定した id 属性は、form.id から取得できない
  • フォーム要素の id 属性を取得するときは、form.getAttribute(“id”) を使うほうが良い

書いてて思ったけど、<input name="getAttribute"> な要素を持つフォームだったら、form.getAttribute すらも上書きされちゃうのかな。そんな名前の input 要素、誰が作るんやという話だけど。