未分類

ここでは、本サイトで実施しているDBのみの手作業移行について解説します。簡単で、用途もデバッグ用なので、方法も適当です。

目的

DBのデータを運用しているサイトからlocalデバッグ環境に移行する

方針

ファイルはgitで管理されているので、DB内のデータのみ移行したいということです。プラグインを使うと、ファイルまで一緒についてきたり、サーバに余計なプラグインを設置しないといけなくなります。ローカルでサーバと同じデータを使ってデバッグ/テストしたいという目的であれば、DBの内容だけ適当にコピーできれば良く、できればコマンド一発で簡単にやりたいというわけです。

mysqlのデータをVALUE SERVER上でファイルに落とす

VALUE SERVER上で以下のスクリプト($HOME/bin/hoge.sh)を使うと、標準出力に、圧縮したWordPress関連のテーブルデータを全て出力できます。

#!/bin/sh
TABLES="wp_commentmeta wp_comments wp_links wp_options wp_postmeta wp_posts wp_term_relationships wp_term_taxonomy wp_termmeta wp_terms wp_usermeta wp_users"
mysqldump --user=$USER -p $USER $TABLES | gzip

実行すると、MySQLのパスワードが聞かれます。このファイルには実行可能属性を付けておきます。

$ chmod u+x $HOME/bin/hoge.sh

なお、使用しているコマンドのmysqldumpはあまり早くない汎用性の高いコマンドです。大量のデータがあって、いつまで経っても終わらない場合は別の方法にすべきです。このサイトは2020年1月23日現在圧縮して1MBに満たないサイズです。

ローカル環境から直接ファイルに落とす

上のスクリプトと、前回整えたsshの公開鍵認証を使って、ローカル環境から直にVALUE SERVER上のMySQLデータをファイルに落とすことができます。以下がそのコマンドです。

$ echo (MySQLパスワード) | ssh xxxxxxxx@xx.valueserver.jp ./bin/hoge.sh > mysqldat.gz

リダイレクトを使うことで、暗号化された経路でコマンドに履歴を残したり、一時ファイルを作成することなく、綺麗にデータを落とすことができています。さらに、VALUE SERVER上のhoge.shすら使用しないローカルスクリプト(export_db.sh)も書いてみました。

#!/bin/sh
SSH_USER="xxxxxxxx"
SSH_SERVER="xx.valueserver.jp"
MYSQL_PASSWD="yyyyyyyyy"
TABLES="wp_commentmeta wp_comments wp_links wp_options wp_postmeta wp_posts wp_term_relationships wp_term_taxonomy wp_termmeta wp_terms wp_usermeta wp_users"
OUTFILE=mysql.dat.gz
echo $MYSQL_PASSWD \
    | ssh $SSH_USER@$SSH_SERVER "mysqldump --user=$SSH_USER -p $SSH_USER $TABLES | gzip" > $OUTFILE

完成したタイミングでVALUE SERVER上のhoge.shは削除しました。sshは痕跡をあまり残さず何でも出来て便利だけど、ここが乗っ取られたら大変なことになることがよく分かります。

落としたファイルをローカルDBに流し込む

今回流し込むローカルDBは以前書いた記事のものを使います。

まずはdockerを起動して

$ docker-compose up

その後、以下のスクリプト(import_db.sh)で取り込みます。

#!/bin/sh
MYSQL_DB="exampledb"
MYSQL_USER="exampleuser"
MYSQL_PASSWD="examplepass"
CONTAINER="docker_db_1"
OUTFILE="mysql.dat.gz"
gzip -dc $OUTFILE | docker exec -i $CONTAINER mysql --default-character-set=utf8 -D $MYSQL_DB -u $MYSQL_USER --password=$MYSQL_PASSWD
docker exec -i $CONTAINER mysql --default-character-set=utf8 -D $MYSQL_DB -u $MYSQL_USER --password=$MYSQL_PASSWD <<EOF
update wp_options set option_value='http://localhost' where option_name in ('siteurl', 'home');
commit;
EOF

実行時にパスワードが引数で渡されることに警告が出ますが、デバッグ用の環境で固有名詞は適当なので、気にしないことにします。

また、取り込んだ後にSQLのUpdate文でWP_OPTIONテーブルを修正していたりもします。しかしその部分も最低限で、リンクなどの修正やその他の細かい変更もしていません。これらは運用サイトの状態をなるべく変えないことを意図しています。

まとめ

dockerが起動していれば、ローカルでexport_db.shとimport_db.shを実行するだけでVALUE SERVERのデータを取り込むことが出来た。

未分類

どこでも腐るほど解説はされてると思うし、VALUE SERVERで特別なこともないとは思いますが、念の為記事にしておきました。

対称鍵の作成

VALUE SERVERではなく今ログインしている端末(Linuxを想定)で、公開鍵認証で使用する公開鍵とその対象鍵である秘密鍵を同時に作成します。

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ユーザー名/.ssh/id_rsa): 

作成した鍵を格納するファイルパスを聞かれるのでそのままEnterを押しておきます。

Enter passphrase (empty for no passphrase):

次にパスフレーズを聞かれます。これは秘密鍵に掛けるパスワードのようなものです。秘密鍵を単独で盗まれてもそこに鍵がまだかかっているような形になります。セキュリティ的にはあるべきですが、理由があればなくても構いません。

Enter same passphrase again:

再度聞かれるので、同じものを入力します。すると、最終的に

  • $HOME/.ssh/id_rsaに秘密鍵
  • $HOME/.ssh/id_rsa.pubに公開鍵

が作られます。

VALUE SERVERに公開鍵を設置

ssh-copy-idというコマンドを使うのが簡単です。下で、xxxxxxxx@xxx.valueserver.jpは、VALUE SERVERのユーザ名とホスト名を@で繋いだ文字列です。

$ ssh-copy-id xxxxxxxx@xxx.valueserver.jp
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/ユーザー名/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
xxxxxxxx@xxx.valueserver.jp's password: 

VALUE-SERVERのパスワードを聞かれるので、従来どおりに入れてください。正しいパスワードで認証されれば、公開鍵をVALUE SERVERに設置してくれます。

VALUE SERVERに公開鍵認証でログイン

いつもどおりログインするだけです。パスワードの代わりにパスフレーズを使うだけ。パスフレーズを設定していなければ、何も聞かれずにログインできます。

user@user-pc:~$ ssh xxxxxxxx@xxx.valueserver.jp
Last login: Wed Jan 22 00:00:00 2020 from somedomain.net

xxxxxxxx@xxx:~$

参考までに公開鍵は、VALUE SERVERの、$HOME/.ssh/authorized_keys に保存されています。

参考

How to Use SSH Public Key Authentication – ServerPilot

未分類

Google検索向けにサイトマップを置きました。検索エンジンさんにこのサイトの構成などを説明するファイルです。

やったことは、Google XML Sitemapsというプラグインを追加して、

設定リンクを押して、

こんな感じのURL(画像のドメイン部分は自分のドメインに読み替えてください)をコピーして、Google Search Consoleでサイトマップの追加をしただけ。

このサイトに人が来ることがあるのかは分かりませんが、少なくとも表札を立てて住んでますっていう張り紙をしたという話でした。

未分類

今回当サイトにLuxeritasのパッチを置いたら、Chromeなどでダウンロードしたときに警告が出てしまいました。

サイトを作ったことがある人は知っているもののようですが、個人サイトなどでダウンロードできるようにすると、「誰が作った何なのか分からない」ので危険物扱いされてしまうようです。リンク先のサイトの管理人さんは、普通にウィルスチェックして、Googleに再チェックを依頼して事なきを得たそうですが、毎回そんなことするの大変です。

Google Search Consoleで問題指摘された際の詳細を見て、その基準を追ってみると…望ましくないソフトウェアのポリシー | Google – Google というページが見つかります。2020/1/22現在そこには以下のような記述があります(他にも項目があります)。

プログラムには、サイト運営者の検証可能な情報を提示する、コードサイニング機関から発行された有効な検証済みのコード署名が必要です。

ソフトウェアのダウンロードは、ユーザーがラベルで明示されたダウンロード ボタンをクリックしてダウンロードに同意した上で開始される必要があります。

望ましくないソフトウェアのポリシー | Google – Google

多分この辺が問題だと判断されたのかなと思います。ようは直リンクで署名されてないアーカイブを検出した場合に所定の手順でチェックが入るのかと推測しています。信頼された検証済みコードは難しいけど、署名=「誰が」の部分を明確にすることは出来そうです。今回は、Googleのチェックということもあり、GoogleDriveに置くことで、発信元を少なくともGoogleが(他の人もですが…)識別できるようにしてみました。

こうすることで、少なくともChromeなどでダウンロードする際に、危険物扱いをされずに済むようです。

未分類

背景

最近Dockerを使った記事を書くにあたり、シンタックスハイライターの対応言語が足りてないことに気付きました。シンタックスハイライターを実装しているのは、WordPressテーマのLuxeritasなのですが、このシンタックスハイライターに使われているprism.jsではもっと大量の言語に対応しています。そこで、今回必要になった、YAMLファイル(docker-compose.yml)、Dockerfile、 iniファイル(php.iniで欲しくなる)の3つの言語に簡単に対応してみました。

Luxeritasのコード解析

簡単とはいえ、説明書があるわけではないので、Luxeritasのコード解析をしないと、どこを修正していいか分かりません。調べてみたところ、大まかにいかの流れでシンタックスハイライターが動いています。

  1. シンタックスハイライターが扱える言語とその言語を表す文字列のリスト(wpfunc.php)を元にシンタックスハイライターの言語選択画面でリストアップする
  2. 記事のDB保存時にユーザーが選んだ言語を表す文字列を個々のコードブロックに紐づけて保存する
  3. 記事の表示時は、シンタックスハイライターを以下のように表示する
    1. コードブロックに紐付けられた言語を表す文字列と、先のリスト (wpfunc.php) を照合して、言語を確認(loadinline.php)
    2. その言語が複数の言語で構成されていればそれらを追加し、記事内で必要な言語のリストを作る(loadinline.php)
    3. 必要な言語のリストを元に、必要なprism.jsのファイルを特定し、最小限の組み合わせでJavaScriptファイルを合成(minify)してクライアントに返す
    4. クライアント側ではprism.jsの機能により、自動的に各言語はparseされてハイライトされていく

言語の追加

今回追加する言語は、複数の言語で構成される言語ではないので(例えばHTMLの中にJavaScriptが入るとか)、 シンタックスハイライターが扱える言語とその言語を表す文字列のリストに必要な言語を追加し、その言語に必要なprism.jsの部品を追加すれば完成です。

シンタックスハイライターが扱える言語の追加

wpfunc.php

diff --git a/inc/wpfunc.php b/inc/wpfunc.php
index fac76e8..7039e66 100644
--- a/inc/wpfunc.php
+++ b/inc/wpfunc.php
@@ -1970,6 +1970,9 @@ function thk_syntax_highlighter_list() {
                'highlight_sql'         => 'SQL',
                'highlight_vbnet'       => 'VB.NET',
                'highlight_vim'         => 'Vim',
+               'highlight_yaml'        => 'YAML',
+               'highlight_docker'      => 'Docker',
+               'highlight_ini'         => 'Ini',
        );
 }
 endif;

必要なprism.jsの部品を追加

今回は現在の最新リリースである、prism.js v1.16.0を取得し、その中からcomponents/prism-言語名.min.jsとなっているファイルを抽出し、必要な言語をLuxeritasパッケージ内にjs/prism/言語名.jsというファイルで配置しました。

パッチ

以下にまとめてあります。

未分類

ルクセリタスのコード表示部分をいじり、パス表示を追加してみました。

diff --git a/js/luxe-blocks.js b/js/luxe-blocks.js
index 47d3ce8..a6e77ef 100644
--- a/js/luxe-blocks.js
+++ b/js/luxe-blocks.js
@@ -4411,6 +4411,10 @@ window.addEventListener("DOMContentLoaded", function() {
         language: {
           type: "string",
           default: ""
+        },
+        filename: {
+          type: "string",
+          default: ""
         }
       },
       supports: {
@@ -4464,45 +4468,66 @@ window.addEventListener("DOMContentLoaded", function() {
             value: ""
           }];
         return Object.keys(luxeHighlighterList).forEach(function(e) {
-          c.push({
-            label: this[e],
-            value: e
-          })
-        }, luxeHighlighterList), l.push(s(d.SelectControl, {
-          label: u("Code Language", "luxeritas"),
-          value: a,
-          options: c,
-          onChange: function(e) {
-            return n({
-              language: e
-            })
-          }
-        })), t = s("p", null, u("* The theme can be changed from the CSS tab of Luxeritas menu.", "luxeritas")), [s(p.InspectorControls, {
-          key: "syntaxHighlighterInspectorControls"
-        }, s(d.PanelBody, {
-          title: u("Settings")
-        }, l, t)), s("div", {
-          className: r + " wp-block-code"
-        }, s(p.PlainText, {
-          value: i,
-          onChange: function(e) {
-            return n({
-              content: e
+            c.push({
+              label: this[e],
+              value: e
             })
-          },
-          placeholder: u("To the right, choose a code language from the block settings.", "luxeritas"),
-          "aria-label": u("Syntax Highlighter", "luxeritas")
-        }))]
+          }, luxeHighlighterList), l.push(s(d.SelectControl, {
+            label: u("Code Language", "luxeritas"),
+            value: a,
+            options: c,
+            onChange: function(e) {
+              return n({
+                language: e,
+                filename: o.filename
+              })
+            }
+          })),
+          l.push(s(d.TextControl, {
+            label: u("File Name", "luxeritas"),
+            value: o.filename,
+            onChange: function(e) {
+              return n({
+                language: a,
+                filename: e
+              })
+            }
+          })), t = s("p", null, u("* The theme can be changed from the CSS tab of Luxeritas menu.", "luxeritas")), [s(p.InspectorControls, {
+            key: "syntaxHighlighterInspectorControls"
+          }, s(d.PanelBody, {
+            title: u("Settings")
+          }, l, t)), s("div", {
+            className: r + " wp-block-code"
+          }, s(p.PlainText, {
+            value: i,
+            onChange: function(e) {
+              return n({
+                content: e
+              })
+            },
+            placeholder: u("To the right, choose a code language from the block settings.", "luxeritas"),
+            "aria-label": u("Syntax Highlighter", "luxeritas")
+          }))]
       },
       save: function(e) {
         var t = e.attributes,
           o = t.content,
           n = t.language.replace("highlight_", "");
-        return "" != n ? s("pre", {
-          className: "line-numbers language-" + n
-        }, s("code", {
-          className: "language-" + n
-        }, o)) : s("pre", null, s("code", null, o))
+        var obj;
+        if ("" != n) {
+          var pre = {
+            className: "line-numbers language-" + n,
+          };
+          if (t.filename) {
+            pre["data-label"] = t.filename;
+          }
+          obj = s("pre", pre, s("code", {
+            className: "language-" + n
+          }, o));
+        } else {
+          obj = s("pre", null, s("code", null, o));
+        }
+        return obj;
       }
     })
   }(),
diff --git a/languages/admin/luxeritas-ja-luxe-blocks.json b/languages/admin/luxeritas-ja-luxe-blocks.json
index 4d04781..8381a3c 100644
--- a/languages/admin/luxeritas-ja-luxe-blocks.json
+++ b/languages/admin/luxeritas-ja-luxe-blocks.json
@@ -200,6 +200,9 @@
                        "Code Language": [
                                "言語"
                        ],
+                       "File Name" : [
+                               "ファイル名"
+                       ],
                        "* The theme can be changed from the CSS tab of Luxeritas menu.": [
                                "※ テーマ(見た目)は Luxeritas メニューの CSS タブから変更できます。"
                        ],

luxe-blocks.jsはminifyされてたので、以下で整形してから編集してdiffを取っています。

$ npx js-beautify -s 2 luxe-blocks.js >luxe-blocks.js.new
$ mv luxe-blocks.js.new luxe-blocks.js

編集後は以下でminifyして使っています。

$ npx -p uglify-js uglifyjs luxe-blocks.js -c --output luxe-blocks.js.new
$ mv luxe-blocks.js.new luxe-blocks.js
$ 手作業でライセンスの貼り付け

上記作業はパッチにして以下にまとめてあります。

未分類

何かと面倒なdockerでpostgresを動かし、SSLで接続した上に、せっかくSSLなのでクライアント認証する、という企画です。

CAと鍵と証明書の作成

privateなnetworkなのでオレオレCAを作成します。

create_cert.sh

#!/bin/bash
set -euo pipefail

# create root CA
openssl req -new -nodes -text -subj "/CN=ca.local" -out ca.csr -keyout ca.key
chmod og-rwx ca.key
openssl x509 -req -in ca.csr -text -days 3650 -extfile /etc/ssl/openssl.cnf -extensions v3_ca -signkey ca.key -out ca.crt
rm ca.csr

# create server cert
openssl req -new -nodes -text -out server.csr -keyout server.key -subj "/CN=server.local" 
chmod og-rwx server.key
openssl x509 -req -in server.csr -text -days 365 -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
rm server.csr

# create client certs
openssl req -new -nodes -text -out client1.csr -keyout client1.key -subj "/CN=client1.local" 
chmod og-rwx client1.key
openssl x509 -req -in client1.csr -text -days 365 -CA ca.crt -CAkey ca.key -CAcreateserial -out client1.crt
rm client1.csr

# change owner for alpine postgres
sudo chown 70 server.key
  • .certが証明書、.keyが秘密鍵
  • caがオレオレルートCAで、serverがpostgresが動くサーバ、client1がクライアント用
  • domainはとりあえず.local
  • CA自己署名証明書は10年、他は1年で作成
  • サーバの秘密鍵はalpine上のpostgresイメージのUIDに合わせて70に

postgresをdockerで起動(初回)

docker-compose.yml

version: "3"

services:
  db:
    image: postgres:12-alpine
    container_name: postgres
    command: -c ssl=on -c ssl_ca_file=/var/lib/postgresql/ca.crt -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
    ports: 
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_DB=postgres
      - POSTGRES_PASSWORD=secret
    volumes:
      - ./ca.crt:/var/lib/postgresql/ca.crt:ro
      - ./server.crt:/var/lib/postgresql/server.crt:ro
      - ./server.key:/var/lib/postgresql/server.key:ro
      - ./data:/var/lib/postgresql/data
  • 公式イメージではありません!

起動!

$ docker-compose up

SSLで接続(パスワード認証)

別のコンテナからホストに繋いで確認します。

$ docker run -it --rm postgres:12-alpine psql "host=(ホストIP) user=postgres password=secret"

結果

psql (12.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# \q

外部からの認証をSSLクライアント認証のみに設定

サーバーを一度落とす

$ docker-compose down

./data/pg_hba.confの最後を以下に書き換える

#host all all all md5
hostssl all all all cert map=cnmap
  • (ローカルを除く)全IPからのTCPホスト接続をパスワード認証
    → (ローカルを除く)全IPからのTCPホスト接続をSSLクライアント認証(ユーザーマッピングはcnmap)

./data/pg_ident.confに以下を加える

cnmap   /^(.*)\.local$  postgres
  • cnmapという名前で、.localで終わるCNにユーザーpostgresをマッピングするルールを定義

設定確認

起動!

$ docker-compose up

パスワード認証で接続

$ docker run -it --rm postgres:12-alpine psql "host=(ホストIP) user=postgres password=secret"

結果

psql: error: could not connect to server: FATAL:  connection requires a valid client certificate
FATAL:  no pg_hba.conf entry for host "172.20.0.1", user "postgres", database "postgres", SSL off

SSLクライアント認証で接続

$ docker run -it --rm \
-v $PWD/client1.crt:/root/.postgresql/postgresql.crt \
-v $PWD/client1.key:/root/.postgresql/postgresql.key \
-v $PWD/ca.crt:/root/.postgresql/root.crt \
postgres:12-alpine psql "host=(ホストIP) user=postgres"
  • ややこしそうに見えるが、~/.postgresql/にクライアント証明書とクライアント鍵と信頼してるCAの証明書を置いて、ホスト+ユーザー指定で繋いでるだけ

結果

psql (12.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# \q

参考リンク

未分類

前提

  • dockerが使える
  • Visual Studio codeが使える
  • x-debugを使用する

docker

docker-compose.yml

version: "3.1"
services:
  wordpress:
    build: .
    restart: always
    ports:
      - 80:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - ./wordpress:/var/www/html
      - ./php.ini:/usr/local/etc/php/php.ini

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - ./db:/var/lib/mysql

volumes:
  wordpress:
  db:

Dockerfile

FROM wordpress:php7.3
RUN pecl install xdebug \
  && docker-php-ext-enable xdebug
WORKDIR /var/www

ビルド時に以下のようなメッセージ(タイミングや環境によって異なる)が出るはずなので、これをphp.iniに追加する。

zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so

php.ini

post_max_size = 50M
upload_max_filesize = 50M
memory_limit = 256M
[xdebug]
xdebug.remote_enable=1
xdebug.remote_autostart=1
; ホスト側のIP
; host.docker.internalはdockerのhostマシンのIPを解決してくれます。
; hostマシン以外のIP/WindowとMAC以外の場合は適宜IPを調べて設定してください。
xdebug.remote_host=host.docker.internal
; 空いているport番号
xdebug.remote_port=9000
; xdebugの出力するログの場所。今回は適当に/tmp配下に。
xdebug.remote_log=/tmp/xdebug.log
; Dockerイメージビルド時のメッセージをコピペして追記
zend_extension=?

Visual Studio code

.vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "port": 9000,
            "pathMappings": {
                // {docker上のdocument root}:{ローカルのdocument root}
                "/var/www/html":"/path/to/wordpress"
            }
        }
    ]
}

参考サイト

未分類

今回は、wgetの–level(リンクの最大深さ)と–convert-link(オフライン閲覧用のリンク修正)の内容を検証する。

対象はGNU Wget 1.20.1。

–level(リンクの最大深さ) の検証

ここでは、最大範囲まで辿られると、時間が増えないことを検証する。

クロール先の作成

HTMLを生成するshellスクリプト

COUNT=$1
BASE_URL="https://elephantcat.work/wget_test"
for i in `seq $COUNT`;do
        FILE="$i.html"

        echo "<html lang=\"en\">" > $FILE
        echo "<body>" >> $FILE
        echo "<ul>" >> $FILE
        MAXJ=`expr $i + 1`
        for j in `seq $MAXJ`;do
                URL="$BASE_URL/$j.html"
                echo "<li><a href=\"$URL\">$j</a></li>" >> $FILE
        done
        echo "</ul>" >> $FILE
        echo "</body>" >> $FILE
        echo "</html>" >> $FILE
done

上記を実行して、50個分のHTMLを生成

$ sh create_htmls.sh 30

カレントディレクトリに1.html~30.htmlが作成されます。例えば3.htmlは以下のようなものになります。

<html lang="en">
<body>
<ul>
<li><a href="https://elephantcat.work/wget_test/1.html">1</a></li>
<li><a href="https://elephantcat.work/wget_test/2.html">2</a></li>
<li><a href="https://elephantcat.work/wget_test/3.html">3</a></li>
<li><a href="https://elephantcat.work/wget_test/4.html">4</a></li>
</ul>
</body>
</html>

3.htmlは4つのリンクを持つHTMLファイルです。同様に、n.htmlは、n+1個のリンクを持つHTMLファイルであり、1.htmlから1つずつリンクを辿る度にファイルが増えるようなネットワーク構造になっています。

測定

1.htmlを起点にレベル1つずつ増やしながら、50回時間を計測します。測定用のスクリプトが以下になります。

for i in `seq 50`; do
        rm -r elephantcat.work/wget_test
        /usr/bin/time --quiet -f %e wget --quiet --recursive --level $i --page-requisites --adjust-extension --span-hosts --convert-links --restrict-file-names=windows --domains elephantcat.work --no-parent https://elephantcat.work/wget_test/1.html
done

実行すると標準出力に実行時間が1行ずつ出るので、リダイレクトしてファイル(time.log)に落とします。

$ sh measure.sh > time.log

後はExcelなどでグラフにすればOK。今回は遊びで、PerlのText::Chartを使ってみました。

use Text::Chart qw(gen_text_chart);

chomp(my @nums=<STDIN>);
my $res = gen_text_chart(
    data => \@nums,
    chart_height => 10,
    type => 'sparkline',
);

binmode(STDOUT, ":utf8");
print $res;

これを使うとテキストで棒グラフが書けます(整形が結構大変)。

結果と考察

                                █  ▄         █     2[秒]
                             ▃▁ █▅▂█▆▂ ▄▃▂▄▂▂█▆▃▂▁
                         ▃▂  █████████▇███████████
                         ██▃▄█████████████████████
                    ▂▂▁▃▇█████████████████████████
               ▁ ▁▂▄██████████████████████████████ 1
             ▅▆█▇█████████████████████████████████
         ▂▃▄▇█████████████████████████████████████
     ▂▃▅▇█████████████████████████████████████████
 ▂▄▆██████████████████████████████████████████████ 
回数     10        20        30        40        50

30回以降横ばいに見えます。レベルは最大回数を超えると取得時間が頭打ちになるということです(当然ですが)。

–convert-link(オフライン閲覧用のリンク修正)の内容検証

前節で生成し、wgetで取得した3.htmlを見てみます。

<html lang="en">
<body>
<ul>
<li><a href="1.html">1</a></li>
<li><a href="2.html">2</a></li>
<li><a href="3.html">3</a></li>
<li><a href="4.html">4</a></li>
</ul>
</body>
</html>

上にあるものと比べると、絶対URLだったリンクURLが、相対URLになってることが分かります。絶対URLでないのは、オフライン閲覧した際にリンク切れになってしまうからです。このように、オフライン閲覧などを考慮すると、(元のサイト次第ですが)HTMLの書き換えが必要となります。以降では絶対URL→相対URLの書き換え以外のリンク書き換えパターンを見ていきます(全部ではありません)。

ディレクトリ

例えば、https://elehantcat.work/、これにもファイル名がありません。通常、この内容はtext/htmlであり、多くの場合、index.htmlやらindex.phpやらindex.jspやらindex.jsやら、拡張子は違えど、共通してindex.*という形式の名前のファイルが処理していることが多いと思います。wgetでは、ディレクトリの内容を取得すると、ファイル名にはindex.htmlを使用します。リンクURLは当然ただのディレクトリなので、オフライン閲覧のためには書き換えが必要になります。

拡張子なし

例えば最近の動的に生成されるページでは、URLを見てコントローラを選ぶルーティング機能を導入しているフレームワークが多いです。そのようなフレームワークを使用すると、URLとコントローラなどが必ずしも一対一対応しないので、URLのファイル名部分に拡張子が付かなくなります。

これらのURLからファイルとして内容をダウンロードした場合、拡張子のないファイルは見た目で何のファイルか分からず、扱いに困ります。そこで、wgetではオプション(–adjust-extension)でContent-Typeから拡張子を付けてくれる機能が追加されています。このようなケースではリンク元は拡張子がついていないため、オフライン閲覧用に書き換えが必要になります。

その他

他にもいろいろありますが、あとは自分で見つけてください。マニュアルは、以下にあります。

https://www.gnu.org/software/wget/manual/wget.html

最後に

–convert-linkによるリンク書き換え処理は全部ダウンロードが終わってからなので、Ctrl+Cで止めると書き換えずに終わってしまいます。ご注意ください。

未分類

今どきオフライン作業なんてあんまりないけど、wgetって単独ファイルだけでなく、JavaScriptなどで作られてない静的なリンクなら辿って次を取得することもできる。昔の貧弱なネットワークの頃は重宝した。

取得してみる

とりあえずこのサイトをwgetで取得してみる。パラメータは以下のサイトを参考にした。(–domains指定は–span-hostsと同時指定禁止なので除外)

shell – How to download an entire site with wget including its images – Super User

$ wget --recursive \
--level 5 \
--page-requisites \
--adjust-extension \
--span-hosts \
--convert-links \
--restrict-file-names=windows \
--domains elephantcat.work \
--no-parent \
elephantcat.work

上記コマンドを実行すると、elephantcat.workから、再帰的に5段階までリンクを辿り、同一ホスト内の全ページをローカルファイルに保存してくれる。保存する際には、ホスト名のディレクトリ下にURLから想定される名前で保存される。HTML内には埋め込まれる要素やリンクなどもあるが、静的なものは残らず解析して取ってきてくれる。クエリなどがあっても、URIとして区別されればちゃんとそれをファイル名にして保存される。

オプション説明

オプション意味
–recursive再帰的に取得
–level 数字指定された深さまで再帰的に取得
–page-requisites画像など HTMLを適切に表示するために必要なすべてのファイルを取得
–adjust-extensionMIMEタイプなどを元に拡張子を自動で修正する
–span-hosts同一ホスト内の リンク のみ辿る
–convert-links同一ホストへのリンクを相対リンクに書き換える
–restrict-file-names=windowsファイル名の大文字/小文字統一や使用禁止文字のエスケープに使用するモードの指定
–domains指定ドメイン以外を取得しない
–no-parent親ディレクトリ側へのリンクを辿らない

注意事項

重要なのは探索範囲と時間に大きく影響する–levelと、–convert-links。

–levelはサイトの大きさやクロール可能な時間に合わせて適宜修正が必要。全ての経路を辿れる深さ以上を指定しても取得時間は増加しない。

ダウンロードされたファイルは必ずしもURLとおりの構成にできるわけではないので、–convert-linksはダウンロードしたファイルをオフライン閲覧できるよう、 HTMLの中身を書き換えることを意味している。そして、この処理は、指定した範囲のファイルを全て取得し終わった後、最後に行われるので注意。「時間かかるからこの辺でやめる→Ctrl+C」とかするとこの変換はかからない。