Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save SARDONYX-sard/4b43ba57618e51875b1b281caf70c142 to your computer and use it in GitHub Desktop.
Save SARDONYX-sard/4b43ba57618e51875b1b281caf70c142 to your computer and use it in GitHub Desktop.
Qiitaに投稿していた記事すべて

動機

Nodejs 16.9.0以上で標準搭載されたらしいcorepackでpnpm、yarn、npmを実行していたのですが、corepackのパッケージマネージャーを指定するコマンドで最新バージョンを簡単に指定できないようなので簡易的にPython3でスクリプトを書きました。

  • なお、この件につきましたはこのようなissueが上がっています。 --latestオプションの検討

nodejs/corepack#72

近いうちにlatestオプションが追加されるかもしれません。それまでのつなぎとして…

「なんでjsじゃないんだ…?Nodejsなのだからjsで書こうよ…(´・ω・`)」という声が聞こえてきそうですが、遺憾ながらこのスクリプトは私のdotfilesを構築している時に書いた別目的の既存コードを改変した副産物なのでご了承ください…

jsで書くには「引数処理を標準モジュールでできるか?」が不明のためちょっとやる気がでない現状です。

ちょっとした解説

やってることはただnpm search <マネージャー(npm|pnpm|yarn)>の結果をripgrepでversionだけ取得し、それをもとにactivateしているだけです。 これで最新バージョンのNodejsのパッケージマネージャーがactivate状態になります。

心配の方は先にdry-runをしてください。

python3 -u corepack-update.py -d

最新アプデ

"""Corepack latest version updater
Requirement:
- python3
- ripgrep: https://github.com/BurntSushi/ripgrep
- Nodejs >=16.9.0 or `npm i -g corepack`
Usage:
- activate latest version
python3 corepack-update.py
- dry-run mode
python3 corepack-update.py --dry-run
python3 corepack-update.py -d
"""


import argparse
from os import system
import subprocess
from typing import Literal


def system_call(command: str):
    """
    Reference https://stackoverflow.com/questions/18739239/python-how-to-get-stdout-after-running-os-system
    """
    return subprocess.getoutput(command)


def corepack_enabled(manager_name: str):
    return system_call(f"corepack enabled {manager_name}")


def activate_corepack(manager_name: str, version: str):
    system(f"corepack prepare {manager_name}@{version}  --activate")


def get_latest_version(manager_name: str, is_debug: bool):
    version_regexp = "[0-9]?[0-9]\\.[0-9]?[0-9]\\.[0-9]?[0-9]"
    cmd = f"npm search {manager_name} | rg \"^{manager_name} .* {version_regexp}\" \
| rg -o \"{version_regexp}\""

    if is_debug:
        print(color("Execute command", "cyan") + f": {cmd}")
    return system_call(cmd)  # `-o` is ripgrep only match option


def get_args():
    parser = argparse.ArgumentParser()

    parser.add_argument(
        "-d",
        "--dry-run",
        help="No activate corepack. just stdout",
        action="store_true")

    parser.add_argument(
        "-e",
        "--enabled",
        help="Enable management by corepack.",
        action="store_true")

    return (parser.parse_args())


def color(string: str, mode: Literal["green", "red", "yellow", "cyan"]):
    colors = {
        "red": "\033[31m",
        "green": "\033[32m",
        "yellow": "\033[33m",
        "cyan": "\033[36m"}
    return f'{colors[mode]}{string}\033[0m'


def main():
    args = get_args()
    managers = ["npm", "pnpm", "yarn"]

    if is_dry_run := args.dry_run:
        print(color("INFO: Dry run mode enabled.\n\
Please visually check that the version assigned by the code is correct.\n", "cyan"))

    for manager_name in managers:
        manager_latest_version = get_latest_version(manager_name, is_dry_run)

        if args.enabled:
            print(color("INFO: Enabling management by corepack...", "cyan"))
            corepack_enabled(manager_name)

        if is_dry_run:
            print(
                f"Probably... the latest {manager_name} version: {manager_latest_version}")

            search_cmd = f"npm search {manager_name}"
            print(color("Execute command", "cyan") + f": {search_cmd}")
            print(
                f"{search_cmd} Docs: https://docs.npmjs.com/cli/v6/commands/npm-search")
            system(search_cmd)
            print("\n")
        else:
            print(f"{manager_name} active: {manager_latest_version}")
            activate_corepack(manager_name, manager_latest_version)


if __name__ == "__main__":
    main()
  • 512文字
  • 読了目安: 1分20秒

実行環境

  • windows11
  • Anaconda3
  • VS Code

手順

1. Anaconda3fprettify(pythonで作られたgfortranフォーマッタ)をインストール

conda install -c programfan fprettify

2. VS Code拡張機能をインストール

https://github.com/pseewald/fprettify

3. 同じくVS Code内で、 ctrl + shift + p → setting.jsonに以下追記

  • <UserName>は自分のPCのユーザーネーム。最初は変数展開を試みたけれどうまく動作せず。
"modernFortranFormatter.fprettifyPath":"C:\\Users\\<UserName>\\Anaconda3\\Scripts\\fprettify.exe",

補足

・(fprettify使い方詳細): https://github.com/pseewald/fprettify

更新ログ

記事の既知の問題

  • 折りたたみのコードの後に「詳細」という折りたたみが居座り続ける。

前書き

  • この記事では、「アメリカのアリゾナ州の天気をテキストファイルに書き込むプログラム」を書いていきます。

  • もっと多くの人がプログラミングを楽しめたらいいと思い、Node.jsとセレニウム(テストを自動化するためのツール)で遊ぶ記事を書きました。

  • 実機で確認しながら進めていますが、至らぬ点があるかもしれません。

  • 次記事 Node.js - セレニウムで遊ぼ!(b ・ω・)b :2日目(中身)

  • サンプル置き場 selenium-playground (なお、README.mdのサンプルも兼ねてMITライセンスとなっていますが、著作無記名でお好きにお使いいただいても問題ありません。WTFPLライセンスと同義とお考えください。)

この記事で使うもの

  • Chrome -version 91
  • Node.js -version 14以上 (fs/promisesの書き方を変えれば、10.17~でも動きます)
  • Visual Studio Code

*補足情報*

  • (筆者は今回、windows10で実行しています。)

  • コードや画像は折りたたんでいます。<>のマークが折り畳みの印として記述統一しています。 中身を閲覧したい場合、<>の部分をクリックしてください。

  • Chrome: ブラウザと呼ばれる、Webサーバに接続するためのソフトウェア。IEやEdge、Safariなど多種存在。

  • Node.js: JavaScriptと呼ばれるプログラム言語の実行環境の1つ。

  • Visual Studio Code: プログラムを書きやすいソースコード編集ツール。

1: Node.js、Chrome、VS Codeのインストール

Chromeのインストール

1. 以下のリンクから、「Chrome をダウンロード」と書かれた青色のリンクをクリックして、Chromeブラウザをダウンロードします。

https://www.google.com/intl/ja_jp/chrome/

2. 「ChromeSetup.exe」というファイルがダウンロードされるのでダブルクリックしてインストール

Node.jsのインストール

1. 以下のリンクから、「LTS版」か、「最新版」と書かれた緑のリンクをクリックして、Node.jsをダウンロードします。

*LTSとは、「Long Term Support」の略で、長期間セキュリティアップデートが提供されるものです。個人的には、こちらをおすすめします。

https://nodejs.org/ja/

2. 「node-v<ヴァージョン>-x<64または32>(例:node-v14.17.1-x64)」というファイルがダウンロードされるのでダブルクリックしてインストール

<セットアップフロー> ![nodejs-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/41d00c5c-9dc8-a37b-7d7a-c9eb62ef78e2.png)

Visual Studio Codeのインストール

1. 以下のリンクから、自分が今お使いのOSの名前が書いてある青色のリンクをクリックして、Visual Studio Codeをダウンロードします。

https://code.visualstudio.com/download

2.「VSCodeUserSetup-x<32や64>-<ヴァージョン名>(例:VSCodeUserSetup-x64-1.57.1)」というファイルがダウンロードされるのでダブルクリックしてインストール

<セットアップフロー>

vscode-flow.png

2: Visual Studio Codeを起動し、プロジェクトフォルダ作成を行う。

ここではVisual Studio Codeを起動を起動した状態で話を進めます。

2.1 プロジェクトの作成フォルダを選択

  • Ctrl + K」を1度目に押し、次に「Ctrl + O」を押してフォルダを開きます。

  • 分かりやすい場所ならどこでもかまいませんが、今回は試しにDドライブ直下に作ります。 (この場合「フォルダを開く」は「Dドライブ」を選択します)

2.2 プロジェクトフォルダの作成

Ctrl + Shift + @」を押して、ターミナルを開き、以下のコマンドを入力します。

mkdir javascript/selenium-playground
<コマンドの意味>

mkdir: make directoryの略で、フォルダを作るコマンド。 mkdir <フォルダパス>で子フォルダまですぐ作成できます。 したがって、今回のコマンドは、 「javascriptというフォルダの中に、selenium-playgroundというフォルダを作る。」 という意味です。

2.3 フォルダに入り、プロジェクトの設定ファイルを作成。

以下のコマンドを実行します。

npm init -y
<コマンドの意味>

npm: 他の人が作った便利なプログラムのコードをネットからダウンロードし、それを管理するもの。 パッケージマネージャと呼ばれます。Node.jsをインストールすると含まれています。

npm init: プロジェクトを作成するときのコマンドです。 実行すると質問に基づきpackage.jsonというファイルが作られます。

-y: yオプションと呼ばれます。「yes」の略で、npm init実行時の質問をすべてデフォルト設定のままにします。

<この時点での画像を表示>

2021-06-28 (1).png

3: 必要なパッケージのインストール

ソース: npm とは何か / Package と module の違い

  • A package is a file or directory that is described by a package.json. This can happen in a bunch of different ways! For more info, see “What is a package?, below.
  • A module is any file or directory that can be loaded by Node.js’ require(). Again, there are several configurations that allow this to happen. For more info, see “What is a module?”, below.

  • パッケージは package.json によって記述されるファイルもしくはディレクトリです。これは様々な方法でブランチを作ります。

  • モジュールは Node.js の require() によってロードされるファイルもしくはディレクトリです。

  • モジュール、パッケージ、ライブラリ(パッケージをまとめたもの)の定義はこちらに従うとして、 ここからは他の人が作ったプログラムをインストールしていきます。

  • 以下のコマンドをコピーして、ターミナル内で実行します。

# `package.json`の`dependencies`(開発環境・本番環境で使用するパッケージ群)という項目に追加されるパッケージ
npm i chalk chromedriver moment selenium-webdriver;

# `package.json`の`devDependencies`(開発環境で使用するパッケージ群)という項目に追加されるパッケージ
npm i -D @types/jest @types/jest-plugin-context;
npm i -D @types/node @types/selenium-webdriver;
npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser;
npm i -D eslint eslint-config-prettier;
npm i -D eslint-plugin-jest eslint-plugin-prettier;
npm i -D husky jest jest-plugin-context lint-staged prettier;
npm i -D ts-jest ts-node typescript;
<コマンドの解説>
`npm i`<インストールしたいパッケージ> :  `npm install`コマンドの略で、パッケージのインストールに使います。  `npm 5.0.0`以降、--saveオプションが自動で付与。(ご使用中のnpmヴァージョンの確認方法は`npm -v`)
`-D`:  `--save`(現在のプロジェクトのみで使うパッケージとして扱うというオプション)と  `--dev`(`dependencies`項目の中に追加するという意味)のオプションの両方を併せ持った複合オプション

先ほどインストールしたパッケージの概要

<コードを表示>
  • 21/6/28現在、筆者がインストールしたものにコメントで概要を記載します。
  • ドライバ: Google Chromeを操作するために必要なソフト。
  "dependencies": {
    "chalk": "^4.1.1", // ターミナルへ出力される文字に色を付けます。
    "chromedriver": "^91.0.1", // お使いのOS(windowsやMac、Linuxなど)とChrome ver.を自動判別し、Chromeドライバをインストール。
    "moment": "^2.29.1", // 日付を求めたり、指定の形式に変換したりしやすいプログラム。
    "selenium-webdriver": "^4.0.0-beta.4" // Seleniumブラウザ自動化ライブラリ。
  },
  "devDependencies": {
    "@types/jest": "26.0.23", // jest(テストに使うライブラリ)の型定義ファイル
    "@types/jest-plugin-context": "^2.9.4", // jest-plugin-context
    "@types/node": "15.12.1", // Node.jsの型定義ファイル
    "@types/selenium-webdriver": "4.0.14", // selenium-webdriverの型定義ファイル
    "@typescript-eslint/eslint-plugin": "4.27.0", // TypeScriptで書かれたコードの問題点を指摘してくれます。
    "@typescript-eslint/parser": "4.27.0", // ESLintがTypeScriptをサポートできるようになります。
    "eslint": "7.29.0", // コードを解析して、問題点を指摘してくれます。
    "eslint-config-prettier": "8.3.0", // 不要なルールや、eslintとPrettierが競合する可能性のあるルールをすべてオフに。
    "eslint-plugin-jest": "24.3.6", // jest(テスト記述時)のコードの問題点について指摘してくれる。
    "eslint-plugin-prettier": "3.4.0", // PrettierをESLintルールとして実行し、個々のESLintの問題として違いを報告してくれます。 
    "husky": "6.0.0", // 「ある行為を行おうとしたとき、登録したコマンドを自動実行」(フックと呼ばれる処理)をしてくれます。
    "jest": "27.0.4", // jestテスト用ライブラリ。
    "jest-plugin-context": "^2.9.0", // jestの機能を増やすプラグインの類。contextという囲いを作る関数を定義。
    "lint-staged": "11.0.0", // ステージングされたgitファイルに対して構文チェック。
    "prettier": "2.3.1", // 自動コ―ド整形。
    "ts-jest": "27.0.3", // TypeScriptでjestを使えるようにするためのものです。
    "ts-node": "10.0.0", // Node.jsでTypeScriptを直接実行するためのものです。
    "typescript": "4.3.4" // javaScriptの構文に型を指定できるようにしたAltJSと呼ばれる類の言語。

4:各種設定ファイルの作成とその設定を行う

4.1 必要となるファイルをまとめて作成

  • プロジェクトのルートディレクトリで、以下のコマンドを実行し、必要なファイルを作成します。

  • セミコロンで行を区切っているため、複数行を一気にコピーし、貼り付けても正常に動作します。

  • ignoreと名の付くファイルは、「その対象から除外する項目を選ぶファイル」のことです。  例: .eslintignoreは、問題を指摘するeslintを実行しないフォルダまたはファイルを定義できます。

# コードエディタの設定ファイル
touch .editorconfig;
mkdir .vscode;
touch .vscode/setting.json;

# コードの問題点を指摘してくれるeslintの設定ファイル
touch .eslintrc;
touch .eslintignore;

# コード整形用の設定ファイル
touch .prettierrc.yml;
touch .prettierignore;

# プロジェクトの概要を書いておくためのファイル
touch README.md;

# option: Gitを使っている場合。
touch .gitignore;

<コマンドの解説> touch: ファイル更新日を最新にするコマンドですが、該当ファイルがなければ、作成されます。

<この時点でのソースツリーを表示>
selenium-playground
  ├─.vscode
  |    └─settings.json
  ├─.editorconfig
  ├─.eslintignore
  ├─.eslintrc
  ├─.gitignore
  ├─.prettierignore
  ├─.prettierrc.yml
  ├─package-lock.json
  ├─package.json
  └─README.md

4.2 作成した設定ファイルを記述

.editerconfig

<コードを表示>
  • 色がついていた方がわかりやすいため、下記.editerconfigではjavaのシンタックスハイライト(色付け)を利用しています。
root = true

[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

.vscode/setting.json

<コードを表示>
  • 通常のJsonではコメントは書けません。そのため、VS Code右下のJsonをクリックし、JSON with Commentsを選択してください。

  • setting.json内部の項目(例:editor.tabCompletionなど)をマウスでホバーした際に解説が出ない場合は、お使いのVS Codeのsetting.jsonをコピーするか、こちらのリンクselenium-playground-sampleからコピーしてください。

  • グロブ(英: glob)とは主にUnix系環境において、ワイルドカード(*)でファイル名のセットを指定するパターンのことです。

{
  // ---------------------------------------------------------------------------------------------------------------------------
  // コードエディター
  // ---------------------------------------------------------------------------------------------------------------------------
  "editor.tabCompletion": "on",
  //  垂直ルーラーを表示する列を指定します。
  "editor.rulers": [128],

  // ---------------------------------------------------------------------------------------------------------------------------
  // ファイル
  // ---------------------------------------------------------------------------------------------------------------------------
  // ファイルやフォルダを除外するためのグロブパターンを設定します。
  "files.exclude": {
    "**/node_modules": true
  },

  // ファイルの監視対象から除外するファイルパスのパターンを設定します。
  // この設定を変更するには再起動が必要です。起動時に多くのCPU時間を消費するコードがある場合、
  // コードが起動時に多くのCPU時間を消費する場合は、大きなフォルダを除外して初期負荷を軽減することができます。
  "files.watcherExclude": {
    "**/.git/objects/**": true,
    "**/node_modules/**": true,
    "lib/**": true
  },

  // ---------------------------------------------------------------------------------------------------------------------------
  // 言語設定
  // ---------------------------------------------------------------------------------------------------------------------------
  "[javascript]": {
    "editor.formatOnSave": true
  },
  "[typescript]": {
    "editor.formatOnSave": true
  },
  "[typescriptreact]": {
    "editor.formatOnSave": true
  },
  "typescript.tsdk": "node_modules//typescript//lib"
}

.eslintrc

<コードを表示>
{
  "env": { "browser": true, "node": true, "es6": true, "jest": true },
  "extends": [
    "eslint:recommended",
    "plugin:prettier/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "plugins": ["@typescript-eslint", "jest"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-unused-vars": [
      "error",
      { "argsIgnorePattern": "^_" }
    ],
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-non-null-assertion": "off",
    "@typescript-eslint/consistent-type-definitions": ["error", "type"],
    "arrow-body-style": ["error", "as-needed"],
    "no-control-regex": "off",
    "sort-imports": [
      "error",
      {
        "ignoreCase": true,
        "ignoreDeclarationSort": true
      }
    ],
    "no-undef": "off"
  }
}

.eslintignore/.prettierignore/.gitignore

<コードを表示>
  • ignoreファイルの設定は3種類とも同じにします。
# ------------------------------------------------------------------------------------------------------------------------------
# 一時ファイル
# ------------------------------------------------------------------------------------------------------------------------------
# OSやエディタによって一時的に作成されたファイル
*~
*#
*.bak
.settings
.tmp
logs
lib

# ------------------------------------------------------------------------------------------------------------------------------
# 動的に作成されたファイル
# ------------------------------------------------------------------------------------------------------------------------------
# Node.jsによって作成されたファイル
node_modules
npm-debug.log

# テストによって作成されたファイル
coverage

.prettierrc.yml

<コードを表示>
# prettierが折り返す行の長さを指定します。
printWidth: 128

# インデントレベルごとのスペースの数を指定します。
tabWidth: 2

# 行のインデントをスペースではなくタブで行うようにします。
useTabs: false

# 文章の最後にセミコロンを表示します。
semi: true

# ダブルクォートの代わりにシングルクォートを使用します。
singleQuote: false

# オブジェクトのプロパティが引用されるタイミングを変更します。
quoteProps: "as-needed"

# JSXでダブルクォートの代わりにシングルクォートを使用します。
# jsxSingleQuote: false

# 複数行の場合、可能な限り末尾コンマを表示します(例えば、1行の配列には末尾コンマは付きません)。
trailingComma: "all"

# オブジェクトリテラルの括弧間のスペースを表示します。
bracketSpacing: true

# 複数行のJSX要素の `>` を、次の行に単独ではなく、最終行の最後に配置します。
# (自分で閉じる要素には適用されません)。
# jsxBracketSameLine: false

# 単独の矢印関数のパラメータを括弧で囲みます。
arrowParens: "always"

# ファイルの一部分のみをフォーマットする.
# この2つのオプションは, 指定された文字オフセットで始まるコードと終わるコードを # フォーマットするために利用できます(それぞれ, 包括的, 排他的です),
# 以下の2つのオプションは、与えられた文字オフセットで始まるコードと終わるコードをフォーマットするために使用できます(それぞれ包括的および排他的)。)
# rangeStart: 0
# rangeEnd: "Infinity"
# 使用するパーサーを指定します。
# babylonパーサーもFlowパーサーも、同じJavaScriptの機能セットをサポートしています(Flowを含む)。
# Prettierは、入力ファイルのパスからパーサーを自動的に推測するので、この設定を変更する必要はありません。
# パーサー "typescript"
# 入力ファイルのパスを指定します。これは、パーサーの推論に使用されます。
filepath: "none"

# Prettierは、ファイルの先頭にプラグマと呼ばれる特別なコメントを含むファイルのみをフォーマットするように自分自身を制限することができます。
# これは、大きくてフォーマットされていないコードベースを徐々にprettierに移行するときに非常に便利です。
requirePragma: false

# Prettierは、ファイルの先頭に、そのファイルがprettierでフォーマットされたことを示す特別な@formatマーカーを挿入することができます。
# これは、--require-pragmaオプションと一緒に使うとうまくいきます。ファイルの先頭に既にdocblockがある場合、このオプションは新しいdocblockを追加します。
# このオプションは、@formatマーカーでそれに改行を追加します。
insertPragma: false

# デフォルトでは、Prettierはマークダウンテキストをそのままラップします。なぜなら、いくつかのサービスは改行を考慮したレンダラを使用しているからです。
# 例えばGitHub commentやBitBucketなどです。場合によっては、エディタやビューアでのソフトな折り返しに頼りたいこともあるでしょう。
# このオプションでは、"never"を選択することができます。
proseWrap: "preserve"

# HTMLファイルのグローバルな空白の感度を指定します, 詳しくはwhitespace-sensitive formattingを参照してください.
# https://prettier.io/blog/2018/11/07/1.15.0.html#whitespace-sensitive-formatting
htmlWhitespaceSensitivity: "strict"

# 歴史的な理由により、テキストファイルでは一般的に使用される2種類の改行コードが存在します。
endOfLine: "lf"

README.md

<コードを表示>
  • TODOコメント項目は人によって変わるところ、または、追記対象のものです。
<h1 align="center">Selenium playground</h1>

<!-- TODO https://shields.io/category/license のサイトでバッジを作れます。 -->
<div align="center"><a href="https://opensource.org/licenses/MIT"><img alt="GitHub" src="https://img.shields.io/github/license/SARDONYX-sard/nodejs-website-autorun-set"></a>
</div>

## 目次

<!-- TODO このように記述すると、VS-Codeが「#」を読み取り、自動で目次を作成してくれます -->
<!-- TOC depthFrom:2 -->

- [目次](#目次)
- [このプログラムについて](#このプログラムについて)
- [クイックスタート](#クイックスタート)
  - [必要なもの](#必要なもの)
- [インストール](#インストール)
- [使い方](#使い方)
  - [selenium](#selenium)
  - [テスト](#テスト)
- [License](#license)

<!-- /TOC -->

## このプログラムについて

SeleniumのプログラムをNode.js上で動かしたものです。

Seleniumのプログラムでは、あらかじめ指定したURLを順番に開くことができます。

## クイックスタート

### 必要なもの

* npm または Yarn
* Node.js 14 またはそれ以上
* Chrome 91


## インストール

1. このリポジトリをクローンします。

<!-- TODO あなたのリモートリポジトリのURLをここに記述 -->
git clone `<yourRipositoryURL>`

<!-- TODO 使い方の欄は、mdのなかで例を書くことができないため、ここでは未掲載 -->

## License

The library is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

<!-- TODO あなたのユーザ―ネームを記述 -->
Copyright 2021 `<yourUserName>` All rights reserved.

4.3 tsconfig.jsonの作成と設定

  • typescriptを使用する際の設定ファイル。

プロジェクトのルートディレクトリ(この記事で言えばselenium-playgroundフォルダ)の直下で、以下のコマンドを実行

npx tsc --init
<実行時の画像表示> ![2021-06-28 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/fc1da89c-943c-cf70-9b3c-bdb6212236ba.png)
<コマンドの解説を表示>

npx:  ローカルプロジェクト(selenium-playgroundフォルダ内)のnode_modules内の実行コマンドを実行するためのコマンド。  ネット上のnpmライブラリ群のコマンドも使用可能。

tsc: TypescriptをJavaScriptに変換するためのコマンド。

--init: tsc --initとするとtsconfig.jsonが作成される。

tsconfig.json

<コードを表示>
  • 以下をコピペするか同じ項目を選び、includeexcludeの項目を追記。
  • コメントを日本語翻訳したものを以下に記載しておきます。
{
  "compilerOptions": {
    /* このファイルについての詳細は https://aka.ms/tsconfig.json を参照してください */

    /* 基本的なオプション */
    "target": "es2015" /* ECMAScript のターゲットバージョンを指定します: 'ES3' (デフォルト), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', または 'ESNEXT'. */,
    "module": "commonjs" /* モジュールコード生成の指定: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
    "lib": ["ESNext", "dom"] /* コンパイル時に組み込むライブラリファイルを指定します。*/,
    "allowJs": false /* javascript ファイルのコンパイルを許可します。*/,
    // "declaration": true, /* 対応する '.d.ts' ファイルを生成します。*/
    "outDir": "./lib" /* 出力構造をディレクトリにリダイレクトする。*/,
    "rootDir": "./src" /* 入力ファイルのルート・ディレクトリを指定する。--outDirで出力のディレクトリ構造を制御するために使用する。*/,
    // "removeComments": true, /* コメントを出力に出さない。*/
    // "isolatedModules": true, /* 各ファイルを個別のモジュールとしてトランスパイルする (ts.transpileModule'と同様)。*/

    /* 厳密なタイプチェックのオプション */
    "strict": true /* すべての厳密なタイプ・チェック・オプションを有効にします。*/,

    /* 追加チェック */
    // "noUnusedLocals": true, /* 未使用のローカルでエラーを報告する。*/
    // "noUnusedParameters": true, /* 未使用のパラメータに関するエラーを報告します。*/
    // "noImplicitReturns": true, /* 関数内の全てのコードパスが値を返さない場合にエラーを報告する。*/
    "noFallthroughCasesInSwitch": true, /* switch文のfallthroughケースのエラーを報告する。*/
    // "noUncheckedIndexedAccess": true, /* インデックス署名の結果に'undefined'を含める */*。
    // "noImplicitOverride": true, /* 派生クラスのメンバーをオーバーライドする際には、'override'修飾子を付ける。*/
    // "noPropertyAccessFromIndexSignature": true, /* インデックス署名から宣言されていないプロパティが要素アクセスを使用することを要求する。*/

    /* モジュール解決オプション */
    "moduleResolution": "node" /* モジュール解決方法の指定: 'node' (Node.js) 又は 'classic' (TypeScript pre-1.6). */,
    "baseUrl": "./" /* 絶対的でないモジュール名を解決するためのベースディレクトリを指定する。*/,
    // "types": [] /* コンパイル時にインクルードされる型宣言ファイル。*/,
    "esModuleInterop": true /* すべてのインポートに対して名前空間オブジェクトを作成することで、CommonJS と ES モジュール間のエミットの相互運用を可能にする。allowSyntheticDefaultImports' を暗示します。*/,

    /* 実験的なオプション */
    // "experimentalDecorators": true, /* ES7 デコレータを実験的にサポートします。*/
    // "emitDecoratorMetadata": true, /* デコレーターのタイプメタデータの出力を実験的にサポートします。*/

    /* 高度なオプション */
    "skipLibCheck": true /* 宣言ファイルの型チェックをスキップする。*/,
    "forceConsistentCasingInFileNames": true /* 同一ファイルへの不整合なケースの参照を許可しない。*/
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "./src/**/*.spec.ts"]
}

4.4 huskyファイルの作成と設定(Gitをお使いの場合)

  • huskyを使って、gitのコミット時にlintとコード整形を行わせます。

  • 以下のコマンドを実行 .huskyフォルダの作成します。

npx husky-init
  • .huskyフォルダ内の_というフォルダを削除
  • 作成された.huskyフォルダの中のpre-commitファイルを編集
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# 以下を追記
npx lint-staged

4.5 package.jsonの修正

  • 「TODO」と書かれたコメントは削除する必要があります
<追加コードの**差分と解説**を表示>
{
  "name": "selenium-playground",
  "version": "1.0.0",
  "description": "",
+  "repository": {        // gitをお使いの場合
+    "type": "git",
+    "url": "<あなたのリモートリポジトリ>"
+  },
-  "main": "index.js",
+  "main": "src/main.ts",
  "scripts": {
+    "selenium": "npx ts-node src/main.ts",     // セレニウムの実行
+    "lint": "eslint ./src/**/*.ts",            // 拡張子がtsのものの問題点を探す
+    "lint:fix": "eslint ./src/**/*.ts --fix",  // --fix オプションで自動修正可能なら実行するの意味
+    "test": "jest --coverage",
+    "clean": "rm -rf ./src/logs ./coverage ./lib", // removeの意味で、ファイルやフォルダの削除に使用
   // -r: 再帰的(フォルダの中のフォルダも)、   -f:強制的にというオプション
   // rmは本来unix系(MacやLinux)のコマンドですが、
   // windowsでもエイリアス(長いコマンドを簡単に実行できるようにした別名)として登録してあるので、これを使用
-    "prepare": "husky install"
  },
  "keywords": [],
+  "author": "<あなたのユーザー名>",
-  "license": "ISC",  
+  "license": "MIT",  // 自由に改変、使用してOKだけど、責任は一切負わないし、
   // 著作権表示および本許諾表示をソフトウェアのすべての複製または重要な部分に記載してねのライセンス
+  "dependencies": {
   // 略
  },
  "devDependencies": {
     // 略
-  }
+  },
+  "husky": {                            // huskyでgitのコミット時にlint-saged(この項目の下の項目)の内容を実行
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  },
+  "lint-staged": { // 先ほどインストールしたlint-stagedの設定
+    "src/**/*.{js,jsx,ts,tsx}": [       // srcフォルダ以下、、js,jsx,ts,tsxの拡張子の付くファイルに
+      "eslint --fix ./src/**/*",        // 問題点のチェックと、
+      "prettier --write ./src/**/*"     // コード整形を行う
+    ]
+  },
+  "peerDependencies": {},
+  "engines": {
+    "node": ">=14" // Node.jsの対応バージョンの記述
+  }
}
< 「MITライセンスを使う側になったら?」についての余談 >

使用したいファイルの最上行に、 Copyright、作成日、作者の名前(例ではSARDONYXと書かれている)の順に記述し、 MITの全文、またはURLを張り付けます。 VS Codeの拡張機能であるlicenserを使うのも手かもしれません。

例:

/**
 * Copyright (c) 2021 SARDONYX
 *
 * This software is released under the MIT License.
 * https://opensource.org/licenses/MIT
 */
<コピペ用: package.jsonを表示>
{
  "name": "selenium-playground",
  "version": "1.0.0",
  "description": "",
  "repository": {
    "type": "git",
    "url": "https://github.com/SARDONYX-sard/selenium-playground" // TODO: あなたのリポジトリを記述
  },
  "main": "src/main.ts",
  "scripts": {
    "selenium": "npx ts-node src/main.ts",
    "lint": "eslint ./src/**/*.ts",
    "lint:fix": "eslint ./src/**/*.ts --fix",
    "test": "jest --coverage",
    "clean": "rm -rf ./src/logs ./coverage ./lib"
  },
  "keywords": [],
  "author": "SARDONYX", // TODO: あなたのユーザー名
  "license": "MIT",
  "dependencies": {
    // TODO バージョンはお使いのものによって変わるので省略。ここはコピーしないようにご注意
  },
  "devDependencies": {
    // TODO バージョンはお使いのものによって変わるので省略。ここはコピーしないようにご注意
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "src/**/*.{js,jsx,ts,tsx}": [
      "eslint --fix ./src/**/*",
      "prettier --write ./src/**/*"
    ]
  },
  "peerDependencies": {},
  "engines": {
    "node": ">=14"
  }
}

セッティング終了

更新ログ

  • 2021/8/18: npm scriptsの記述間違いを修正
  • 2021/8/10: 既知のミスについての言及。
  • 2021/7/16: < addon.ts >を追加。forループによる複数URL探索の関数を定義していきます。

  • 下記サイトの続きです。

https://qiita.com/SARDONYX/items/89491bda9769b8e75e42


4: srcフォルダとその中身の作成

ターミナル内で以下のコマンドを入力します。 一括コピー&ペーストでも動作します。

  • srcsourceの略です。

  • spec.tsと付いたファイルはテスト用ファイルです。

  • フォルダ名の先頭に@を付与することで、フォルダツリーの最初に並ぶようにしています。

# srcフォルダにプロジェクトコードを入れることで、設定ファイルとソースコードを分離させる
mkdir src;
# プログラムの実行開始ファイル
touch src/main.ts;

# 型を指定するファイルの作成
mkdir @types;
touch tabs.d.ts;
touch urls.d.ts;

# ----------------------------------------------------------------------------------------------------
# main.tsで使う部品の作成
# ----------------------------------------------------------------------------------------------------
mkdir src/utils;

# ターミナルのコマンドをNode.js内で実行する処理を記述するファイル
touch src/utils/command.ts;

# エラーキャッチされていない非同期処理を記述するファイル
touch src/utils/error-handle.ts;

# ファイルの読み書き(input/output)についての処理を記述するファイル
touch src/utils/file-io.spec.ts;
touch src/utils/file-io.ts;

# utilsフォルダのモジュール全てを楽にインポートするためのファイル
touch src/utils/index.ts;

# セレニウムでブラウザを起動するときの設定関数を記述しておくファイル
touch src/utils/settings.ts;

# 主にタブの遷移に関連するファイル
touch src/utils/tabs.spec.ts;
touch src/utils/tabs.ts;

# 開くURLを書いておくファイル
touch src/utils/urls.spec.ts;
touch src/utils/urls.ts;
<この時点での画像を表示> ![2021-06-28 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/eb994ead-aa9d-a241-0b9b-166f1446f2bc.png)

command.ts

<スクリプトを表示(30行)>
  • 後ほど、バッチファイルを書いて、ダブルクリックするだけでWebサイトからコンテンツを取り込み、結果を見たいのでpauseコマンドを使用します。そのためにコマンドを実行できる処理を書きます。

  • process.argvでターミナルでコマンドを実行したときのオプションとして渡した文字列が配列として手に入ります。  例: ts-node src/main.ts --pause  process.argv[0] にはts-node  process.argv[1] にはsrc/main.ts  process.argv[2] には--pause

import { exec } from "child_process";

/**
 * ターミナルのコマンドを実行する関数
 * @param command - 実行するコマンド名
 * @param args - option: コマンド実行のトリガーとなる引数
 *
 * @example:pauseコマンドを実行する
 * // 1. main.tsの中で
 * execCommand("pause", /[^(-{1})](-{2})?pause/);
 *
 * // 2. ターミナルにて以下のコマンドを実行
 * npx ts-node src/main.ts --pause
 * //または
 * npx ts-node src/main.ts pause
 */
export const execCommand = (command: string, arg: string | RegExp = command): void => {
  // arg引数が文字列だったらこちらの処理を行う
  if (typeof arg === "string" && arg === process.argv[2]) return execute(command);

  // arg引数が正規表現だったらこちら
  if (arg == RegExp(arg) && arg.test(process.argv[2])) return execute(command);
};

/**
 * シェルコマンドを実行する関数
 * @param command 実行したいコマンド
 */
export const execute = (command: string): void => {
  exec(command, (error) => console.error(`[ERROR] ${error}`));
};

file-io.ts

<スクリプトを表示>
  • Webサイトからとってきた情報を保存しておきたいので、ファイルの書き込みについての関数を作ります。
  • fs(file Systemの略)ライブラリのwriteFileは、配列の書き込みに非対応なので、自作関数で包みます(「ラップする」といいます)。
import { mkdir, writeFile } from "fs/promises";
// import { promises as fsp } from "fs"; // Node.js 10.17 ~ 12をお使いの場合 (fsp.writeFileのように使います)
import chalk from "chalk";

/**
 * 文字列、文字列の配列をファイルに書き込む関数
 * @param path - 書き込みたいファイル名
 * @example `./src/${moment().format("YYYY-MM-DD")}.txt`
 * @param contents - 書き込む内容
 */
export const writeFiles = async (path: string, contents: string | string[]): Promise<void> => {
  try {
    // ディレクトリが存在しない場合、作成します
    const dir_path = path.replace(/(?:[^/]+?)??$/, "");
    await mkdir(dir_path, { recursive: true }); // recursiveオプションで、フォルダ内のフォルダがない場合もすべて作成

    // カラーコードを除去し、ファイルへ書き込み
    await writeFile(path, stripAnsi(`${contents}`));
    console.log(chalk`{green 書き込みに成功しました}`);
    return;

    // error catch
  } catch (error) {
    console.log(chalk`{red 書き込みに失敗しました}\n${error}`);
    throw error;
  }
};

/**
 * chalkで付けたカラーコードを削除する関数
 * @param text
 */
export const stripAnsi = (text: string): string =>
  text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");

index.ts

<スクリプトを表示(5行)>
  • Re exportと呼ばれる、よく使われる手法を使います。

  • 利便性の観点から、utilsフォルダを選択したらそのフォルダに含まれるすべての関数を取り出せるようにします。

<使用例を表示(5行)>
// utilsフォルダを選択したらVS Codeは自動でindexファイルをインポートする挙動をとります
// import { build } from "./utils/index"と同義
import { build } from "./utils" // utilsフォルダを選ぶだけで、ビルド関数を呼び出せている

build()
export * from "./settings";
export * from "./urls";
export * from "./tabs";
export * from "./command";
export * from "./file-io";

error-handle.ts

<スクリプトを表示>
*chalk: 色を付ける *process.on(イベントのタイプ, イベントが起きたら実行する関数)
import chalk from "chalk";

// イベントリスナーと呼ばれる類のものです。ある特定の条件がそろうと、第2引数の関数が実行されます。
process.on("unhandledRejection", (error, promise) => {
  console.error(chalk`Error: {red ERR!} ${error}`);
  console.error(chalk`promise: {red ERR!} ${promise}`);
  process.exit(1); // exitコード1はエラーが起きて終了、0は正常に終了
});

setting.ts

<スクリプトを表示>
  • このファイルに、セレニウムでChromeブラウザを起動する処理を書きます。

  • 定型文のようなものなので、記述の仕方は覚えず、どのようなオプション(headlessなど)があるのかだけ、頭に入れておきます。(これもコメントで補足しておけば覚えずに済みます)

  • /* */という複数行コメントを関数のすぐ上に書くことで、関数をマウスでホバーしたとき、関数の説明が読めます。それを利用し、オプションの詳細を記載しておきます。

  • @paramのようなコメントは、JSDocと呼ばれるアノテーション(注釈)を記述するマークアップ言語です。

  • import {} from "module": 分割代入を利用したインポート方法です。必要なコードだけを選んで取り寄せられます。

import "chromedriver";
import { Builder, ThenableWebDriver } from "selenium-webdriver";
import chrome from "selenium-webdriver/chrome";
// utils
import "./error-handle";

/**
 * @param options
 * @example
 * const driver = build([
 *  "--headless", // ブラウザを不可視状態のバックグラウンド実行。
 *   "--disable-gpu", // ヘッドレスモードでの実行の際に必要なオプション (じきに不要に。)
 *   "--disable-extensions", // 拡張機能の無効化
 *   "--no-sandbox",
 *   `--window-size=1980,1200`,
 * ]);
 */
export const build = (options: string[] = []): ThenableWebDriver => {
  const chromeOptions = new chrome.Options().addArguments(...options);

  return new Builder().setChromeOptions(chromeOptions).forBrowser("chrome").build();
};

tabs.ts

<スクリプトを表示>
  • import typeは型のみのインポートであるという表明です。

  • executeScriptでJavaScriptのコードを実行し、window.open(arguments[0], '_blank')で新しいタブを作成します。

  • driver.getAllWindowHandles();で現在のタブをすべて取得してから、driver.switchTo().window()でタブ遷移しています。

import type { ThenableWebDriver } from "selenium-webdriver";

/**
 *  タブを新規作成する関数
 * @param url - URLを文字列で入れる
 * @param driver - buildしたdriverを入れる
 */
export const createNewTab = async (url: string, driver: ThenableWebDriver): Promise<void> => {
  try {
    await driver.executeScript("window.open(arguments[0], '_blank')", url);
  } catch (error) {
    console.error(error);
    throw new Error("新規タブ作成に失敗しました");
  }
};

/**
 * タブを切り替える関数
 * @param count -  切り替えたいタブは何番目かを入れる
 * @param driver - buildしたdriverを入れる
 */
export const switchNewTab = async (count: number, driver: ThenableWebDriver): Promise<void> => {
  try {
    const tabs = await driver.getAllWindowHandles();
    await driver.switchTo().window(tabs[count + 1]);
  } catch (error) {
    console.error(error);
    throw new Error("タブの切り替えに失敗しました");
  }
};

urls.ts

<スクリプトを表示>
  • ここでは、「URLを指定指定して、HTML(DOM)要素を取得する関数を指定し、手に入れた要素を返す」という関数を作成しています。
import chalk from "chalk";
// utils
import type { ScrapingFunc } from "../@types/urls";

export const default_urls = [
  "https://www.google.com/",
  "https://www.google.com/",
  "https://www.google.com/",
  "https://www.google.com/",
  "https://www.google.com/",
  "https://www.google.com/",
];

/**
 * Webサイトから情報を取得するための関数
 *
 * @param domain - 例: "google.com"
 * @param url - 例: "https://www.google.com/"
 * @param getElement1 - Webサイトの要素を手に入れる1つめの関数
 * @param getElement2 - Webサイトの要素を手に入れる2つめの関数
 * @param driver - 例: `const driver = build()`
 */
export const getUrlContent: ScrapingFunc<string, string> = async ({
  url = "https://www.google.com/",
  domain = "google.com",
  getElement1,
  getElement2,
  driver,
}) => {
  // isURL?
  if (RegExp(`^https?://.*${domain}.*`).test(url)) {
    try {
      // サイトのタイトルを取得
      const title = await driver.getTitle();

      // サイトの要素を手に入れる関数の結果1つ目
      const element1 = await getElement1(driver);

      // サイトの要素を手に入れる関数の結果2つ目
      const element2 = await getElement2(driver);

      // 結果を返します
      return chalk`
        サイトタイトル: {yellow ${title}}
        取れた要素1: {cyan ${element1}}
        取れた要素2: {green ${element2}}
        `;

      // Catch error
    } catch (e) {
      const error_log = chalk`{red ${url} の要素取得に失敗しました}`;
      console.log(error_log);
      console.log(e.message);
    }
  }
};

urls.d.ts

<スクリプトを表示>
  • 複合的な任意の型を指定できます。直接コードを記述もできますが、オブジェクトのプロパティが増大すると、記述が冗長と化し、可読性が下がる(読みづらい)ので、名前を付けて使いまわします。

  • TUについて:  ジェネリック型(ジェネリクス)と呼ばれるものです。いわば型定義版の変数です。「型引数」と呼ばれます。  関数を実行するときに指定して決めます。  TUには、stringundefinedといったものが入ります。

<詳細なジェネリクスの解説を閲覧>
  • 慣習的に大文字のアルファベット1文字を使用しているだけで、実際には任意の名前を付けられます。 これは、Errorなどと名前を付けてしまうことによる、「ジェネリクスとクラス名との衝突を避けるため」のようです。 参考サイト: ジェネリクスの型引数名は<T>じゃないとだめなのか?

  • ジェネリクスの慣習例

ジェネリクス 略語の意味 意味
T, U ... Type 型引数。 左から1番目、2番目…と続く
K Key  オブジェクトのキー
R Return 返り値
V Value  
P Property or Parameter オブジェクトのプロパティまたは引数
// --------------------------------------------------------------------------
// 型定義
// --------------------------------------------------------------------------
// この例では、`SomethingTypeName`という独自の型を定義し、
// そのプロパティには、`fizz`と`bazz`が入らなければならないオブジェクト(`{}`)を作っている
type SomethingTypeName<T, U> = {
  "fizz": T;
  "bazz": U;
}

// --------------------------------------------------------------------------
// 定義した型を使ってオブジェクトを作成
// --------------------------------------------------------------------------
// fizzにstring、bazzにはnumberの値を入れるオブジェクトの作成
const obj: SomethingTypeName<string, number> = {
  "fizz": "1",
  "bazz": 2
}

// fizzとbazzにnumber型の値を入れるオブジェクトの作成
const obj2: SomethingTypeName<number, number> = {
  "fizz": 1,
  "bazz": 2
}
import { ThenableWebDriver } from "selenium-webdriver";

export type ScrapingKit<T, U> = {
  domain?: string;
  url?: string;
  getElement1: (driver: ThenableWebDriver) => Promise<T>;
  getElement2: (driver: ThenableWebDriver) => Promise<U>;
  driver: ThenableWebDriver;
};

export type ScrapingFunc<T, U> = (content: ScrapingKit<T, U>) => Promise<string | undefined>;

5: [サンプル]アメリカ合衆国・アリゾナ州の天気情報を取得する関数を記述、そして実行

どのようにして要素を取得して目的のものを得るか?

  • 最も簡単だと思われるのは、xpathを利用した取得です。以下の手順で取得します。

1.取得したい情報のサイトへ赴きます。 今回は以下のサイト

https://www.google.com/search?q=arizona+weather&gl=us&hl=en&pws=0&gws_rd=cr

<今回取得するサイトの画像を表示> ![2021-06-28 (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/71e4199e-930d-593e-3dec-bcceaa2c0999.png)

2.サイトへ赴いたら欲しい情報をマウスで選択した状態で、   F12または、Ctrl + Shift + I、   もしくは「右クリック→検証というメニュー」を押し、そのサイトのHTMLを表示させます。

<要素を選択した状態の画像を表示> ![2021-06-28 (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/6404df7a-dac0-80a4-330e-cf1167f947fd.png)

3.HTMLのコードが出たら、選択した要素が青線で表示されるので、そこを右クリックし、CopyCopy Full Xpathというものを選択します。

< DevToolの画面(検証の画面)を表示 >

2021-06-28 (9).png

4.By.Xpathという関数内のかっこにコピーした値を入れます。

driver.findElement(By.xpath("<ここに入れる>")).getText()

入れた後。

driver.findElement(By.xpath("/html/body/div[7]/div/div[9]/div[1]/div/div[2]/div[2]/div/div/div[1]/div/div/div/div[2]/span/div[1]")).getText()

これは、driverfindElement関数の中で指定された要素を取得し、getText()で取得した要素の中のテキストを取得しています。

utils/urls.ts

<スクリプトを表示>
  • インポートの部分を変更、型を追加インポートし、getArizonaWeatherFromGoogleという関数を追加定義しています。

  • PartialというTypeScriptが提供するタイプを使うことで、必須項目として型定義したもの全てを、オプションとすることが出来ます。

  • 引数をオブジェクトにすることで、指定したい引数を楽に選択可能にします。

import chalk from "chalk";
import moment from "moment";
import { By } from "selenium-webdriver";
// utils
import { build, execCommand, writeFiles } from ".";
import type { ScrapingContent, ScrapingFunc } from "../@types/urls";

export const getUrlContent: ScrapingFunc<string, string> = async ({
  url = "https://www.google.com/",
  domain = "google.com",
  getElement1,
  getElement2,
  driver,
}) =>
 // 記載を省略
};

/**
 * google検索から、「アメリカ合衆国・アリゾナ州」の天気を取得し、ファイルに書き込んでくれるお試し関数
 *
 * @param url @default "https://www.google.com/search?q=arizona+weather&gl=us&hl=en&pws=0&gws_rd=cr"
 *
 * 他地域用のURLを取得するためのクエリ: アメリカの場合は URL + "&gl=us&hl=en&pws=0&gws_rd=cr"
 *
 * @param sleepMs 読み込み待機時間。単位:ミリ秒 - @default 3000
 * @param writeLogPath 書き込むファイルパス - @default `src/selenium/logs/${moment().format("YYYY-MM-DD")}.txt`
 * today: ${moment().format("YYYY-MM-DD")}
 */
export const getArizonaWeatherFromGoogle = async ({
  url = "https://www.google.com/search?q=arizona+weather&gl=us&hl=en&pws=0&gws_rd=cr",
  sleepMs = 3000,
  writeLogPath = `src/logs/${moment().format("YYYY-MM-DD")}.txt`,
  buildOptions,
}: Partial<ScrapingContent> = {}): Promise<string | undefined> => {
  // setting
  const driver = build(buildOptions);

  try {
    // URLを開きます
    await driver.get(url);
    await driver.sleep(sleepMs);

    const log = await getUrlContent({
      url,
      // URLが指定したドメインに引っかかるかテストします
      domain: "google.com/search",

      // 1つめのHTML要素の取得関数を記述
      getElement2: async () => {
        // 今日の曜日 (例: Monday)
        const dow = await driver
          .findElement(
            By.xpath("/html/body/div[7]/div/div[9]/div[1]/div/div[2]/div[2]/div/div/div[1]/div/div/div/div[2]/span/div[2]"),
          )
          .getText();
        const today = moment().format("YYYY-MM-DD"); // 日付 (ex.2021-6-23)
        return `本日の日付: ${today} ${dow}`;
      },

      // 2つめのHTML要素の取得関数を記述
      getElement1: async () => {
        // 気温: °C(例: 27)
        const celsius = await driver.findElement(By.id("wob_tm")).getText();
        // 降水確率: (例: 60%)
        const pp = await driver.findElement(By.id("wob_pp")).getText();
        // 天気:(例: Light rain showers)
        const weather = await driver.findElement(By.id("wob_dc")).getText();

        return `
                  気温: ${celsius}°C
                  降水確率: ${pp}
                  天気: ${weather}
              `;
      },

      driver,
    });

    if (log) {
      console.log(
        chalk`{green ---------------- 結果 -------------------}
            ${log}`,
      );

      // 結果が取れているかどうか
      // 取れていたらファイルに書き込みます
      await writeFiles(writeLogPath, log);
      return log;
    }

    // 取れずに終わったとき
    console.error(chalk`{red 値が取得できませんでした。取得した値は ${log} です}`);

    // エラー処理
  } catch (error) {
    console.error(error);
    throw new Error("googleサイトからデータ取得に失敗しました");

    // エラーまたは正常終了で実行される
  } finally {
    await driver.quit();
    execCommand("pause");
  }
};

urls.d.tsに追記

<スクリプトを表示>
import { ThenableWebDriver } from "selenium-webdriver";

export type ScrapingKit<T, U> = {
  domain?: string;
  url?: string;
  getElement1: (driver: ThenableWebDriver) => Promise<T>;
  getElement2: (driver: ThenableWebDriver) => Promise<U>;
  driver: ThenableWebDriver;

export type ScrapingFunc<T, U> = (content: ScrapingKit<T, U>) => Promise<string | undefined>;
// ---------------------------------------------------------------------------------------------
// 以下を追記
// ---------------------------------------------------------------------------------------------
export type ScrapingContent = {
 url: string;
 sleepMs: number;
 writeLogPath: string;
 buildOptions: string[];
};

main.ts

<スクリプトを表示>
import { getArizonaWeatherFromGoogle } from "./utils";

// sample
getArizonaWeatherFromGoogle({
  buildOptions: [
    "--headless",
    "--disable-gpu",
    // "--no-sandbox",
    `--window-size=1980,1200`,
  ],
}).catch((error: Error): void => {
  console.log(error.message);
  process.exit(1);
});

実行

  • 以下のコマンドを実行します。

*なお、npm-scriptsのタスク実行方法まとめの記事の設定を行うとマウスでnpm scriptsが実行できるようになります。

*虫のアイコンはデバッグ用。右矢印のアイコンは実行です。

< NPMスクリプトの拡大画像を表示 > ![2021-06-28 (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/bfdbc82b-d53d-710f-dd18-fce78fd2dd6f.png)
npm run selenium

# または
npx ts-node src/main.ts

# yarnをお使いの場合
yarn run selenium
  • 無事に実行出来たら、以下のような結果を得られるはずです。

2021-06-28 (6).png

お疲れさまでした。

6:テストコード

describecontext,itはグループとしての区切りを表す関数です。それぞれ第1引数にテスト内容、第2引数には関数を入れます。 *beforeAllは、グループ内のテストが実行される前に実行され、afterAllはテスト実行後に実行されます。

jest.config.js

<スクリプトを表示>
  • jest.config.jsを記述し、テストライブラリの設定します
// 各設定プロパティについての詳細な説明は、こちらをご覧ください。
// https://jestjs.io/docs/en/configuration.html

module.exports = {
  // テストのたびに、モックの呼び出しとインスタンスを自動的にクリアする
  clearMocks: true,

  // テストの実行中にカバレッジ情報を収集するかどうかを示す
  collectCoverage: true,

  // Jest がカバレッジファイルを出力するディレクトリ
  coverageDirectory: "coverage",

  // Jest がカバレッジレポートを書く際に使用するレポーター名のリスト
  coverageReporters: [
    "html",
    //   "json",
    "text",
    "lcov",
    //   "clover"
  ],

  // Jestの設定のベースとして使用されるプリセット
  preset: "ts-jest",

  // runner: "jest-runner",

  // 各テストの前にテスト環境を設定したり、セットアップするためのコードを実行するモジュールのパス
  // 1日目で入れたライブラリjest-plugin-contextで、
  // 「context」というグループを使えるようにします
  setupFiles: ["jest-plugin-context/setup"], 


  // テストに使用されるテスト環境
  testEnvironment: "node",

  // Jestがテストファイルの検出に使用するグロブパターン
  testMatch: [
    // "**/__tests__/**/*.[jt]s?(x)",
    // "**/?(*.)+(spec|test).[tj]s?(x)",
    "**/src/**/?(*.)+(spec|test).[tj]s?(x)",
  ],
};

file-io.spec.ts

<スクリプトを表示>
import moment from "moment";
import { promises as fsp } from "fs";

import { writeFiles } from "./file-io";

describe("file-io", () => {
  it("should remove the color code", async () => {
    // 必要な情報を作成
    const today = moment().format("YYYY-MM-DD");
    const path = `src/test/${today}-file-io.txt`;
    const dir = path.replace(/(?:[^/]+?)?(?:-test)?$/, "");

    try {
      await writeFiles(
        path,
        // カラーコードを付けたHelloWorldという文字列を書き込む
        `
        \u001b[30mH \u001b[31me \u001b[32ml \u001b[33ml \u001b[0mo
        \u001b[34mW \u001b[35mo \u001b[36mr \u001b[37ml \u001b[0md
        `,
      );

      // 書いたものを読み込む
      const result = (await fsp.readFile(path, "utf-8")).replace(/\s*/g, "");
      return expect(result).toBe("HelloWorld");

      // error catch
    } catch (error) {
      return expect(error).toMatch("error");

      // テストに使用したものを削除
    } finally {
      await fsp.unlink(path);
      await fsp.rmdir(dir);
    }
  });
});

tabs.spec.ts

<スクリプトを表示>
import { ThenableWebDriver } from "selenium-webdriver";

import { build, createNewTab, switchNewTab } from ".";

let driver: ThenableWebDriver;
describe("tabs", () => {
  context("when it works fine", () => {
    beforeAll(() => {
      driver = build(["--headless", "--disable-gpu", "--window-size=1024,768"]);
    });

    afterAll(async () => await driver.quit());

    it("should be able to create and switch tabs", async () => {
      try {
        const url = "https://www.google.com/";
        // URLを開きます
        await driver.get(url);

        // 新規タブを作成
        await createNewTab(url, driver);

        // 先ほど作ったタブに切り替えます
        await switchNewTab(0, driver);

        // 現在フォーカスが当たっているタブのサイトタイトルを取得します
        const title = await driver.getTitle();

        // 取得したタイトルは、"Google"という文字列かどうか?
        expect(title).toBe("Google");
      } catch (e) {
        return expect(e).toMatch("error");
      }
    }, 30000);
  });
});

urls.spec.ts

<スクリプトを表示>
import { promises as fsp } from "fs";
import moment from "moment";
// utils
import { getArizonaWeatherFromGoogle } from "./urls";

describe("getArizonaWeatherFromGoogle", () => {
  it("should be able to get the date", async () => {
    // テストに必要な情報を作成します
    const today = moment().format("YYYY-MM-DD");
    const path = `src/test/${today}-test.txt`;
    const dir = path.replace(/(?:[^\\/]+?)?$/, "");
    try {
      // テストしたい関数の実行
      const log = await getArizonaWeatherFromGoogle({
        writeLogPath: `src/test/${today}-test.txt`,
      });
      //"タイトル(何かの文字)取れた要素1(何かの文字)取れた要素2(何かの文字)"として文字列がとれたかどうか?
      return expect(log).toMatch(/タイトル:[\s\S]*取れた要素1:[\s\S]*取れた要素2:/g);

      // catch error
    } catch (e) {
      return expect(e).toMatch("error");

      // テスト時に作成したファイルの削除
    } finally {
      await fsp.unlink(path);
      await fsp.rmdir(dir);
    }
  }, 30000);
});
  • 以下のコマンドを実行
npm run test
# Yarn使用時は
yarn run test 
<テスト実行時の結果の画像を表示> ![2021-06-28 (12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1048212/0db11238-e972-b7d1-a603-4253ee41690e.png)

7: おまけ(随時更新するかもしれません)

バッチファイルを書いて簡単実行

  • 今回のバッチファイルの記述に必要なのは、「プロジェクトのルートディレクトリまでの絶対パス」です。

  • 以下の手順で絶対パスを取得していきます。

  1. プロジェクト(selenium-playgroundフォルダ) をVS Codeで開きます。
  2. Ctrl + Shift + @でターミナルを開きます。
  3. ターミナル内でpwdと入力し、Enter(Macの方はReturn)キーを押します。
  4. プロジェクトフォルダの絶対パスが表示されるので、コピーします。
pushd <プロジェクトフォルダまでのディレクトリ>/selenium-playground
npm run selenium pause

bashを書いて実行(Mac, Linux用)

# 使い方
# 「  . selenium.sh 」とターミナルで入力
# 「.」は現在のディレクトリ

#! /bin/bash

pushd /mnt/d/Programing/JavaScript//selenium-playground
npm run selenium pause

batファイルを書いて実行 (windows用)

  • ダブルクリックで実行できるようになります
@echo off

rem pushdコマンドでNode.jsのセレニウムプロジェクトのパスまで移動します
pushd D:\javascript\selenium-playground

rem コマンドを実行します。結果を確認したいのですが、`npm run selenium`だけですと、すぐにプロンプトが閉じてしまいます。
rem そのため、`pause`引数を渡します。
npm run selenium pause
  • オプション: 以下のフォルダにバッチファイルを置けば、PC起動時に自動実行されます
C:/Users/<TODO: あなたのPCのユーザー名>/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup
< cmdでの実行時の結果の画像を表示 >

2021-06-28 (13).png

属性の値を取得したい

urls.tsの例を使います

<div class="wob_dts" id="wob_dts">Monday 2:00 AM</div>

getText()の部分を、getAttribute("id")に変更します。

driver.findElement(By.xpath("/html/body/div[7]/div/div[9]/div[1]/div/div[2]/div[2]/div/div/div[1]/div/div/div/div[2]/span/div[1]")).getAttribute("id");
**<結果を表示>**
  • この画像のようにwob_dtsという値が取れます

2021-06-28 (11).png

tabs.ts使ってない件

  • 汎用性を鑑みて追加しておりますが、どう使えばいいのかご不明かと存じますので、使い方の例をこちらにご紹介いたします。
    ちなみに今回作成する関数は、「google.comを5つ開き、タブを順にみていき、サイトタイトルを得る」というものです。

  • 注意: 同一サイトへの高速な連続大量アクセスはサイトに負荷がかかってしまう(場合によっては処罰の対象になります)ため、最低でも3秒の感覚をあけ、出来るだけ人間がWeb閲覧するときのようなスピードにすることを意識してください。

  • 追加コンテンツというわけで、addon.tsというファイルを作成し、そこへ記述しています。

  • utils/urls.tsで以前定義した< getUrlContents >と組み合わせれば、お好みの情報を得ることが出来るでしょう。

< スクリプトを表示 >

 簡単にいうと、関数内に関数を定義し、その中で外側の関数に定義された変数を取り出せるという意味あいです。

< 今回の例: >
  1. 最初の関数を実行した時、driverと名のついた変数が定義され、Returnで関数が返されます。

  2. その後、getSitesTitle関数で定義された関数を呼びます。

  3. この時try, catch, finallyでエラーハンドリングを行っており、driverを終了させています。

  4. もし変数drivertry構文に内部定義すると外部から参照できなくなるため、エラーハンドリングで参照するためには外部スコープ(コードの影響が及ぶ範囲) に定義する必要が出てきます。

  5. 今回はそのために関数内に関数を定義しています。

  • 今回はローカル変数としておきたいがためにこうしましたが、グローバルスコープによる変数を定義したとしても、ファイル別にされた今回のケースでは、importしない限り外部ファイルの関数などは使えないので、グローバル汚染の心配はありません。
    ただし同一のファイル内にて同じ変数名を使用している場合、思わぬバグが生まれる恐れがあります。
    そのときは、eslintが警告を発してくれます。
    また、テストのしやすさの観点から見ても関数グローバル変数に関数を依存させるのはお勧めしません。
import { ThenableWebDriver } from "selenium-webdriver";
import type { AsyncFunc } from "src/@types/addon";

import { build } from "./settings";
import { createNewTab, switchNewTab } from "./tabs";
import { default_urls } from "./urls";

/**
 * 配列として渡されたURLを探索し、サイトタイトルの配列を返すクロージャ関数。
 * @param url_lists @default ["https://www.google.com/"] * 5
 * @param waitMs 次のサイトへ行くまでの待機時間 ms. @default 5000
 * @param buildOpts @default {}
 * @example <caption>ビルド設定項目例</caption>
 * [
 *  "--headless", // ブラウザを不可視状態のバックグラウンド実行。
 *   "--disable-gpu", // ヘッドレスモードでの実行の際に必要なオプション (じきに不要に。)
 *   "--disable-extensions", // 拡張機能の無効化
 *   "--no-sandbox",
 *   `--window-size=1980,1200`,
 * ]);
 * @returns `サイトタイトルの配列`
 * ---
 * @example <caption>この関数の使用例</caption>
 * import { getSitesTitle } from "./utils/addon";
 * getSitesTitle()();
 */
export function getSitesTitle(url_lists = default_urls, waitMs = 5000, buildOpts?: string[]): AsyncFunc<string[] | undefined> {
  let driver: ThenableWebDriver;

  /**
   * Looping URLs
   */
  return async (): Promise<string[] | undefined> => {
    try {
      const titles: string[] = [];

      // ドライバーのビルド
      driver = build(buildOpts);

      // 起点となるサイト(無いとエラー)
      await driver.get(url_lists[0]);

      let count = 0;
      for (const url of url_lists) {
        await createNewTab(url, driver);

        await switchNewTab(count, driver);

        // タイトルの取得
        titles.push(await driver.getTitle());

        await driver.sleep(waitMs);
        count++;
      }

      return titles;
    } catch (e) {
      console.error(e);
    } finally {
      await driver.quit();
    }
  };
}
  • main.tsにて
import { getSitesTitle } from "./utils/addon";
const getTitleFunc = getSitesTitle();
getTitleFunc();

バグがある…

  • さて、ここまで実際にやってきた方はお気づきかと思いますが、HTML要素の取得位置が間違っているせいで、温度のデータが欲しい場所に降水確率がのってしまっています。 時間があるときに修正するつもりですが、ここまで行ってきた方であれば修正方法がすぐに思いつくかと思います。

 また、「℃(セルシウス)ではまずありえない数値データが取れた場合のためのテストというのも書く必要がある」ということが分かります。

ぜひご自分の手でより良いプログラムになるよう目指してみてください。

あとがき

  • 今回の執筆にあたって、なるべく丁寧に解説できるよう心掛けましたが、コードの解説が予想以上に膨大になってしまいました。

 そのため、余裕があればサンプルコードの中にマークダウン(md拡張子のファイル)または、別フォルダにtsファイル形式で解説を書いておきます。

 また、今回の記事で、少しでもプログラミングの楽しさを知っていただけたら幸いです。

 見づらい文章校正で大変恐縮ではございますが、ありがとうございました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment