IT・技術研修ならCTC教育サービス

サイト内検索 企業情報 サイトマップ

研修コース検索

コラム

スーパーエンジニアの独り言

CTC 教育サービス

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes 

第60回 オペラ座の怪人 2016年9月

今宵は「オペラ座の怪人」("The Phantom of the Opera", 原題: "Le Fantôme de l'Opéra") です。

新聞記者であり小説家でもあったガストン・ルルー(Gaston Leroux) の手に拠るこの物語は、実在の「オペラ座」を舞台に過去の陰惨な事件をモチーフとして、史実である事件に纏わる幽霊が介在する噂話である虚構をミクチャーさせたミステリアスでゴシックな物語として創作されました。

「虚構と現実」と言えばですが「マトリックス」"The Matrix" を想起します。「虚構と現実」を意識して「行き来する」のですが、「混濁する」のであれば「インセプション」(Inception) が挙げられるかもしれません。「オペラ座の怪人」同様に「混在する」という意味での物語としては「シン・ゴジラ」"GODZILLA Resurgence" と同種としての分類が可能なのかもしれません。更にそれらを超えて「虚構のドキュメンタリー」という切り口で「モキュメンタリー」"Mockumentary"(「モック」"mock" +「ドキュメンタリー」"documentary" のかばん語です)という手法もありまして「パラノーマルアクティビティ」"Paranormal Activity" といった映画までも挙げることが出来ましょう。

ここで気が付きました。映画を視聴すると目前に映し出される物語に感情移入するあまりに(筆者自身が)忘れがちなのですが、そもそも映画自体が「虚構」そのものです。そして「虚構」の中にこそ「真理」があるのです。ですから稚拙な説明や範疇分けする事は無用な行為でしょうし、何度も映画化やミュージカルとして上演されている本作「ファントム・オブ・ジ・オペラ」であれば既に多くの方がご覧になっていて「実物」(実態)をよくご存知なのかとは思います。

「オペラ座の怪人」は、1909年初版が世に出て以来、数多くの舞台や映像化され登場した様子なのですが原作オリジナルに忠実に創られる以外にも過去にタイムスリップするストーリーや現代風解釈で意訳して創られるなどバリエーションも豊富です。

それら数ある作品の中でもグロテスク専門の役者とする怪奇スター「ロン・チェイニー」(Lon Chaney) が 主役「ファントム」(Phantom) を演じる1925年に公開されたサイレント映画と、「アンドルー・ロイド・ウェバー」(Andrew Lloyd Webber) によるミュージカル(1986年初演)が公表を博したものを映画化した「オペラ座の怪人」"The Phantom of the Opera" 2004年公開版が有名です。

この2004年版「オペラ座の怪人」では歌姫「エミー・ロッサム」(Emmy Rossum) が一躍注目を浴びましたが、「ファントム」(Phantom) も負けず劣らずとても見事で「ジェラルド・バトラー」(Gerard Butler) が主役を射止めて演じています。ジェラルド・バトラーは「ザック・スナイダー」(Zack Snyder) 監督による映画「300」(スリーハンドレッド)(2006年公開)では、スパルタ王「レオニダス」(Leonidas) を演じてギリシャ彫刻を再現したかの様な肉体美を披露しています。映像も斬新なので未見であればオススメさせて頂きます。

今回はグロテスクなロン・チェイニーやイケメンマッチョのジェラルド・バトラーとは別の「ファントム(怪人)」(Phantom) が引き起こす「幽霊(ゴースト)」(Ghost) の物語の顛末をご覧にいれたいと思います。

『序章』(切欠)

今回の不可解な事件を省みると探偵(筆者)は結果として事件解決に繋がるような手助けは一切出来なかったのでありますが、自らの戒めを含めてこの事件の経過を改めて最初から振り返ってみたいと想います。

いつもの様に何の前触れもなく唐突に事件の調査依頼が舞い込みました。
ワトソン(直人)が依頼者から伝え聞いた依頼内容(質問趣旨)は以下の様なものでした。

「もしかして『ファントムジェーエス』"PhantomJS" という『ヘッドレス・ブラウザ』 "headless browser" ご存知ですか?
『セレニウム』"Selenium" で使えるのですが、『スクレイピング』"web scraping" しようとしてみると、どうやらプロキシ経由だと動かないらしくて困っているみたいなのです。」

これは筆者にとって「トーフビーツ」、「森は生きている」の再来、オノマトペの連続で御座います(以前の拙作コラムをお読みください)。このまま依頼を受けて仕舞えば、生きている森が深く茂る樹海の中で困惑のまま彷徨ってしまうことを容易く想像ができました。ワトソン君も筆者が容易に回答できる事件ではないことは十分承知している筈です。「もし何か知っていれば教えてください」程度の事であろうとは阿吽の呼吸で理解しています。ですから今回は依頼を断ろうかという考えが一瞬、頭を過りました。

ですがワトソン君への返答は「調べてみる」と翻って伝えたのです。

その理由はワトソン君が依頼者に威勢よく「こんな事できるよ」と紹介した情報が事件の発端らしい事を知ったのです。どうやらワトソン君の情報を信じた依頼者が試してみると偶然事件に遭遇してしまったという経緯らしいのです。それを言いづらそうに事件の事を語るワトソン君の口調から察した次第です。

親友であり理解者であるワトソン君の名誉のためにもここは一肌脱がなければと「ドン・キホーテ」(Don Quixote)と変身した筆者の勝手な想い込みでこの事件の真相究明に闘志を露わにしたのでした。

『身元照会』(登場人物紹介)

兎に角、まずは聴き込んだ情報を基に登場人物の整理と身辺調査を行うことにしました。最初に各々の容疑者の背景を知って動機となる要因を洗い出し絞り込みを行いたいと考えました。

容疑者一号:「スクレイピング」

「スクレイピング」"web scraping" は情報を収集して抽出する行為を指す用語の一つです。
似たような用語として検索エンジンの提供サービスでは「クローラー」"web crawler" や「スパイダー」"web spidering" という種類のソフトウェアを使って無数のウェブサイトから収集した情報を蓄積してサービス利用しています。これを「クローリング」"crawling" と呼びます。
検索サービスに於ける準備動作としての行為はクローリングによって収集したデータを検索し易いように整理、分類が更に必要となりますが、これを「インデクシング」"web indexing" と呼びます。
これらと同様に「スクレイピング」"web scraping" も任意のサイトからデータを抽出する行為を指します。スクレイピングは取得データから任意のデータを抽出して加工するところまでの工程までを意味します。データを収集するだけを指すクローリングとの相違点になります。

容疑者二号:「ファントムジェーエス」

「ファントムジェーエス」"PhantomJS" はブラウザです。
伝統的なブラウザを再現したものですが、ブラウザを画面無しで実行できるという代物です。なので「ヘッドレス・ブラウザ(首なしブラウザ)」 "headless browser" とも言われておりオープンソースで提供されています。「首なし」と聴くとまるで「幽霊」の様でビクッとします。それは NCSA Mosaicが登場して以来 GUI (Graphical User Interface) が付いているブラウザが当たり前の存在なっているからなのですが、その一方でコマンドライン実行できるようなテキストベースのブラウザも存在していたのを思い出しました。「ヘッドレス・ブラウザ」 "headless browser" もその類で括れると思います。この「ヘッドレス・ブラウザ」はフロントエンドのテストを目的に開発されたものだそうです。
また PhantomJS ブラウザの心臓部分にあたる「レンダリング・エンジン」"rendering engine" は、「ウェブキット」"Webkit" ベースで実装されています。 Webkit は HTMLレンダリング・エンジンです。アップルが中心となって開発が進められオープンソースとして公開されています。「サファリ」"Safari" などのブラウザで採用されています。
加えてこの Webkit には JavaScriptCore (Nitro) という JavaScript エンジンが内包されています。ですから PhantomJS では Safari と同等に JavaScript を理解し実行することが可能となるようです。

容疑者三号:「セレニウム」

「セレニウム」"Selenium" はWebブラウザ自動化ツールです。
Selenium を利用すると人手を煩わせることなくブラウザを操作することができます。主たる用途として UIのテストツールがあります。 Selenium によってブラウザ操作が自動化できることを利用して Webアプリケーションのテスト自動化を図るものとして利用されています。クロスブラウザ、クロスプラットフォームであることが魅力ともなります。プログラミング言語も多数サポートされており(Ruby, Python, Java, C#, JavaScript)、各種言語のAPIから「ラッパー」"wrapper library" を経由して機能を利用することが可能です。今回は Python プログラムからの利用を想定しているのですが、Python バインディングが用意されています。

容疑者四号:「ウェブ・ドライバー」

「セレニウム ウェブ・ドライバー」"Selenium WebDriver" とは、"Selenium RC" と "WebDriver" を統合したものです。
Selenium RC (Remote Control) は操作のためのスクリプト(Java, Python等)を JavaScript に変換して、対象ページに埋め込んで実行するというものです。従来のSeleniumではスクリプトをWebサーバー上に配置する必要があったのですがこの手間が軽減できました。ですが、Selenium RCのこの仕組みのために JavaScript のセキュリティによる制限を受けてしまうという別の問題が発生しました。これを解決(回避)するものとして開発が進められたのが WebDriver であり後に Selenium と統合されて Selenium WebDriver となりました。
Selenium WebDriver ではクライアント(プログラム)は各ドライバー経由でブラウザを操作します。ドライバーはブラウザ毎の実装(ブラウザ拡張機能、ネイティブ実装、等々)であり各々のブラウザに適したSeleniumからの操作が可能な形で提供されています。クライアントとドライバーの通信は REST (Representational State Transfer) で行います。 Selenium が定義している "JSON Wire Protocol" に従ってリクエストを送りブラウザを操作します。
但し、現行では各種ブラウザ毎のドライバーが潤沢に用意されていませんので、対応ドライバーが無い場合には直接ドライバーにリクエストを送るのではなくて中継サーバーを経由して操作する別の方法も用意されています。
ここで登場したのが "Selenium Server" です。Javaで実装されている中継サーバーです。各クライアントからサーバーにリクエストを送ることで中継して機能させるというものですが、今回の事件(ストーリー)に直接関係ないと憶測されますので詳細は追いません。この Selenium Server 以外にも関連ツールとして Selenium IDE と Selenium Builder がありますが、今回は表立って登場してきませんので同じく被疑者のリストからは割愛させて頂きます。

容疑者五号:「ゴースト・ドライバー」

「ゴースト・ドライバー」"Ghost Driver" というのは、PhantomJS を WebDriver 経由で利用するための機能です。 PhantomJS が "JSON Wire Protocol" を話せるように通訳するのが、Ghost Driverの役割です。これによりPhantomJS を Selenium から透過的に利用することが可能になりますが、その反面にプロトコルで規定されているAPI制限が PhantomJS に加わります。PhantomJS で用意されるため Ghost Driver は JavaScript で実装されています。また PhantomJS 1.8以降には Ghost Driver が標準でバンドルされるらしく別途用意することなく利用できます。
尚、曖昧さ回避の明記として「仮面ライダーゴースト」(KAMEN RIDER GHOST) に変身するための「眼魂」(アイコン)を装填するための変身ベルトも「ゴースト・ドライバー」"Ghost Driver" と呼ばれますが、今回の容疑者とは別者です。

『模擬現場』(環境構築)

主だった容疑者は把握出来ましたので、次に事件を再現することで検証を重ねようと思います。そのために模擬現場を用意しました。実際の事件現場に関する詳細情報が得られてないので些少の食い違いがあるでしょうが、まずは問題が再現できるか否かを確認すべく実験環境を用意しました。

Environments:
Platform: Windows 7 sp1
Browser: PhantomJS version 2.1
Language: Python version 3.5.2
Browser Automation: Selenium version 2.53.6

上記を諸元とした模擬現場(実験環境)を用意するため各々を調達してインストールしました。

『初動捜査』(確認事項)

最初に確認したい項目として通称「ヘッドレス・ブラウザ」と呼ばれる PhantomJS が、そもそもプロキシ・サーバーをサポートしているのかを確認します。PhantomJSのマニュアルを参照するとコマンドライン・オプションが用意されているのでそれを試してみます。

    C:\> phantomjs.exe --proxy=proxy.eddie.jp:8080 --proxy-auth=username:password sample.js

プロキシ経由で使用できることを確認しました。コマンドライン引数で最後に指定しているのはPhantomJSを使用するサンプルのJavaScriptスクリプトです。スクリプトでは指定しているURLのページを開いて画面のスクリーンキャプチャを行っていますが見事にできました。アクセス可能であると確認できたのです。プロキシの指定はコマンドライン・オプションで指定しましたが、ユーザ認証も同様にオプションにて指定できました。

『潜入捜査』(模擬コード)

次に問題を再現するための覆面捜査官(模擬コード)を用意します。
事件発生時もコードは Python を使用しているのは判明しています。そのため模擬コードである Selenium からでドライバーを叩くのは Python で実装します。ますはドライバーを生成する方法を確認します。

サンプルコード(ドライバー使用手順):

# how2use_driver.py
    # ライブラリの読み込み
    from selenium import webdriver
    # ドライバー・インスタンスの生成
    driver = webdriver.Firefox()    # ブラウザが起動
    # 引数で指定したURLをブラウザ経由でGETリクエスト
    driver.get('http://icanhazip.com')
    # ドライバー終了
    driver.quit()       # ブラウザが終了

「セレニウム」モジュールから「ウェブ・ドライバー」を読み込んで、使いたいドライバーのインスタンスを生成してから、インスタンスに対して操作を行えば良さそうです。この手順で簡単な模擬コードを創ります。模擬コードでは容疑者の一人である「怪人(ファントム)」ドライバーを呼び出します。インスタンス生成時に於けるコンストラクタの引数はドライバーによって微妙に異なる様子ですが、「ファントムジェーエス」ドライバーで指定可能なオプションでプロキシの指定を試みようと考えています。

模擬コード(覆面捜査官):

# undercover_phantomdriver.py
    from selenium import webdriver
    # PhantomJS ドライバーを使います
    phantomjs_path = '/phantomjs/bin/phantomjs.exe'
    service_args = [ '--proxy=proxy.eddie.jp:8080', '--proxy-type=http', ]
    driver = webdriver.PhantomJS(executable_path = phantomjs_path,  service_args = service_args)
    driver.get('http://icanhazip.com')      # スクレイピング開始
    print(driver.page_source)
    driver.quit()

手順を踏んで潜入させる覆面捜査官の準備ができました。

『事件再現』(模擬実行)

先ほど用意した模擬コードで本当に事件が再現できるのかを模擬環境で検証してみます。実際に PhantomJS を Selenium 経由で実行を試みるのです。

    C\:> python undercover_phantomdriver.py
    Traceback (most recent call last):
      File "undercover_driver.py", line 5, in <module>
        driver = webdriver.PhantomJS(executable_path = phantomjs_path,  service_args = service_args)
      :
      (中略)
      :
    selenium.common.exceptions.WebDriverException: Message: (エラーメッセージの出力、省略。WebコンテンツフィルターからのHTMLページが出力されている模様。)

見事に事件を再現することができました。
模擬コードを実行すると意味不明のエラーを吐いてプログラムが死んでしまったのです。そしてこの謎の「ダイイング・メッセージ」"dying message" に悩まされることになったのです。

『犯行推理』(障害特定)

事件を再現した結果からは、ドライバーのインスタンスを生成したタイミングでエラーが発生することが判明しました。しかし、この例外発生から得られた情報には、まるで WebコンテンツフィルターからのようなHTMLページがエラーメッセージとなって出力されたのです。インスタンスを生成しただけで既に該当プロキシの機能にアクセスしたかの様な挙動からのメッセージが謎を産みました。この意味不明な「ダイイング・メッセージ」"dying message" に惑わされてしまい迷宮に陥ってしまったのです。

推理を行うとすると、どうしても脳裏からエラーメッセージから離れなくなってしまいます。この不明なエラーの発生原因に囚われてしまったのです。幾度リセットしようとしても何故動作しないのかを筋道立てて追求できなくなってしまったのです。
そうすると猜疑心が湧いてきて、そもそも正しい模擬コードが書けているのか?フィルタの設定はどうなっているのか?なぜプロキシにアクセスしているのか?呼び出しているドライバーが正しい挙動なのか?など無数の疑念が入れ替わり立ち替わり容疑者が登場してくるためにすべてが怪しく思えて疑ってしまいます。
何某かの手掛りでも入手できればと思いつく限り色々コードを工夫して試してみたのですが、出口の見えない迷路を歩き続けて時間だけが無駄に経過する事象に陥りました。

何日も堂々巡りが続いたので、もう一度エラー発生と向き合い最初から見直してみることにしました。
エラーが発生したのはPhantomJSインスタンスを生成したタイミングということで、該当クラスのコンストラクタを精査する必要がありそうです。必然的にソースコードを遡上する羽目になってしまいました。

『タレコミ』(情報入手)

探偵(筆者)は諦めずにソースコードと睨めっこしながら推理に没頭していましたが、依然として事件は解決の糸口すらも見つからず何日が経過していました。

ここで「タレコミ」(密告)がありました。

ワトソン(直人)が手順を教えた「スクレイピング」を実際に試している被害者からのフィードバックでした。どうやら事件に遭遇したその当事者自身も色々試していた様子だったのですが、その方をソース(情報源)としたタレコミがワトソン経由で情報提供がありました。「環境変数をいじったら動いた」との事らしいのです。どうやらこれを手掛りにして今回の犯人が使用した「凶器」(原因)が特定できそうです。もう一度、犯人の足取りを追うことにしました。

『犯人追跡』(原因究明)

タレコミを信じて「環境変数」で関連ありそうな所を弄って模擬(再現)してみた所、本当に原因が取り除かれたのかは不明なのですが動作確認が出来ました。正常動作したのです。
事件に使用された「凶器」は「環境変数」だと判明しました。問題はどこでその凶器が使用されたのかという「犯行場所」であり、プロセスが「キル」"kill" された「殺害現場」を特定しようと考えました。もう一度ソースコードを遡ることになりますが、今度は手掛りとなる道標があるのできっと突き止められる筈です。

まずは、問題を誘発した「ふりだし」である模擬コードから出発します。

模擬コード(断片):

# undercover_driver.py
    from selenium import webdriver
    driver = webdriver.PhantomJS('phantomjs')  #=> エラー発生
    # 
    #  do webdriver stuff here.
    # 
    driver.quit()

最初に確認した様に、ドライバー・インスタンスを生成しただけでエラーが発生しています。先ずはドライバーの初期化処理内容を確認するためにコンストラクタを確認します。ライブラリからセレニウム ウェブ・ドライバーのソースコードを探して、コンストラクタの実装から見ていくことにします。

参照している複数のソースコード(の断片だけ掲載)の足跡を辿った番号を記載しましたので、その順番に足取りを確認していきます。

ソースコード:

# source code --> selenium.webdriver.phantomjs.webdriver.py
    from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
    from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
    from .service import Service    # <==== ② ここに行きます。
    class WebDriver(RemoteWebDriver):
        def __init__(self, executable_path="phantomjs",
                     port=0, desired_capabilities=DesiredCapabilities.PHANTOMJS,
                     service_args=None, service_log_path=None):
            self.service = Service(
                executable_path,
                port=port,
                service_args=service_args,
                log_path=service_log_path)
            self.service.start()    # ====> ① ここが怪しい。

最初はコンストラクタです。コンストラクタに指定した引数を利用していますが、その実装本体は「サービス」(Service) のインスタンスを生成して呼び出して引数を設定している様子です。そしてサービスに用意されたメソッドである「スタート」"start()" を呼び出すことで処理を実行するものと憶測されます。次に、「サービス」(Service) のソースコード探して診てみます。

# source code --> selenium.webdriver.phantomjs.service.py
    from selenium.webdriver.common import service   # <==== ④ 次は、ここに行きます。
    class Service(service.Service):
        def __init__(self, executable_path, port=0, service_args=None, log_path=None):
            self.service_args = service_args
              :
              (中略)
              :
            service.Service.__init__(self, executable_path, port=port, log_file=open(log_path, 'w'))
            # ====> ③ ここまでは怪しい箇所が無かったのでスーパークラスを掘ってみます。

「サービス」(Service) のインスタンスを生成している箇所に該当するのはコンストラクタですが、どうやら怪しそうな箇所は見つかりませんでした。そこでスーパークラスの「サービス・サービス」(service.Service) へと更に遡ります。

# source code --> selenium.webdriver.common.service.py
    from selenium.common.exceptions import WebDriverException
    from selenium.webdriver.common import utils
    class Service(object):
        def __init__(self, executable, port=0, log_file=PIPE, env=None, start_error_message=""):
            self.path = executable
            self.port = port
            if self.port == 0:
                self.port = utils.free_port()
            self.start_error_message = start_error_message
            self.log_file = log_file
            self.env = env or os.environ    # ====> ⑤ ありました。ここで実行時に環境変数を取得しています。
        :
        (中略)
        :
        def start(self):   # <=== ⑥ 容疑者が冒頭の①呼び出しているメソッドがコレです。
            try:
                cmd = [self.path]
                cmd.extend(self.command_line_args())
                self.process = subprocess.Popen(cmd, env=self.env,    # <==== ⑦ ここで環境変数をセット。
                                                close_fds=platform.system() != 'Windows',
                                                stdout=self.log_file, stderr=self.log_file)
            except TypeError:
                raise
            except OSError as err:
                if err.errno == errno.ENOENT:
                    raise WebDriverException(
                        "'%s' executable needs to be in PATH. %s" % (
                            os.path.basename(self.path), self.start_error_message)
                    )
                elif err.errno == errno.EACCES:
                    raise WebDriverException(
                        "'%s' executable may have wrong permissions. %s" % (
                            os.path.basename(self.path), self.start_error_message)
                    )
                else:
                    raise       # <=== ⑧ 例外がここで発生していると思われる。
            except Exception as e:
                raise WebDriverException( 
                    "The executable %s needs to be available in the path. %s\n%s" %
                    (os.path.basename(self.path), self.start_error_message, str(e)))

「サービス・サービス」(service.Service) のコンストラクタに環境変数を操作している箇所が確かにありました。
実行環境の「環境変数」を確かに取得しています。取得した「環境変数」を使って新しいプロセスを生成しようとしているところで失敗していると考えられます。事件が発生した現場がやっと特定できました。では何故ここでプロセス生成に失敗したのかと追い続けるべきかもしれませんが、「犯行現場」と「凶器」が特定できたので回避策が打てるでしょう。これ以上の深追いは止めます。この続きがあればという事でまたの機会に取って置こうと思います。

『事件収束』(障害回避策)

結論に直結する手掛りを教えて貰ったことで真相に近づくことが出来ました。見ての通り探偵は事件解決に何ら寄与した訳ではないのですが、犯人が事件に至る生い立ち(背景)を見つけ出すことの検証と探求を試みました。まだ道半ばですが、同じような悲劇を繰り返さない様にという意図です。ここでの結論としてはは関連する環境変数を削除することで回避できました。

Windows で設定済みであったプロキシの環境変数('HTTP_PROXY')の削除方法は以下です。

環境変数の確認(Windowsの場合):

    > set http_proxy
    http_proxy=http://username:password@proxy.eddie.jp:8080/

環境変数の削除(Windowsの場合):

    > set http_proxy=

イコール (=) 記号の後ろには何も入れないで下さい。これで Windows で環境変数を削除する事が出来ます。

環境変数を削除することで障害回避できることは、下記のソースコード修正にても確認できましたので付記しておきます。
いちいち環境を変えるのは面倒であるという事である場合に、ソースコードにパッチする方法も試してみたのです。今回の現象は PhantomJS ドライバー固有の問題と仮定して該当コードに修正を施してみました。

ソースコード:

# source code --> selenium.webdriver.phantomjs.service.py
    class Service(service.Service):
        def __init__(self, executable_path, port=0, service_args=None, log_path=None):
            # 下記の行に修正してみました。
            env_tmp['HTTP_PROXY'] = ''              # 環境変数の削除
            service.Service.__init__(self, executable_path, port=port, log_file=open(log_path, 'w'), env=env_tmp)

予想通りにこの修正でも動きました。改めて「環境変数」("HTTP_PROXY") が犯人であったことは明白になりました。

しかも、リモートドライバーでも同様の現象が発生することで PhantomJS 固有の問題では無いということまで把握が出来ました。やはりサブプロセス生成時に環境変数が何らかの支障を来たしているのは間違いなさそうです。

未解決事件とはなりますが、今回の追及はここまでとさせて頂きます。

『事件再考』(振り返り)

当初想定していたのは、今回使用した PhantomJS は、他の Webドライバーが呼び出す実体(バイナリ)とは異なって環境変数でのプロキシ設定が出来ないことに起因するのでは?と憶測しました。これは初動捜査で判明済みであるようにプロキシの指定が PhantomJS 実行時のコマンドライン・オプションであったからです。最初に怪しいと当たりをつけたのです。

これを Selenium から実行する際には WebDriver から Ghost Driver 経由で PhantomJS にコマンドライン・オプションを渡すことになります。今回は Python プログラムから情報提供する必要があるのですが、Selenium が用意している Python バインディング では、ドライバクラスのコンストラクタの引数 (service_args) で指定可能になっていました。

ですが実際に実行してみると、このパラメータが効いていないどころかインスタンスを生成しただけでエラーが発生するのが、難題となりました。更には犯人を見間違う一因となったのがこのエラーメッセージにあり、あたかもプロキシにアクセスしてその結果をメッセージに反映しているかの様相だったのです。一体、これはどういったことなのかと首を傾げてしまいました。これが迷走の始まりだったのです。

ソースコードを遡上する過程で判明したのは、実行時の環境変数を参照してWebドライバーを探索して使用可能であり共通パッケージ (common) 内に同梱されているスーパークラス (Service) での定義になっていたことです。プログラム実行時に、前述の処理が災いして PhantomJS を起動するプロセス生成に問題が露呈した結果となりました。

では何故、PhantomJS をサブプロセスとして起動する際に該当の環境変数が邪魔をしたのか?が問題なのですが、詳細は不明です。真相究明には標準ライブラリの探索をしなければなりませんが、前述のようにこの点については、まだ追求していません。

『後日譚』(それからどうした)

障害回避策にても記述しましたが、ドライバーをサブプロセスとして生成する際の問題は PhantomJS 固有の問題であると決めつけていたのですが、どうやら案外そうではなさそうなのです。

冒頭の調査に提示していました Selenium RC の後継として Selenium WebDriver には、リモートWebドライバー (Remote Web Driver) というような機能が付属しています。つまりは、ローカルに立ち上げた通信用サーバー(Selenium Server など)とリモート接続することでスクレイピングできるような仕組みです。
ソースコードを観た限りではドライバーの起動時に直にドライバーをサブプロセスとして起動するか、リモートドライバーを使ってサーバー経由で実行するのかを判断するような事も可能な様子でした。これを観たところで PhantomJS をリモートドライバー経由で使用することを試みました。

以下がその方法です。

1. 事前に PhantomJS をドライバーモード(--webdriverオプション付き)で起動しておきます。

    C:\> phantomjs.exe --proxy=proxy.hoge.jp:8080 --proxy-auth=username:password sample.js --webdriver=4444
[INFO - 2016-09-02T06:55:47.778Z] GhostDriver - Main - running on port 4444

PhantomJS に付属の GhostDriver が指定ポートで待ち受け状態になりました。準備完了です。

2. Python プログラムからリモートドライバーでアクセスを試みます。

模擬コード(断片):

# undercover_remotedriver.py
    driver = webdriver.Remote(
       command_executor='http://127.0.0.1:4444/',
       desired_capabilities={'browserName': 'ABCBrowser',
                             'version': '100',
                            'javascriptEnabled': True})
    driver.get("http://docs.python.org")
    print(driver.page_source)
    driver.quit()

これも動作が確認できました。前述と同じ様に実行出来ます。但し、これまた前述と同様に「環境変数」("HTTP_PROXY") を外す必要があります。やはり「環境変数」("HTTP_PROXY") が犯人であったことは裏付けられることになりました。

しかも、リモートドライバーでも同様の現象が発生することで PhantomJS 固有の問題では無いらしいということまで把握が出来ました。やはりサブプロセス生成時に環境変数が何らかの支障を来たしているのは間違いなさそうです。
裏付けのない推理に過ぎないのですが、Webドライバー・インスタンスを生成するのにはドライバー経由で実体である該当するブラウザのプロセスを生成します。この時にブラウザ特化したプロキシ設定を行うのではないのか?と考えられます。このプロセスの初期化処理ためにブラウザがプロキシにアクセスすることが背後で行われており、その際に環境変数が「邪魔」をするのではないのかと推理しています。これまで獲た状況証拠だけの推理であり完全に憶測です。

未解決事件とはなりますが、今回の追及はここまでとさせて頂きます。

『後記』(あとがき)

今回は「ファントム(怪人)」"PhantomJS" が「幽霊(ゴースト)」"Ghost Driver" の正体であったのですが、怪人を正体不明の如く幽幽に映す「セレン(セレニウム)」"Selenium" が犯人だったのです。
セレニウムという金属は光伝導性を持つため」コピー機で「感光」するのに使われており、有害な「毒」も併せ持っているのでした。セレニウムは感光するのに微量ならば必要である(環境変数)なのですが、大量に摂取することで人体に有害な「毒」(問題の環境変数)までも摂り込んでしまった事、それが問題を顕在化させた原因とも言えましょう。
つまりは、セレニウムが感光することで幽霊に見せ掛けていたのでありそれに躍らせてしまいました。「幽霊の正体見たり枯れ尾花」という顛末でありました。

ところで題材にした「オペラ座の怪人」"Phantom of the Opera" と聴くと筆者の場合は「アイアン・メイデン」(IRON MAIDEN) のファーストアルバム「鋼鉄の処女」"Iron Maiden" (1980年リリース)のA面最後四曲目に収録された転調を繰り返す長尺の楽曲を想い出します。
これは英国で起こったムーブメント「ニューウェイヴ・オブ・ブリティッシュ・ヘヴィメタル」"New Wave Of British Heavy Metal" という長い名称で略して "NWOBHM" として時代を切り開いた記念碑であり、同時に「鋼鉄の処女」"Iron Maiden" が NWOBHM 最高傑作であると断言できるのが「アイアン・メイデン」(IRON MAIDEN) のファーストアルバムです。

当時高校に進学してロックの洗礼を受けた思春期で成長途中に無知な筆者はロックの系譜を辿る巡礼と新境地の開拓を図りますが、「ヤードバーズ」(The Yardbirds) を代表に当時時点で過去リリースされた楽曲を回帰して聴くと如何な巡礼の旅と雖も馴染まない違和感と旧態然とした古めかしい籠った音に苛まれ嫌気が差して鬱屈として悶々とした感情が蔓延し滞留したままであったのです。
何かもっとこう突き抜けた音が出るのではという「まどろっこしい」思いと、もっと過激にエクストリームになって欲しいと願っていたのですが、時代の空気が共有されたのか筆者の願望をそのままに音源と視覚で具現化してくれたのが英国からやってきた彼等「アイアン・メイデン」(IRON MAIDEN) だったのでした。

LPレコードでは全八曲の構成でA面からレコードを裏返してB面へと物語が起承転結と流れアルバム一枚で構成されています。レコードではパンクロック・テイストでスピード感がある曲が二、三分の尺であるのに対してA面最後の曲である「オペラ座の怪人」"Phantom of the Opera" は七分以上の長尺で舞台が切り替わるように場面転換としての転調を繰り返しながら物語を語ってくれるのです。ベーシストでバンドリーダーの「スティーブ・ハリス」(Steve Harris)の執拗な嗜好性がまさにこの楽曲に露出しているのです。

しかも曲が終わって感慨に浸っているのも束の間、続きを聴くためにレコードをひっくり返そうとおもったら急に地の底から反響したエコーが重なり合って聴こえる恐ろしい声が叫んできます。聴き取り辛いですが曲中での歌詞と同じフレーズで「You torture me back at your lair!」(君という隠れ家の中で拷問されているんだ)とボーカルの「ポール・ディアノ」(Paul Di'Anno) が叫んでいるのです。暫らく(十秒程の)間を置いてからですのでてっきり曲が終わったと思っているのでびっくりします。歌詞は恋慕する男性の片想いという在り来りのものですが、モチーフにしているオリジナル「オペラ座の怪人」"Phantom of the Opera" の物語と同じであり時代を超えても変わらない人の感情を表現しようという目論見かとも憶測します。

このLPアルバム「鋼鉄の処女」"Iron Maiden" のジャケットに描かれているのは「エディ」"Eddie the Head" というキャラクターが初登場するのですが、その見た事がない過激なジャケット画に魅入られて購入したのですが、レコードに針を落とした一曲目「プローラー」"Prowler" のイントロから強力に吸引されて惹きつけられて、そのままのめり込み擦り切れるまで繰り返し聴きました。擦り切れるまでというのは言葉通り(字面通り)で帯が付いた日本版で大枚はたいて購入したこの「鋼鉄の処女」"Iron Maiden" はレコードとレコード針が擦り切れるまで数え切れないほどに聴きました。擦り切れるまで聴いたレコードはこれ以前に輸入盤で安価に購入した「レッド・ツェッペリン IV」"LED ZEPPELIN IV" がありました。木目のフォーウェイ・スピーカー・ステレオで歪んだレコード盤面の溝が無くなるまで繰り返し聴きました。

実はこのステレオがリビングにあったためレコード聴きまくる小僧が邪魔くさくなった(筆者の)親父さんはそのうち専用のステレオを二階の部屋に買ってくれることになりました。ギタリストの「ジョージ・ベンソン」(George Benson) が「ギブミー・ジーセブン (俺にG7をくれ)」"Give me 'G7'" という台詞でテレビ・コマーシャルしたパイオニアのシステムコンポです。実際に購入したのはG7ではなく同シリーズのG3でしたが、それでも父親が高価な品物の購入を容認するという奇蹟が起こったのです。ジャズプレイヤーが "G7"(和音を奏でるギターコード)を得たのと同じく筆者に「遊び場」を与えてくれたのです。事実、地元の旭川にあった電気屋さんに父親と一緒に行って買って貰いました。親父は普段通り無口でしたが「もっと安いのでいい」という態度で満ち溢れて、かなり不満気で機嫌が良くなったという記憶が朧げにあります。「ケチ」(倹約家)なのですが、それにはそれなりの生い立ちがあります。

父親は貧しい幼少期を過ごしたと人づてに聞いています。まるで「北国の帝王」"Emperor of the North Pole" の「リー・マーヴィン」(Lee Marvin) の如く線路脇から蒸気機関車が牽く車両の屋根に跳び乗って学校に通ったそうです。飛び乗った屋根には真っ赤に焼けた石炭の残りカスが機関車の煙突から飛んでくるのです。学校の帰りは石炭車から落ちた石炭を拾い集めるために線路沿いを歩いて帰ります。集めた石炭を売って学校に納める学費に充てるのです。父親の背中には蒸気機関車の屋根でついた火傷の跡が沢山ありました。風呂場に呼ばれて親父の背中を流させられたので、無数についた火傷で爛れた跡を覚えています。

そんな貧乏で困窮する生活だったために見事難関入試を突破した学校には遠方で進学費用が嵩む為に行かせて貰えず卒業後に直ぐに入隊して自衛官として働き始めました。これも親父が長男で歳の離れた弟妹の養育費を稼ぐ為でもあったのです。
父さんの葬式の時、通夜の席で末弟の叔父さんが兄貴(筆者の父親)の思い出を聴かされました。文房具などの勉強や生活に必要なものは兄貴が買ってくれたのだそうです。その買って貰ったノートに弟(叔父さん)が落書きしたら「粗末にするな!」と怒られて「オヤジ(兄貴)にぶん殴られた」のだそうです。一番下の弟である叔父さんと親父は十五歳も離れていたので弟妹とって親同然の存在であっただそうです。ですから兄貴のことを親父(オヤジ)と呼んでいたのだそうです。悲しみにくれた叔父さんが深夜になって呂律が回らない程に酩酊しながら語ってくれました。

そういえば、筆者も子供の時に良く親父にブン殴れたのを想い出しました。自衛隊で鍛え上げた親父が力いっぱい殴るのですから子供には溜まったものではありません。悪ガキ(筆者)としても逃げ回った末に頭を殴られないように頭部は布団に隠れて尻だけを突き出しておくのです。頭を出しておくと脳震盪を起こしてしまいます。親父は鬼神と化して自らの手も痛くなる程に力いっぱい尻を殴ります。嵐が通り過ぎて鬼が居なくなったのを見計らって外に出てみると、既に尻は腫れ上がり座る事すら当分無理な状態となります。勿論、涙で眼も腫れ上がっています。無口で怖い親父さんでしたが、殴られたのは筆者が悪さをしたので自業自得です。

そんな恐くてケチな親父が当時でも高価なステレオ・コンポを無知蒙昧な愚息に買い与えたのは奇蹟です。今から考えればですが、毎朝時報の様にきっちり早朝に起床して勤務に向かう父親が仕事を終え駐屯地から帰宅してから、テレビを見ながら晩酌してリラックスできる筈の場所であるリビングを占拠してしまったのが、よっぽど邪魔だったのだと思います。

思いかけず、死んだ父さんの事を想い出しました。亡くなってからもう何年も経つのだと今更気が付きました。親父も幽霊となって見守ってくれているのかと思いますが、天国から見下ろして「しっかりしろ」とも言わずに行き成り無言で拳骨(げんこつ)を頂くのかもと思うと今でもちょっと怖いです。

でも、どこかで見守っていてくれると思うと安心しますし、怖いものがあるということは健全だと想います。

次回もお楽しみに。

 


 

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes