技術的な話題 に関する投稿を表示しています

docker の出すログファイルでディスクが 100% になった

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

まとめると?

  • タイトル通り。 docker の出すログファイルでディスクが 100% になった。
  • ログファイルの削除と daemon.json の設定をして完。

起きたこと

環境依存的に難しいので docker を使っているバッチスクリプトがあるのだが、このスクリプトがモリモリと標準出力に出している。や、本来は捨てられる前提のものなので、問題はあるけどない、みたいな状態。
docker は docker 内で動いているメインプロセスの標準出力のログを取っているっぽく、そのログが溜まりに溜まって、ホストマシンのディスクを埋め尽くしてしまった。

それをたまたま、同じホストマシン上の、まったく違う docker コンテナで動いている web アプリケーション上で、セッションがうまく動いていないっぽいという事象が発生したところから、調べてみると、ホストマシンのディスクがいっぱいだった、ということが判明した。

やったこと

何が容量を食っているのか確認した

du コマンドでチマチマ確認したのだが、もうちょっと効率のいい方法がしりたい。一発でバチコーンとわかるような何か。

# du -sh /* | sort -nr
...

# du -sh /var/lib/docker/* | sort -nr
436K    /var/lib/docker/containerd
104K    /var/lib/docker/network
72K     /var/lib/docker/volumes
37G     /var/lib/docker/containers
22M     /var/lib/docker/image
20K     /var/lib/docker/plugins
20K     /var/lib/docker/builder
8.1G    /var/lib/docker/overlay
4.0K    /var/lib/docker/trust
4.0K    /var/lib/docker/tmp
4.0K    /var/lib/docker/swarm
4.0K    /var/lib/docker/runtimes

# du -sh /var/lib/docker/containers/* | sort -nr
372K    /var/lib/docker/containers/df16f7083353204424150c80cb7c87dc9dda19627c7f09632e797a48d3bea2cf
372K    /var/lib/docker/containers/0d9b9a46c096f977921b682b40d7aa9de5dee443ea01a8d0b157f8f366bce764
288K    /var/lib/docker/containers/8d813f2238daa78186881017d1d6b675715f7602549cfb8285cbb08f5832f88d
252K    /var/lib/docker/containers/ce637310ed46de127cfc78813df94b1a269370a07a0f8ab24dc11f2c87bbadc8
228K    /var/lib/docker/containers/fd837df4343e6c54f5c145385bd93e23e8705114243f8a3d856458bf89c454a0
64K     /var/lib/docker/containers/bc4cdc66666f835980147bbc8105741a45db5018777ca883e35fdd3a072a7970
37G     /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80
36K     /var/lib/docker/containers/8d9bb861db601facac528d60ee855fd5b38a05dcdc2949fee72e9c3dcb4dd177

# du -sh /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/* | sort -nr
37G     /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80-json.log
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/resolv.conf.hash
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/resolv.conf
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/hosts
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/hostname
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/hostconfig.json
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/config.v2.json
4.0K    /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/checkpoints
0       /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/shm

# tail /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80-json.log
....
// このログしってる!ぼくが標準出力に出すようにしたやつだ!

ログファイルの削除をした

中身を確認した上で、完全に不要なものだったので削除した。お手軽 echo 。

# echo > /var/lib/docker/containers/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80/4d81d71c1ea8eaea101cab745f2c16a2bcb096a2bba78f9aecd385f560e62e80-json.log

/etc/docker/daemon.json の設定をした

実際には次のような設定を書いている。これで docker 自体が勝手にログファイルの容量制限とローテートをしてくれるっぽい。べんりちゃん。

# cat /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {"max-size": "10m", "max-file": "3"}
}

容量制限とローテートされた様子。上で出したものと別コンテナだけど。

# du -sh /var/lib/docker/containers/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66/* | grep json.log
4.6M    /var/lib/docker/containers/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66-json.log
9.6M    /var/lib/docker/containers/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66-json.log.1
9.6M    /var/lib/docker/containers/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66/61ffb4d88381cd2556a91f6944a2b2d2b57bb7837d8ad9a21ed772748f490b66-json.log.2

公式ドキュメントを見ると他にも設定項目がいろいろあって、デーモン自体の設定なので、どんな権限で動かすか、とか、そういう…。あまり変更するような出番はないかも。
daemon — Docker-docs-ja 17.06.Beta ドキュメント

log-driver として json-file 以外にも fluentd や AWS CloudWatch なども指定できるので、もしかしたら何か活用できる…?具体的にどういう活用方法があるんだろう
Fluentd logging driver | Docker Documentation


docker 入れてる環境、事故防止のためにもログローテート系の設定はとりあえず入れると良いとおもった一件

DELL XPS 13 の Windows 10 を使っているが無線 LAN の電源管理オプションが表示されなくてちょっとこまった

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

会社では Windows 10 のノート PC 、というか DELL XPS 13 を使っている。
無線 LAN の親機の設定が変わったのか、こっちが変わったのかよくわからないが、ここ最近で無線 LAN がよく切れるようになってしまった。

特に電源ケーブルにつないでいない状態で、一部の SSID にまったく接続できなくなったのはかなり謎。元々は問題なく利用できていたので完全に親機の問題だと思う(設定かえたとか言ってたし…)
一向に対応される気配がないので、諦めて子機側でできることをやってみようと思った。

 

「無線 LAN 切れる Windows」とか「無線 LAN 不安定 Windows」などで調べると、おおむね「電源管理の設定を開いて最大パフォーマンスにしよう!」という内容が出てくる。たとえば…
Windows10のLAVIEでWi-Fiがすぐにつながらなくなるので省電力設定を直したぞ! | むねさだブログ

 

が!!!

「電源オプション」の中の「ワイヤレスアダプターの設定」が表示されていない。 DELL ありがてえ(ありがたくない)
「表示されない」、とかで調べるとてもドンピシャな内容がヒットせず。。

 

DELL サポートに連絡するのもアレなので、レジストリエディタからほげほげできないかなあと思いながら調べていると、まさしくな記事が見つかった。

Windowsの電源オプションで設定可能な項目を増やす - らどこの消費生活 続き

 

ワイヤレスアダプターの件について書かれてはいないのだが、同じようなあたりに設定値おるやろってことで順番に見ていくと wlansvc.dll と指定されているものいて、無線 LAN っぽい名前だしこれだろうな~~と。
ここの Attributes の値を、上記のブログに書かれていたように 2 にしたら設定項目がでてきましたとさ。

環境によって違うかもだけど、レジストリキーはこれ。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\PowerSettings\19cbb8fa-5279-450e-9fac-8a3d5fedd0c1\12bbebe6-58d6-4636-95bb-3217ef867c1a

 

本題に戻ると、この設定で接続不安定なところは改善された。たまに瞬断も起きているっぽいが、まあまあ問題ない。というか親機…

 

OEM 的な目線(?)で考えれば、これって不用意にいじられてぶっ壊されたり、メーカーが想定している適切なパフォーマンスが発揮できない、みたいな減少を回避するために、意図的に項目を隠しているんじゃなかろーか、と思った。コンパネ的な設定項目だったら「エコにしよう!!!」とかですぐ弄れるけれど、レジストリなら弄れるには弄れるが、たまたまたどり着くことは難しいと思うので、それでいいんだと思う。

Apache 2.4 で WebSocket もリバースプロキシする

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

Apache 2.4 で WebSocket をプロキシしたい。以前書いた docker-compose のフロントとして Apache を立てると、バックエンドで WebSocket を使う場合にすこしの設定を足す必要がある。

docker-compose で LDAP 認証のリバースプロキシを設定する | ごみばこいん Blog

nginx 使えよって話、めっちゃわかる。そのうちやる…たぶん…

ちなみに Apache 2.2 だとパッチがいたりするそうなので、がんばって!って感じっぽい。

 

httpd.conf あるいはその手の Apache 設定ファイルで以下のような設定を足すと良い。

# ...略...

# WebSocket をプロキシするモジュールを読み込み
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so


# ...略...


# 例えば Jupyter の場合はこんなパスをプロキシしてあげる

# websocket
ProxyPass /api/kernels/ ws://${REVERSE_PROXY}/api/kernels/
ProxyPassReverse /api/kernels/ ws://${REVERSE_PROXY}/api/kernels/

ProxyPass /terminals/websocket/ ws://${REVERSE_PROXY}/terminals/websocket/
ProxyPassReverse /terminals/websocket/ ws://${REVERSE_PROXY}/terminals/websocket/

# http
ProxyPass / http://${REVERSE_PROXY}/
ProxyPassReverse / http://${REVERSE_PROXY}/

 

REVERSE_PROXY は環境変数で、バックエンド( Jupyter )のホスト(とポート)を指定する。ぼくの手元のやつは docker-compose を使ってるのでこんな感じに書いている。

version: "3"

services:
  jupyter:
    container_name: "${APP_NAME}_jupyter"
    build: ./docker-jupyter
    command: start-notebook.sh --NotebookApp.token=''
    volumes:
      - ./notebooks:/home/jovyan/work
    environment:
      GRANT_SUDO: 1
      NB_UID: ${NB_UID}
      NB_GID: ${NB_GID}
  proxy:
    container_name: "${APP_NAME}_proxy"
    build: ./docker-proxy
    ports:
      - "${HTTP_PORT}:80"
    links:
      - jupyter:jupyter
    environment:
      REVERSE_PROXY: jupyter:8888

httpd:alpine の Docker イメージを利用しても WebSocket のプロキシが出来るので、そんな感じ。
いじょ

docker-compose 自体を更新する

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

Github にリリースされていっているので、そこを見て、コマンドを実行したらよいといえば良い。

Releases · docker/compose

が、毎回見るのもあれなので、思い立ったときに動かせるようにしたいなあと思ったので更新するやつを作った。

sters/docker-compose-updater: Semi automatic docker-compose updater

原理はめっちゃそのままで、こんな処理をしている。

  • インストールされている docker-compose を which で探す
  • インストールされていなければ終了
  • curl と cut で Github のリリースページから最新版のバージョンを探す
  • OSとアーキテクチャを uname で識別(ドキュメントのまま)
  • Github のリリースページから最新版をダウンロードし、インストールされているものを置き換える(ドキュメントのまま)
  • chmod で実行権限を渡しておわり

Makefile がいいよ!って言われたので試しに見よう見まねで Makefile でやった。 Makefile のお試したかっただけ、みたいなところはある。なるほどなあ~~

 

どうやるのがいちばん良さそうかわからないけれど、docker-compose を扱う docker イメージがあってそっちを使うほうが楽そう…?

とはいえコンテナ内から docker-compose な操作をほにゃほにゃするとなると /var/run/docker.sock とか docker-compose.yml もといアプリケーションディレクトリをマウントしないと行けないので、記述がめんどうになりそうな気がする… というかちょっと試した程度ではできなかったのでうーん。
docker-compose って名前でエイリアスして、実態は docker/compose イメージを docker run してますよ~~みたいなものが出来ると、 docker-compose の準備する手間が無くてちょっとだけ便利になりそうなのだけど…

 

ちなみに作ったものと同じように、手元へと docker-compose バイナリを取り出すのはこんな感じ。

$ docker run --rm --entrypoint "" -v $(pwd):/tmp/bin docker/compose:1.18.0 cp /usr/local/bin/docker-compose /tmp/bin/

$ ls -l docker-compose
-rwxr-xr-x 1 root root 8479184 Jan 18 12:10 docker-compose

※ root が所有者になってるので移動させたりするときは sudo しないとだめ

docker-compose で LDAP 認証のリバースプロキシを設定する

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

色々が色々あって、社内限定ツールに Active Directory を参照した LDAP 認証を掛けることがある。
それ自体は別にいいのだが Docker を使ってペペッと作って展開するといったときがやや面倒になった。

ので、それを docker-compose を使ってリバースプロキシを作ったら解決できたよ、というメモ。

 

こんな感じで docker-compose.yml を作って。

version: "2"

services:
  application:
    container_name: "${APP_NAME}_application"
    build: ./docker-app
    volumes:
      - ./src:/var/www/html
  proxy:
    container_name: "${APP_NAME}_proxy"
    build: ./docker-proxy
    ports:
      - "${HTTP_PORT}:80"
    links:
      - application:application

docker-compose をやったことがある人はご存知と思うが、このときの ${APP_NAME} や ${HTTP_PORT} は環境変数を指していて .env ファイルなどを作り APP_NAME=hogehoge みたいなことをしたらいい。

余談なのだけど、ドキュメントみたら version 3 のフォーマットがいつの間にかリリースされていて(しらなかった!!)、そっちに移行していく流れのほうがいいよねって思った。
Compose file version 3 reference | Docker Documentation

 

こんな感じで ./docker-proxy/Dockerfile を作って。

FROM httpd:alpine
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf

 

こんな感じで ./docker-proxy/httpd.conf を作って。

# default configures
ServerRoot "/usr/local/apache2"
Listen 80
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so

<IfModule unixd_module>
    User daemon
    Group daemon
</IfModule>

ServerAdmin you@example.com
ServerName localhost:80

<Directory />
    AllowOverride none
    Require all denied
</Directory>

DocumentRoot "/usr/local/apache2/htdocs"

<Files ".ht*">
    Require all denied
</Files>

ErrorLog /proc/self/fd/2
LogLevel warn

<IfModule log_config_module>
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
    LogFormat "%h %l %u %t \"%r\" %>s %b" common
    <IfModule logio_module>
      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
    </IfModule>
    CustomLog /proc/self/fd/1 common
</IfModule>

<IfModule mime_module>
    TypesConfig conf/mime.types
    AddType application/x-compress .Z
    AddType application/x-gzip .gz .tgz
</IfModule>

<IfModule ssl_module>
    SSLRandomSeed startup builtin
    SSLRandomSeed connect builtin
</IfModule>



# Load modules
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule auth_basic_module modules/mod_auth_basic.so

LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
LoadModule ldap_module modules/mod_ldap.so

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so


# LDAP
LDAPTrustedMode SSL
LDAPVerifyServerCert off


# Proxy
<VirtualHost *:80>
    ProxyRequests Off
    ProxyPreserveHost Off
    AllowEncodedSlashes On
    KeepAlive Off

    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>

    <Location />
        AuthName "Basic Auth"
        AuthType Basic
        AuthBasicProvider ldap
        AuthLDAPURL [ここにLDAPを参照するのURI]
        Require valid-user
    </Location>

    RequestHeader unset Authorization

    ProxyPass / http://application/
    ProxyPassReverse / http://application/
</VirtualHost>

 

あとは docker-compose up -d --build などと実行したら出来上がる。

$ docker-compose up -d --build

 

Apache をつかっている理由としては nginx の場合は公式イメージの中で LDAP のモジュールが提供されていないこと、組み込む場合にビルドが必要なことがあげられる。 Apache のイメージであればデフォルトで LDAP のモジュールが提供されている。さらに言えば
alpine イメージでも提供されているので debian ベースなイメージなどと比べて、軽量で高速上げ下げ出来たりしてめっちゃ良いよねってところもあった。調べると debian ベースにビルドしてるものもあって、良いんだけどう~~ん。

まあ「docker nginx ldap proxy」とか普通のワードで調べたらイメージだったり Dockerfile だったりが出てくるので、そのあたりを使ってもよかったのかもしれない。

debian ベースで nginx 1.11.13 をビルドする。 PR したらマージされる感あるのでバージョンアップもやっていけそう。
h3nrik/nginx-ldap - Docker Hub

debian ベースで nginx 1.9.9 をビルドする。2 年前から更新されていないので今後も更新されなさそう感
confirm/docker-nginx-ldap: nginx Docker image with ldap support

nginx 公式なのだけど nginx で LDAP ではなく、横にいる LDAP してくれるやつがよろしくやる、というものでちょっと違うかも。
nginxinc/nginx-ldap-auth: Example of LDAP authentication using ngx_http_auth_request_module

あるいは nginx-proxy っていうめっちゃ便利なリバースプロキシできる君があるので、これをベースに LDAP 認証のモジュールを追加するっていうのが、お手軽楽ちんで一番いいかもしれない。
jwilder/nginx-proxy: Automated nginx proxy for Docker containers using docker-gen

 

とりあえず一旦のところは Apache でやったよっていう話と、 nginx でも調べたら出てくるよ、ってまとめでおわり。

phpcs の自作ルールを作ってみた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

phpcs を使って、他のルールセットにはない、独自のルールを設定する方法を調べた。

存在するルールセットを組み合わせる

そもそも、存在しているルールセットを組み合わせたり、無効にしたりするには、 composer を使ってパッケージを入れるなどして xml ファイルに記載をしていけば OK 。
これは Github の Wiki にもやり方が記載されている。

Coding Standard Tutorial · squizlabs/PHP_CodeSniffer Wiki

xml ファイルの例

<?xml version="1.0"?>
<ruleset name="oreore_ruleset">
    <description>名前空間は無くてもいいよ</description>
    <rule ref="PSR2">
        <exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" />
    </rule>
</ruleset>

コマンドの例

$ phpcs -n --colors --standard="oreore_ruleset.xml" src/

調べたのはこれではじゃないよ

自分で 1 からルールを定義する

こちらも Github の Wiki 、同じページ(Coding Standard Tutorial)にも書いてある。 Creating the Sniff の項目。

が、ちょっとトラップがあって、 Creating the Sniff の項目だけ読んで進めると、足りないものがいろいろあって NG 。1からルールを定義するときにも xml ファイルは必要だった。

それを読みながら試しに作ってみたのがこれ。

sters/phpcs-myruleset-test: Create phpcs sniffer test repo.

実際に触ってみて、いくつかの制約があるっぽい(というか書いてある)

  • ルールを格納するサブディレクトリを作らないといけない
  • ファイル名の末尾が Sniff.php でないといけない
  • PHP_CodeSniffer\Sniffs\Sniff インタフェースを利用する
  • register と process メソッドを実装する

個別にもう少し詳しく深掘りする。

ルールを格納するサブディレクトリを作らないといけない

こんな感じで掘ると良さそう。
ルールセット名の下に Sniffs という名前にすると、自分で設定しているルールセットだよ~的な意味になるっぽい。

MyRule/Sniffs/HogeSniff.php

phpcs の実装としてはこのあたりにそう書いてあるように見える。
PHP_CodeSniffer/Ruleset.php at master · squizlabs/PHP_CodeSniffer

で、作ったディレクトリと同じ階層に xml ファイルを置く。中身は説明書き程度で、もし他のルールを継承したものを作っていく場合にはここに書き足していくと良いっぽい。

MyRule/rule.xml

<?xml version="1.0"?>
<ruleset name="MyRule">
	<description>MyRule Coding Standards</description>
</ruleset>

アプリケーションと同じところに Sniffer は入れないと思うので(関心の分離的にも別で管理しよう!)、別管理する気持ちで作っていくと良いと思う。

ファイル名の末尾が Sniff.php でないといけない

phpcs が Sniff.php でファイルを探している。実装はこのあたり。

PHP_CodeSniffer/Ruleset.php at master · squizlabs/PHP_CodeSniffer

なので、ルールを書いていくファイルは HogeSniff.php のようなファイル名でないといけない。

MyRule/Sniffs/HogeSniff.php

PHP_CodeSniffer\Sniffs\Sniff インタフェースを利用する

このあたりからは、ぼくの試した実装とにらめっこしてみたほうがいいかもしれない。

これは phpcs の Wiki にも書いてあるものと同じ、ハッシュコメントを警告するための Sniff 。

phpcs-myruleset-test/DisallowHashCommentsSniff.php at master · sters/phpcs-myruleset-test

インタフェースの利用については特にいうこともないので、はい。

register と process メソッドを実装する

引き続き、ハッシュコメントを警告するための Sniff を見ながら。

phpcs では対象となった php ファイル(あるいは他のファイルも指定できる)に対して、トークナイザを実行し、指定されたトークンに対してのみ process メソッドが動くように作られている。
その指定されたトークンを register メソッドで指定する。こういう感じで。

public function register()
{
    return [
        T_COMMENT
    ];
}

トークンの一覧は php.net の方にある。

PHP: パーサトークンの一覧 - Manual

php.net のリストを見ると T_COMMENT は 「 // or #, and /* */ 」にあたると書いてある。
これでコメントのトークンに限定して process メソッドを実行できる。

process メソッドはこんな感じで書いている。

public function process(File $phpcsFile, $stackPtr)
{
    $tokens = $phpcsFile->getTokens();

    if ($tokens[$stackPtr]['content']{0} === '#') {
        $error = 'Hash comments are prohibited; found %s';
        $data  = [trim($tokens[$stackPtr]['content'])];
        $phpcsFile->addError($error, $stackPtr, 'Found', $data);
    }
}

$phpcsFile は PHP_CodeSniffer\Files\File クラスのインスタンスで、今はどのファイルを対象にしているか、そのファイルのトークンは、といった情報が入っている。

$stackPtr はトークンスタックのうち現在の位置を示す int 。 register で指定したトークンが見つかった位置になる。当然のことながら 1 ファイルで複数見つかることもあるので、そのときは毎回 process メソッドが呼ばれるようだ。

$phpcsFile->getTokens() でトークンの一覧を取ることができ、これはトークンの二次元配列になっている。なので $stackPtr で参照することで、現在の位置のトークンが分かる。

トークンは以下のような形式で表現されている。

array(8) {
  ["type"]=>
  string(9) "T_COMMENT"
  ["code"]=>
  int(377)
  ["content"]=>
  string(8) "# echo
"
  ["line"]=>
  int(3)
  ["column"]=>
  int(1)
  ["length"]=>
  int(6)
  ["level"]=>
  int(0)
  ["conditions"]=>
  array(0) {
  }
}

そのトークンが、コード中の何行目にあって~、どのネストレベルで~、とかとか。このうち content に着目すると、そのトークンが持っている中身が見られる。

コメントの場合はコメント自体がまるっとトークンになるので、そのうちの先頭 1 文字をみて # だったらエラーを出すことで、ハッシュコメントを警告する Sniff の出来上がり。

自作のルールも phpcbf で自動でなおるようにしたい

お試したリポジトリには入れていなかったのだが addFixableError と fixer->replaceToken を使うことで出来るっぽい。

PHP_CodeSniffer/ClassDeclarationSniff.php at master · squizlabs/PHP_CodeSniffer

$fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeKeyword', $data);
if ($fix === true) {
    $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
}

複雑なルールを作っていきたい

もしかしたら場合によって、もっと複雑なルールを設定していきたいような場合もあるのでは…?
そんなときどうするんだろうなあと思ったけれど、結局のところは気合で処理を書いていかないといけない(そりゃそうだ)

それは phpcs に含まれる PSR のルールを見てもそうだし Packagist にあるルールセットを見てもみんながんばってるのが分かる。

例えば PSR-2 のクラス定義に関する Sniff 。
PHP_CodeSniffer/ClassDeclarationSniff.php at master · squizlabs/PHP_CodeSniffer

「 1 つ前のトークンが空白で、改行で、その前が abstract か final だったらエラー!」とかそういう。

 

例えば… とおもって Packagist みていたら phpcs でセキュリティチェックしてくれる君があった。
FloeDesignTechnologies/phpcs-security-audit

その中に簡易的に XSS のチェックをしてくれるのがあるが、やっぱり頑張る
phpcs-security-audit/EasyXSSSniff.php at master · FloeDesignTechnologies/phpcs-security-audit

とはいえ、全部書くのもあれなので、 phpcs 側で用意されているユーティリティメソッドだったり、自分で便利に使うものを用意すると Sniff 本体が簡潔に書けて良さそうだ。

 

例えば処理の中や後ろにコメントを書くようなあれを警告する Sniff を作ってみたのがこんな感じ。

phpcs-myruleset-test/DisallowInlineCommentsSniff.php at master · sters/phpcs-myruleset-test

public function process(File $phpcsFile, $stackPtr)
{
    $this->tokens = $phpcsFile->getTokens();
    
    $tokenPrev = $this->findSomethingFirstOnLine($phpcsFile, $stackPtr);
    $tokenNext = $this->findSomethingLastOnLine($phpcsFile, $stackPtr);

    if ($tokenPrev !== false || $tokenNext !== false) {
        $message = "Disallow Inline comments.";
        $errorCode = 'inline_comments';
        $placeholder = [];
        $phpcsFile->addError($message, $stackPtr, $errorCode, $placeholder);
    }
}

findSomething.. は同じ行の前後に、自分以外の別トークンがいるかを探すメソッド。
findFirstOnLine というのが File クラスのメソッドにはあるが、微妙にそういう意図じゃないんだよ!というところだったので、参考にしつつ作ったのがコレ。行の情報があるので、ぐるぐるして、同じ行で別トークンがあるかな?を探しているだけで、特別難しいことは何もしていないとは思う。

おわり

特定のキーワード、特定の処理方法を警告するようなものは簡単に作れそうなので、余力があれば、チーム独自ルールみたいなものを phpcs に掛けられるようにすると、レビューコストが下がったり(?)、変な記述が生まれにくくて、いいなあと思ったとさ。

命名規則のようなちょっとむずかしそうなルールも、頑張ったら実装していくことは出来るので、そういうのを統一していきたいんだよね、みたいな事象のときに活用できそうだ。

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる MC-value について理解した(つもり)

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

FLR の件と同じ論文で出ていた MC-value について。「単名詞バイグラムによらない用語スコア付け」として挙げられている

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる FLR について読んだ | ごみばこいん Blog

元の論文

FLR と同様に「出現頻度と連接頻度に基づく専門用語抽出」という論文で語られているのでスキップ。
そして MC-value を語る前に、その元になる C-value を語らないと行けない。

C-value のものすごいざっくりした理解

  • コンテンツにおける専門用語って名詞が連続すること多いよね
  • 連続する名詞って入れ子になることあるよね
  • 形態素解析の結果って名詞かどうかわかるよね
  • → できるじゃん!!

C-value の仕組み

例として以下の連続する名詞が上がっているとして…。

トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム

  1. CN = 複合名詞
    1. 例) トライグラム 統計
  2. length(CN) = CNの長さ(構成する単名詞の数)
    1. 例) length(トライグラム 統計) = 2
  3. n(CN) = コーパスにおけるCNの出現回数
    1. 例) n(トライグラム 統計) = 2
  4. t(CN) = CN を含むより長い複合名詞の出現回数
    1. 例) t(トライグラム 統計) = 1
  5. c(CN) = CN を含むより長い複合名詞の種類数
    1. 例) n(トライグラム 統計) = 1
  6. C-value(CN) = (length(CN) - 1) * (n(CN) - (t(CN) / c(CN)))
    1. 例) C-value(トライグラム 統計) = (2 - 1) * (2 - (1 / 1)) = 1

このとき length(CN) = 1 、つまり単名詞のときに数値が 0 になってしまうという問題がある。専門用語は単名詞になることもあるだろう。

MC-value のものすごいざっくりした理解

  • C-value で連続する名詞のスコア計算が出来たけど単名詞…
  • -1 してたものをなくせばいいじゃん!

MC-value の仕組み

例として以下の連続する名詞が上がっているとして…。

トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム

  1. CN = 名詞
    1. 例) トライグラム
  2. length(CN) = CNの長さ(構成する単名詞の数)
    1. 例) length(トライグラム) = 1
  3. n(CN) = コーパスにおけるCNの出現回数
    1. 例) n(トライグラム) = 10
  4. t(CN) = CN を含むより長い複合名詞の出現回数
    1. 例) t(トライグラム) = 7
  5. c(CN) = CN を含むより長い複合名詞の種類数
    1. 例) n(トライグラム) = 5
  6. MC-value(CN) = length(CN) * (n(CN) - (t(CN) / c(CN)))
    1. 例) MC-value(トライグラム) = 1 * (10 - (7 / 5)) = 8.6

論文上では 5.6 と書かれているけどこの数字がどうやって出てきたかわからんかった…。
n(CN) の計算がもしかしたら違うかも。

例1(図2)の場合,MC-value(トライグラム)=(7−7/5) = 5.6である

まとめと感想

そもそも C-value が TF-IDF や FLR と異なり、全ての文字列に対して下準備をする必要がないのと、数えることがメインなので計算量すくなく、データ量をどんどん増やすようなことをしてもお手軽に使えそうな気がする。

論文中でもいい感じに取れるぜ!的なことが書いてあるので、もうちょっと実践的に?入れて様子を見ようかなあと思いましたとさ。

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる FLR について読んだ

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

以前 TF-IDF について調べた。

TF-IDF ってのを使うと単語の重要度がわかるよって聞いたので調べた | ごみばこいん Blog

まったく違うアプローチをしている重要語を抽出するようなものがないかなーと調べてたら FLR というものに行き着いた。

元の論文

「出現頻度と連接頻度に基づく専門用語抽出」という論文で語られている。

機関リポジトリ内のページ
UTokyo Repository - 東京大学学術機関リポジトリ

関連サイト
専門用語(キーワード)自動抽出システム”のページ

ここで実際どうなるのよ、が試せるっぽいのと、ライブラリが配布されているので組み込むのもまあまあ容易に行けそう。

FLR のものすごいざっくりした理解

  • コンテンツにおける専門用語って名詞が連続すること多いよね
  • 形態素解析の結果って名詞かどうかわかるよね
  • → できるじゃん!!

FLR の仕組み

  1. 単名詞バイグラム、LNnとRNnのリストを作る
    1. [LNn N](#Ln)
    2. [N RNn](#Rn)
    3. 例)トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム
      1. LNn
        1. [単語 トライグラム](3)
        2. [クラス トライグラム](1)
        3. [文字 トライグラム](1)
      2. RNn
        1. [トライグラム 統計](2)
        2. [トライグラム 抽出](1)
  2. 単名詞スコア1 として、連接種類数 LDN(N)とRDN(N) を作る
    1. 種類の数なので、単語の出現頻度にはさほど影響されない
    2. 例)
      1. LDN(トライグラム) = 3
      2. RDN(トライグラム) = 2
  3. 単名詞スコア2 として、連接頻度 LN(N)とRN(N)を作る
    1. 単名詞バイグラムを特徴付けるものとして、全ての出現回数の合計を取る
    2. 例)
      1. LN(トライグラム) = 3 + 1 + 1 = 5
      2. RN(トライグラム) = 2 + 1 = 3
  4. 複合名詞スコア LR(CN) を作る
    1. 専門用語は複合名詞が多いが、その複合名詞の長さによって重要度はかわらない
    2. 単名詞Nに対し、左側スコアFL(N)、右側スコアFR(N)
      1. 単名詞スコア1 か 2 を利用する
      2. 複合名詞 CN = N1, N2, ... NL
      3. 複合名詞に含まれる各単名詞について FL, FR を計算し、相乗平均を取り、CNの長さLの逆数のべき乗を取ったものが LR(CN)
      4. LR(CN) = pow( GeometricMean( (FL(Ni) + 1) * (FR(Ni) + i) ) , 1/2L)
      5. 例)連接頻度をスコアとすると
      6. LR(トライグラム) = pow( (5+1) * (3+1), 1/2 ) = sqrt(6 * 4) ~= 4.90
  5. 出現回数を複合名詞スコアに加える FLR(CN)
    1. f(CN) = CNが単独で出現した頻度、他の複合名詞に含まれていないこと
    2. FLR(CN) = f(CN) * LR(CN)
    3. 例)連接頻度をスコアとすると
      1. FLR(トライグラム) = 3 * 4.90 = 14.70

まとめと感想

名詞に対して、よくでる連続した名詞がスコア高くなるような仕組みでスコアを付けて、それをピックアップできるものだった。
何かに特化した専門的なものを説明するようなコンテンツだとその節があると感じる。(例えばこの記事だと "名詞" は上位にピックアップされるんじゃないかな)

というか、最初に重要語が~みたいな話を書いたけど、「重要とは何か?」という考え方、指標、スコアを設ければそれで重要ってキメになるんだろうな。

  • 出現回数「よく出る単語は重要だ!」
  • TF-IDF「このドキュメントだけよく出るから重要だ!」
  • FLR「この名詞はよくくっついて出るから重要だ!」

ちなみにこの論文では MC-value というものも提案されており、それはまた書く。

PHP で本文抽出したいよね、という

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

前回の記事: ExtractContent を PHP で書き換えた | ごみばこいん Blog

「今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい」といいつつ、他のネタが挟まり、ようやくにして比較しました。

比較した結論

お試しにはこちらのリポジトリをどうぞ。
sters/compare-article-extractors: Compare web article extractors.

日本語環境下で難しい話

php-goose 、または php-web-article-extractor では、どちらも内部にストップワード辞書を持っていて、プログラム上で言語を指定、あるいは lang 属性や meta タグから読み取った言語名から、適切なストップワード辞書を選択しています。そうして決まったストップワードの一覧が、文字列にどれくらいあるかを確認して、本文かどうか?といった判断を進めているようです。

ストップワードとはよく使われがちな単語のことで、自然言語処理の前処理として行なうことでデータ量を減らしたり、精度を上げたり出来ます。

自然言語処理における前処理の種類とその威力 - Qiita

例えば英語では「This is a pen.」といったようにスペースで区切られているので「ストップワードの is と a の 2 文字がある!こいつはコンテンツだ!」と簡単に出来るのですが、日本語の場合はそう簡単な話ではありません。日本語はスペース区切りではありませんし、前後にある文字によって、まったく異なる意味合いになったりもしますので、一概に辞書でドバーッと指定することは適切な処理でない可能性が高いです。

じゃあどうするかというと分かち書きの出番なのですが、分かち書きも簡単なものではありません。 Chasen や Mecab といった既存の技術を利用すればお手軽にもできますが php-goose や php-web-article-extractor がそれを利用して日本語対応するかというとちょっと違いそうだなあ、という気持ちです。

php-goose に出したプルリクは諦めてドバーッと処理する形ではありますが、日本語だけ特殊化せざるを得ないので、そうやってみて出してみたものの、なんだかなあと。いやそもそも 元になってる Goose はそういう実装になっていないだろうし…。

そのほかのアプローチ方法

DOM構造や、画面上の位置情報も利用するのはめっちゃ有効的だと思います。
PuppeteerでWebページからメインコンテンツっぽいところを抽出してみる - Qiita

上記の記事の「まとめ」でも出てはいますが、機械学習して取るようにするのもよさそうです。
seomoz/dragnet: Just the facts -- web page content extraction

WordPress と Twitter を連携するテスト

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

テストだよーん。

====

追記してみるよ。

今まで Simple Tweet を利用していたのですが、Twitter アプリの見直しをして、うっかり全部消してしまい、再度アプリを作成しても使えなくなっていました。
PIN コードの入力…、PIN どこ… と、いうような状況です。

Simple Tweet — WordPress Plugins

調べたところ WP to Twitter がよさそうだったので、これを入れました。

WP to Twitter — WordPress プラグイン

設定画面が日本語化されていますし、流れで設定していけるので簡単に利用できます。

1 2 3 4 5 6 7 8 9 10 11 12 13