PhantomJS でWebサイトの画面キャプチャをとる

PHP で単にスクレイピングをするだけなら、file_get_contents して解析したり、 curl を使ったり、Simple HTML DOM Parser(http://simplehtmldom.sourceforge.net/) を使ったり、 DOMDocument を使ったり、様々が方法があります。

しかしサイトの画像キャプチャを取得するとなると、PHP のみでは一筋縄ではいきません。
今回は PhantomJS を利用する際のメモになります。

PhantomJS とは:

PhantomJS is a headless WebKit scriptable with a JavaScript API. It has fast and native support for various web standards: DOM handling, CSS selector, JSON, Canvas, and SVG.
http://phantomjs.org/

ということで、ひらたくいえば JavaScript で動かすヘッドレスブラウザということになります。


まずはこちら http://phantomjs.org/download.html から必要なものをダウンロードします。 その時点での最新版(Windows であれば phantomjs-2.1.1-windows.zip)をダウンロードします。

圧縮ファイルを解凍すると、bin/phantomjs.exe という実行ファイルがあり、これが本体になります。 これを任意のフォルダに配置します。

使い方は簡単です。JavaScript コードを書いたファイルを引数に指定して、起動します。

phantomjs.exe hello.js

公式の Hello World はこうです。

hello.js

console.log('Hello, world!');
phantom.exit();

phantom というオブジェクトがいて、exit() を呼ぶことで処理を終了するようです。 これを呼ばないと終了しないので注意しましょう。
APIのドキュメントも準備されていますので、必要に応じて参照します。 API | PhantomJS


本題です。
画面キャプチャを取ってみます。コードはずばり以下の通りです。

var page = require('webpage').create();

page.open('https://news.yahoo.co.jp/', function(status) {
  page.render('cap.png');
  phantom.exit();
});

open メソッドに指定したURLにアクセスし、ページがロードされると指定したコールバックが呼び出されます。
ここではチェックしていませんが status には 'success' か 'fail' が入ってくるので、それを見てエラー処理を行うことができます。

さらに render メソッドで出力先の画像ファイルパスを指定すると、そのファイルに画面キャプチャが出力されます。

キャプチャする範囲を指定する場合は clipRect を使用します。

page.clipRect = { top:0, left:0, width:760, height:400 };
page.render('cap.png');

また、デフォルトのユーザエージェント文字列だと意図しないレンダリングになる可能性がありますが、 その場合は任意のユーザエージェント文字列を指定することができます。

// 当たり前ですが open の前に設定します
page.settings.userAgent = ...任意のユーザエージェント文字列...;
page.open('https://news.yahoo.co.jp/', function(status) {



PHP から使う場合には PhantomJS のライブラリもありますが、 単に exec で phantomjs の実行ファイルを起動することでも利用できます。

exec("phantomjs.exe {$filePath}", $output, $resultCode);




C# で SQLite を使う (Windows)

前回はPHPSQLite を扱う方法をご紹介しました。今回は、 C#SQLite を扱う方法をご紹介したいと思います。
Visual Studio 2017 を使用します。

Visual Studio での依存ライブラリの管理は NuGet が便利です。NuGet を使って、SQLite に必要なパッケージをインストールします。

f:id:venturenet:20180511134756p:plain

パッケージマネージャが開いたら 参照 を選択して "SQLite" と打ち込んで検索し、検索結果から System.Data.SQLite.Core を選択してインストールします。

f:id:venturenet:20180511134842p:plain

準備はこれだけです。

コードも簡単です。まずはデータベースファイルを開きます。
ファイル名を指定すればOKです。

using (var conn = new SQLiteConnection("Data Source=C:\\temp\\db.sqlite"))
{
    conn.Open();


SQL文は、コマンドを作成して設定します。

using (SQLiteCommand command = conn.CreateCommand())
{
    command.CommandText = "SELECT * FROM some_table WHERE id=:id";
    // プレースホルダのバインドはこのように
    command.Parameters.Add("id", System.Data.DbType.String).Value = "hoge";


結果の1行を、キーをカラム名、値はすべて文字列の Dictionary で取得する場合は、以下のようにできます。

using (var reader = command.ExecuteReader())
{
    var schemaTable = reader.GetSchemaTable();
    
    while (reader.Read())
    {
        var row = new Dictionary<string, string>();

        foreach (DataRow schemaRow in schemaTable.Rows)
        {
            var columnName = schemaRow[schemaTable.Columns["columnName"]].ToString();
            row.Add(columnName, reader[columnName].ToString());
        }
    }
}



環境や構成によっては、"DLL 'SQLite.Interop.dll' を読み込めません:指定されたモジュールが見つかりません。 (HRESULT からの例外:0x8007007E)" といったエラーが出る場合があります。 この場合は、<ソリューションのルート>\packages\System.Data.SQLite.Core.<バージョン>\build\<.NETバージョン>\<プロセッサアーキテクチャ> フォルダの下にある SQLite.Interop.dll を実行ファイルがある場所にコピーして置くと動作するかもしれません。
.NETバージョンとプロセッサアーキテクチャは適切なものを選択してください。例えば .NET 4.6 の x86 であれば、build\net46\x86 の中にあるものを使います。


このように簡単に SQLite を使用することができますが、ここでご紹介した機能はごくごく一部です。
System.Data.SQLite についてのより深い情報を知りたい場合は こちら から。

PHP で SQLite を使う

CSVなどのデータでも強力な検索機能を使用したいことがあります。
データ量が少ない場合は、Excel を使って VLOOKUP 関数や INDEX 関数、MATCH 関数を駆使することで乗り切れる場合もありますが、 万~数十万オーダのデータになるとなかなか厳しくなってくるかと思います。

そんなときに便利なのが SQLite です。 SQLite はデータベースが1ファイルととても軽量であり、PHP であれば 5.3 以降はデフォルトで使用できます。 軽量であるにも関わらず、SQL も必要そうなものはすべて使用可能です。WITH句だって使用できます。
数十万件くらいのデータならなんなく扱えます。

PHP での基本的な使い方は以下の通りです。

// $filename にはデータベースファイル名を指定する
$sqlite = new SQLite3($filename);

// $queryにはSQL文を指定する
// プリペアに失敗した場合は false が返る
$pstmt = $sqlite->prepare('SELECT id, name FROM some_table WHERE id=:id');

// パラメタに値をセットする
$pstmt->bindValue('id', 4);

// 実行!
$result = $pstmt->execute();

while ($record = $result->fetchArray(SQLITE3_ASSOC)) {
    // $record['name'] のようにカラム名を指定してデータを取る
}


もちろん、上のコード以外にもたくさんの機能、オプションがあります。公式のドキュメントを参照しましょう。



さて、ゴールデンウィークも後半、いかがおすごしでしょうか。
今日はみどりの日、こういった仕事をしていますとなかなか自然にふれる機会が少ないと思います。
たまには大自然にふれ、リフレッシュしてみてはいかがでしょうか。

FUTOKAサーバでWordpressログイン不可

最近 Wordpress の話題ばかり投稿していますが、その流れでもう1つ。

レンタルサーバはいろいろありますが、あるとき FUTOKAサーバさんWordpress サイトを構築する機会がありました。
Wordpress は利用者が多いのか、各種レンタルサーバでは Wordpress の自動インストールをサポートしているところもあります。 FUTOKAサーバさんにも Wordpress の自動インストール機能はあるのですが、このときは他のサーバからの移転であったためそれは使わず、FTPでファイル一式をアップロード、データベースを作成して wp-config.php を書き換える という手順を実施しました。

特に何の問題もなく、サイトは表示されました。

ところがある日、「サイトにログインできない」という連絡がありました。
試してみたところ、ログイン画面にアクセスできず、403エラーになってしまっていました。

詳しく調べてみると、サイト全体にアクセスできなくなったわけではなく、wp-login.php のみアクセスできないような状態でした。
どういうことなのか... と情報を集めていると、「Wordpressの自動インストールではデフォルトで国外IPアクセス拒否になっている」という記事を発見。
症状をみるにこれが原因のように思えたものの自動インストールはしていないので、これではないだろうと早合点してしまいました。

しかし原因はこれ(と思われる)でした。
すべてのケースでそうなのかはわかりませんが、どうやら、自動インストールでなくても国外IPアクセス拒否が設定されることがあるようです。

公式サイトでも案内されている、.htaccess の修正で403エラーは解消しました。

SetEnvIf Request_URI ".*" AllowCountry


他のレンタルサーバでも同じようなことがあるかもしれませんので、このような Wordpress サイトでログイン画面だけがアクセスできないという現象に突き当たった場合には、レンタルサーバに導入されているIPアクセス制限などのセキュリティ関連の設定を調べてみるとよいかもしれません。

Wordpress の API で BASIC 認証を使う

WordpressAPI を使用するにあたり、いくつかの機能では認証が必要になります。
認証の方法にはいくつかありますが、高いセキュリティが求められていない場合は簡単な方法にしたいところです。
サポートされている認証の1つに BASIC認証(Basic Authentication) がありますので、これを使用する際のポイントをまとめておきたいと思います。

プラグインの導入が必要

BASIC認証を使用するためには、プラグインの導入が必要です。といっても大仰なプラグインを導入する必要はなく、 https://github.com/WP-API/Basic-Auth から basic-auth.php をダウンロードして、wp-content/plugins ディレクトリに配置するだけです。
配置したあと、有効化するのを忘れないようにしましょう。

.htaccess の修正が必要な場合がある

ここがハマるポイントだと思います。
上記のプラグインでは、$SERVER['PHP_AUTH_USER'] と $SERVER['PHP_AUTH_PW'] を参照して、BASIC認証のユーザとパスワードを取得しています。
しかし Apache を採用しているレンタルサーバ等では、デフォルトではこれらの値を取得することができない場合があります。
その場合は .htaccess に以下のような設定を追加する必要があります。

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

ポイントは、Wordpress が自動的に生成するリライトルールの前に書くという点です。
これを Wordpress が自動的に生成するリライトルールの後に書いた場合、正常に動作しません。


これで、BASIC認証による API の認証ができるはずです。
どうしても 403 Forbidden になってしまう場合は、以下を確認するとよいでしょう。

  • クライアント側からちゃんと BASIC 認証のヘッダが送信されているか
  • Wordpress 側で PHP_AUTH_USER / PHP_AUTH_PW が取得できているか

WAF や SSL などの目的のために設置されたリバースプロキシによって、うまく情報が渡っていないケースも考えられます。

動作検証などで「何でもいいからとにかく動かしたいんだ!」という場合には、basic-auth.php をいじってしまうという手もあります。
結局は、渡ってきたユーザ/パスワードで Wordpress の内部的な認証をかけているだけなので、basic-auth.php の以下の部分で $username と $password に任意のユーザとパスワードを与えれば、認証は通るようになります。

$user = wp_authenticate( $username, $password );



Wordpress の API を利用する

Webサイトの構築に Wordpress を利用することが多くなっています。
最近のバージョンの Wordpress では標準で REST API が使用できるため、これを自動化やシステム連携に活かさない手はありません。

API には、認証が必要なものと不要なものがあります。例えば投稿の一覧を取得する場合、/wp-json/wp/v2/posts にアクセスするだけで取得することができます。 以下は、公式のデモサイト http://demo.wp-api.org/wp-json/wp/v2/posts のレスポンスです。

f:id:venturenet:20180413182213p:plain

API のレスポンスはすべて JSON 形式です。

公開されている記事や固定ページ、カテゴリの取得など、公開されている系の情報を取得するAPIは認証なしで利用することができます。
記事を投稿するなど、通常 Wordpress にログインしていなければできない操作系のAPIでは、認証が必要になります。
認証には、こちら にあるようにいくつかの方式がありますので、いずれかを選択する必要があります。

なお、バージョンによりエンドポイントやインタフェースが異なることがあるため、公式のドキュメントをチェックするようにしたいところです。
https://ja.wp-api.org/ ← 日本語
https://developer.wordpress.org/rest-api/ ← 英語ですが、情報がフレッシュ

Salesforce データローダのメモ

このブログでは、技術的な事柄を備忘録として残していきます。
記念すべき1つ目は、Salesforce のデータローダに関するメモです。最近業務でデータインポートする機会がありましたので、そのときに発生したエラーや気付きを残します。
※データローダのバージョンは42

インポートデータが UTF-8 の場合は、BOMを外す

インポートデータが UTF-8CSV の場合、BOM が付いていると読み込めません。
なお ExcelUTF-8CSV を開くときには、逆に BOM が付いていないと読み込めません。(少なくとも Excel 2010 では)

インポートデータが CSV の場合は、ダブルクォーテーションでデータを囲む

インポートデータが UTF-8CSV の場合、内容によるのか必ずそうなのかは不明ですが、ダブルクォーテーションでデータが囲まれていないと、読み込みでエラーが発生します。 追記:やはり必ずしもエラーになるわけではありませんでした。ダブルクォーテーションで囲む、囲まないは統一した方が良さそうです。

作成日と最終更新日をセットするには特別な権限が必要

データの移行では、レコードの作成日と最終更新日を設定したいこともありますが、デフォルトではこれらの項目に値を設定することはできません。
システム権限「レコードの作成時に監査項目を設定」が必要になります。
参考:https://help.salesforce.com/articleView?id=000213290&language=ja&type=1

作成日が最終更新日より後だとエラー

当たり前といえば当たり前ですが、作成日が最終更新日より後だと、FIELD_INTEGRITY_EXCEPTION:Last Modified Date ~ というエラーが発生します。 移行元が古いシステムだったり、いいかげんなデータの持ち方をしているシステムだったりすると、必ずしも日時データの整合性の取れた状態になっていないこともありますので、注意が必要です。

DELETE なのに「値を入力してください」エラー

インポートに限った話ではありませんが、DELETE しているのにも関わらず「値を入力してください」というエラーが出ることがあります。
INSERT/UPDATE しているわけでもないのに、と 一瞬混乱してしまいますが、このようなときは、親オブジェクトの必須項目が空になっていないか確認するとよいかもしれません。

  1. 親オブジェクトINSERT ※このときは項目Aは必須でなく空を設定
  2. 子オブジェクトINSERT
  3. 親オブジェクトの項目Aが必須化
  4. 子オブジェクトDELETE → そのレコードの親の項目Aが空だとエラー


マッピングファイルは作っておいた方が便利

インポート対象が多い場合、リレーションが複雑な場合は、マッピングファイル(*sdl)を作っておいた方が便利です。

Invalid session id エラー

データローダの画面を開いたまましばらく放置して、そのあとにインポートしようとすると、Invalid session id というエラーダイアログが表示される場合があります。
タイムアウトしているだけなので、一度ログアウトしてから再度インポートすればOKです。

UNABLE_TO_LOCK_ROW エラー

インポート対象が多く、思わず複数台で並列にインポートを実施したところ、 UNABLE_TO_LOCK_ROW というエラーが頻発しました。 関連のあるオブジェクトでは並列のインポートは行なわない方が無難かもしれません。