paloma blog

NWエンジニアやってます。主に自宅環境のお遊びを書きます。Pythonもちょっと。タイトルは好きなカクテルから。

GTAオンラインの極秘貨物の利益率をpythonでグラフ化する グラフ化偏

前回そろえたデータをmatplotlibでグラフ化したいと思います。

データ

貨物数ごとの売却額、利益等まとめられています。
元々あったデータをCSV化したものです。

長いので抜粋。

PS C:\Users\tsuru\tools\cargo-profits> cat '.\special cargo profits.csv' -head 5
Crates,Sell,PPC (Price per crate),Profit @ 2k PC,Profit @ 4k PC,Profit @ 6k PC,Time @ 2k,Time @ 4k,Time @ 6k,PPH @ 2k,PPH @ 4k,PPH @ 6k
1,10000,10000,8000,6000,4000,5:00,7:00,8:00,92664,50633,29162
2,22000,11000,18000,14000,10000,13:00,7:00,8:00,80838,118143,72904
3,36000,12000,30000,24000,18000,22:00,17:00,8:00,83565,83624,131227
4,52000,13000,44000,36000,28000,30:00:00,17:00,19:00,88829,125436,86331

グラフ化対象

前回も書きましたが

  • Crates(箱数)
  • Sell(売却額)
  • Profit @ xk(利益)
  • PPH @ xk(1時間当たりの利益)

を出します。

仕入れ数ごと(1個、2個、3個)に上記を出すので3つのグラフを作成します。

コード

50行以内に収まったので全部張ります。
3つのグラフを一気に出したかったので、保存することにしました。

  • profits.py
import csv
import matplotlib.pyplot as plt

with open('special cargo profits.csv' , 'r') as f:
    reader = csv.DictReader(f)
    data = [x for x in reader]

def Aggregate(key):
    item = []
    for i in data:
        item.append(int(i[key])) # Convert integer
    return item

def Create_graph(profit, pph, filename=None):
    fig = plt.figure()
    ax1 = fig.add_subplot()
    # Add second axes
    ax2 = ax1.twinx()

    l1 = ax1.bar(Cr, Se, color='orange')
    l2 = ax1.plot(Cr, profit, color='red')
    l3 = ax2.plot(Cr, pph)

    # Add legend [f:id:paloma69:20210403132835p:plain]and labels
    plt.legend((l1[0],l2[0],l3[0]), ('Sell','Profit','Profit Per Hour'))
    plt.title('Special cargo profits at {}'.format(filename))
    ax1.set_xlabel('Crates')
    ax1.set_ylabel('Sells and profits ($)')
    ax2.set_ylabel('Profits Per Hour ($)')

    # For auto scale
    plt.tight_layout()

    plt.savefig('{}.png'.format(filename))

Cr = Aggregate('Crates')
Se = Aggregate('Sell')
Pr6 = Aggregate('Profit @ 6k PC')
Ph6 = Aggregate('PPH @ 6k')
Pr4 = Aggregate('Profit @ 4k PC')
Ph4 = Aggregate('PPH @ 4k')
Pr2 = Aggregate('Profit @ 2k PC')
Ph2 = Aggregate('PPH @ 2k')

Create_graph(Pr6, Ph6, '6k purchase')
Create_graph(Pr4, Ph4, '4k purchase')
Create_graph(Pr2, Ph2, '2k purchase')

実行

上記を実行すると3つのグラフが作成されます。

PS C:\Users\tsuru\tools\cargo-profits> ls


    ディレクトリ: C:\Users\tsuru\tools\cargo-profits


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2021/04/02     23:50           1309 profits.py
-a----       2021/03/26     23:32           1401 special cargo profits.csv
PS C:\Users\tsuru\tools\cargo-profits> python .\profits.py
PS C:\Users\tsuru\tools\cargo-profits> ls


    ディレクトリ: C:\Users\tsuru\tools\cargo-profits


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2021/04/03     11:33          52959 2k purchase.png
-a----       2021/04/03     11:33          51299 4k purchase.png
-a----       2021/04/03     11:33          51554 6k purchase.png
-a----       2021/04/02     23:50           1309 profits.py
-a----       2021/03/26     23:32           1401 special cargo profits.csv

グラフ

作成できたところで各グラフを見てみましょう。
仕入れ数を増やすごとに1個当たりの仕入れ値が上がります。
売却額も売る個数によって高くなっていきます。

1時間当たりの利益が今回の比較対象になります。
ソロ売りの貨物9個を比較します。

1個ずつ仕入れ: 仕入れ値は1個あたり$2000

売値に対して利益が高いです。
1時間当たりの利益は波がありますね。
これは集荷ミッションの度にクールタイムがあるので1個ずつで溜めると中々の時間がかかります。 9個売りだと$92500ちょい下の利益率です。

f:id:paloma69:20210403133825p:plain

2個ずつ仕入れ: 仕入れ値は1個あたり$4000

2個ずつ仕入れだと同じく偶数個ずつ売っていけば1時間当たりの利益が上がりますね。
9個売りだとタイミングが悪く$115000付近の利益ですね。

f:id:paloma69:20210403133836p:plain

3個ずつ仕入れ: 仕入れ値は1個あたり$6000

3個だと売価に対する利益はだいぶ下がりますが、1時間当たりの利益はそこそこですね。
集められるスピードが効いています。
9個売りだと仕入れ数との関係で波がトップに来ていて$140000ほどの利益になります。

f:id:paloma69:20210403133848p:plain

まとめ

1時間当たりの利益で比較した場合は集める時間の少ない3個仕入れが効率が良いようです。
とは言っても攻略サイト等で研究されつくしており、今回わざわざ検証するようなことでもないのですが、久しぶりにmatplotlibを触りたくなったので今回の記事を思いつきました。

2軸目を使って棒グラフと折れ線を出力させるというのがテーマにありましたが、意外と手こずらずに作成することができました。
いちおうExcelで予行練習もしましたが、期待通りのグラフが作成できてよかったですw
今思い返すと2軸目も単位を揃えた方がよりいいグラフになったと思いますが、とりあえず今回はこの辺で。

2軸目はfigure関数が肝ですね。この他にも複数のグラフを一度に出力などもできるみたいなのでまた機会があれば触ってみます。

おまけ

極秘貨物で儲けるとオフィスが散らかってきます。
だいぶ売ったのでいい感じに散らかってきましたw

f:id:paloma69:20210403132943p:plain

参考サイト

Matplotlib-2軸グラフの書き方 | ブログ一覧 | DATUM STUDIO株式会社

【Python】Matplotlibで複数のグラフを並べよう!│Python初心者の備忘録

GTAオンラインの極秘貨物の利益率をpythonでグラフ化する 仕込み偏

GTAオンラインが面白くて相変わらずプレイしているのですが、資金を増やす方法の1つに極秘貨物売却というビジネスがあります。

これはゲーム内の違法サイトから貨物を仕入れて違法サイト経由で売却し、利益を得るという仕組みです。
仕入れて売るのは現実と同じで違法サイトはダークウェブみたいな感じですね。

違法の物を売るのはゲーム内の他のビジネスでもありますが、極秘貨物の面白いのが仕入れ数と売却量を自分で決められるというところです。

仕入れ値は

  • 1個 $2000
  • 2個 $8000
  • 3個 $18000

と決まっており、売却額は一定量の単位から選択します。

攻略の定石としては常に3個仕入れで、ある程度溜まったら全箱売却が時間当たりの利益が良いとされていますが、本当にそうなのかpythonを使って可視化してみようと思います。

データ

仕入れた貨物数で都度売却額が変わるのですが、このデータは自分で作らなきゃダメかと思っていたところいいまとめがありました。
流石全世界で遊ばれているだけあってナレッジ量が桁違いです。

www.reddit.com

グラフ化の前提

サイトは売却貨物の全データがありますが、私は寂しいことにソロプレイがほとんどなので小型倉庫分(16箱)だけのデータを使います。

倉庫の大きさは溜めて置ける貨物の量が違って

  • 小型倉庫 16箱
  • 中型倉庫 42箱
  • 大型倉庫 111箱

溜めることができます。

大きい倉庫は仕入れて溜めるのが時間がかかりますが、売却額も増加します。
売る数が増えるほど輸送台数も増えるので邪魔されるリスクも増えます。

人を雇えば輸送台数が多くても捌けますが、私はもっぱら9箱のソロ売りです。
(9箱まで1台で輸送できる)

ちなみに小型倉庫はこんな感じ。

f:id:paloma69:20210401225659p:plain

売却画面。
3個しか溜まってないですが、仕入れ値が$18000だったので3箱売れば$18000の利益が出ます。

f:id:paloma69:20210401225759p:plain

環境

メイン機のWindows10で行います。
グラフ化はおなじみのmatplotlibで。

PS C:\Users\tsuru> $psversiontable

Name                           Value
----                           -----
PSVersion                      5.1.18362.1171
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.18362.1171
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1


PS C:\Users\tsuru> python --version
Python 3.8.3
PS C:\Users\tsuru> pip list | findstr matplotlib
matplotlib      3.4.0

データインポートのテスト

専用ディレクトリ作りました。
データはあらかじめ16個分までコピペしてcsv化してあります。

PS C:\Users\tsuru\tools\cargo-profits> ls


    ディレクトリ: C:\Users\tsuru\tools\cargo-profits


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2021/04/01     19:16              0 profits.py
-a----       2021/03/26     23:32           1401 special cargo profits.csv

コードはこれから書くのでとりあえずpythonプロンプト内でデータ読み込んでみます。
csvはDictReaderで読み込むのが何かと便利です。

PS C:\Users\tsuru\tools\cargo-profits> python
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import csv
>>> import matplotlib.pyplot as plt
Matplotlib is building the font cache; this may take a moment.
>>> with open('special cargo profits.csv' , 'r') as f:
...   reader = csv.DictReader(f)
...   data = [x for x in reader]
>>> data[0]
{'Crates': '1', 'Sell': '10000', 'PPC (Price per crate)': '10000', 'Profit @ 2k PC': '8000', 'Profit @ 4k PC': '6000', 'Profit @ 6k PC': '4000', 'Time @ 2k': '5:00', 'Time @ 4k': '7:00', 'Time @ 6k': '8:00', 'PPH @ 2k': '92664', 'PPH @ 4k': '50633', 'PPH @ 6k': '29162'}

OKですね。
キーごとまとめるため本番ではもう一回ループさせます。

今回は、

  • Crates(箱数)
  • Sell(売却額)
  • Profit @ xk(利益)
  • PPH @ xk(1時間当たりの利益)

を1、2、3個の仕入れ数ごとにグラフを出してみようと思います。

ほとんどGTAオンラインの紹介になってしまいましたが、長くなりそうなので次回へ続きます。

後編

GTAオンラインの極秘貨物の利益率をpythonでグラフ化する グラフ化偏 - paloma blog

おまけ

実はGTAオンラインがらみで似たようなことを経験済みです。
前回は資産の推移でしたが、今回は2軸を使っての利益率を出してみます。

paloma69.hatenablog.com

pixEOSでドット調アバターを作ってみた

ライフハッカーを見ていたら面白そうなアバター作成サイトがあったので作ってみました。

www.lifehacker.jp

詳しく見ていませんが、本来のサイトは名の通りピクセルアートを作っているんでしょうか?

pixeos.io

アバター作成サービスはその一環のようです。

avatar.pixeos.art

ドット調で、というかもろドットで作成できます。

パターンは少なく感じながらも色も変えられるのでバリエーションは多いですね。
サンプルスクショです。

初期画面

背景も選べます。

f:id:paloma69:20210326221723p:plain

髪形

ここは種類も多くて色も多いので印象がガラッと変わりますね。

f:id:paloma69:20210326221740p:plain

帽子

見たことある帽子もチラホラ。

f:id:paloma69:20210326221758p:plain

自分のアバター作成

自分のアバターはこんな感じ。
前髪と襟足が長いのが意外と似ていますw
あとは私服でもYシャツをよく着るので襟のある(様に見える)服にしました。

f:id:paloma69:20210326221826p:plain

画像はダウンロードできましたがブラウザの関係かセーブはできませんでした。

元記事のClubhouseはやっていませんが、せっかくなのではてなブログプロフィール画像はこれにしようかな。
自分のプロフィールを作るなら顔写真も載せたいところですが、ちょっと恥ずかしいと言うことでこれなら安心ですw

pythonで少し人間味のあるwait関数を作る

クローリングを行うときあらゆる本や記事でサーバ負荷削減のため適度にwaitを挟めと記載があります。

しかしプログラムのビルトイン関数の処理だときっちり同じ間隔になるのでスクリプトだとばれてしまいます。
まあ、ばれても問題ないのですが、少し人間味のある間隔は作れるのかと言うことで今回試してみたいと思います。

私はpythonを少し使えますのでpythonで。

ライブラリ

randomだけで行います。

環境

ubuntuのpython3.8で行います。

masashi@PC-ubuntu:~$ python3 --version
Python 3.8.5

ランダム秒waitする関数作成

使用する関数の動き確認
  • random

ランダムの値はrandomを使います。 0〜1までのランダムな値がでます。
整数だけだと規則性が出てしまう可能性があるのでmsecまで仕込みたいというわけです。

>>> random.random()
0.3750366528221827
>>> random.random()
0.9079915001558875
>>> random.random()
0.7066434487326565
  • randint

waitの秒数は1〜5秒待つようにrandintを使います。

>>> random.randint(1,5)
3
>>> random.randint(1,5)
4
>>> random.randint(1,5)
4
>>> random.randint(1,5)
1
  • round

randomの桁数が多いのでroundで丸めます。
小数点3桁くらいで。

>>> round(random.random(), 3)
0.031
>>> round(random.random(), 3)
0.255
>>> round(random.random(), 3)
0.314
関数作成

上記を組み合わせて作成します。

>>> def Jitter():
...     f = random.random()
...     r = random.randint(1,5)
...     return round(f+r, 3)
... 
>>> 

関数名のJitterとは「揺らぎ」という意味で電気通信やパケット通信の遅延等に使われる用語です。(単語の本来の意味は知りませんが)
Delayとかだと直訳でつまらないのでネットワーク系で使われる用語にしてみました。

動かしてみる

>>> Jitter()
1.4
>>> Jitter()
4.905
>>> Jitter()
2.091

いいですね。

クローラーの処理に仕込んだテイでループで数回出してみます。
時間計測用にtimeライブラリを使います。

>>> for a in range(10):
...     w =  time.strftime('%H:%M:%S')
...     time.sleep(Jitter())
...     print(w, 'Clicked something link.')
... 
13:05:49 Clicked something link.
13:05:50 Clicked something link.
13:05:55 Clicked something link.
13:05:56 Clicked something link.
13:06:01 Clicked something link.
13:06:06 Clicked something link.
13:06:08 Clicked something link.
13:06:11 Clicked something link.
13:06:15 Clicked something link.
13:06:16 Clicked something link.

ちょっと間隔が短くて怪しいかもしれませんが、人が何か考えながらクリック等した様な少し不規則な出力になりました!
小数も出しておけばよかった。
randintの範囲は調整の余地ありですね。

まとめ

うろ覚えですが人間味のある遅延というタイトルで同じような内容の記事をネットで見た記憶があり、探しても見つからなかったので今回書いてみました。

今回はpythonですがshellでも作ってみたいですね。
wgetに仕込んだら自然に見えるかな?
User-Agentでバレるかw

shellのみでcisco ASAのshowコマンドをスクレイピングする

今やってるPJでcisco ASAを構築するのですが、久しぶりに触るのでコマンドを忘れてしまいました。
そんな時に頼りになるのがコマンドリファレンスです。
しかし公式サイトを探すと膨大なページが出てきて探すのも読むのも大変です。

そんなわけで今回は今まで培ったテクでコマンドを抜いてみようと思います。 全部やるとこれまた大変なのでとりあえず構築後のステータス確認できるようにshowコマンドのみに絞ります。

今回はpythonを使わずshellだけでやります。

環境

職場では都合上cygwinを使っていますが、今回は自宅のubuntuを使います。

masashi@PC-ubuntu:~/asacommand$ lsb_release -a ; uname -a ; bash --version
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal
Linux PC-ubuntu 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
GNU bash, バージョン 5.0.17(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

仕込み1 下調べ

まずはサイトを取得しないといけませんが、大抵のOSのコマンドはバージョン毎に固定で、サイトが変更されるものではないのでクローリングは行わずローカルに落としてから抽出しようと思います。

ASAのコマンドは膨大なので項目ごとに分けられています。

これがASAのSから始まるコマンドのURIです。

https://www.cisco.com/c/ja_jp/td/docs/security/asa/asa-command-reference/S/cmdref3.html

この中により詳細のコマンド毎のリンクがあります。 showコマンドは

https://www.cisco.com/c/ja_jp/td/docs/security/asa/asa-command-reference/S/cmdref3/s2.html

というURIとなっており、 s2.html 〜 s14.htmlまでがアルファベット順でshow XXX のコマンドの範囲になっています。

まずはこのリソースをローカルに落とします。

仕込み2 wgetでリソース取得

html取得と言ったらwgetcurlですね。
backgroundで動かせるオプションがあるので私はwgetの方が好みです。

wget正規表現が使えないのでechoでURIを出してから取得します。
xargsに渡すついでにプロセスも分けて時短します。
wgetとxargsは相性がいいですね。

今回は試しついでに計測してみますのでフォアグラウンドで実行します。

masashi@PC-ubuntu:~/asacommand$ time echo https://www.cisco.com/c/ja_jp/td/docs/security/asa/asa-command-reference/S/cmdref3/s{2..14}.html | tr " " "\n" | xargs -P 20 wget -q

real    0m1.137s
user    0m0.098s
sys 0m0.080s

わずか1秒ちょっとでs2.html 〜 s14.htmlまで取得です。素晴らしい。

追記

別件でxargsを使う機会があったのですが、ーPを使う場合は-nか-Lで引数を渡さないとうまく動かないようです。
改めて試してみましたが、動いていたのか結果は変わらずでした。

masashi@PC-ubuntu:~/asacommand$ time echo https://www.cisco.com/c/ja_jp/td/docs/security/asa/asa-command-reference/S/cmdref3/s{2..14}.html | tr " " "\n" | xargs -n1 -P 20 wget -q

real    0m1.183s
user    0m0.112s
sys 0m0.122s

ちゃんと取れてます。

masashi@PC-ubuntu:~/asacommand$ ls | sort -V
s2.html
s3.html
s4.html
s5.html
s6.html
s7.html
s8.html
s9.html
s10.html
s11.html
s12.html
s13.html
s14.html

仕込み3 htmlタグ調査

私はHTMLタグのスクレイピングをよく行うのですが、タグ情報が必要なのでコマンドが書かれているタグを調べます。

masashi@PC-ubuntu:~/asacommand$ grep '>show aaa' s2.html | head 
<li><a href="/c/ja_jp/td/docs/security/asa/asa-command-reference/S/cmdref3/s2.html">show aaa kerberos コマンド~ show asdm sessions コマンド</a></li>
                    <p>show aaa kerberos コマンド~ show asdm sessions コマンド</p>
<li><a href="#pgfId-1493821">show aaa kerberos</a></li>
<li><a href="#pgfId-1362247">show aaa local user</a></li>
<li><a href="#pgfId-1548368">show aaa login-history</a></li>
<li><a href="#pgfId-1448582">show aaa-server</a></li>
  <h2 class="pCT_ChapTitle"><a name="pgfId-1186909"></a><a name="show_aaa_kerberos_コマンド~_show_asdm_sessions_コマンド"></a><a name="51328"></a>show aaa kerberos コマンド~ show asdm sessions コマンド 
  <h2 class="pCRC_CmdRefCommand"><a name="pgfId-1493821"></a><a name="show_aaa_kerberos"></a>show aaa kerberos</h2> 
    <span><a name="pgfId-1564126"></a><code class="cExPlain">ciscoasa</code># <span class="cExBold">show aaa kerberos keytab</span> </span> 
  <h2 class="pCRC_CmdRefCommand"><a name="pgfId-1362247"></a><a name="show_aaa_local_user"></a>show aaa local user</h2> 

この辺のタグが良さそうですね。

<li><a href="#pgfId-1493821">show aaa kerberos</a></li>
<li><a href="#pgfId-1362247">show aaa local user</a></li>
...

ページ内リンクっぽいですが、コマンドを抽出する分には大丈夫でしょう。

抽出

htmlタグの中身を抽出するならプログラミング言語でのスクレイピングを思いつきますが、 shellでも十分に拾えます。

  • grepがやはり頼りになる

grepの-PはPerl正規表現が使えるようになるオプションです。
ホント有能。大好き。
タグの中身だけ抜くように正規表現を書きます。(詳細は参考サイトで)

masashi@PC-ubuntu:~/asacommand$ grep -oP '<li><a href="#pgfId-\d+">\K.*?(?=</a>)' s2.html
show aaa kerberos
show aaa local user
show aaa login-history
show aaa-server
show access-list
show activation-key
show ad-groups
show admin-context
show alarm settings
show arp
show arp-inspection
show arp rate-limit
show arp statistics
show arp vtep-mapping
show asdm history
show asdm image
show asdm log_sessions
show asdm sessions

OK!
いいですね。

一気に取得してリダイレクトしちゃいます。

masashi@PC-ubuntu:~/asacommand$ grep -oP '<li><a href="#pgfId-\d+">\K.*?(?=</a>)' s*.html > "show command.txt"

確認

masashi@PC-ubuntu:~/asacommand$ cat 'show command.txt' | sort -V | head
s2.html:show aaa kerberos
s2.html:show aaa local user
s2.html:show aaa login-history
s2.html:show aaa-server
s2.html:show access-list
s2.html:show activation-key
s2.html:show admin-context
s2.html:show ad-groups
s2.html:show alarm settings
s2.html:show arp
masashi@PC-ubuntu:~/asacommand$ cat 'show command.txt' | sort -V | tail
s14.html:show wccp
s14.html:show webvpn anyconnect
s14.html:show webvpn csd(廃止)
s14.html:show webvpn group-alias
s14.html:show webvpn group-url
s14.html:show webvpn hostscan
s14.html:show webvpn kcd
s14.html:show webvpn sso-server(廃止)
s14.html:show xlate
s14.html:show zone
masashi@PC-ubuntu:~/asacommand$ wc 'show command.txt' 
  428  1270 12431 show command.txt

show a 〜 show zまで取れてます。OK!
これを参考に試験項目も作らないとですね。

まとめ

shellのみでリソース取得から特定タグのスクレイピングまで行いました。

プログラミング言語でのスクレイピングはクローリングやjavascript対応といった小技を合わせるのには便利ですが、今回の様なサイトの操作にはshellでも全然間に合います。

一度ローカルに落としておくのがミソで、いくら試してもサイトに負荷がかからないのがいいですね。

参考サイト

プリキュアで学ぶワンライナーWebスクレイピング - Qiita

ここでshellでのスクレイピングを覚えましたw

pythonのjinja2で1:Nの可変パラメータをテンプレートに渡した時のメモ debug編

前回書いたコードで各変数の中身がどうなっているか見てみます。

手動debug

手動ですが、中身を見ます。

今回の環境はubuntu20.04LTSのpython仮想環境です。

(python3) masashi@PC-ubuntu:~$ uname -a ; python --version
Linux PC-ubuntu 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
Python 3.8.5
csv読み込み

辞書型で格納されています。 3.6からOrderdDictで返ってきましたが、3.8から辞書型になったそうです。
職場のは3.6ですが、どちらでも動きは同じですね。

>>> import csv
>>> with open('poolsetting.csv', 'r') as f:
 ...     data = csv.DictReader(f)
...     settings = [ x for x in data ]
...
with>>> with open('poolmember.csv', 'r') as f:
...     data = csv.DictReader(f)
...     members = [x for x in data]
...
>>> settings
[{'poolname': 'POOLA', 'port': '80', 'monitor': 'tcp'}, {'poolname': 'POOLA_test', 'port': '80', 'monitor': 'tcp'}, {'poolname': 'POOLB', 'port': '8080', 'monitor': 'tcp'}]
>>> members
[{'POOLA': 'node1', 'POOLA_test': 'node4', 'POOLB': 'node6'}, {'POOLA': 'node2', 'POOLA_test': 'node5', 'POOLB': None}, {'POOLA': 'node3', 'POOLA_test': None, 'POOLB': None}]

それぞれcsv.DictReaderで読み込んだ値が入っていますね。

membersの中身をキー毎にまとめる
>>> poolmember = {}
>>> # キーとリストを格納
>>> for k in members[0]:
...     poolmember[k] = []
...
>>> # 各キーに中身を追加
>>> for m in members:
...     for k,v in m.items():
...        poolmember[k].append(v)
...
>>> poolmember
{'POOLA': ['node1', 'node2', 'node3'], 'POOLA_test': ['node4', 'node5', None], 'POOLB': ['node6', None, None]}

ここでmembersの中身をキーごとにまとめてます。

テンプレートに渡すように各csvを各行にマージする

updateが肝ですね。
settingsのキーにpoolmemberのキーがマッチしたら辞書をマージします。
そしてテンプレートファイルの変数になるようキーをリネームします。

>>> for s in settings:
...     for k,v in poolmember.items():
...         # キーが同じだったらマージ
...         if s['poolname'] == k:
...             s.update({k:v})
...             # テンプレートに渡すようにリネーム
...             s['member'] = s.pop(k)
...
>>> settings
[{'poolname': 'POOLA', 'port': '80', 'monitor': 'tcp', 'member': ['node1', 'node2', 'node3']}, {'poolname': 'POOLA_test', 'port': '80', 'monitor': 'tcp', 'member': ['node4', 'node5', None]}, {'poolname': 'POOLB', 'port': '8080', 'monitor': 'tcp', 'member': ['node6', None, None]}]
>>> settings[0]
{'poolname': 'POOLA', 'port': '80', 'monitor': 'tcp', 'member': ['node1', 'node2', 'node3']}
>>>

これでテンプレートに渡す各キーと値がセットできました!

出力のサンプル

サンプルとしてsettings1つ目の各値をテンプレートと比較してみます、

データを整形したものです。

{
 'poolname': 'POOLA', 
 'port': '80', 
 'monitor': 'tcp', 
 'member': ['node1', 'node2', 'node3']
}

各データのキーがこのテンプレートに渡り、

### create {{ poolname }}

tmsh create ltm pool {{ poolname }} { \
  load-balancing-mode least-connections-members \
{%- for node in member %}
{%- if node != None %}
  members add { \
    {{ node }}:{{ port }} \
  } \
{%- endif %}
{%- endfor %}
  monitor {{ monitor }} \
}

紐づかれた値がこう出力されます。

### create POOLA

tmsh create ltm pool POOLA { \
  load-balancing-mode least-connections-members \
  members add { \
    node1:80 \
  } \
  members add { \
    node2:80 \
  } \
  members add { \
    node3:80 \
  } \
  monitor tcp \
}

というわけで中身のデバッグでした。
手動じゃなくちゃんとした手法も覚えないといけませんね。

pythonのjinja2で1:Nの可変パラメータをテンプレートに渡した時のメモ

NW機器のconfigを作るとき別のconfigを置換するのもいいんですが、jinja2でテンプレートから読み込んで作るという方法を覚えてから精度とスピードが格段に上がり、以前にはもう戻れません。

csvから読み込むようにするとcsvだけ管理すればいいので楽チンです。

今回いわゆる1:1と1:Nの値が複合したパラメータをレンダリングが出来たのでメモしておきます。
職場で使っているのをそのまま書くわけには行きませんので仮値での再現です。

ケース

今回はBIG-IP LTMのプール作成の際に本件に直面しました。
本番、テスト環境といったバーチャルサーバをいろいろ立てるけどそれぞれメンバー数が違ったりね。
BIG-IPはGUIでポチポチ設定するのは面倒くさいので、なるべくコマンドで流すようにしています。

メンバーの様な可変になる値もcsvから読み込んで、テンプレートからこういう出力を作りたいわけです。

  • poolA
    • node1:80
    • node2:80
    • node3:80
  • poolA_test
    • node4:80
  • poolB
    • node5:80

これを1つのテンプレートから起こしたい。

やりたいことの整理

LBの設計次第ですが、今回のケースは以下です。

  • 固定のパラメータ作成
    • プール名
    • バランシング方式
    • ポート
    • モニター
  • 可変のパラメータ作成
    • プールメンバー
  • 上記をまとめたパラメータをテンプレートに渡す

固定の方は完全に1:1書き方になるので、この2つのcsvは分けないといけませんね。

最終形を整理する

最終形を最初に想定できると書くべき内容が想像しやすいのでいいですね。

csvから読み込むと辞書型で格納されます。
テンプレートに送る段階で各値の{key1: value1, key2: value2 ... }が格納されていればOKということになりますね。

jinja2触りたての頃はいろんな変数がごっちゃになっていましたがようやく見るべきところがわかってきました。

やったこと

  1. 固定パラメータ用の変数作成
  2. 可変パラメータ用の変数作成
  3. 可変パラメータ用変数を加工
  4. テンプレートに送る用の変数にマージ
  5. ループさせてrenderに送る

各ファイル

  • createpool.py

処理の本体です。

from jinja2 import Environment, FileSystemLoader
import csv

env = Environment(loader=FileSystemLoader('./', encoding='utf8'))
template = env.get_template('temple.txt')

# 1. 固定パラメータ用の変数作成
with open('poolsetting.csv', 'r') as f:
    data = csv.DictReader(f)
    settings = [ x for x in data ]

# 2. 可変パラメータ用の変数作成
with open('poolmember.csv', 'r') as f:
    data = csv.DictReader(f)
    members = [x for x in data]

poolmember = {}

# 3. 可変パラメータ用変数を加工
# キーとリストを格納
for k in members[0]:
    poolmember[k] = []

# 各キーに中身を追加
for m in members:
    for k,v in m.items():
       poolmember[k].append(v)

# 4. テンプレートに送る用の変数にマージ       
for s in settings:
    for k,v in poolmember.items():
        # キーが同じだったらマージ
        if s['poolname'] == k:
            s.update({k:v})
            # テンプレートに渡すようにリネーム
            s['member'] = s.pop(k)

# 5. ループさせてrenderに送る
for param in settings:
    print(template.render(param))

  • poolsetting.csv

固定パラメータ用のファイル

poolname,port,monitor
POOLA,80,tcp
POOLA_test,80,tcp
POOLB,8080,tcp
  • poolmember.csv

可変パラメータ用のファイル

プール名がキーになるので書き方が先ほどと逆になります。

POOLA,POOLA_test,POOLB
node1,node4,node6
node2,node5
node3
  • temple.txt

値を渡すテンプレートです。
私はltmのshellからそのまま入力しますので、tmshコマンドから書いてます。
投入パラメータが見やすいように改行してます。

### create {{ poolname }}

tmsh create ltm pool {{ poolname }} { \
  load-balancing-mode least-connections-members \
{%- for node in member %}
{%- if node != None %}
  members add { \
    {{ node }}:{{ port }} \
  } \
{%- endif %}
{%- endfor %}
  monitor {{ monitor }} \
}

debug(手動で)

各処理を見ていこうと思いましたが、本件が長くなってしまうので次回に切り出したいと思います。

詳細はこちらに記載しました。
pythonのjinja2で1:Nの可変パラメータをテンプレートに渡した時のメモ debug編 - paloma blog

最終形の出力だけ書きます。

>>> settings[0]
{'poolname': 'POOL1', 'port': '80', 'monitor': 'tcp', 'member': ['node1', 'node2', 'node3']}

想定通りの形になりました。
settingsをループで回して各キーがテンプレートに渡りますね。
member内の値はtemple.txt内のループで更に渡されると。

実行

実行します。

(python3) masashi@PC-ubuntu:~/python3$ python createpool.py 
### create POOLA

tmsh create ltm pool POOLA { \
  load-balancing-mode least-connections-members \
  members add { \
    node1:80 \
  } \
  members add { \
    node2:80 \
  } \
  members add { \
    node3:80 \
  } \
  monitor tcp \
}

### create POOLA_test

tmsh create ltm pool POOLA_test { \
  load-balancing-mode least-connections-members \
  members add { \
    node4:80 \
  } \
  members add { \
    node5:80 \
  } \
  monitor tcp \
}

### create POOLB

tmsh create ltm pool POOLB { \
  load-balancing-mode least-connections-members \
  members add { \
    node6:8080 \
  } \
  monitor tcp \
}

OK!
それぞれのプールでメンバーがバラけてます。
これをファイルにリダイレクトすれば投入Configの出来上がりです。
リダイレクトはshellでもpython内に書いてもどちらでもいいですね。

まとめ

jijna2を使ってパラメータを1:1で渡す分にはそれほど難しくないのですが、今回の様な可変の値を入れ込もうとするとアイデアが浮かばず頓挫していました。
ある日パラメータをマージするという知見(辞書型のupdate)を得ましたので、今回望む結果を得ることができました。

ciscoACLでも使えそうですが、テンプレファイルがブレースだらけで汚くなりそうなので使わないでおこうと思いますw

参考サイト

Jinja2の基本的な使い方 - Qiita

おまけ

調べたらbuilt-inのstring.Templateという似たクラスがあるようですね。
こっち先に出会っていたらこれで試していたかもな。

python標準のstring.Templateを用いたcisco config作成スクリプト - 技術メモ