HTMLのタイトルを取得する [Node.js, シェルスクリプト]


httpリクエストでHTMLのページを取得して正規表現でタイトルを引っ張り出します。Node.jsを使う方法とシェルスクリプト(Bash)を使う方法で出来たので書き残しておきます。

ちなみに、Node.jsもシェルスクリプトも初心者です。

環境
Mac OS 10.14.6

Node.js

Node.jsからダウンロードします。

$ node -v
v12.18.1

requestモジュール

Node.js標準でもHTTPリクエストできるそうですがrequestモジュールを使うと楽、らしい。

$ npm install request

これはできるだけ上位のディレクトリでやったほうがいい。インストールしたところ以下でrequestが使えるがそれより上のディレクトリでは使えない。(個人的な実験の結果)(npmについてのまとめ - Qiitaに詳しいことが書いてある(グローバルインストールとやらに失敗したので上の階層に入れた))

実装

get_title.js
var request = require('request');

var URL = process.argv[2];

var re_title = RegExp("(?<=<title.*>).*(?=</title>)")

function callback(error, response, body) {
  if (!error && response.statusCode == 200) {
    var title = re_title.exec(body);
    if(!title){
      console.log("No title")
    }else{
      console.log(title[0]);
    }
  }else{
    console.log("Error!");
  }
}

request(URL, callback);
  • 1行目 requestモジュールを使えるようにする
  • 3行目 コマンドライン引数からURLを取得する
  • 5行目 title要素を取得する正規表現(後述)
  • 7行目~ requestの結果に対するコールバック関数(後述)
  • 最終行 request実行

実行

$ node get_title.js https://...
タイトル

正規表現

ここで使う正規表現は

"(?<=<title.*>).*(?=</title>)"

です。(参考 正規表現 - JavaScript | MDN

  • (?<=<title.*>): これは「後読み」と呼ばれ、<title.*>に続くものをマッチさせますが<title.*>自体はマッチしません
  • .*: これは「任意の文字の(.)」「0回以上の繰り返し(*)」にマッチします
  • (?=</title>): これは「先読み」と呼ばれ、</title>が後に続くものをマッチさせますが</title>自体はマッチしません

この正規表現でHTMLを探索することでtitle要素の中身にマッチさせることができます。ちなみに、title開始タグを見つける「後読み」部分が<title>ではなく<title.*>を使っているのは、title開始タグにオマケがついているページがあったからです。ここ→request - npmなんですけど、ソースを見ると

<title data-react-helmet="true">request  -  npm</title>

って書いてあります。何のこっちゃわからないのですが。

requestのコールバック関数

そんなに深入りしないです。

リクエストがエラー吐いたりステータスコードが200じゃなかったら"Error!"と出力して終わる。
OKだったら正規表現でbodyから.exec()メソッドで最初にマッチするところを探して結果をtitleに入れる。
マッチすればtitle[0]がタイトルだし、マッチしなければnullが返ってくるので"No title"と出力。

まあ基本正常に返ってくることを期待しているので。

Bash

こっちのほうが、複雑だが面白かった。

実装

get_title.sh
#!/bin/bash

read URL

#RegExp='s/.*<title.*>\(.*\)<\/title>.*/\1/p'
RegExp='/.*<title.*>\(.*\)<\/title>.*/{s/.*<title.*>\(.*\)<\/title>.*/\1/p;q;}'

title=`curl -s $URL | sed -n $RegExp`

echo -e "\n[$title]($URL)\n" #markdown式
  • 1行目 おまじない。よくわからんけど書いとけ
  • 3行目 標準入力からURLを受け取る
  • 5,6行目 sedに送るコマンド
  • 8行目 curlとsedでページタイトルの取得
  • 10行目 出力(markdown式にカスタマイズ)

実行

$ bash get_title.sh
https://qiita.com/

[Qiita](https://qiita.com/)

curl

$ curl -s {URL}

{URL}の中身であるHTML文書を取得。さらにパイプで次のsedコマンドに流している。-sは進捗の表示をさせないオプション

sed

パイプでやるという条件を揃えて書くと

#!/bin/bash
$ echo -e {複数行文字列} | sed -n s/{置換前}/{置換後}/p

で、{複数行文字列}から{置換前}を含む行があれば{置換後}に置き換えて出力。-nは、デフォルトでは置換の有無にかかわらず毎行出力していたのを無効にするオプション。pは明示的に出力するフラグ

#!/bin/bash
$ echo -e "hogehoge\nfugafuga" | sed -n "s/g.*g/QQ/p"
hoQQe
fuQQa

5行目のコマンド

s/.*<title.*>\(.*\)<\/title>.*/\1/p

大枠 = s/{置換前}/{置換後}/p
{置換前} = .*<title.*>\(.*\)<\/title>.*
{置換後} = \1
  • {置換前}: 「任意文字列(.*)」「<title.*>」「.*」「<\/title>」「.*」の文字列を見つける。真ん中は「\(.*\)」としてエスケープつきの括弧で囲むことでグループ化して一時保存しています
  • {置換後}: 先ほど一時保存したところを\1で呼び出しています
#!/bin/bash
$ echo -e "hogehoge\nfugafuga" | sed -n "s/.*g\(.*\)g.*/\1/p"
eho
afu

6行目のコマンド

/.*<title.*>\(.*\)<\/title>.*/{s/.*<title.*>\(.*\)<\/title>.*/\1/p;q;}

大枠 = /{文字列}/コマンド
文字列 = .*<title.*>\(.*\)<\/title>.*(さっきの探索文字列)
コマンド = {s/.*<title.*>\(.*\)<\/title>.*/\1/p;q;}
  • 大枠: {文字列}がある行に対してコマンドを実行
  • コマンド: {}で囲んで複文にする。前半s/.*<title.*>\(.*\)<\/title>.*/\1/p;は先述の通りの置換&表示コマンド。後半q;はsedコマンドの終了。

これがなぜ欲しいかというと、request - npmの中になんだか知らんがtitle要素が出現するところが2行あるので。1回マッチしたら{変換&終了}する。

その他

sedの後一回変数に入れたいなぁと思って色々調べたら、式自体をバッククォートで囲んで代入すれば良いとのこと。

bashで改行付きでechoするためには-eオプションが必要→echo -e "\n[$title]($URL)\n"

おしまい

bashの方が色々設定とかせずにすみそうで良いな

参考