Robot FrameworkでHeadless Chromeを使ってみた

Robot FrameworkにはSelenium2Libraryという、Seleniumと連携するためのライブラリがあります。

 
サポートされているブラウザは、ChromeFirefox・PhantomJSなど、ひと通り揃っています。
http://robotframework.org/Selenium2Library/Selenium2Library.html#Open%20Browser

 
そんな中、

を目にしました。

 
そのため、Robot FrameworkでHeadless Chromeが使えるのかを試してみたくなりました。

Python + SeleniumでHeadless Chromeを使う記事がありましたので、今回はこれを元に書いてみようと思います。
SeleniumからHeadless Chromeを使ってみた - Qiita

 
目次

 

環境

  • Mac OS X 10.11.6
  • Python 3.6.1
  • Google Chrome 59.0.3071.115 (stable)
  • chromedriver 2.30 (stable)
  • RobotFramework 3.0.2
  • Selenium2Library 1.8.1
    • pipでインストール可能
    • pip install robotframework-selenium2library

 

ソースコード

上記のQiita記事をRobotFramework用に書き直しただけなので、ソースコードだけ記載します。

なお、stable Chrome 59を使っているせいか、スクリーンショットは1x1の画像のままです。Qiita記事によるとChrome 61では解消されているようです。

また、GitHubにも上げてあります。selenium2_chrome_headless_sampleディレクトリ以下が今回のコードです。
thinkAmi-sandbox/RobotFramework-sample

selenium_google.robot

*** Settings ***

# Libraryは大文字小文字の区別があるようで、libraryとしてしまうとIDEが認識しない
Library  Selenium2Library


*** Keywords ***
GoogleでPythonを検索してスクリーンショットを撮り、結果を出力する
    # 以下のコードをRobot Framework風にした
    # http://qiita.com/orangain/items/db4594113c04e8801aad

    # 以下を参考に、Chromeのオプションを追加して、Chromeを起動する
    # https://groups.google.com/d/msg/robotframework-users/gPsiVaMo19A/cBRH7mr2BAAJ
    ${options} =  evaluate  sys.modules['selenium.webdriver'].ChromeOptions()  sys
    call method  ${options}  add_argument  --headless
    create webdriver  Chrome  chrome_options=${options}

    # Googleのトップ画面を開く
    go to  https://www.google.co.jp/

    # タイトルにGoogleが含まれていることを確認する
    ${page_title} =  get title
    should contain  ${page_title}  Google

    # 検索後を入力して送信する
    input text  name=q  Python
    # Robot FrameworkではEnterキーは\\13になる
    # https://github.com/robotframework/Selenium2Library/issues/4
    press key  name=q  \\13

    # Ajax遷移のため、適当に2秒待つ
    sleep  2sec

    # タイトルにPythonが含まれていることを確認する
    ${result_title} =  get title
    should contain  ${result_title}  Python

    # スクリーンショットを撮る
    # stableなChrome59だと、1x1の画像しか撮れない...
    capture page screenshot  filename=result_google_python.png

    # ログを見やすくするために改行を入れる
    log to console  ${SPACE}

    # 検索結果を表示する
    # ForでElementを回したかったことから、WebElementを取得し、そのAPIを利用する
    # http://robotframework.org/Selenium2Library/Selenium2Library.html#Get%20Webelements
    @{web_elements} =  get webelements  css=h3 > a
    :for  ${web_element}  in  @{web_elements}
    \  ${text} =  get text  ${web_element}
    \  log to console  ${text}
    # 以下を参考に、WebElementからattribute(href)を取得
    # https://groups.google.com/d/msg/robotframework-users/xx3KYxpDu_w/0hyulqKPKQAJ
    \  ${href} =  call method  ${web_element}  get_attribute  href
    \  log to console  ${href}

    # ブラウザを終了する
    close browser


*** TestCases ***

GoogleでPythonを検索するテスト
    GoogleでPythonを検索してスクリーンショットを撮り、結果を出力する

 

実行結果

コンソールには以下が表示されました。HeadlessのChromeでも動作するようでした。

$ robot selenium_google.robot 
==============================================================================
Selenium Google                                                               
==============================================================================
GoogleでPythonを検索するテスト                                         
Python - ウィキペディア
https://ja.wikipedia.org/wiki/Python
Pythonとは - python.jp
https://www.python.jp/about/
Python チュートリアル — Python 3.6.1 ドキュメント
https://docs.python.jp/3/tutorial/index.html
Python基礎講座(1 Pythonとは) - Qiita
http://qiita.com/Usek/items/ff4d87745dfc5d9b85a4
【入門者必見】Pythonとは?言語の特徴やシェア、仕事市場を徹底解説 | 侍 ...
http://www.sejuku.net/blog/7720
Python入門
http://www.tohoho-web.com/python/
Pythonに咬まれるな : 注意すべきセキュリティリスクのリスト | プログラミング ...
http://postd.cc/a-bite-of-python/
Download Python | Python.org
https://www.python.org/downloads/
初心者でもほぼ無料でPythonを勉強できるコンテンツ10選 - paiza開発日誌
http://paiza.hatenablog.com/entry/2015/04/09/%E5%88%9D%E5%BF%83%E8%80%85%E3%81%A7%E3%82%82%E3%81%BB%E3%81%BC%E7%84%A1%E6%96%99%E3%81%A7Python%E3%82%92%E5%8B%89%E5%BC%B7%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%8410
Pythonとは - はてなキーワード - はてなダイアリー
http://d.hatena.ne.jp/keyword/Python
GoogleでPythonを検索するテスト                                        | PASS |
------------------------------------------------------------------------------
Selenium Google                                                       | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed

受け入れテストのフレームワークRobot Frameworkで、組込機能であるBuiltin Libraryを使ってみた

Python製の受け入れテストのフレームワークとして、Robot Frameworkがあります。

 
当初、ドキュメントが充実しているのでテストコードが書きやすいかなと思いました。

ただ、いざ書いてみるようとすると「こういう時はどう書けばよいのか」と悩みました。

そこで今回、Robot Frameworkの組込機能であるBuiltin Libraryを使ってみた時のメモを残します。

 
目次

 

環境

  • Python 3.6.1
  • Robot Framework 3.0.2
    • pipでインストール可能
    • pip install robotframework
  • テストコードを含むファイル:builtin_lib_test.robot
    • Robot Frameworkの対象ファイルは、拡張子がrobotのもの

 

コンソール出力まわり

コンソールにHello, world!を出力する

log to consoleキーワードを使い、コンソールへと出力します。

*** Settings ***
Documentation  ビルトインの機能を使ったテスト

*** TestCases ***
コンソールを使ったテスト
    # Hello, world!
    log to console  Hello, world!

ポイントは

  • log to consoleキーワードに値を渡す場合、半角スペース2つ + 出力したい文字列
    • Robot Frameworkでは、半角スペース2個をキーワードの区切りとして認識するため
  • キーワードに渡す文字列は、クォートで囲む必要なし
    • クォートで囲むと、そのクォートまでコンソールに出力
  • コメントはPython同様、文頭に#を書く

です。

 
robotコマンドを使って実行したところ、Hello, world!が出力されました。

$ robot builtin_library_samples/builtin_lib_test.robot 
==============================================================================
Builtin Lib Test :: ビルトインの機能を使ったテスト                            
==============================================================================
コンソールを使ったテスト                                              Hello, world!

なお、昔からあるコマンド

  • pybot
  • jybot
  • ipybot

などはdeprecated & 将来的には削除されると、GitHubのREADMEに記載がありました。
robotframework/robotframework: Generic test automation framework.

 

日本語をコンソールへ出力する

日本語を出力したい場合も、そのまま書けば良いです。

log to console  こんにちは!
# => こんにちは!

 

文字列まわり

文字列の中にスペースを2つ以上使う

Hello, world!では、文字列の中にスペースが1つ含まれていました。

ただ、2つ以上のスペースを入れると、文字列の中のスペースではなくキーワードの区切りと認識されてしまいます。

 
そのため、2つ以上のスペースを入れたい時は、組込定数${SPACE}を使います。

# スペースが2個以上必要な場合は、組込変数${SPACE}を利用する
log to console  スペースが${SPACE}${SPACE}${SPACE}3個必要
# => スペースが   3個必要

# 拡張変数表記も使える
log to console  スペースが${SPACE * 3}3個必要
# => スペースが   3個必要

 

文字列を結合する

Catenateを使います。

なお、デフォルトでは文字間にスペースが入ります。

文字間にスペースが不要な場合は、SEPARATOR=という書き方をします。

# 文字間スペースあり
${スペースあり} =  Catenate  スペース  あり
log to console  ${スペースあり}
# => スペース あり

# 文字間スペース無し
${スペースなし} =  Catenate  SEPARATOR=  スペース  無し
log to console  ${スペースなし}
# => スペース無し

 

文字列に該当の文字が含まれているか確認する

Should Containを使います。

Should Contain 対象の文字列 検索する文字列 の順に指定します。

# Pythonという文字列の中に、thという文字列が含まれるか
Should Contain  Python  th

 
なお、

もあります。

 

キーワードまわり

キーワードを自作する

今までは*** TestCases ***の下に直接log to consoleなどを記述しました。しかし、それだと他のテストケースでは再利用できません。

そのため、キーワード(いわゆる関数)の自作を行うため、

*** Keywords ***
コンソールに文字を出力する
    log to console  Hello, world!

と、*** Keywords ***に定義します。

*** TestCases ***では

*** TestCases ***
コンソールを使ったテスト
    コンソールに文字を出力する

のように、自作キーワードを利用します。

 
なお、キーワードには半角スペース1個を含めることもできます。

スペース があるキーワード を実行する
    log to console  スペースのあるキーワードです

 

キーワードの仮引数を定義する
[Arguments]を使う

[Arguments]で、キーワードの仮引数を定義できます。

なお、[Arguments]は一行で書く必要があります。キーワード内に[Arguments]が複数あると、used multiple timesエラーが発生します。

引数を受け取ってメッセージを出力する1
    [Arguments]  ${foo}  ${bar}
    log to console  ${foo}
    log to console  ${bar}

   
上記のキーワードは

*** TestCases ***
コンソールを使ったテスト
    引数を受け取ってメッセージを出力する1  foo=フー1  bar=バー1

と使うことができます。

実行すると、

フー1
バー1

となります。

 

継続行を使って[Arguments]を複数行表記にする

[Arguments]は一行で書く必要があるため、引数が多くある場合は見づらくなってしまいます。

そのため、...を使って継続行の表記もできます。
python 2.7 - If ElseIf in Robot Framework - Stack Overflow

 
上記の例の継続行表記は、

引数を受け取ってメッセージを出力する2
    [Arguments]  ${foo}
    ...          ${bar}
    log to console  ${foo}
    log to console  ${bar}

です。

なお、three-dotの後にはスペースを2つ以上入れないと、正しく認識されません。

 

キーワードの中に仮引数を埋め込む

キーワードの中に仮引数を埋め込むこともできます。

キーワードを埋め込んでコンソールに${メッセージ}を出力する
    log to console  ${メッセージ}

 
使う側は

キーワードを埋め込んでコンソールに分かりにくいハローを出力する
#=> 分かりにくいハロー

となります。

これを使う場合は分かりづらくなるため、「」などで囲んで引数であることを明示したほうがいいかもしれません。

 

キーワードの戻り値を定義する
[Return]を使う

[Return]で戻り値を定義できます。

Returnで戻り値を取得する
    [Return]  戻り値1

 
使う側は

${foo4} =  Returnで戻り値を取得する

となります。

 

Return From Keywordを使う

Return From Keyword[Return]と同じです。

ReturnFromKeywordで戻り値を取得する
    Return From Keyword  戻り値2

 

Return From Keyword Ifを使う

条件によって戻り値を返す場合は、Return From KeywordIfを使います。

ReturnFromKeywordIfで戻り値を取得する
    Return From Keyword If  ${true}  戻り値3

 

変数まわり

変数に値を設定する

set variableキーワードで値を設定します。

=の左側はスペース1つ、右側はスペース2つになります。

また、キーワードと引数を区別するため、set variableと実際に設定する値の間はスペースが2つ必要です。

${variable} =  set variable  こんばんは!
log to console  ${variable}
# => こんばんは!

 

条件付きで変数に値を設定する

set variable ifキーワードを使います。

以下の書き方は常に「条件を満たさない値」がコンソールに出力されます。

${condition_value} =  set variable if  False  空データ  条件を満たさない値
log to console  ${condition_value}
# => 条件を満たさない値

 

リスト変数を用意する

変数のタイプとして、${}の他に、

などがあります。

 
今回はリスト変数なので、@{FRUIT}のように、@を使って定義します。

今回は、*** Variables ***に記載して、グローバルな変数として使ってみます。

*** Variables ***
@{FRUIT}  りんご  みかん

*** Keywords ***
コンソールに配列を出力する
    log to console  ${\n}---$を出力---
    log to console  ${FRUIT}
    log to console  ---@を出力---
    log to console  @{FRUIT}
    log to console  @{FRUIT}[0]
    log to console  @{FRUIT}[1]

結果です。

---$を出力---
['りんご', 'みかん']
---@を出力---
りんご
りんご
みかん

 
また、set variableでも配列を変数に設定できます。

@{medals} =  set variable  金  銀  銅
log to console  ${medals}
# => ['金', '銀', '銅']

 

Ifまわり

条件に応じてキーワードを実行する(クォート版)

Run Keyword Ifを使います。

ポイントは

です。

キーワードの定義は以下となります。

Ifを使って処理を切り替えてログを出力する
    [Arguments]  ${引数}
    Run Keyword If  '${引数}' == '${EMPTY}'  log to console  空っぽです1
    ...  ELSE IF  '${引数}' == '${false}'  log to console  Falseです1
    ...  ELSE  log to console  ${引数}

使う側は

# 準備として、空の値を取得する
${empty_value} =  ReturnFromKeywordで空の戻り値を取得する

# 実際にキーワードを使うところ
Ifを使って処理を切り替えてログを出力する  引数=${empty_value}
Ifを使って処理を切り替えてログを出力する  引数=${false}
Ifを使って処理を切り替えてログを出力する  引数=値が入ってます1

です。

なお、キーワードで[Arguments]引数という文字列を定義しているため、キーワードを使う側も文字列引数を指定して値を渡しています。

 

条件に応じてキーワードを実行する(評価ネームスペース版)

もう一つの書き方として、評価ネームスペースを使う方法があります。

Robot Framework 2.9以降の場合、

Robot Framework 2.9 からは、変数自体を評価ネームスペース (evaluation namespace) の中で使えるようになりました。 ネームスペース中の変数は、波括弧のない $variable のような特殊な変数の書き方で表わせます。 この書き方にした変数はクオートする必要はなく、テストファイル中で一旦文字列に置き換えられたりもしません。

http://robotframework-ja.readthedocs.io/ja/latest/lib/BuiltIn.html#evaluating-expressions

とのことです。

試してみたところ、

  • ${EMPTY} ではうまく動作しない(クォートする必要がある)
    • Variable '$EMPTY' not found. エラー
  • 引数や${false}などは評価ネームスペースを使ってもうまく動作する

でした。

そのため、書き方は

Ifを使って処理を切り替えてログを出力する(評価ネームスペース版)
    [Arguments]  ${引数}
    Run Keyword If  $引数 == '${EMPTY}'  log to console  空っぽです2
    ...  ELSE IF  $引数 == $false  log to console  Falseです2
    ...  ELSE  log to console  ${引数}

となります。

キーワードを使う側は変わりません。

# 準備として、空の値を取得する
${empty_value} =  ReturnFromKeywordで空の戻り値を取得する

# 実際にキーワードを使うところ
Ifを使って処理を切り替えてログを出力する(評価ネームスペース版)  引数=${empty_value}
Ifを使って処理を切り替えてログを出力する(評価ネームスペース版)  引数=${false}
Ifを使って処理を切り替えてログを出力する(評価ネームスペース版)  引数=値が入ってます2

 

Forループまわり

Forループにはいくつか方法があります。
Robot Framework User Guide

今回は IN RANGE を使う方法と、listを使う方法を記載します。

IN RANGEを使ったForループ

Pythonのfor in rangeと似たようなものです。

ループブロックに関する注意点は

  • \ (バックスラッシュ)で示す
  • バックスラッシュの後ろには、スペースを2つ以上入れる

です。

Forループを使う(InRange版)
    :FOR  ${i}  IN RANGE  1  4
    \   log to console  ${i}回目

 

listを使ったForループ

今回はCreate Listキーワードを使ってリストを作成し、そのリストをループしてみます。

使い方は以下です。

@{items} =  create list  1  4  6
:FOR  ${v}  IN  @{items}
\   log to console  リストの値は${v}

 

Forループを抜ける

Exit For Loop If を使います。

:FOR  ${v}  IN  one  two  three
\   exit for loop if  $v == 'two'
\   log to console  ${v}

 

キーワードの実行まわり

今回は

  • Fail
  • Run Keyword And Return Status
  • Run Keyword And Ignore Error

について記載します。

Run Keyword系には、その他にも色々とありますので、公式ドキュメントを読んでみると良いかもしれません。

 

強制的にキーワードを失敗させる

Failを使います。失敗した時のメッセージを引数として渡せます。

Fail  常に失敗する

 

キーワードの成否を確認する

Run Keyword And Return Statusを使うと、失敗する可能性のあるキーワードの実行に対する成否判定を行うことができます。

そのため、「想定していた失敗であれば、テストが落ちないようにしたい」などの用途に使えます。

${成否} =  Run Keyword And Return Status  常に失敗するキーワードを実行する
log to console  ${成否}
#=> False

 

キーワードが失敗しても無視する

Run Keyword And Ignore Errorを使うと、

  • キーワードが失敗しても、テストを継続できる
  • キーワードが失敗した時のエラーメッセージを取得できる

を行えます。

用途しては、「キーワード失敗時のメッセージにより処理を分けたい」などでしょうか。

${エラーメッセージ} =  Run Keyword And Ignore Error  常に失敗するキーワードを実行する
log to console  ${エラーメッセージ}
#=> ('FAIL', '常に失敗する')

 

Pythonコードを実行する

Evaluate を使います。

 
以下では、Pythonコードを使ってゼロ埋めをしています。
python - Robot Framework string padding - Stack Overflow

${結果} =  Evaluate  '%06d %s' % (1, 'ゼロ埋めです')
log to console  ${結果}

 

条件を満たしていない場合、テストをパスする

Pass Execution Ifを使います。

Pass Execution If  ${true}  テストをパスしました!

なお、常にパスしたい時は Pass Execution を使います。

 

現在のテストまわりの情報を取得する

組込変数が用意されています。

例えば、

*** TestCases ***
コンソールを使ったテスト
    現在のテスト名を取得する

というテストケースの場合、

*** Keywords ***
現在のテスト名を取得する
    log to console  ${TEST NAME}
    #=> コンソールを使ったテスト

    log to console  ${SUITE NAME}
    #=> Robotframework Sample.Builtin Library Samples.Builtin Lib Test

という結果になります。

 

ソースコード

GitHubに上げました。builtin_library_samples/builtin_lib_test.robotファイルが、今回作成したファイルです。
thinkAmi-sandbox/RobotFramework-sample

#pycamp #glnagano #nseg Python Boot Camp in 長野にTAとして参加しました

6/10にギークラボ長野で開催された「Python Boot Camp in 長野」にTAとして参加しました。

 
日々Pythonをさわっているのですが、使っても使っても新たな発見があり、楽しく過ごしています。

そんな中、「Python Boot Camp in 長野」が企画され、講師として寺田さんがいらっしゃるということを聞きました。

せっかくの機会なので参加したいと相談したところ、TA(講師アシスタント)枠での参加をすすめられたため、TAとして参加しました。

TAは初めての経験であり、うまくできるか不安でした。ただ、事前にいろいろと資料が配布されて説明していただけるなど、TAに対するサポートがありました。そのため、当日どんなふうに動けばよいかのイメージをつかむことができ、不安がやわらぎました。

 
当日の講義中は、寺田さんのお話を聞きつつ、参加者の間を動いてTAのお仕事をしていました。

特に、「WindowsPythonをインストールしたはずなのにPythonの対話モードが起動しない」と詰まっていたのを解決できた時はホッとしました。

ただ、うまくいかないと相談されたときには対応できたものの、参加者がうまくいっていないのか考え込んでいるのかを見分けることが難しく、このあたりは自分の課題だと感じました。

 
また、休憩中には、Python 3 エンジニア認定基礎試験の出題範囲である「Pythonチュートリアル 第3版」に、寺田さんのサインをいただきました。試験を受ける時のお守りにしたいと思います。
基礎試験 | 一般社団法人Pythonエンジニア育成推進協会

 
その後の懇親会でもPythonに関するお話をうかがうことができ、いろいろとためになりました。

 
最後になりましたが、「Python Boot Camp in 長野」を企画・運営・参加されたみなさま、ありがとうございました。

 

WindowsでインストールしたはずなのにPythonの対話モードが起動しない時

参考までに対応した時の行動をメモしておきます。

  • WindowsPythonをダウンロード・インストールしたものの、Pythonの対話モードが起動しないという相談を受ける
  • コマンドプロンプトからset PATHを入力し、Windows環境変数PATHpython.exeが登録されているか確認
    • 登録されていない
  • ダウンロードしたPythonインストーラーを確認
    • amd64という記載があったため、Pythonは64bit版でインストールされることを確認
  • よく使われるC:\Program FilesC:\Program Files (x86)Pythonがいるか確認
  • PyCharmがインストールされていたため、設定されていたinterpreterを確認
    • %USERPROFILE%\AppData\Local\Programs\Python\Python36が指定
  • 参加者に環境変数に上記パスを追加していただいた
    • GUIから操作する時のメモ
      • Windows10のGUIの場合、Win + Xキーでシステムを選択 > システムの詳細設定 > 環境変数ボタン
      • AppDataは隠しフォルダのため、コントロールパネルのエクスプローラーのオプションから、隠しフォルダを一時的に表示する
  • 再度、Pythonの対話モードを試したところ、無事に起動

 
原因としては、Pythonインストーラーのデフォルトが

  • Install for all usersにチェックが入っていないため、%USERPROFILE%\AppData\Local\Programs\Python\Python36にインストールされる
  • Add Python to environment variablesにチェックが入っていないため、環境変数PATHに追加されない

であり、インストール時にInstall Nowを選択してインストールしたためかなと思いました*1

f:id:thinkAmi:20170611082231p:plain

(自分のWindowsはPython3.6が導入済みのため、Python3.5.3のインストール画面のスクリーンショットになりますが、内容は同じです)

*1:自分はいつもCustomize installationを選択してデフォルトから変更していたため、現場では気づかなかったものの記事を書いている時に気づきました

Docker + Alpine3.5 + Apache2.4 + Python3.xで、mod_pythonをソースコードからインストールしてみた

以前、Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonソースコードからインストールしてみました。
Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonをソースコードからインストールしてみた - メモ的な思考的な

 
その時はPython2.x系で試したため、今回はPython3.x系でmod_pythonコンパイル&インストールし、Hello worldしてみます。

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25
    • 前回同様、Apacheはprefork MPMで動作するようにDockerfileを修正
  • Python3系は以下の2種類を試す
    • Alpine3.5のパッケージにあるPython3.5
    • Alpine EdgeのパッケージにあるPython3.6
      • Alpine3.6ならPython3.6が入るが、今回はAlpine3.5で検証
  • mod_python
    • GitHub上のmasterの最新版
    • 今回利用したコミットは8acf1b7

 
なお、動作させるためのDockerfileやhttpd.conf、Hello worldスクリプトは、以前のPython2系で使ったものを流用・一部修正していきます。

 

まとめ

長いので最初にまとめます。

  • 最新のmaster(8acf1b7)ではPython3.x系は動作しない*1
  • ブランチ 3.5.xソースコードであれば、Python3.x系は動作する
  • 最新のmasterで動作させるには、ビルド用のファイルをブランチ3.5.xのコミットへと戻す
    • configure〜make installはあまり詳しくないので、3.5.xから最新までの間のどこでビルド用のファイルが壊れたかまでは追っていない

 

準備:prefork MPMで動作するApache2.4のDockerイメージを作成

今回の件とはあまり関係ないですが、後で再利用できるよう、Apache2.4 + prefork MPMのDockerイメージを作成しておきます。

(長いので省略しますが)Dockerfileは以下を使います。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/blob/master/alpine_apache_prefork/Dockerfile

 
thinkami:httpd24_preforkという名前でDockerイメージを作成します。

$ docker image build -t thinkami:httpd24_prefork .

 

masterをコンパイル&インストー

mod_pythonのmasterをPython3.5でコンパイル&インストールするには、以前のDockerfileを以下の内容で修正します。

  • mod_pythonで使用するpythonpython3へと変更
  • mod_pythonをビルドする時に使うpython-devpython3-devへと変更
  • configureする時のwith-pythonオプションを--with-python=/usr/bin/python3へと変更
RUN apk --update --no-cache add python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache && \
    # mod_pythonで必要なパッケージを追加
    apk add --no-cache --virtual .mod_python_build_libs git && \
    apk --no-cache --virtual .mod_python_build_libs add python3-dev && \
    apk add --no-cache --virtual .mod_python_build_libs apache2-dev && \
    apk add --no-cache --virtual .mod_python_build_libs flex && \
    # ./configureで必要
    apk add --no-cache --virtual .mod_python_build_libs build-base && \
    # sudo make installで必要
    apk add --no-cache --virtual .mod_python_build_libs sudo && \
    # GitHubからソースコードを持ってきてインストール
    cd /tmp && \
    mkdir mod_python && \
    cd mod_python && \
    git clone https://github.com/grisha/mod_python.git . && \
    ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python3 --with-flex=/usr/bin/flex && \
    make && \
    sudo make install && \
    # 不要なパッケージやソースコードを一括削除
    apk del .mod_python_build_libs && \
    rm -r /tmp/mod_python

 
docker image builddocker container runします。

# Dockerイメージのビルド
$ docker image build -t thinkami:python3_httpd24_mod_python_1 .
...
Successfully built e2669af0e6a4

# Dockerコンテナの起動 => エラーで起動せず
$ docker container run -p 8081:80 --name mod_python_1 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_1
httpd: Syntax error on line 19 of /usr/local/apache2/conf/httpd.conf: Cannot load modules/mod_python.so into server: Error relocating /usr/local/apache2/modules/mod_python.so: PyErr_BadArgument: symbol not found

 
エラーPyErr_BadArgument: symbol not foundが表示され、Dockerコンテナが起動しません。

ブランチ3.5.xをコンパイル&インストー

最新のmasterでは動作しなかったため、動作するブランチを探したところ、以下のIssueにコメントがありました。
mod_python 3.5.0 (with support for Python 3!) available for pre-release testing. · Issue #9 · grisha/mod_python

3.5.xブランチを使えばいいようです。

 
そのため、Dockerfileの中でgit cloneしている部分をmasterから3.5.xブランチへと修正します。

# git clone https://github.com/grisha/mod_python.git . && \
git clone -b 3.5.x https://github.com/grisha/mod_python.git . && \

 
docker image build & docker container runします。

$ docker image build -t thinkami:python3_httpd24_mod_python_2 .

$ docker container run -p 8081:80 --name mod_python_2 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_2
[Fri Jun 09 03:51:43.408568 2017] [:notice] [pid 1] mod_python: Creating 8 session mutexes based on 256 max processes and 0 max threads.
[Fri Jun 09 03:51:43.408632 2017] [:notice] [pid 1] mod_python: using mutex_directory /tmp 
[Fri Jun 09 03:51:43.445271 2017] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.25 (Unix) mod_python/3.5.0-9d223c3 Python/3.5.2 configured -- resuming normal operations
[Fri Jun 09 03:51:43.445310 2017] [core:notice] [pid 1] AH00094: Command line: 'httpd -D FOREGROUND'
[Fri Jun 09 03:52:06.023976 2017] [:notice] [pid 6] mod_python: (Re)importing module 'mptest'
172.17.0.1 - - [09/Jun/2017:03:52:05 +0000] "GET /test/mptest.py HTTP/1.1" 200 12

mod_pythonが起動したようです。

 
curlで動作確認をします。

$ curl localhost:8081/test/mptest.py -D - -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /test/mptest.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Fri, 09 Jun 2017 03:52:05 GMT
Date: Fri, 09 Jun 2017 03:52:05 GMT
< Server: Apache/2.4.25 (Unix) mod_python/3.5.0-9d223c3 Python/3.5.2
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-9d223c3 Python/3.5.2
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain
Content-Type: text/plain

< 
* Connection #0 to host localhost left intact
Hello World!

Hello worldが表示されたため、正常に動作しているようです。

 

masterにブランチ3.5.xをマージして、コンパイル&インストー

上記ではブランチ3.5.xで動かしましたが、3.5.xブランチの最新コミットは2013年10月であるため、それ以降に修正された不具合は反映されていません。

試しにブランチ3.5.xとmasterの差分を見ると、以下の4コミットでした。 Comparing master…3.5.x · grisha/mod_python

4コミットには以下の8ファイルが含まれていました。

 
そこで、masterにブランチ3.5.xを単純にマージし、動作するか試してみます。

 
Dockerfileでは

  • git cloneするのをmasterから3.5.xブランチへ
  • git mergeするために、ユーザの設定を任意の値へ

を修正します。

cd mod_python && \
# gitを使うためにユーザ設定を実施
git config --global user.email "you@example.com" && \
git config --global user.name "Your Name" && \
# 現在のmasterを取得
git clone https://github.com/grisha/mod_python.git . && \
# 3.5.xブランチを取得
git branch 3.5.x origin/3.5.x && \
# 3.5.xブランチをmasterにマージ
git merge 3.5.x && \
./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python3 --with-flex=/usr/bin/flex && \

 
docker image build & docker container runします。

$ docker image build -t thinkami:python3_httpd24_mod_python_3 .

$ docker container run -p 8081:80 --name mod_python_3 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_3
[Fri Jun 09 03:54:21.257839 2017] [:notice] [pid 1] mod_python: Creating 8 session mutexes based on 256 max processes and 0 max threads.
[Fri Jun 09 03:54:21.257983 2017] [:notice] [pid 1] mod_python: using mutex_directory /tmp 
[Fri Jun 09 03:54:21.391315 2017] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.25 (Unix) mod_python/3.5.0-7e4861f Python/3.5.2 configured -- resuming normal operations
[Fri Jun 09 03:54:21.393658 2017] [core:notice] [pid 1] AH00094: Command line: 'httpd -D FOREGROUND'

mod_pythonが起動したようです。

 
curlで動作確認をします。

$ curl localhost:8081/test/mptest.py -D - -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /test/mptest.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
HTTP/1.1 500 Internal Server Error
< Date: Fri, 09 Jun 2017 03:54:36 GMT
Date: Fri, 09 Jun 2017 03:54:36 GMT
< Server: Apache/2.4.25 (Unix) mod_python/3.5.0-7e4861f Python/3.5.2
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-7e4861f Python/3.5.2
< Content-Length: 528
Content-Length: 528
< Connection: close
Connection: close
< Content-Type: text/html; charset=iso-8859-1
Content-Type: text/html; charset=iso-8859-1

< 
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>500 Internal Server Error</title>
</head><body>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error or
misconfiguration and was unable to complete
your request.</p>
<p>Please contact the server administrator at 
 you@example.com to inform them of the time this error occurred,
 and the actions you performed just before this error.</p>
<p>More information about this error may be available
in the server error log.</p>
</body></html>
* Closing connection 0

Internal Server Errorになりました。

 
Docker containerのコンソールを見ると

[Fri Jun 09 03:54:36.714955 2017] [:error] [pid 6] make_obcallback: could not import mod_python.apache.\n
[Fri Jun 09 03:54:36.716445 2017] [:error] [pid 6] make_obcallback: Python path being used "['/usr/lib/python35.zip', '/usr/lib/python3.5/', '/usr/lib/python3.5/plat-linux', '/usr/lib/python3.5/lib-dynload']".
[Fri Jun 09 03:54:36.716560 2017] [:error] [pid 6] get_interpreter: no interpreter callback found.
[Fri Jun 09 03:54:36.716596 2017] [:error] [pid 6] [client 172.17.0.1:36616] python_handler: Can't get/create interpreter.
172.17.0.1 - - [09/Jun/2017:03:54:36 +0000] "GET /test/mptest.py HTTP/1.1" 500 528

と、interpreterのcallbackが見当たらないようです。

よって、単純なマージではダメなようです。

 

masterのビルド用ファイルをブランチ3.5.xの時点に戻して、コンパイル&インストー

差分のあった8ファイルのうちビルド用ファイル

を、ブランチ3.5.x時の状態に戻した上でコンパイル&インストールします。

また、コンパイルにはAPKのEdgeにあるPython3の最新版を利用してみます。

Dockerfileの変更は

# Python3まわり
RUN apk --update --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/main/ add python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache && \
    # python3-devもEdgeから持ってくる
    # そうでないと、Python3のバージョンが合わずに、Downgrading python3が発生する
    apk --no-cache --virtual .mod_python_build_libs --repository http://dl-cdn.alpinelinux.org/alpine/edge/main/ add python3-dev && \
...

# mod_pythonまわり
    cd mod_python && \
    # gitを使うためにユーザ設定を実施
    git config --global user.email "you@example.com" && \
    git config --global user.name "Your Name" && \
    # 現在のmasterを取得
    git clone https://github.com/grisha/mod_python.git . && \
    # 3.5.xブランチを取得
    git branch 3.5.x origin/3.5.x && \
    # 3.5.xブランチをmasterにマージ
    git merge 3.5.x && \
    # python3用に、masterブランチのうちの一部ファイルを3.5.xブランチの状態へ戻す => OK
    git checkout 2d013b353631feb7ffe29fe9946327a08fe55381 scripts/mod_python.in && \
    git checkout 54d42b1810d5abfffe5b407d8e763402a0d4b2f0 configure.in && \
    git checkout 9d223c39a8f27345d8c046d026468fc0c1ad5d53 configure && \
    git checkout 51f7b0366d66b58b5b82874fe8a2db346f77d7fc src/include/mod_python.h && \
    git checkout 51f7b0366d66b58b5b82874fe8a2db346f77d7fc src/include/mod_python.h.in && \
    git checkout affc26bd69b3f5e67e8233166ff0d40c43fb7302 src/mod_python.c && \
    # ビルド
    ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python3 --with-flex=/usr/bin/flex && \
...

です。

 
docker image build & docker container runします。

$ docker image build -t thinkami:python3_httpd24_mod_python_4 .

$ docker container run -p 8081:80 --name mod_python_4 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_4
[Fri Jun 09 03:56:17.740963 2017] [:notice] [pid 1] mod_python: Creating 8 session mutexes based on 256 max processes and 0 max threads.
[Fri Jun 09 03:56:17.741173 2017] [:notice] [pid 1] mod_python: using mutex_directory /tmp 
[Fri Jun 09 03:56:17.959878 2017] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.25 (Unix) mod_python/3.5.0-2b3da9d Python/3.6.1 configured -- resuming normal operations
[Fri Jun 09 03:56:17.960025 2017] [core:notice] [pid 1] AH00094: Command line: 'httpd -D FOREGROUND'

 
curlで動作確認をします。

$ curl localhost:8081/test/mptest.py -D - -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /test/mptest.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Fri, 09 Jun 2017 03:56:34 GMT
Date: Fri, 09 Jun 2017 03:56:34 GMT
< Server: Apache/2.4.25 (Unix) mod_python/3.5.0-2b3da9d Python/3.6.1
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-2b3da9d Python/3.6.1
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain
Content-Type: text/plain

< 
* Connection #0 to host localhost left intact
Hello World!

Hello Worldが表示されました。

念のため、Dockerのコンソールを見ると

[Fri Jun 09 03:56:35.416889 2017] [:notice] [pid 6] mod_python: (Re)importing module 'mptest'
172.17.0.1 - - [09/Jun/2017:03:56:34 +0000] "GET /test/mptest.py HTTP/1.1" 200 12

と、無事に動いているようです。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Docker_Apache-sample

関係するディレクトリは以下の通りです。

  • alpine_apache_python3x_mod_python
    • 今回試したソースコード一式
    • executable_py36_all_in_oneは、Alpine3.5のDockerイメージから、Apache2.4(prefork) + EgeのPython3.x + mod_python が動作する環境を作るDockerfile
  • alpine_apache_prefork
    • Apache2.4をprefork MPMでビルドするDockerfile

*1:前回の記事にある通りこのコミットは自分のプルリクのものですが、動作しない原因は自分のコミットではありません、たぶん

初めてのOSSへのプルリクとマージ

会社のまわりの人たちが普通にプルリクを送ってるのを見ていいなーと思いつつ、プルリク未体験で過ごしてきました。

そんな中、ようやく初めてのプルリクとマージを体験できたので、メモしておきます。

 
目次

 

作成したプルリク

mod_pythonをPython3で使うとSyntaxErrorが出るのを修正するプルリクです。

github.com

 

見つけた経緯

前回、Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonソースコードからインストールしてみました。
Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonをソースコードからインストールしてみた - メモ的な思考的な

 
では、次はPython3で動かしてみようと思い、色々やってソースコードからビルドしたところ、

byte-compiling /usr/lib/python3.5/site-packages/mod_python/psp.py to psp.cpython-35.pyc
  File "/usr/lib/python3.5/site-packages/mod_python/psp.py", line 274
    raise et, ev, etb
            ^
SyntaxError: invalid syntax

byte-compiling /usr/lib/python3.5/site-packages/mod_python/python22.py to python22.cpython-35.pyc
byte-compiling /usr/lib/python3.5/site-packages/mod_python/version.py to version.cpython-35.pyc
byte-compiling /usr/lib/python3.5/site-packages/mod_python/apache.py to apache.cpython-35.pyc
writing byte-compilation script '/tmp/tmpu5k2chgw.py'
/usr/bin/python3 -OO /tmp/tmpu5k2chgw.py
  File "/usr/lib/python3.5/site-packages/mod_python/psp.py", line 274
    raise et, ev, etb
            ^
SyntaxError: invalid syntax

removing /tmp/tmpu5k2chgw.py

と、ログにSyntaxErrorが表示されました。

エラーにならず最後までいったものの、気になったのでソースコードを見たところ、

# lib/python/mod_python/psp.pyの270行目くらい
if PY2:
    raise et, ev, etb
else:
    raise et(ev).with_traceback(etb)

と、Python2系でしか使えないシンタックス raise et, ev, etbがありました。
Cheat Sheet: Writing Python 2-3 compatible code — Python-Future documentation

これが原因のようです。

 

プルリクの作成

以下を参考にプルリクを作成してみました。

 
流れは以下の通りです。

  • mod_pythonリポジトリで、既存のIssueやpull requestを読んで雰囲気をつかむ & 良さそうな表現を収集
  • mod_pythonリポジトリで、Issueを作成
    • この時に採番されたIssue番号をメモっておく
  • Forkボタンで自分のリポジトリにfork
  • ローカルにgit clone
  • ローカルでブランチを作成してcommit
  • 自分のリポジトリにブランチをpush
  • 自分のリポジトリにpull requestボタンが出るので押す
  • 頑張って英語でプルリクを作成*1
  • お返事を待つ

 
マージ or リジェクトまでどれくらいの時間がかかるのだろうと思っていたところ、1時間くらいでお返事が来ました。早い。

しかし、追加の説明が必要そうなお返事でした。descriptionで手を抜きすぎました。

そのため、必死に他のプルリクを参考に良さそうな英語表現を集めて返信したところ、無事にマージされました。

とても嬉しかったです。

 
あとはIssueを閉じて完了でした。

 

GitHub上のforkしたリポジトリmod_python本家に追従

せっかくなので、mod_pythonリポジトリの内容を、自分のForkしたリポジトリへと反映します。

# 本家をupstreamとして登録
$ git remote add upstream https://github.com/grisha/mod_python.git

# 本家にfetdh
$ git fetch upstream
remote: Counting objects: 1, done.
remote: Total 1 (delta 0), reused 1 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), done.
From https://github.com/grisha/mod_python
...
 * [new branch]      3.5.x                               -> upstream/3.5.x
...
 * [new branch]      master                              -> upstream/master
...

# 本家がリポジトリにいることを確認
$ git branch -a
* master
  py3-syntax-error-in-psppy
...
  remotes/origin/3.5.x
  remotes/origin/HEAD -> origin/master
...
  remotes/origin/master
...
  remotes/upstream/3.5.x
...
  remotes/upstream/master
...

# マージ
$ git merge upstream/master
Updating 1edbde6..8acf1b7
Fast-forward
 lib/python/mod_python/psp.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

# その後自分のリポジトリへpush

参考:GitHubでFork/cloneしたリポジトリを本家リポジトリに追従する - Qiita

 
その結果、自分のForkしたリポジトリThis branch is even with grisha:master. と表示されていることを確認します。

 

その他

プルリクの中でコミッターのかたに教わったPython2とPython3で互換性のある書き方をする方法のWikiが良かったので、ここでもリンクをはっておきます。python.orgにあるのが安心感があっていいです。
PortingToPy3k/BilingualQuickRef - Python Wiki

*1:コードで分かり合える世界だと、複雑な英語を要求されなくて済むのがありがたいです

Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonをソースコードからインストールしてみた

今までPython3.x & Apache2.4.xを使って、CGIをひと通り試してきました。

次は、CGIを別のものに置き換えてWebアプリケーションを作りたくなりました。

PythonのWebアプリケーションというとWSGIが思い浮かびます。ただ、CGI以降WSGI以前に登場したものを使ってみたくなりました。

調べてみると、

がありました。

 
PyApacheは、

Runs with Python1.X/2.X under Apache 1.3.X/2.0 (configurable).

[Sunday 18th. January 2004]

PyApache module

との記載があり、現在では使えそうにありませんでした。

 
mod_snakeも

I no longer have the time or motivation to work on mod_snake, so instead of letting it sit here, I have decided to kill this project.

RIP - Mod Snake

Jon Travis Last modified: Thu May 9 19:00:01 PDT 2002

RIP - Mod Snake

とあり、プロジェクトが終了していました。

 
mod_python

などの記事から終了したのかなと思いました。

ただ、その後、

にある強い想いとともに復活したようです。

今年に入ってからもGitHub上でcommitがあり、最新版であるmod_python 3.5の日本語ドキュメントも存在することからも、プロジェクトとして動いているようでした。

また、Python3対応も

The latest release is 3.5.0. Key feature of mod_python 3.5.0 is Python 3 support.

mod_python - Apache / Python Integration

とあり、一応対応できているようでした。

 
そこで今回、以下を参考に、Python2系でmod_pythonをインストールし、Hello worldしてみます。
インストール — Mod_python 3.5.0-1330047 ドキュメント

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25 + Python 2.7.13
  • mod_python
    • GitHub上の最新版
    • 今回利用したコミットは8acf1b7

 

調査

Alpine Linuxのパッケージにはmod_pythonが無い

Docker + Alpine Linuxを使うため、Alpineのパッケージでmod_pythonを調べました。しかし、mod_pythonは無いようでした。
*python*での検索結果 | Alpine packages

 
Alpineでmod_pythonを使うには、自分でソースコードからインストールする必要がありそうでした。

 

mod_pythonコンパイル・インストールするために必要なパッケージについて

ドキュメントのインストールに記載がありました。
インストール — Mod_python 3.5.0-1330047 ドキュメント

Alpine Linuxの場合、以下のパッケージを用意すれば良さそうでした。

 

Dockerで配布しているApacheの状態について

今回はDockerで配布しているApache(2.4/alpine/Dockerfile)を使うことにします。
httpd - Docker Store

 
どんな状態のApacheなのかを調べてみます。

# Dockerコンテナを起動
$ docker container run -p 8081:80 --name httpd24 httpd:2.4.25-alpine

# 別のコンソールから、Apacheの状態を確認
$ docker container exec -it httpd24 httpd -V
Server version: Apache/2.4.25 (Unix)
Server built:   Mar  3 2017 21:57:00
Server's Module Magic Number: 20120211:67
Server loaded:  APR 1.5.2, APR-UTIL 1.5.4
Compiled using: APR 1.5.2, APR-UTIL 1.5.4
Architecture:   64-bit
Server MPM:     event
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)
Server compiled with....
 -D APR_HAS_SENDFILE
 -D APR_HAS_MMAP
 -D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
 -D APR_USE_SYSVSEM_SERIALIZE
 -D APR_USE_PTHREAD_SERIALIZE
 -D SINGLE_LISTEN_UNSERIALIZED_ACCEPT
 -D APR_HAS_OTHER_CHILD
 -D AP_HAVE_RELIABLE_PIPED_LOGS
 -D DYNAMIC_MODULE_LIMIT=256
 -D HTTPD_ROOT="/usr/local/apache2"
 -D SUEXEC_BIN="/usr/local/apache2/bin/suexec"
 -D DEFAULT_PIDLOG="logs/httpd.pid"
 -D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
 -D DEFAULT_ERRORLOG="logs/error_log"
 -D AP_TYPES_CONFIG_FILE="conf/mime.types"
 -D SERVER_CONFIG_FILE="conf/httpd.conf"

Server MPM: eventのため、event MPMで動いているようです。

ただ、mod_pythonに関する記事ではpreforkで動いているのを目にするのが多かったため、今回はeventからpreforkへMPMを切り替えることにします。

 
Apache2.2まではコンパイル時にMPMを指定する必要がありましたが、Apache2.4からはLoadModuleだけでMPMが切り替えられそうでした。
MPMが普通のモジュールになった | Apache2.2の設定ファイルをApache2.4に移植するためにやったことまとめ - Qiita

 
そのため、デフォルトでインストールされるモジュールを見てみたところ、

# modulesディレクトリには、mpmモジュールが存在しない
$ docker container exec -it httpd24 pwd
/usr/local/apache2

$ docker container exec -it httpd24 ls -al modules/
...
-rwxr-xr-x    1 root     root        177416 Mar  3 21:57 mod_lua.so
-rwxr-xr-x    1 root     root         23488 Mar  3 21:57 mod_macro.so
-rwxr-xr-x    1 root     root         27920 Mar  3 21:57 mod_mime.so
-rwxr-xr-x    1 root     root         37208 Mar  3 21:57 mod_mime_magic.so
-rwxr-xr-x    1 root     root         46872 Mar  3 21:57 mod_negotiation.so
...

# コンパイル時のモジュールを確認
$ docker container exec -it httpd24 httpd -l
Compiled in modules:
  core.c
  mod_so.c
  http_core.c
  event.c

# ロードされているモジュールを確認
$ docker container exec -it httpd24 httpd -M
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
Loaded Modules:
 core_module (static)
 so_module (static)
 http_module (static)
 mpm_event_module (static)
 authn_file_module (shared)
 authn_core_module (shared)
 authz_host_module (shared)
 authz_groupfile_module (shared)
 authz_user_module (shared)
 authz_core_module (shared)
 access_compat_module (shared)
 auth_basic_module (shared)
 reqtimeout_module (shared)
 filter_module (shared)
 mime_module (shared)
 log_config_module (shared)
 env_module (shared)
 headers_module (shared)
 setenvif_module (shared)
 version_module (shared)
 unixd_module (shared)
 status_module (shared)
 autoindex_module (shared)
 dir_module (shared)
 alias_module (shared)

と、デフォルトではMPM系のモジュールが無いようです。

これより、公式のDockerイメージではprefork MPMで動かすのが難しいと考えました。

 
そのため、公式のDockerfileを一部修正し、Apacheソースコードからインストールする方針とします。

修正部分は、./configureする時のオプションにおける

  • --with-mpm=preforkで、デフォルトのMPMをpreforkにする
  • --enable-mpms-shared=allで、MPM系モジュールを生成する

の2点です。

 

準備

Dockerfile

今回用意するDockerfileは

です。

#--------------------------------------------------
# Apache2.4のインストール:公式のDockerfileを流用
#--------------------------------------------------
FROM alpine:3.5

# ensure www-data user exists
RUN set -x \
  && addgroup -g 82 -S www-data \
  && adduser -u 82 -D -S -G www-data www-data
# 82 is the standard uid/gid for "www-data" in Alpine
# http://git.alpinelinux.org/cgit/aports/tree/main/apache2/apache2.pre-install?h=v3.3.2
# http://git.alpinelinux.org/cgit/aports/tree/main/lighttpd/lighttpd.pre-install?h=v3.3.2
# http://git.alpinelinux.org/cgit/aports/tree/main/nginx-initscripts/nginx-initscripts.pre-install?h=v3.3.2

ENV HTTPD_PREFIX /usr/local/apache2
ENV PATH $HTTPD_PREFIX/bin:$PATH
RUN mkdir -p "$HTTPD_PREFIX" \
  && chown www-data:www-data "$HTTPD_PREFIX"
WORKDIR $HTTPD_PREFIX

ENV HTTPD_VERSION 2.4.25
ENV HTTPD_SHA1 bd6d138c31c109297da2346c6e7b93b9283993d2

# https://issues.apache.org/jira/browse/INFRA-8753?focusedCommentId=14735394#comment-14735394
ENV HTTPD_BZ2_URL https://www.apache.org/dyn/closer.cgi?action=download&filename=httpd/httpd-$HTTPD_VERSION.tar.bz2
# not all the mirrors actually carry the .asc files :'(
ENV HTTPD_ASC_URL https://www.apache.org/dist/httpd/httpd-$HTTPD_VERSION.tar.bz2.asc

# see https://httpd.apache.org/docs/2.4/install.html#requirements
RUN set -x \
  && runDeps=' \
    apr-dev \
    apr-util-dev \
    perl \
  ' \
  && apk add --no-cache --virtual .build-deps \
    $runDeps \
    ca-certificates \
    coreutils \
    dpkg-dev dpkg \
    gcc \
    gnupg \
    libc-dev \
    # mod_session_crypto
    libressl \
    libressl-dev \
    # mod_proxy_html mod_xml2enc
    libxml2-dev \
    # mod_lua
    lua-dev \
    make \
    # mod_http2
    nghttp2-dev \
    pcre-dev \
    tar \
    # mod_deflate
    zlib-dev \
  \
  && wget -O httpd.tar.bz2 "$HTTPD_BZ2_URL" \
  && echo "$HTTPD_SHA1 *httpd.tar.bz2" | sha1sum -c - \
# see https://httpd.apache.org/download.cgi#verify
  && wget -O httpd.tar.bz2.asc "$HTTPD_ASC_URL" \
  && export GNUPGHOME="$(mktemp -d)" \
  && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \
  && gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \
  && rm -r "$GNUPGHOME" httpd.tar.bz2.asc \
  \
  && mkdir -p src \
  && tar -xf httpd.tar.bz2 -C src --strip-components=1 \
  && rm httpd.tar.bz2 \
  && cd src \
  \
  && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
  && ./configure \
    --build="$gnuArch" \
    --prefix="$HTTPD_PREFIX" \
    --enable-mods-shared=reallyall \
    # デフォルトはpreforkにする
    --with-mpm=prefork \
    # ただし、他のMPMもビルドする:この指定がない場合、MPMのmoduleがないので切り替えられない
    # http://tt4cs.blogspot.jp/2013/01/how-to-enable-dso-for-apache-2.4.html
    --enable-mpms-shared=all \
  && make -j "$(nproc)" \
  && make install \
  \
  && cd .. \
  && rm -r src man manual \
  \
  && sed -ri \
    -e 's!^(\s*CustomLog)\s+\S+!\1 /proc/self/fd/1!g' \
    -e 's!^(\s*ErrorLog)\s+\S+!\1 /proc/self/fd/2!g' \
    "$HTTPD_PREFIX/conf/httpd.conf" \
  \
  && runDeps="$runDeps $( \
    scanelf --needed --nobanner --recursive /usr/local \
      | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
      | sort -u \
      | xargs -r apk info --installed \
      | sort -u \
  )" \
  && apk add --virtual .httpd-rundeps $runDeps \
  && apk del .build-deps

# ローカルでchmod 755しておく
COPY httpd-foreground /usr/local/bin/

EXPOSE 80
CMD ["httpd-foreground"]


#--------------------------------------------------
# mod_pythonのインストール
#--------------------------------------------------
RUN apk --update --no-cache add python && \
    # mod_pythonで必要なパッケージを追加
    apk add --no-cache --virtual .mod_python_build_libs git && \
    apk add --no-cache --virtual .mod_python_build_libs python-dev && \
    apk add --no-cache --virtual .mod_python_build_libs apache2-dev && \
    apk add --no-cache --virtual .mod_python_build_libs flex && \
    # ./configureで必要
    apk add --no-cache --virtual .mod_python_build_libs build-base && \
    # sudo make installで必要
    apk add --no-cache --virtual .mod_python_build_libs sudo && \
    # GitHubからソースコードを持ってきてインストール
    cd /tmp && \
    mkdir mod_python && \
    cd mod_python && \
    git clone https://github.com/grisha/mod_python.git . && \
    ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python --with-flex=/usr/bin/flex && \
    make && \
    sudo make install && \
    # 不要なパッケージやソースコードを一括削除
    apk del .mod_python_build_libs && \
    rm -r /tmp/mod_python


#--------------------------------------------------
# Apacheの設定
#--------------------------------------------------
# ローカルのhttpd.confをコピー
COPY httpd.conf /usr/local/apache2/conf/

 

httpd.conf

CGIで使ったものを流用します。

ただし、

  • CGI関係の記述を削除
  • MPMモジュールの指定を追加
    • LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
  • mod_python向けの記述を追加
    • LoadModule python_module modules/mod_python.so
  • mod_pythonのWebアプリを/usr/local/apache2/htdocs/testに配置するための記述を追加
    • <Directory "/usr/local/apache2/htdocs/test">ディレクティブ以下

を変更します。

# ServerRoot: The top of the directory tree
ServerRoot "/usr/local/apache2"

ServerName localhost
# Listen: Allows you to bind Apache to specific IP addresses and/or ports
Listen 80

# LoadModule
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_host_module modules/mod_authz_host.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 setenvif_module modules/mod_setenvif.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so

# for mod_python
LoadModule python_module modules/mod_python.so

# for running prefork MPM
LoadModule mpm_prefork_module modules/mod_mpm_prefork.so


# unixd_module settings
User daemon
Group daemon

# 'Main' server configuration

# ServerAdmin: Your address, where problems with the server should be e-mailed.
ServerAdmin you@example.com

# Deny access to the entirety of your server's filesystem.
<Directory />
    AllowOverride none
    Require all denied
</Directory>

# DocumentRoot
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

<Directory "/usr/local/apache2/htdocs/test">
    AddHandler mod_python .py
    PythonHandler mptest
    PythonDebug On
</Directory>

# The following lines prevent .htaccess and .htpasswd files
<Files ".ht*">
    Require all denied
</Files>

# Log settings
ErrorLog /proc/self/fd/2
LogLevel warn

# log_config_module settings
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog /proc/self/fd/1 common

# mime_module settings
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz

 

mod_pythonを使ったWebアプリ

チュートリアルにあったテストファイルを、ホストのpath/to/htdocs/testの中にmptest.pyとして用意します。

#!/usr/bin/python
# coding: utf-8
from mod_python import apache

def handler(req):
    req.content_type = 'text/plain'
    req.write("Hello World!")
    return apache.OK

Dockerコンテナ起動時に、このファイルをホストとDockerコンテナとで共有します。  
なお、shebangの値は、以下で調べたものを使いました。

$ docker container exec -it mod_python which python
/usr/bin/python

 

起動と動作確認

Dockerコンテナの起動

Dockerコンテナのイメージを作成し、起動します。

# Dockerコンテナイメージを作成
$ docker image build -t alpine:python27_httpd24_mod_python .

# Dockerコンテナを起動し、ホストのhtdocs以下をコンテナと共有
$ docker container run -p 8081:80 --name mod_python -v `pwd`/htdocs/:/usr/local/apache2/htdocs alpine:python27_httpd24_mod_python

 

Apacheの状況確認
# modulesディレクトリには、mpmモジュールが存在する
$ docker container exec -it mod_python pwd
/usr/local/apache2

$ docker container exec -it mod_python ls -al modules/
...
-rwxr-xr-x    1 root     root         37208 Jun  1 09:49 mod_mime_magic.so
-rwxr-xr-x    1 root     root         91720 Jun  1 09:49 mod_mpm_event.so
-rwxr-xr-x    1 root     root         48408 Jun  1 09:49 mod_mpm_prefork.so
-rwxr-xr-x    1 root     root         67720 Jun  1 09:49 mod_mpm_worker.so
...

# コンパイル時のモジュールを確認
$ docker container exec -it mod_python httpd -l
Compiled in modules:
  core.c
  mod_so.c
  http_core.c

# ロードされているモジュールを確認
$ docker container exec -it mod_python httpd -M
Loaded Modules:
 core_module (static)
 so_module (static)
 http_module (static)
 authz_core_module (shared)
 authz_host_module (shared)
 mime_module (shared)
 log_config_module (shared)
 env_module (shared)
 setenvif_module (shared)
 unixd_module (shared)
 status_module (shared)
 python_module (shared)
 mpm_prefork_module (shared)

# Vオプションで状況確認
$ docker container exec -it mod_python httpd -V
Server version: Apache/2.4.25 (Unix)
Server built:   Jun  1 2017 09:48:37
Server's Module Magic Number: 20120211:67
Server loaded:  APR 1.5.2, APR-UTIL 1.5.4
Compiled using: APR 1.5.2, APR-UTIL 1.5.4
Architecture:   64-bit
Server MPM:     prefork
  threaded:     no
    forked:     yes (variable process count)
Server compiled with....
 -D APR_HAS_SENDFILE
 -D APR_HAS_MMAP
 -D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
 -D APR_USE_SYSVSEM_SERIALIZE
 -D APR_USE_PTHREAD_SERIALIZE
 -D SINGLE_LISTEN_UNSERIALIZED_ACCEPT
 -D APR_HAS_OTHER_CHILD
 -D AP_HAVE_RELIABLE_PIPED_LOGS
 -D DYNAMIC_MODULE_LIMIT=256
 -D HTTPD_ROOT="/usr/local/apache2"
 -D SUEXEC_BIN="/usr/local/apache2/bin/suexec"
 -D DEFAULT_PIDLOG="logs/httpd.pid"
 -D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
 -D DEFAULT_ERRORLOG="logs/error_log"
 -D AP_TYPES_CONFIG_FILE="conf/mime.types"
 -D SERVER_CONFIG_FILE="conf/httpd.conf"

mod_pythonがあり、prefork MPMで動作しているようです。

 

Webアプリの動作確認

curlを使って動作確認をします。

$ curl localhost:8081/test/mptest.py -D -
HTTP/1.1 200 OK
Date: Thu, 01 Jun 2017 10:38:23 GMT
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-8acf1b7 Python/2.7.13
Transfer-Encoding: chunked
Content-Type: text/plain

Hello World!

動作しているようです。

 
ただ、コンテナを起動しているコンソールでは

[Thu Jun 01 10:39:29.700391 2017] [:notice] [pid 9] mod_python: (Re)importing module 'mptest'
172.17.0.1 - - [01/Jun/2017:10:39:29 +0000] "GET /test/mptest.py HTTP/1.1" 200 12
[Thu Jun 01 10:39:29.712520 2017] [:error] [pid 9] make_obcallback: could not import mod_python.apache.\n
ImportError: No module named mod_python.apache
[Thu Jun 01 10:39:29.712558 2017] [:error] [pid 9] make_obcallback: Python path being used "['/usr/lib/python27.zip', '/usr/lib/python2.7', '/usr/lib/python2.7/plat-linux2', '/usr/lib/python2.7/lib-tk', '/usr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload']".
[Thu Jun 01 10:39:29.712566 2017] [:error] [pid 9] get_interpreter: no interpreter callback found.

と表示されていますが、現段階では気にしないことにします。

 
なお、DockerでApacheの再起動を普通にすると、

...
[Thu Jun 01 11:13:46.312764 2017] [core:notice] [pid 1] AH00052: child pid 129 exit signal Segmentation fault (11)
[Thu Jun 01 11:13:46.312770 2017] [core:error] [pid 1] AH00546: no record of generation 0 of exiting child 129
〜以降繰り返し〜
...

のようにSegmentation faultします。

 

ソースコード

GitHubに上げました。alpine_apache_python27_mod_pythonディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

Pythonで、日本語をCookie値へ設定する方法を調べてみた

以前、PythonCGICookieを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのCookieを使ってみた - メモ的な思考的な

その時は日本語をCookie値へ設定する方法が気になっていたものの、深入りしませんでした。

 
そこで今回、以下を参考に、日本語をCookie値へ設定する方法について調べたため、メモします。

 
なお、内容に誤りがあった場合にはご指摘いただけるとありがたいです。

目次

 

環境

 

まとめ

長いので最初にまとめます。

 

Cookie仕様のRFC6265について

現在のCookieの仕様はRFC6265とのことです。

 
その 4.1.1. SyntaxにCookie値の仕様が記載されていました。
https://tools.ietf.org/html/rfc6265#section-4.1.1

cookie-pair       = cookie-name "=" cookie-value
cookie-name       = token
cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
                       ; US-ASCII characters excluding CTLs,
                       ; whitespace DQUOTE, comma, semicolon,
                       ; and backslash

cookie-octetを見ると、日本語をそのまま設定するのはダメそうです。

また、cookie-octetで使える文字は書いてあるものの、日本語などの使えない文字に対する符号化方式は書かれていないようです。

 
そのため、試しに、Pythonhttp.cookies.SimpleCookieを使いSet-Cookieヘッダを作成してみます。

try_python_SimpleCookie.py

from http.cookies import SimpleCookie
import binascii

# RFC6265 4.1.1.Syntax のcookie-octetより
# https://tools.ietf.org/html/rfc6265#section-4.1.1
ENABLE_OCTET = {
    'x21': '!',
    'x23': '#',
    'x24': '$',
    'x25': '%',
    'x26': '&',
    'x27': "'",
    'x28': '(',
    'x29': ')',
    'x2a': '*',
    'x2b': '+',
    'x2d': '-',
    'x2e': '.',
    'x2f': '/',
    # x30 ~ x39は0-9なので省略
    'x3a': ':',
    'x3c': '<',
    'x3d': '=',
    'x3e': '>',
    'x3f': '?',
    'x40': '@',
    # x41 ~ x5aはA-Zなので省略
    'x5b': '[',
    'x5d': ']',
    'x5e': '^',
    'x5f': '_',
    'x60': '`',
    # x61 ~ x7aはa-zなので省略
    'x7b': '{',
    'x7c': '|',
    'x7d': '}',
    'x7e': '~',
}

DISABLE_OCTET = {
    'x09': '\t', # 水平タブ
    'x0a': '\n', # LF(改行)
    'x0d': '\r', # CR
    'x22': '"',  # ダブルクォート
    'x2c': ',',  # カンマ
    'x3b': ';',  # セミコロン
    'x5c': '\\',  # バックスラッシュ
    'whitespace': '\x20'  # 半角スペース
}

STRINGS = ['a', 'あ', 'a,a', 'a;a', 'a a', 'a あ',]


def print_cookie_header_from_list(characters):
    for c in characters:
        try:
            cookie = SimpleCookie(f'key={c}')
            print(f'{c}: {type(cookie)} -> {cookie}')
        except Exception as ex_info:
            print(f'{c}: {ex_info}')


def print_cookie_header_from_dict(char_dict):
    for key, value in char_dict.items():
        # http://qiita.com/atsaki/items/6120cad2e3c448d774bf
        h = binascii.hexlify(value.encode('utf-8'))
        try:
            cookie = SimpleCookie(f'{key}={value}')
            print(f'{value} ({key}), hex:{h}, {type(cookie)} -> {cookie}')
        except Exception as ex_info:
            print(f'{key}: {ex_info}')


if __name__ == '__main__':
    print_cookie_header_from_dict(ENABLE_OCTET)
    print('-'*20)
    print_cookie_header_from_dict(DISABLE_OCTET)
    print('-'*20)
    print_cookie_header_from_list(STRINGS)

 
実行結果は以下の通りです。

$ python try_cookie_value.py
! (x21), hex:b'21', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x21=!
# (x23), hex:b'23', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x23=#
$ (x24), hex:b'24', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x24=$
% (x25), hex:b'25', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x25=%
& (x26), hex:b'26', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x26=&
' (x27), hex:b'27', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x27='
( (x28), hex:b'28', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x28=(
) (x29), hex:b'29', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x29=)
* (x2a), hex:b'2a', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2a=*
+ (x2b), hex:b'2b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2b=+
- (x2d), hex:b'2d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2d=-
. (x2e), hex:b'2e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2e=.
/ (x2f), hex:b'2f', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2f=/
: (x3a), hex:b'3a', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3a=:
< (x3c), hex:b'3c', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3c=<
= (x3d), hex:b'3d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3d==
> (x3e), hex:b'3e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3e=>
? (x3f), hex:b'3f', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3f=?
@ (x40), hex:b'40', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x40=@
[ (x5b), hex:b'5b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5b=[
] (x5d), hex:b'5d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5d=]
^ (x5e), hex:b'5e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5e=^
_ (x5f), hex:b'5f', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5f=_
` (x60), hex:b'60', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x60=`
{ (x7b), hex:b'7b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7b={
| (x7c), hex:b'7c', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7c=|
} (x7d), hex:b'7d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7d=}
~ (x7e), hex:b'7e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7e=~
--------------------
     (x09), hex:b'09', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x09=

 (x0a), hex:b'0a', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x0a=
 (x0d), hex:b'0d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x0d=
" (x22), hex:b'22', <class 'http.cookies.SimpleCookie'> -> 
, (x2c), hex:b'2c', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2c=,
; (x3b), hex:b'3b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3b=
\ (x5c), hex:b'5c', <class 'http.cookies.SimpleCookie'> -> 
  (whitespace), hex:b'20', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: whitespace=
--------------------
a: <class 'http.cookies.SimpleCookie'> -> Set-Cookie: key=a
あ: <class 'http.cookies.SimpleCookie'> -> 
a,a: <class 'http.cookies.SimpleCookie'> -> Set-Cookie: key=a,a
a;a: <class 'http.cookies.SimpleCookie'> -> 
a a: <class 'http.cookies.SimpleCookie'> -> 
a あ: <class 'http.cookies.SimpleCookie'> -> Set-Cookie: key=a

 
例外は発生していませんが、http.cookies.SimpleCookieでは、cookie-octet以外の文字を使うとSet-Cookieヘッダが正しく作成されないようです。

 

cookie-octet以外の文字の符号化方式を調べてみると、よく使われているのはURLエンコードでした。

URLエンコードについて調べると、2種類の方法がありました。

 
そこで、両方の符号化方式を調べてみました。

 

RFC3986に基づくURLエンコード(パーセントエンコード)について

RFC3986にて、使用可能な文字が記載されています。

また、RFC3986とその前のRFC2396における、使用可能な文字の比較などは以下が参考になりました。

 
RFC3986より、予約されていない文字(=使用可能な文字)は

2.3.  Unreserved Characters

   Characters that are allowed in a URI but do not have a reserved
   purpose are called unreserved.  These include uppercase and lowercase
   letters, decimal digits, hyphen, period, underscore, and tilde.

      unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

とのことです。

 
そのため、

  • -
  • .
  • _
  • ~

以外がパーセントエンコードされれば良さそうです。

 
PythonにはURLエンコードする関数として

  • urllib.parse.quote()
  • urllib.parse.quote_plus()

の2つがあります。

そこで、引数はデフォルトのまま両方の関数を試してみます。

try_url_encode.py

from urllib.parse import quote, quote_plus

WHITE_SPACE = '\x20'  # 半角スペース
ALPHABET = 'aA'
DIGIT = '0'
JAPANESE = 'あ'

RFC3986_RESERVED = (
    ":/?#[]@"  # gen-delims
    "!$&'()"   # sub-delims 1行目
    "*+,;="    # sub-delims 2行目
)
RFC3986_UNRESERVED = '-._~'


def main(func):
    print(func.__name__)
    print('-'*20)
    print('[WHITE_SPACE]')
    percent_encode(func, WHITE_SPACE)
    print('[ALPHABET]')
    percent_encode(func, ALPHABET)
    print('[DIGIT]')
    percent_encode(func, DIGIT)
    print('[JAPANESE]')
    percent_encode(func, JAPANESE)

    print('[RFC3986_RESERVED]')
    percent_encode(func, RFC3986_RESERVED)
    print('[RFC3986_UNRESERVED]')
    percent_encode(func, RFC3986_UNRESERVED)

def percent_encode(func, characters):
    for c in characters:
        print(f'{c}: {func(c)}')


if __name__ == '__main__':
    # quote()やquote_plus()の挙動確認
    print('-'*20)
    main(quote)
    print('-'*20)
    main(quote_plus)

 
urllib.parse.quote()の実行結果は  

--------------------
quote
--------------------
[WHITE_SPACE]
 : %20
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: /
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: %2A
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: %7E

となり、以下がRFC3986に準拠していません。

  • チルダ(~)が変換されてしまっている
  • スラッシュ(/)が変換されていない

 
一方、urllib.parse.quote_plus()の実行結果は、

--------------------
quote_plus
--------------------
[WHITE_SPACE]
 : +
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: %2F
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: %2A
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: %7E

となり、以下がRFC3986に準拠していません。

  • チルダ(~)が変換されてしまっている
  • 半角スペースが、RFC3986の予約語である+になっている

ただ、半角スペースの挙動は、urllib.parse.quote_plus()の公式ドキュメント通りです。

quote() と似ていますが、クエリ文字列を URL に挿入する時のために HTML フォームの値の空白をプラス記号「+」に置き換えます。オリジナルの文字列に「+」が存在した場合は safe に指定されている場合を除きエスケープされます。safe にデフォルト値は設定されていません。

urllib.parse.quote_plus() | 21.8. urllib.parse — URL を解析して構成要素にする — Python 3.6.1 ドキュメント

 
以上より、Pythonを使ってRFC3986に準拠するためには、urllib.parse.quote()の引数safeにチルダ(~)を設定すれば良さそうです。

try_url_encode.py

# 以下の関数を追加
def quote_rfc3986_strictly(character):
    # 半角スペースと/は%エンコードするが、チルダはそのまま
    return quote(character, safe='~')


if __name__ == '__main__':
    ...
    # 以下を追加
    main(quote_rfc3986_strictly)

 
結果です。スラッシュとチルダが仕様通りになりました。

--------------------
quote_rfc3986_strictly
--------------------
[WHITE_SPACE]
 : %20
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: %2F
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: %2A
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: ~

 

HTML5の仕様に基づくURLエンコードについて

HTML5の仕様では

If the byte is 0x20 (U+0020 SPACE if interpreted as ASCII) eplace the byte with a single 0x2B byte (“+” (U+002B) character if interpreted as ASCII).

If the byte is in the range 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A Leave the byte as is.

Otherwise Let s be a string consisting of a U+0025 PERCENT SIGN character (%) followed by uppercase ASCII hex digits representing the hexadecimal value of the byte in question (zero-padded if necessary).

Encode the string s as US-ASCII, so that it is now a byte string.

Replace the byte in question in the name or value being processed by the bytes in s, preserving their relative order.

https://www.w3.org/TR/html5/forms.html#url-encoded-form-data

とのことです。

 
そのため、HTML5において、記号は

  • *
  • -
  • .
  • _

はそのまま、

  • (半角スペース)

+へと変換、それ以外はURLエンコードされれば良さそうです。

 
半角スペースを+へと変換する必要があるため、Pythonではurllib.parse.quote_plus()を使います。

そこで、以下を追加して試してみます。

try_url_encode.py

# 定数を追加
HTML5_ENABLE_OCTET = (
    '*'  # x2a
    '-'  # x2d
    '.'  # x2e
         # x30 ~ x39は0-9なので省略
         # x41 ~ x5aはA-Zなので省略
    '_'  # x5f
         # x61 ~ x7aはa-zなので省略
)

def main(func):
    ...
    # 以下を追加
    print('[HTML5_ENABLE_OCTET]')
    percent_encode(func, HTML5_ENABLE_OCTET)

 
quote_plus()の実行結果を見ると、*がURLエンコードされてしまっています。

--------------------
quote_plus
--------------------
...
[HTML5_ENABLE_OCTET]
*: %2A
-: -
.: .
_: _

 
そのため、urllib.parse.quote_plus()の引数safeアスタリスク(*)を追加すれば良さそうです。

try_url_encode.py

...
# 追加
def quote_html5_strictly(character):
    # *はそのまま、スペースは+にする
    return quote_plus(character, safe='*')


if __name__ == '__main__':
    ...
    # 追加
    print('-'*20)
    main(quote_html5_strictly))

 
実行してみます。アスタリスク(*)が変換されなくなりました。

--------------------
quote_html5_strictly
--------------------
[WHITE_SPACE]
 : +
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: %2F
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: *
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: %7E
[HTML5_ENABLE_OCTET]
*: *
-: -
.: .
_: _

 

Cookieで使えない文字の符号化を確認

RFC3986やHTML5におけるURLエンコードの仕様を確認したところで、Cookieで使えない文字の符号化を確認します。

以下を追加します。

try_url_encode.py

...
COOKIE_DISABLE_OCTET = (
    '\t'    # 水平タブ (x09)
    '\n'    # LF(改行) (x0a)
    '\r'    # CR (x0d)
    '"'     # ダブルクォート(x22)
    ','     # カンマ(x2c)
    ';'     # セミコロン(x3b)
    '\\'    # バックスラッシュ(x5c)
    '\x20'  # whitespace
)

def main(func):
    ...
    # 以下を追加
    print('[COOKIE_DISABLE_OCTET]')
    percent_encode(func, COOKIE_DISABLE_OCTET)

 
結果を見ると、いずれの方法でも、Cookieで使えない文字が符号化されたり、使える文字へと変換されました。

そのため、Cookie値のURLエンコード用に使うなら、quote()quote_plus()をデフォルトのまま使っても問題なさそうです。

--------------------
quote
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : %20
--------------------
quote_plus
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : +
--------------------
quote_rfc3986_strictly
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : %20
--------------------
quote_html5_strictly
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : +

 

その他

CookieのKey名について

CookieのKey名に関しては、こちらに詳しい内容がありました。
技術/HTTP/FormやCookieのkey名に"=“を含めたらどうなるのか? - Glamenv-Septzen.net

 

ソースコード

GitHubに上げました。e.g._urllib_parse_quote_quote_plusディレクトリの中が今回のものです。
thinkAmi-sandbox/python_misc_samples