TORICO Tech ブログhttps://tech.torico-corp.com/blog/2024-03-28T13:33:38+00:00株式会社TORICO 技術開発チームのブログdjango admin画面に複数選択可能なカスタムリストフィルターを追加する2023-09-27T09:09:45+00:002024-03-28T10:01:06+00:00工藤淳https://tech.torico-corp.com/blog/author/kudou/https://tech.torico-corp.com/blog/django-admin%E7%94%BB%E9%9D%A2%E3%81%AB%E8%A4%87%E6%95%B0%E9%81%B8%E6%8A%9E%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%AA%E3%82%B9%E3%83%88%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%BC%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B/<div>django admin画面のフィルター機能に不満があるので、 複数選択可能なListFilterを作成します。</div>
<br/>
<h4>modelを準備する</h4>
<div>以下のようなmodelを準備します。<br/> 商品マスタ product<br/> 作家マスタ author<br/> 出版社マスタ publisher</div>
<pre>class Product(models.Model):
product_id = models.BigAutoField(
primary_key=True,
verbose_name='商品マスタID'
)
product_name = models.TextField(
verbose_name='商品名'
)
product_price = models.IntegerField(
verbose_name='商品価格',
)
author = models.ForeignKey(
Author,
on_delete=models.PROTECT
)
publisher = models.ForeignKey(
Publisher,
on_delete=models.PROTECT
)
class Meta:
managed = False
db_table = 'product'
verbose_name = '商品マスタ'
verbose_name_plural = '商品マスタ'
def __str__(self):
return self.product_name
class Publisher(models.Model):
publisher_id = models.BigAutoField(
primary_key=True,
verbose_name='出版社マスタID'
)
publisher_name = models.TextField(
verbose_name='出版社名'
)
class Meta:
managed = False
db_table = 'publisher'
verbose_name = '出版社マスタ'
verbose_name_plural = '出版社マスタ'
def __str__(self):
return self.publisher_name
class Author(models.Model):
author_id = models.BigAutoField(
primary_key=True,
verbose_name='作家マスタID'
)
author_name = models.TextField(
verbose_name='作家名'
)
class Meta:
managed = False
db_table = 'author'
verbose_name = '作家マスタ'
verbose_name_plural = '作家マスタ'
def __str__(self):
return self.author_name
</pre>
<br/>
<h4>商品マスタを管理画面で呼び出せるようにadmin.pyに以下のように記述します</h4>
<pre>@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = (
'product_id',
'product_name',
'product_price',
'author',
'publisher',
)
list_display_links = (
'product_name',
)
list_filter = (
'author',
'publisher',
)
search_fields = (
'product_name',
)
ordering = (
'product_id',
)
</pre>
<div>これで商品マスタ product が表示できるようになりました。</div>
<br/>
<h4>新たにコミックや小説などのカテゴリを記録するためのカテゴリマスタを作成します</h4>
<pre>class Category(models.Model):
category_id = models.BigAutoField(
primary_key=True,
verbose_name='カテゴリマスタID'
)
category_name = models.TextField(
verbose_name='カテゴリ名'
)
class Meta:
managed = False
db_table = 'category'
verbose_name = 'カテゴリマスタ'
verbose_name_plural = 'カテゴリマスタ'
def __str__(self):
return self.category_name
</pre>
<div>商品マスタ product にも外部キーでカテゴリマスタを設定します。</div>
<pre> category = models.ForeignKey(
Category,
on_delete=models.PROTECT
)
</pre>
<div>これでカテゴリも表示できるようになりました。<br/> admin.pyに追加すれが表示できるのですが、カテゴリは複数選択できるようにしたいとおもいました。<br/> そこで複数選択可能なカスタムリストフィルターを作成します。<br/>
<h4>複数選択可能なカスタムリストフィルター作成する</h4>
<div>admin.pyに ListFilter を継承した複数選択可能なカスタムリストフィルターを作成します。</div>
<pre> class MultiSelectListFilter(admin.ListFilter):
"""
複数選択可能なリストフィルター
admin の ListFilter を継承しています
"""
title = '' # 項目として表示される名称
model = '' # 使用するmodel名
parameter_name = '' # リクエストのパラメータ名
field_name = '' # 使用するmodelのフィールド名
def __init__(self, request, params, model, model_admin):
# ListFilter を継承
super().__init__(request, params, model, model_admin)
# 複数選択にしたいのでリクエスト値を in にします
self.lookup_kwarg = '%s__in' % self.parameter_name
# ここらへんは SimpleListFilter と同じ
if self.lookup_kwarg in params:
value = params.pop(self.lookup_kwarg)
self.used_parameters[self.lookup_kwarg] = value
lookup_choices = self.lookups(request, model_admin)
if lookup_choices is None:
lookup_choices = ()
self.lookup_choices = list(lookup_choices)
def has_output(self):
# SimpleListFilter と同じ
return len(self.lookup_choices) > 0
def value(self):
# SimpleListFilter と同じ
return self.used_parameters.get(self.lookup_kwarg)
def expected_parameters(self):
# SimpleListFilter と同じ
return [self.lookup_kwarg]
def lookups(self, request, model_admin):
ids = {}
if self.value():
# リクエスト値がある場合、選択済みの値を除外したtupleを返す
for v in self.model.objects.all():
current = self.value().split(',')
parameter_name = str(getattr(v, self.parameter_name))
if parameter_name in current:
current.remove(parameter_name)
else:
current.append(parameter_name)
str_ids = ','.join(list(current))
ids.update({str_ids: getattr(v, self.field_name)})
return tuple(ids.items())
else:
# デフォルトは全件表示。パラメータ名、フィールド名のtupleを返す
return self.model.objects.all().values_list(
self.parameter_name,
self.field_name
)
def choices(self, changelist):
# 全て を表示
yield {
'selected': self.value() is None,
'query_string': changelist.get_query_string(
remove=[self.parameter_name]
),
'display': _('All'),
}
# 選択済みの値はselectedで表示する。この判定のためにis_match関数を作成している
for lookup, title in self.lookup_choices:
yield {
'selected': self.is_match(lookup, self.value()),
'query_string': changelist.get_query_string(
{self.lookup_kwarg: lookup}
),
'display': title,
}
def queryset(self, request, queryset):
if self.value():
# 選択されたデータで絞り込んで表示
query = {
self.lookup_kwarg: self.value().split(','),
}
return queryset.filter(**query)
else:
# デフォルトの表示
return queryset.all()
@staticmethod
def is_match(lookup: str, value: str):
"""
選択済みの値とリクエストの値が一致しているか判定する
:param lookup: 選択済みの値
:param value: リクエストの値
:return:
"""
if not lookup:
return True
if not value:
return False
if all(v in lookup.split(',') for v in value.split(',')):
return False
else:
return True
</pre>
<div>admin.py に カテゴリマスタとカスタムリストフィルターでの表示を記載します。</div>
<pre>@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
class CategoryListFilter(MultiSelectListFilter):
"""
カテゴリマスタのカスタムフィルターの設定
"""
title = 'カテゴリ'
model = Category
parameter_name = 'category_id'
field_name = 'category_name'
list_display = (
'product_id',
'product_name',
'product_price',
'author',
'publisher',
'category', # カテゴリマスタの追加
)
list_display_links = (
'product_name',
)
list_filter = (
'author',
'publisher',
CategoryListFilter, # カテゴリマスタのリストフィルターの追加
)
search_fields = (
'product_name',
)
ordering = (
'product_id',
)
</pre>
<div>商品マスタの表示から「小説」「文庫」を選択</div>
<div style="display: block; margin-left: auto; margin-right: auto;" width="512px"><img alt="" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/kudou/screenshot-2023.09.29-17_02_17.png"/></div>
<div>これで複数選択の絞り込みができるようになりました。</div>
<div style="display: block; margin-left: auto; margin-right: auto;" width="512px"><img alt="" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/kudou/screenshot-2023.09.29-17_02_34.png"/></div>
<div>SimpleListFilter が便利なのですが、ちょっと痒いところに手が届かなかったので作成しました。</div>
</div>django admin画面に簡単にファイルの入力・出力を追加する2023-09-19T03:26:10+00:002024-03-28T11:39:15+00:00工藤淳https://tech.torico-corp.com/blog/author/kudou/https://tech.torico-corp.com/blog/django-admin%E7%94%BB%E9%9D%A2%E3%81%AB%E7%B0%A1%E5%8D%98%E3%81%AB%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%85%A5%E5%8A%9B%E5%87%BA%E5%8A%9B%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B/<div>django admin画面はmodelのデータ管理に非常に便利な機能です。<br/> csvなどのファイルでのデータの追加、出力ができるとさらに便利です。<br/> <code>django-import-export</code>プラグインを導入することでこの機能を簡単に導入することができます。</div>
<br/>
<h4>django-import-exportプラグインを追加する</h4>
<pre>pip install django-import-export
</pre>
<br/>
<h4>modelを準備する</h4>
<div>以下のようなmodelを準備します。<br/> 商品マスタ product<br/> 作家マスタ author<br/> 出版社マスタ publisher</div>
<pre>class Product(models.Model):
product_id = models.BigAutoField(
primary_key=True,
verbose_name='商品マスタID'
)
product_name = models.TextField(
verbose_name='商品名'
)
product_price = models.IntegerField(
verbose_name='商品価格',
)
author = models.ForeignKey(
Author,
on_delete=models.DO_NOTHING
)
publisher = models.ForeignKey(
Publisher,
on_delete=models.DO_NOTHING
)
class Meta:
managed = False
db_table = 'product'
verbose_name = '商品マスタ'
verbose_name_plural = '商品マスタ'
def __str__(self):
return self.product_name
class Publisher(models.Model):
publisher_id = models.BigAutoField(
primary_key=True,
verbose_name='出版社マスタID'
)
publisher_name = models.TextField(
verbose_name='出版社名'
)
class Meta:
managed = False
db_table = 'publisher'
verbose_name = '出版社マスタ'
verbose_name_plural = '出版社マスタ'
def __str__(self):
return self.publisher_name
class Author(models.Model):
author_id = models.BigAutoField(
primary_key=True,
verbose_name='作家マスタID'
)
author_name = models.TextField(
verbose_name='作家名'
)
class Meta:
managed = False
db_table = 'author'
verbose_name = '作家マスタ'
verbose_name_plural = '作家マスタ'
def __str__(self):
return self.author_name
</pre>
<br/>
<h4>商品マスタを管理画面で呼び出せるようにadmin.pyに以下のように記述します</h4>
<pre>@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = (
'product_id',
'product_name',
'product_price',
'author',
'publisher',
)
list_display_links = (
'product_name',
)
list_filter = (
'author',
'publisher',
)
search_fields = (
'product_name',
)
ordering = (
'product_id',
)
</pre>
<div>これで商品マスタ product が表示できるようになりました。</div>
<br/>
<h4>ファイルのインポート・エクスポート機能を追加する</h4>
<div>admin.pyにプラグインの読み込みを記述します</div>
<pre>from manga_server_django.admin.import_export import CommonImportExportSetting
</pre>
<div>ファイルのインポート・エクスポート機能を追加するためにadmin.pyにresourcesを追加します。<br/> 出力するmodelの項目名を指定します。<br/> 項目名の日本語名への変換や、取り込み時に外部キーで対応する項目名の指定などもできます。</div>
<pre>class ProductResource(resources.ModelResource):
# modelの項目名を指定。フィールド名・日本語表記の変換などもここで行う
product_id = Field(
attribute='product_id',
column_name='商品マスタID'
)
product_name = Field(
attribute='product_name',
column_name='商品名'
)
product_price = Field(
attribute='product_price',
column_name='商品価格'
)
# 外部キーのフィールドの場合はForeignKeyWidgetを記述する
publisher_name = Field(
attribute='publisher_name',
column_name='出版社名'
widget=ForeignKeyWidget(Publisher, 'publisher_name'),
)
author_name = Field(
attribute='author_name',
column_name='作者名'
widget=ForeignKeyWidget(Author, 'author_name'),
)
class Meta:
model = Product
# 出力設定
# ヘッダーに出力する文字列
headers = (
'商品マスタID',
'商品名',
'商品価格',
'出版社名',
'作者名',
)
# 出力するフィールド指定
fields = (
'product_id',
'product_name',
'product_price',
'publisher_name',
'author_name',
)
# 出力するフィールドの順番指定
export_order = (
'product_id',
'product_name',
'product_price',
'publisher_name',
'author_name',
)
# 入力設定
# 入力の主キーとなる項目の指定
import_id_fields = ('product_id',)
# 入力するフィールドの指定
import_order = (
'product_id',
'product_name',
'product_price',
'publisher_name',
'author_name',
)
# 更新のない行はスキップする
skip_unchanged = True
# 結果表示はスキップしない
report_skipped = False
</pre>
<div>ProductAdminに<code>ImportExportMixin</code>を追加。<br/> <code>resource_class</code>に先ほど作成した<code>ProductResource</code>を指定します。</div>
<pre>@admin.register(Product)
class ProductAdmin(ImportExportMixin, admin.ModelAdmin):
# 省略
resource_class = ProductResource
# 入力・出力するフォーマット。下記ではcsvとExcelファイルにしています。他にもtsv、YAML、XMLなども指定できます。
formats = [
base_formats.CSV,
base_formats.XLS,
base_formats.XLSX,
]
# 出力する文字コード指定
to_encoding = 'CP932'
# 入力する文字コード指定
from_encoding = 'CP932'
</pre>
<div>これで管理画面から商品マスタのインポート・エクスポートができるようになりました。</div>
<br/>
<h5>admin画面 上部の表示</h5>
<div style="display: block; margin-left: auto; margin-right: auto;" width="512px"><img alt="" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/kudou/screenshot-torico.manga-server.com-2023.09.19-13_14_35.png"/></div>
<h5>エクスポート画面</h5>
<div style="display: block; margin-left: auto; margin-right: auto;" width="512px"><img alt="" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/kudou/screenshot-torico.manga-server.com-2023.09.19-13_27_21.png"/></div>
<h5>インポート画面</h5>
<div style="display: block; margin-left: auto; margin-right: auto;" width="512px"><img alt="" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/kudou/screenshot-torico.manga-server.com-2023.09.19-13_30_30.png"/></div>
<br/>
<div>ちょっとしたプラグインの追加でdjango admin画面で大量のデータをより使いやすくできるので非常に便利です。</div>Raspberry Pi Pico W で Httpサーバ(microdot)とセンサーによるHTTPリクエスト機能を同時に稼働させる2022-12-11T08:40:53+00:002024-03-28T08:36:20+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/raspberry-pi-pico-w-microdot-http-server-and-request-async-await/<p></p>
<p>Raspberry Pi Pico W が発表されました。日本ではまだ未発売ですが、技適は取得されたようですので近いうちに国内販売がされそうです。</p>
<p>試しに、Webサーバ ( Microdot )とWebクライアント(urequest) を uasyncio で並列実行するコードを書きましたので、紹介します。</p>
<p>今回作成したコードや動作している動画は、Github で公開しています。</p>
<p><a href="https://github.com/ytyng/rpi-pico-w-webserver-and-client" target="_blank">ytyng/rpi-pico-w-webserver-and-client: Raspberry Pi Pico W webserver and client sample code</a></p>
<h2>Raspberry Pi Pico W とは</h2>
<p>コストパフォーマンスが高いマイクロコントローラです。カテゴリとしては Arduino 等に近く、今までの Raspberry Pi のように、Linux OS を動作させるようなマシンではありません。</p>
<p>RP2040 というラズベリーパイ財団が開発したチップのデモボードという位置づけとなります。</p>
<p>実際の商品開発では、Raspberry Pi Pico で製品のR&Dを行い、実際は RP2040 を搭載した製品として生産するという流れとなると思いますが、ホビーや SOHOでは Raspberry Pi Pico をそのまま使うことも多いと思います。</p>
<p>実際、当社でも Raspberry Pi Pico を用いてイベント用の機材を作る場合がありますが、<br/>RP2040 を使ったの製品を作るわけではなく、Raspberry Pi Pico をそのままケースに入れて使います。</p>
<p>MicroPython が動作するため、Python に慣れていれば開発は容易にできます。</p>
<p>商品名に W がつかない今までの機種は、ネットワーク機能はありませんでしたが、今回 Raspberry Pi Pico W となって無線チップが搭載され、コストパフォーマンスと使い勝手が最高の IoT デモボードとなりました。</p>
<p><img alt="" height="857" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/raspberry-pi-pico/img_7649.jpg" width="1140"/></p>
<p>↑ 左が Raspberry PI Pico, 右 が無線LAN チップが搭載された Raspberry Pi Pico W</p>
<h2>考えられる用途</h2>
<p>Raspberry Pi Pico W の用途で多く使われると考えられる用途は、</p>
<ul>
<li>接続させているデバイスのセンシング情報を元に、HTTP リクエストを発生させる</li>
<li>HTTP サーバを起動し、外部から HTTP リクエストを受け取って、接続されているデバイスを動作させる</li>
</ul>
<p>この2つが主なものとなると考えられます。</p>
<p>今回は、この2つを Raspberry Pi Pico W の中で同時に実行する方法を書きます。</p>
<h2>一通りのチュートリアル</h2>
<p>Raspberry PI の公式ページが提供している PDF が充実います。</p>
<p><a href="https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf" target="_blank">https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf</a></p>
<p>ただ、PDF なので少し読みにくいのと、Thonny に関しては言及されていないため、Mac や Windows を普段使われている方は、この PDF だけでなく、他のサイトで紹介されているような Thonny を使ったセットアップを行うと良いでしょう。</p>
<h2>ファームウェアの準備</h2>
<p><a href="https://micropython.org/download/rp2-pico-w/">https://micropython.org/download/rp2-pico-w/</a></p>
<p>上記 URL で、Raspberry Pi Pico 用の MicroPython ファームウェアの uf2 ファイルが入手できます。</p>
<p>最新版への直リンクはこちらです。 <a href="https://micropython.org/download/rp2-pico-w/rp2-pico-w-latest.uf2">https://micropython.org/download/rp2-pico-w/rp2-pico-w-latest.uf2</a></p>
<p>ファームウェアのファイルをダウンロードし、 Pico へコピーしてください。</p>
<p>W 対応でない uf2 ファームウェアは別に存在します。そちらを使った場合、Wi-fi の機能が使えませんのでご注意ください。</p>
<h2>コピー方法</h2>
<p>Pico の BOOTSEL ボタンを押したまま USB で PC に接続すると、PCが Pico をストレージとして認識します。</p>
<p>ダウンロードした uf2 ファイルを Pico にドラッグアンドドロップでコピーすると、自動的にファームウェアがロードされ、 Pico が再起動します。</p>
<h2>Wifi に接続する</h2>
<p>一番最初に、Wifi に接続する必要があります。<br/>SSID と パスワードが変数化されていれば、後は簡単なコードで接続が行えます。</p>
<p>接続用の関数を作っておくと便利で、他の方を見ても関数化しているようです。</p>
<p>StackOverflow の話題を見ると、Wifi との接続は main.py の中でやらずに boot.py の中でやったほうがいい、というコメントをいくつか見かけましたが、私は 開発のしやすさから main.py の中で行うようにしています。</p>
<p>Wi-fi に接続するコード</p>
<p>https://github.com/ytyng/rpi-pico-w-webserver-and-client/blob/main/network_utils.py</p>
<pre>import rp2<br/>import network<br/>import uasyncio<br/>import secrets<br/><br/><br/>async def prepare_wifi():<br/> """<br/> Prepare Wi-Fi connection.<br/> https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf # noqa<br/> """<br/> # Set country code<br/> rp2.country(secrets.COUNTRY)<br/><br/> wlan = network.WLAN(network.STA_IF)<br/> wlan.active(True)<br/><br/> wlan.connect(secrets.WIFI_SSID, secrets.WIFI_PASSWORD)<br/><br/> for i in range(10):<br/> status = wlan.status()<br/> if wlan.status() < 0 or wlan.status() >= network.STAT_GOT_IP:<br/> break<br/> print(f'Waiting for connection... status={status}')<br/> uasyncio.sleep(1)<br/> else:<br/> raise RuntimeError('Wifi connection timed out.')<br/><br/> # CYW43_LINK_DOWN (0)<br/> # CYW43_LINK_JOIN (1)<br/> # CYW43_LINK_NOIP (2)<br/> # CYW43_LINK_UP (3)<br/> # CYW43_LINK_FAIL (-1)<br/> # CYW43_LINK_NONET (-2)<br/> # CYW43_LINK_BADAUTH (-3)<br/><br/> wlan_status = wlan.status()<br/><br/> if wlan_status != network.STAT_GOT_IP:<br/> raise RuntimeError(<br/> 'Wi-Fi connection failed. status={}'.format(wlan_status))<br/><br/> print('Wi-fi ready. ifconfig:', wlan.ifconfig())<br/> return wlan</pre>
<h2>接続させているデバイスのスイッチ(センサー)情報を元に、HTTP リクエストを発生させる</h2>
<p><a href="https://picockpit.com/raspberry-pi/raspberry-pi-pico-w-wi-fi-doorbell-tutorial-http-requests-ifttt/" target="_blank">Raspberry Pi Pico W Wi-Fi Doorbell tutorial (HTTP requests & IFTTT) — PiCockpit | Monitor and Control your Raspberry Pi: free for up to 5 Pis!</a></p>
<p>こちらの方が開発されているドアベルのコードが参考になります。<br/>Youtube 動画もあってわかりやすいです。</p>
<h4>urequests</h4>
<p>Python でよく使う、<code>requests</code> ライブラリに変わり、MicroPython では似たような使い勝手の <code>urequests</code> ライブラリを使うことができます。</p>
<p>ネットワーク接続が確立されていれば、あとは <code>urequests.get(...)</code> 等で簡単にリクエストが発行できます。</p>
<p>Thonny の tools -> Manage packages からインストールできます。</p>
<h2>HTTP サーバとして稼働させる</h2>
<h3>ソケットをそのまま使って簡易的な HTTP サーバにする</h3>
<p>こちらの記事が参考になりました。大変わかりやすく日本語で説明されているので、Pico の初学者にもおすすめします。</p>
<p><a href="https://kotamorishita.com/raspberry-pi-pico-w-wireless-led-control/" target="_blank">Raspberry Pi Pico W で無線Lチカ</a></p>
<p>リンクされている、mimoroni 社のコードは、 Pico 用の拡張されたファームウェア</p>
<p><a href="https://github.com/pimoroni/pimoroni-pico/releases">https://github.com/pimoroni/pimoroni-pico/releases</a></p>
<p>や、 Pico W 用の各種ユーティリティコードがあり、開発の参考になります。</p>
<p><a href="https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/examples/pico_inky/show_ip_address" target="_blank">このコードの HTTP サーバの部分</a>は、TCP ソケットをそのまま使い、リクエスト本文の中のパス名と文字列一致して判定してい分岐を行っています。</p>
<p><a href="https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf" target="_blank">Raspberry Pi 公式のチュートリアルPDF</a>でもその方式で行っていました。</p>
<p>規模が小さいようであれば十分だと思いますが、HTTP ヘッダーを扱いたい場合や、少し規模を拡張したい場合はこの形では難しいでしょう。</p>
<h3>Microdot を起動する</h3>
<p>Flask や Bottle、fastApi が動けば良いのですが、現状は動作しません。<br/>代わりに、<strong>Microdot</strong> という ウェブフレームワークがあり、使い勝手としては Flask や Bottle によく似ていて大変勝手が良いです。</p>
<p>こちらを使って ウェブサーバを起動してみます。</p>
<p><a href="https://microdot.readthedocs.io/en/latest/" target="_blank">Microdot</a></p>
<p>Thonny で Tools -> Manage packages からインストールすることができます。</p>
<p>ネットワーク接続が確立したら、</p>
<pre>app = Microdot()<br/><br/>@app.get('/')<br/>async def _index(request):<br/> return 'Microdot on Raspberry Pi Pico W'<br/><br/>app.run(port=80)</pre>
<p>このような親しみやすいコードで HTTP サーバが起動します。</p>
<h2>ウェブサーバとセンサーリクエストを同時に使う</h2>
<p>Raspberry Pi Pico は、通常シングルスレッド動作です。(ちなみにCPUはデュアルコアです)</p>
<p>一応、 _threading という疑似スレッドができるライブラリはありますが、処理によっては本体が暴走したり固まることが多く、かなりおすすめしません。<br/>暴走すると、最悪、何度もファームウェアのリセットをするこになり、開発体験は良くありません。</p>
<p>代わりに、 asyncio を使ったコルーチン処理を標準で行うことができ、こちらは安定して動作しますので、 Pico で開発する際は、基本的にメソッドはコルーチンで書くのをおすすめします。</p>
<p>Pico は、待機ループで sleep を使うことも多いですし、コルーチンと相性が良いと感じます。</p>
<p>Microdot も非同期対応の起動ができるものが既に開発されています。</p>
<p>Pico 上の MicroPython でのコルーチンは、通常の Python にビルトインされている asyncio を使うのではなく、<br/>uasyncio というパッケージを使います。</p>
<p>Pico 用の uf2 ファームウェアに含まれていますので、別途新たなインストールは必要ありません。</p>
<p>例えば下記のようなコードで、asyncio が有効な処理を開始することができます。</p>
<pre>import uasyncio<br/><br/>async def main():<br/> uasyncio.create_task(any_async_method())<br/> await other_async_method()<br/><br/><br/>if __name__ == '__main__':<br/> uasyncio.run(main())</pre>
<p>Pico の起動後、無線 LAN に接続した後は、スイッチ押下待機のループと、Micorodot の起動を<br/>両方ともコルーチンで書くことで、無理なく並列動作をさせることができます。</p>
<p>実際に動作するコードは Github で公開しています。</p>
<p><a href="https://github.com/ytyng/rpi-pico-w-webserver-and-client/blob/main/main.py" target="_blank">rpi-pico-w-webserver-and-client/main.py at main · ytyng/rpi-pico-w-webserver-and-client</a></p>
<p>メインのコードとしてはこのようになります。</p>
<pre>"""<br/>Raspberry Pi Pico Web Server with Microdot and Switch Sample Code.<br/>Pin 14 is used for switch input.<br/>"""<br/>import machine<br/>import urequests<br/>import network_utils<br/>from microdot_asyncio import Microdot<br/>import uasyncio<br/><br/><br/>async def switch_loop():<br/> """<br/> Switch listener loop<br/> Pin 14 is used for switch input.<br/> When press switch, send request to http web server.<br/> """<br/> print('start switch_loop')<br/> switch_pin = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)<br/><br/> while True:<br/> current_state = switch_pin.value()<br/> if current_state:<br/> # Change the URL to your own server, IFTTT, Slack, etc.<br/> response = urequests.get('https://example.com/')<br/> print(response.content)<br/> response.close()<br/> await uasyncio.sleep(2)<br/> else:<br/> await uasyncio.sleep(0.1)<br/><br/>async def run_web_server():<br/> """<br/> Start microdot web server<br/> https://microdot.readthedocs.io/en/latest/index.html<br/> """<br/> app = Microdot()<br/> led_pin = machine.Pin('LED', machine.Pin.OUT)<br/><br/> @app.get('/')<br/> async def _index(request):<br/> return 'Microdot on Raspberry Pi Pico W'<br/><br/> @app.get('/led/<status>')<br/> async def _led(request, status):<br/> """<br/> /led/on : LED ON<br/> /led/off : LED OFF<br/> """<br/> if status == 'on':<br/> led_pin.on()<br/> return 'LED turned on'<br/> elif status == 'off':<br/> led_pin.off()<br/> return 'LED turned off'<br/> return 'Invalid status.'<br/><br/> print('microdot run')<br/> app.run(port=80)<br/><br/><br/>async def main():<br/> wlan = await network_utils.prepare_wifi()<br/> print('LED ON: http://{}/led/on'.format(wlan.ifconfig()[0]))<br/><br/> uasyncio.create_task(switch_loop())<br/> await run_web_server()<br/><br/><br/>if __name__ == '__main__':<br/> uasyncio.run(main())</pre>Web ページの動作検証のためのボットスクリプトを Windows 上で作る2022-08-28T08:30:24+00:002024-03-28T08:35:10+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/python-web-bot-develop-on-windows/<p></p>
<p>この文書は、開発経験の無いチームがウェブアプリケーションの動作検証の責任を持つケースで、検証を簡単なプログラムで行うアプローチについての手法を解説しています。</p>
<p>Webアプリケーションの動作検証の際、手動で実行する以外にプログラムで検証すると便利です。開発者であればは検証コードを書くのは簡単ですが、開発経験の無い方はどこから始めたらいいかわからないと思いますので、比較的用意にスクリプトに入門できるように紹介します。OS は Windows を対象としています。</p>
<p><span style="color: #800000;">本記事で紹介しているようなプログラムによるリクエストは悪意の有無にかかわらず、不正アクセス禁止法での不正アクセスとみなされたり、電子計算機損壊等業務妨害罪等に問われる可能性があります。実際にリクエストを行う場合は、自社の管理する、許可されたサーバに対してのみ行うようにしてください。</span></p>
<h2>HTTP の リクエスト・レスポンスの仕組みを知る</h2>
<p>まずは、ウェブサーバと通信している HTTP のリクエストがどのようなものか知ることが必要です。この知識があいまいなままだとボットのスクリプトは書けません。</p>
<p>まずは、ウェブブラウザに搭載されている開発者ツールを使ってリクエストやレスポンスを観察するのが、良い勉強になります。</p>
<p>ウェブサーバに対して、</p>
<ul>
<li>どの URL に対して</li>
<li>どのHTTP メソッドで (GET, POST, HEAD, PUT 等)</li>
<li>どのような HTTP ヘッダーで
<ul>
<li>cookie</li>
<li>referer</li>
<li>user-agent</li>
</ul>
</li>
<li>どのようなリクエスト本文(body)で</li>
</ul>
<p>以上を意識して、ブラウザの行っているリクエストをスクリプトで再現できれば、どのようなクライアントを使おうが、ウェブサーバからはブラウザでのリクエストと同じようにレスポンスが返ってきます。</p>
<h3>Chrome のデベロッパーツールの使い方</h3>
<ol>
<li>Google Chrome を起動してください。</li>
<li>Shift + Ctrl + I を押してください。<br/>右側にデベロッパーツールが表示されます。</li>
<li>デベローパーツールの上部のタブで、Network を選択してください。</li>
<li>ブラウザの URL 欄に適当にページを打ち込み、ページを表示されてください。</li>
<li>リクエスト一覧が表示されますので、適当なリクエストをクリックして選択してください。</li>
<li>Headers にリクエストヘッダ、レスポンスヘッダ<br/>Payload にリクエスト本文<br/>Response にレスポンス本文<br/>が確認できます。<br/>特に、Headers のリクエストヘッダでどのようなヘッダを送っているか、確認してください。</li>
</ol>
<p>Headers の中でも、cookie は特に注目してください。cookie の扱いはボット作成の中で最上位に重要な項目です。不安があれば、他のサイトや書籍を参考に学習してください。</p>
<p><img alt="" height="814" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-08.png" width="707"/></p>
<p>また、 cookie の内容は認証情報を含むため、サイトのログインパスワードと同じぐらい重要な秘密情報です。安易にコピーは行わず、また絶対に他者に教えないようにしてください。ブラウザの cookie を格納している場所は安全ですが、他の場所にコピペすると漏洩リスクとなり、ログイン権限を奪われる危険性があります。</p>
<h3>HTTPヘッダとクッキーの学習ができるサイト</h3>
<p>HTTP ヘッダ、クッキーについての概要は、とほほ先生のサイト、わわわ先生のサイト、あと Wikipedia を読めば理解できます。</p>
<h4>HTTPヘッダ</h4>
<ul>
<li><a href="https://www.tohoho-web.com/ex/http.htm" target="_blank">HTTP入門 - とほほのWWW入門</a></li>
<li><a href="https://wa3.i-3-i.info/word1844.html" target="_blank">HTTPリクエストヘッダとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典</a></li>
<li><a href="https://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol" target="_blank">Hypertext Transfer Protocol - Wikipedia</a></li>
</ul>
<h4>Cookie</h4>
<ul>
<li><a href="https://www.tohoho-web.com/wwwcook.htm" target="_blank">とほほのCookie入門 - とほほのWWW入門</a></li>
<li><a href="https://wa3.i-3-i.info/word1725.html" target="_blank">クッキー (cookie)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典</a></li>
<li><a href="https://ja.wikipedia.org/wiki/HTTP_cookie" target="_blank">HTTP cookie - Wikipedia</a></li>
</ul>
<h2>ブラウザを手動で操作する以外での HTTP リクエストを行う方法</h2>
<p>HTTP リクエストは、結局は特定の文字をサーバに送るだけですので、様々なクライアントで行うことができますが、よく使われるものを紹介します。</p>
<h3>Postman</h3>
<p><a href="https://www.postman.com/">https://www.postman.com/</a><br/>フリーの Windows クライアントがあります。GUI でリクエストを構築することができますし、スクリプトによる制御処理も書けます。有用なツールですが、最初は機能が多く複雑なため戸惑うかもしれません。</p>
<h5>長所</h5>
<ul>
<li>GUI で完結する</li>
<li>大量のテストリクエストの管理がしやすい</li>
<li>署名の計算等、複雑な計算を伴う処理も行える。</li>
</ul>
<h5>短所</h5>
<ul>
<li>複雑なため習得が難しい</li>
<li>単純なリクエストを出すだけであれば冗長</li>
</ul>
<h3>curl</h3>
<p>mac や linux を使う方には定番のコマンドラインツールです。Windows 10 からは標準でインストールされています。</p>
<ul>
<li><a href="https://ascii.jp/elem/000/004/021/4021036/" target="_blank">ASCII.jp:Windows 10で標準で用意されるようになったcurlを使ってみる (1/2)</a></li>
<li><a href="https://zenn.dev/o2z/articles/f4077e72efa455" target="_blank">Windows10に標準搭載されたcurlコマンドを使ってWEBサイトの死活確認をする簡単なバッチファイル</a></li>
</ul>
<h5>長所</h5>
<ul>
<li>インストール済みなのですぐに使える</li>
<li>単純なリクエストを1発出すだけなら一番適している</li>
</ul>
<h5>短所</h5>
<ul>
<li>計算を伴う順次リクエストには向かない</li>
</ul>
<h3>Selenium や Puppeteer</h3>
<p>プログラムで Chrome などのブラウザを実際に起動し、自動操作するための Selenium や Puppeteer といったツールがあります。</p>
<p>ブラウザを起動するため、ページ内で JavaScript を豊富に扱うページも自動操作し、検証することができます。</p>
<p>最近のウェブサイトは、React や Vue といった JavaScript を用いて表現するサイトが多くなってきており、その場合は Postman, curl, 後述するシンプルなスクリプトでは十分な検証ができない場合がありますので、 ページ内の JavaScript の動作検証が必要になる場合実際のブラウザを実行させる以外に無く、自動操作するには Selenium や Puppeteer を使うしかありません。</p>
<p>扱うには高度なプログラミング知識が必要ですので、今回は言及しません。</p>
<h5>長所</h5>
<ul>
<li>ブラウザでの JavaScript の実行が必要であればこれ一択</li>
<li>表示レイアウトの確認にも使える</li>
</ul>
<h5>短所</h5>
<ul>
<li>実行環境の構築が難しくて、手間がかかる。</li>
<li>他の、単純なリクエストを出す方式に比べると遅い。</li>
<li>様々な要因により安定させて動作させるのは難しい。業務レベルで使うには高い技術が必要。</li>
</ul>
<h3>Rest Client ( .http)</h3>
<p>ドットエイチティーティーピーファイルを作り、その記述したリクエストを簡易に何度も再現させることができます。VSCode や JetBrains エディタの機能として扱うことができ、記述が簡単で読みやすいため私は(当社内でも)かなり使います。</p>
<ul>
<li><a href="https://qiita.com/toshi0607/items/c4440d3fbfa72eac840c" target="_blank">VS Code上でHTTPリクエストを送信し、VS Code上でレスポンスを確認できる「REST Client」拡張の紹介 - Qiita</a></li>
</ul>
<h5>長所</h5>
<ul>
<li>習得が容易</li>
<li>スクリプトの構文が容易で書きやすく読みやすい</li>
<li>スクリプトのチーム内共有が容易</li>
<li>レスポンスへの簡易的なテストを行うことができる</li>
</ul>
<h5>短所</h5>
<ul>
<li>計算を伴う複雑な逐次処理はできない</li>
</ul>
<h3>プログラミング言語でスクリプトを組む</h3>
<p>Python, PHP, Javascript, Ruby などで、既にある便利な HTTP クライアントライブラリを使ってスクリプトを組む方法です。<br/>今回の記事ではこちらを今回紹介します。</p>
<h5>長所</h5>
<ul>
<li>条件分岐を含む複雑な制御処理が得意</li>
<li>複数セッションによる並列リクエストを再現したい場合は一択</li>
<li>完全無人での自動実行が容易</li>
<li>書いたコードの再利用が容易</li>
<li>処理結果の外部ツールへの連携が柔軟に行える</li>
</ul>
<h5>短所</h5>
<ul>
<li>環境構築が手間</li>
<li>単発のリクエストを検証したい場合は RestClient や Curl, Postman と比べても冗長</li>
</ul>
<h2>Windows に Python 実行環境をインストールする</h2>
<h3>Microsoft Store からの Python のインストール</h3>
<p><span>Microsoft</span> Store で Python パッケージが提供されるようになり、昔と比べて環境構築が楽になりました。</p>
<p>Microsoft が提供している、初心者向けの Python の開発ガイドが良くできています。この流れにそって進めれば問題なく進められますので、こちらも参考にしてください。</p>
<ul>
<li><a href="https://docs.microsoft.com/ja-jp/windows/python/beginners" target="_blank">Windows での Python (初心者向け) | Microsoft Docs</a></li>
</ul>
<p>Microsoft Store のアプリを開き、 Python を検索して Python 3.10 をインストールします。</p>
<p><img alt="" height="626" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-01.png" width="533"/></p>
<p>インストール後、念の為再起動を行い、その後コマンドプロンプトを起動して python と打ち込んで、 python が起動するか確かめてください。</p>
<h3>Python 公式サイトからの Python のインストール</h3>
<p>コマンドプロンプトで、 python と打ち込んで<strong> python が起動しないようであれば</strong>、Microsoft Store からインストールした Python はアンインストールし、 Python の公式サイトから Windows 版のインストーラパッケージをダウンロードしてインストールしてください。</p>
<p>その際、PATH を追加編集するかのオプションが表示されるので、チェックを入れてください。</p>
<p><a href="https://www.python.org/downloads/" target="_blank">https://www.python.org/downloads/</a><a href="https://www.python.org/downloads/"></a></p>
<p><img alt="" height="638" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-02.png" width="1347"/></p>
<h3>requests ライブラリのインストール</h3>
<p>Python の インストールが完了したら、ウェブを操作するボットの作成に必須ともいえる、 requests ライブラリをインストールします。</p>
<p>コマンドプロンプトや PowerShell で、</p>
<pre>python -m pip install requests</pre>
<p>と入力すると、インストールが完了します。</p>
<p><img alt="" height="409" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-03.png" width="891"/></p>
<h2>Visual Studio Code のインストール</h2>
<p>エディタは Visual Studio Code を使います。</p>
<p>Microsoft Store から Visual Studio Code を検索してインストールしてください。</p>
<h2>プロジェクトフォルダの準備</h2>
<p>Windows の、ドキュメントフォルダの下にtest-bot フォルダを作ってください。</p>
<p>VSCode を起動し、 File -> Open Folder で test-bot フォルダを開いてください。</p>
<p>フォルダを開いたら、左側のペインで右クリックし、 New File から first_bot.py というファイルを作ってください。</p>
<p><img alt="" height="670" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-04.png" width="589"/></p>
<p>作成後、右下に「インタープリターを選択」と表示されているようであればクリックしてください。Microsoft Store からインストールした Python が、おすすめに表示されているので選択します。</p>
<p>右下に CRLF と表示されている箇所は、改行コードの設定が表示されています。CRLF は一般的ではないため、クリックして LF に変更しておきます。</p>
<p>Python の機能拡張のインストールがおすすめされると思いますので、Python, Pylance の機能拡張をインストールします。</p>
<h2>スクリプトを書く</h2>
<h3>レスポンス本文を表示するだけのスクリプト</h3>
<pre>import requests<br/>response = requests.get('https://www.torico-corp.com/')<br/>print(response.text)</pre>
<p>これは、 <a href="https://www.torico-corp.com/" target="_blank">https://www.torico-corp.com/</a> のレスポンス本文を表示するだけの単純なプログラムです。<br/>書いたら、右上の ▶ ボタンを押して、実行させてください。<br/>出力結果がずらっと表示されます。</p>
<p><img alt="" height="661" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-05.png" width="1022"/></p>
<h4>requests ライブラリについてのドキュメント</h4>
<ul>
<li><a href="https://requests-docs-ja.readthedocs.io/en/latest/" target="_blank">Requests: 人間のためのHTTP — requests-docs-ja 1.0.4 documentation</a></li>
</ul>
<h3>レスポンスの経過時間とステータスコードを表示するスクリプト</h3>
<pre>import requests<br/>response = requests.get('https://www.torico-corp.com/')<br/>print('経過時間 {}ms'.format(response.elapsed.microseconds / 1000))<br/>print('ステータスコード {}'.format(response.status_code))</pre>
<p>このスクリプトは、レスポンスの応答時間とステータスコードをコンソールに表示します。</p>
<p>Python に慣れてきたら、結果をファイルに記録するように改修することで、簡易的な負荷監視などに応用できます。</p>
<p><img alt="" height="660" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/yotsuyanagi/python-bot/python-bot-06.png" width="1024"/></p>
<h3>サイトの検索結果ページからを解析するスクリプト</h3>
<p>Webサイトの検索ページにリクエストを行い、結果の HTML をパースしてコンソールに表示するスクリプトです。</p>
<p>HTMLをプログラムで扱えるように解析するために、 BeautifuSoup というライブラリをインストールします。</p>
<h4>BeautifulSoup のインストール方法</h4>
<p>コマンドプロンプトで</p>
<pre>python -m pip install beautifulsoup4</pre>
<p>でインストールできます。</p>
<h4>BeautifulSoup の解説記事</h4>
<ul>
<li><a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" target="_blank">Beautiful Soup Documentation — Beautiful Soup 4.9.0 documentation</a></li>
<li><a href="https://office54.net/python/scraping/beautifulsoup4-html" target="_blank">【Python】スクレイピング:BeautifulSoup4によるHTML解析 | OFFICE54</a></li>
<li><a href="https://atmarkit.itmedia.co.jp/ait/articles/1910/18/news015.html" target="_blank">[Python入門]Beautiful Soup 4によるスクレイピングの基礎:Python入門(1/2 ページ) - @IT</a></li>
</ul>
<h4>コード</h4>
<pre>import requests<br/>from bs4 import BeautifulSoup<br/>response = requests.get('https://tech.torico-corp.com/search/?q=docker')<br/>soup = BeautifulSoup(response.content, features='html.parser')<br/><br/>for h2 in soup.find_all('h2'):<br/> a = h2.find('a')<br/> if not a:<br/> continue<br/> print(a.text)<br/> print(a['href'])</pre>
<p>上記スクリプトは、TORICO の技術開発ブログを「docker」で検索し、出てきた記事のタイトルとリンクURL を表示しています。</p>
<h3>メールアドレスとパスワードでウェブサイトにログインする</h3>
<p>最後に、メールアドレスとパスワードでログインをするスクリプトの雛形を記載します。</p>
<p>requests ライブラリは、クッキー管理を行うことのできる Session というしくみがありますので、それを使います。</p>
<p>解説記事</p>
<ul>
<li><a href="https://requests-docs-ja.readthedocs.io/en/latest/user/advanced/#session-objects" target="_blank">Advanced Usage — requests-docs-ja 1.0.4 documentation</a></li>
</ul>
<p>検証するサイトによりますが、多くの場合は、ログイン時に「<strong>CSRFトークンの検証</strong>」と「<strong>Refererヘッダの検証</strong>」<br/>「<strong>User-Agent が悪質なボットでないかの検証</strong>」を行っていると思いますので、そこを考慮してスクリプトを作れば、ログインが行えるはずです。</p>
<p><span style="color: #800000;">下記のような自動ログインのスクリプトは、必ずご自身が権限を持つサーバにのみ行うようにしてください。他者のサーバに行うと罪に問われる可能性があります。</span></p>
<p>URL 等は架空のものです。</p>
<pre>import requests<br/>from bs4 import BeautifulSoup<br/><br/># session を作る<br/>s = requests.session()<br/># User-Agent を設定する場合<br/>s.headers['User-Agent'] = 'Tester Python Bot'<br/><br/># ログインフォームを取得する<br/>response = s.get('https://example.com/login-form/')<br/><br/># HTTP のステータスコードに異常が無いか確認<br/>response.raise_for_status()<br/><br/># ログインフォームをパースする<br/>soup = BeautifulSoup(response.content, features='html.parser')<br/><br/># パースしたログインフォームから CSRF トークンを取得する<br/>csrf_token = soup.find('input', {'name': 'csrftoken'})['value']<br/><br/># ユーザー名とパスワードをいれてログインフォームを送信する。<br/>response = s.post(<br/> 'https://example.com/login-form/',<br/> data={<br/> 'email': 'tester@example.com',<br/> 'password': 'MY_AWESOME_PASSWORD',<br/> # 先程取得した CSRF トークンを付与<br/> 'csrftoken': csrf_token<br/> },<br/> headers={<br/> # Referer を付与<br/> 'Referer': 'https://example.com/login-form/',<br/> })<br/><br/># HTTP のステータスコードに異常が無いか確認<br/>response.raise_for_status()<br/><br/># ログイン後の URL が正しいものであるか確認<br/>assert response.url == 'https://example.com/mypage/'<br/><br/># この時点で、セッション s はログイン済みの状態なので、<br/># マイページ等をリクエストすることが可能<br/>response = s.get('https://example.com/mypage/myprofile/')</pre>ジュニアエンジニアの業務内容2022-04-28T09:52:01+00:002024-03-28T06:25:04+00:00高津のぶひろhttps://tech.torico-corp.com/blog/author/TakatsuNobuhiro/https://tech.torico-corp.com/blog/%E6%96%B0%E5%8D%92%EF%BC%91%E3%83%B6%E6%9C%88%E7%9B%AE%E3%81%AE%E6%A5%AD%E5%8B%99%E5%86%85%E5%AE%B9/エンジニアの高津です。<br/>今回はこの1ヶ月でどのような業務を行ったのか紹介していきたいと思います。<br/>TORICOにエンジニアとして入社を検討している人に少しでも参考になれば幸いです。<br/>
<h2>主な業務内容</h2>
<p>自分はコーマス開発部で、<a href="https://www.mangazenkan.com/" target="_blank">漫画全巻ドットコム</a>の開発をメインでやっています。</p>
<p><strong>簡単なバグ(UI)の修正</strong>と<strong>漫画全巻ドットコムのリニューアル</strong>の大きく2つに分けられます。</p>
<h3>簡単なバグ(UI)の修正</h3>
<p>こちらは数時間で終わるような簡単なUIの修正(改修)で入社して3日後にはプルリクエストを出していました。</p>
<ul>
<li>アイコン(font-awesome)が正しく表示出来るようにする</li>
<li>個人情報同意フォームの改修</li>
<li>電話番号記入欄のに数字が4文字入るようにする<br/>などの業務を行いました。</li>
</ul>
<h3>漫画全巻ドットコムのリニューアル</h3>
<p>漫画全巻ドットコムは10年以上続く歴史のあるサービスで最初はPHPで作られていました。<br/>メンテナンス性に問題があったのでDjango+Nuxt.jsにリプレイスしています。<br/>今回自分は<a href="https://www.mangazenkan.com/e-books/new.php" target="_blank">電子新着ページ</a>+<a href="https://www.mangazenkan.com/e-books/sale/" target="_blank">電子割引ページ</a>をリニューアルしました。(ブログを書いている時点で実装は終わっていますがレビューが終わってないので本番にはまだ反映されていません)<br/>Nuxt(フロントエンド)は先輩方が作ってくれた雛形を軽く修正して利用出来るのでDjnago(バックエンド)の実装がメインでした。<br/>今回はその中でも難しかったポイントをいくつか列挙したいと思います。</p>
<h4>キャッシュを効かす</h4>
<div>同じ値を取得して返すだけなのに毎回SQLを叩くのは無駄なのでkye-value型のNoSQLであるredius(メモリ)に一定時間値を保管し、値がキャッシュされていなければSQL等を叩く処理を行います。(keyは引数、valueは返り値で保存)<br/>ページ単位(view)単位でキャッシュする方法とクラスメソッド単位でキャッシュする方法の2パターンあります。<br/>カテゴリー別の作品数を取得する処理はページ間(異なるurl)でも共通したしょりなのでクラスメソッド単位でキャッシュする必要があり、少々手こずりました。<br/><br/></div>
<h4>SQLの実行回数(IO)を極力少なくしパフォーマンスをあげる</h4>
<p>今回一番苦戦しましたポイントです。<br/>ただ実装するだけであればすぐ終わったのですが、最初の実装ではSQLを12回叩いてしまっていたのでリファクタリングする必要がありました。(俗に言うN+1問題が発生していました)<br/>こちらはやり方を先輩方にご教示頂き、MySQLにだけサポートされているconcat関数をraw_queryで使い1回で取得することに成功しました。<br/>実際には以下のようなSQLに落ち着きました。</p>
<pre><span>select GROUP_CONCAT(sample_id) AS ids, group_type from<br/></span><span>dtb_sample WHERE aggregate_type=<br/></span><span>'%s' AND product_type = 2<br/></span><span>group by group_type ORDER BY sort_key;</span></pre>
<h4>テスト</h4>
<p>TORICOの開発ではただ動くものを作るだけでなくその後の保守運用のことも考慮して単体テスト、統合テストも書くように徹底されています。<br/>特にDB設計が少々複雑なこともありテストデータを作るところはかなり苦戦しました。<br/><br/>具体的には以下のようなことをテストしました。</p>
<ul>
<li>各URLにgetして正しい値やstatusが返ってくるか</li>
<li>各メソッドのすべての条件分岐において正しく動作するかどうか</li>
</ul>
これらのテストを書くことでテストのしずらいメソッドが見つかり、それをリファクタリングすることで保守性の高いコードに改善出来ます。<br/><br/>また、仕様が分からない人がみても理解できるようにWhy「なぜこの処理を書くのか?」を極力書くように意識しました。<br/>
<h2>まとめ</h2>
<p>如何でしたでしょうか?</p>
<p>今現在23卒の<a href="https://www.wantedly.com/projects/805560" target="_blank">エントリー</a>を受け付けています。<br/>少しでも興味を持ってくれた人は是非応募して頂けると幸いです。</p>実務経験で出会った便利なあれこれ2021-09-27T17:02:16+00:002024-03-28T02:50:06+00:00清瀬遼平https://tech.torico-corp.com/blog/author/r.kiyose/https://tech.torico-corp.com/blog/%E5%AE%9F%E5%8B%99%E7%B5%8C%E9%A8%93%E3%81%A7%E5%87%BA%E4%BC%9A%E3%81%A3%E3%81%9F%E4%BE%BF%E5%88%A9%E3%81%AA%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C/<p>新卒エンジニアとして半年間働いてきて、現場でさまざまなことを勉強させていただきました。その中でも、もっと早く知っておきたかった便利なツール、Python の書き方など、幅広いあれこれを記事にしたいなと思います。</p>
<p>あれこれ、というざっくりとした括りになってしまっているのでまずは紹介したいものを示しておきたいと思います。</p>
<ul>
<li>git submodule</li>
<li>Fabric3</li>
<li>Flake8</li>
<li>Python
<ul>
<li>型ヒント</li>
<li>整形文字列 f-string</li>
</ul>
</li>
</ul>
<h3>git submodule</h3>
<p>git submodule とは、git のレポジトリをサブモジュール化して複数のプロジェクトで共有することができる機能のことです。これを使うことで一度開発したコードを別のプロジェクトで簡単に再利用できます。例えばTORICOでは、 <a href="https://id.torico-corp.com/" title="TORICO-ID">TORICO-ID</a> のソーシャルログイン機能や決済機能だけでなく、ユーティリティ関数などを Django, Vue, Flutter それぞれのフレームワークでまとめたレポジトリが用意されてあります。</p>
<p>導入も至ってシンプルです。プロジェクト内で</p>
<p><code>git submodule add <リポジトリURL> <インストールパス></code></p>
<p>たったこれだけで、今まで開発してきたコードを再利用することができます。わざわざプロジェクト間でコピペをしてくる必要はありません。さらに、git submodule はバージョン管理もしてくれます。サブモジュールを更新しても影響があるのはそのプロジェクト内だなので安心してサブモジュールの改良をすることができます。</p>
<p>1点注意として、submodule はメインのプロジェクト内で git clone や git pull を行っても自動で更新されることはありません。そのため、</p>
<p><span color="#e83e8c" style="color: #e83e8c;"><span>git submodule update (初回のみ -i オプションで初期化)</span></span></p>
<p>コマンドを忘れずに実行する必要があります。</p>
<h3>Fabric3</h3>
<p>こちらは Python のモジュールの一つで、所定のアクションをコマンド一つで呼び出すことができます。例えば、デプロイするときの一連の流れを Python コードにしておくことで、ターミナルに fab deploy と入力するだけでその流れを自動で行ってくれるようになります。流れは以下のように記述します。</p>
<p><code>env.hosts = ['app1.sample.com', 'app2.sample.com']<br/>def deploy():<br/></code><code> # 通知などを行う<br/></code><code> slack_announce('deploy')</code><br/><code> with cd ('/var/src'):</code><br/><code> run('git checkout master')</code><br/><code> run('git pull origin master')</code><br/><code> run('git submodule update')</code></p>
<p>このように一連の流れをコードとして残しておくことで、それをコマンド一つで呼び出すことができるようになります。さらに、新規に加入したメンバーでも簡単にデプロイができるというメリットがあります。その環境に慣れていない人が、例えば git submodule などのコマンドを忘れる心配もありません。また、私のような実務経験の無い人でもコードを確認することで流れを理解することができます。</p>
<p>TORICO では上記の git submodule を使ってほぼ全てのプロジェククトに deploy, ssh, flake8(後述), dsh (docker 環境にSSH)のコマンドが用意されています。</p>
<h3>Flake8</h3>
<p>こちらも Python のモジュールで、自動でコードレビューをしてくれます。導入は pip でインストールするだけです。実行は flake8 コマンドとオプションをターミナルにします。</p>
<p><code>flake8 <span>--exclude="*migrations/*,venv/*,.venv/*,~* .</span></code></p>
<p><code><img alt="flake8 result" height="202" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/TORICO-ID/flake8.png" width="1096"/></code></p>
<p>すると、このようにコードレビューをして規約に反する部分を教えてくれます。規約コードも教えてくれるため、わからない部分は検索できるようになっています。また、どうしても諸事情で変更できない場合は</p>
<p><code>from django.contrib import admin # NOQA: F401</code></p>
<p>とコメントをつけることで、指定した規約コードの違反を行単位で無視させることもできます。</p>
<p>TORICO では毎回オプションを入力するのは面倒なため、上の Fabric3 を使って実行を簡単にしています。</p>
<h3>Python 型ヒント</h3>
<p>Python3.5 から導入された機能で型ヒントというものがあります。これは引数や帰り値の型をコードに書くことで可読性を向上させることができる機能です。</p>
<p><code>def hello(name: str) -> str:<br/></code><code> return f'hello, {name}.'</code></p>
<p>型ヒントはあくまでも補助的な役割のため、宣言とは異なる型を渡したところでエラーになることはありません。ただ、PyCharmでは型ヒントとは違う型を渡すと警告をしてくれます。可読性が飛躍的にあがるので、ぜひとも書くことを癖にしたいなと思っています。</p>
<h3>Python F文字列</h3>
<p>上の例でもしれっと使っていましたが、F文字列を使用することで .format 部分を短縮することができます。さらに、f文字列では変数だけでなく式なども使用できます。<code><br/></code></p>
<p><code>print({a} + {b} = {a + b}) # format では a + b はエラーになる</code></p>
<p>format で記述するとコードが長くなる傾向があるので、スッキリと書けるf文字列は積極的に使用しています。</p>
<h3>最後に</h3>
<p>今回は現場で知った便利なあれこれを記事にしました。個人で勉強をしているとフレームワークや言語の知識は増やせますが、運用に関わる部分はなかなか知ることができないと思います。便利だなと思ったら、ぜひ積極的に使用してみてください。</p>新卒エンジニアが今になって就職前にやっておけば良かったと思うこと3選2021-09-27T11:19:53+00:002024-03-28T02:50:08+00:00鈴木海人https://tech.torico-corp.com/blog/author/k.suzuki/https://tech.torico-corp.com/blog/%E6%96%B0%E5%8D%92%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%8C%E4%BB%8A%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E5%B0%B1%E8%81%B7%E5%89%8D%E3%81%AB%E3%82%84%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%91%E3%81%B0%E8%89%AF%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%A8%E6%80%9D%E3%81%86%E3%81%93%E3%81%A83%E9%81%B8/<br/><br/>就社してからは初めてのブログ投稿となります。<br/>お久しぶりです。開発部の鈴木海人です。<br/><br/>株式会社TORICOにエンジニアとして入社して、半年が経ちました。<br/>今回のブログでは、過去の内定をいただいてから入社までの間に対して何もしなかった自分に対して<br/><strong>入社までにやっておいたほうがいいこと</strong>についてまとめました。<br/>過去の自分のような過ちを他の人が犯さないようにまとめましたのでエンジニア内定をもらって何をすればいいかわからない人はぜひ参考にしてみてください。<br/><br/><br/><br/>私の簡単な経歴はこちら↓<br/>都内私立文系大学卒業<br/>大学3年時の夏に某大手プログラミングスクールに通い、プログラミングの基礎について学ぶ<br/>スクール卒業後、都内のスタートアップの会社で2ヶ月ほどインターン(作業内容は主にLPの作成を行っていました)<br/>大学4年時は就活を行い、株式会社TORICOに内定をいただき、今に至ります。<br/><br/><br/><br/><code>注意事項</code><br/>簡単なコーディング知識があることを前提にお話しします。もしプログラミングが全くわからないという人はprogateなどのプログラミングを簡単に学べるサイトでまずは学びましょう。<br/><br/><br/><br/>それでは本題に戻ります。<br/>まず結論からお話しします。以下の3つになります。<br/>
<ol>
<li>
<h3>タイピング強化</h3>
</li>
<li>
<h3>会社で使用する言語の参考書を1冊読んでおく</h3>
</li>
<li>
<h3>ドキュメントで調べる癖をつける</h3>
</li>
</ol>
<br/><br/><br/><br/>それでは1つずつ説明していきます。
<h3>1.タイピング</h3>
<br/>これはどんな人でも絶対にやっておきましょう。<br/>目安としましてはe-typingで安定してA以上や寿司打で1万円コースクリアでしょうか。<br/>上記のサイトですと日本語入力ですので英単語を打つようなサイトを探してみてもいいかもしれません。<br/>僕は入社してから、過去タイピング練習をしてこなかったことを最も後悔しています。<br/><br/>タイピングを強化しておくメリットには下記が挙げられます。<br/>
<ol>
<li><strong>仕事スピードが上がる</strong></li>
<li><strong>成長スピードが上がる</strong></li>
<li><strong>教えていただいている時の時間を少なくできる</strong></li>
</ol>
<br/><br/>考えてみれば当たり前なのですが、<strong>タイピングスピードが2倍になればかけるコードも2倍になり、そのため成長スピードも2倍に</strong>なります。<br/>逆に<strong>タイピングスピードが1/2倍になればかけるコードも1/2倍になり、そのため成長スピードも1/2倍</strong>になります。<br/>もはやエンジニアにとって一番重要なのではと思っています。<br/>もちろん最初はコードを書くことよりも調べたり読んだりする時間の方が長いので、タイピングスピードの恩恵をあまり受けられないかもしれません<br/>しかし後々大きく影響してくるので鍛えておきましょう。<br/>後、単純にタイピングで遅くてミスりまくると恥ずかしいです。<br/>1日10分とかでもいいので毎日タイピングの練習をするのがおすすめです。私も練習中です<br/>タイピングに慣れてきたら数字や記号などもしっかりと打ち込めつように練習しましょう。<br/><br/>また少し話はタイピングから話がずれてしまうのですが、<br/>よく使用するショートカットキーの暗記やカーソルの移動スピードmaxなどの使いやすいPC設定も行っておきましょう<br/>こちらもPCを使う上での基礎スキルとなり、使っているか使っていいないかで作業効率が大幅に変わるので意識してみてください。<br/><br/><br/><br/><br/><br/><br/>
<h3>2.会社で使用する言語の参考書を1冊読んでおく</h3>
<br/>参考書を読むというのに抵抗感がある人は多いのではないでしょうか?<br/>実際僕もそうでした。ネットなどで調べてみると「ネットに全部載っているのに本を買う必要はない」、「わからないことはその都度ググって調べればいい」など<br/>本に対しては比較的、良い情報が流れていないようなイメージが僕にはあります。<br/>これは私の上司から教えていただいて、確かにとなったのですが、<strong>本は体系的(一つ一つのものがある系統に従ってまとまっているさまのことという意味みたい)になっているため</strong><strong>正確な情報をしっかりとインプットできるのです。</strong><br/>今までとりあえずわからなくなったらググってを繰り返していたのですが1通り本を読むことにより、もちろん完全暗記はできませんがコードを書いているとき、あれが使えるかなとか、それが出てこなくても<br/>調べて出てきたメソッドなど、そういえばこんなのあったなと思い出せます。<br/>また、おすすめの参考書なのですが<br/>私の上司のおすすめの参考書はとりあえず分厚い本みたいです。。。<br/>残念ながら優しくて短い本では情報量が少なすぎたりであまりお勧めをしていないようです。<br/><strong>参考書を買うときは分厚くて情報量の多い参考書を選びましょう。</strong><br/>こちらもタイピングと同様少ない時間でもいいので移動時間などを活用して少しずつ読み進めましょう。<br/><br/><br/><br/><br/><br/><br/>
<h3>3.ドキュメントで調べる癖をつける</h3>
<br/>皆さんはドキュメントで調べ物をしていますか?<br/>僕は基本的にQiitaだったり個人ブログなどを参考にすることが多いです。。。<br/>わかりやすいですよね。。。<br/>なるべく意識はしていますが今でもあまりできていないのが現状です。<br/><strong>ドキュメントで調べる癖をつけた方がいい理由は、正確な情報が手に入れられるからです。</strong><br/>調べ物をしているとき正確な情報じゃないことや、記事が古く参考にならなかったり、バージョン違いで動作しなかったりと<br/>結構クソみたいな記事が上に表示されることはあるあるではないでしょうか<br/>ドキュメントで調べる癖をつけておくと正確な情報を手に入れることができるのはもちろんなのですが<br/><strong>英語で文を読む癖がついたり、英語で調べたりする癖がつくのでためになる</strong>と思います。<br/>日本語検索とは比べ物にならないほど英語の情報は出てくるので、英語めっちゃできるぜ!って感じを目指さなくてもいいですが<br/>グーグル翻訳を使いながらでも少しずつ調べ物ができるようになると良いです。<br/>プログラミングをしている人にとって英語は切っても切り離せない関係なので<br/>早い段階で慣れておきましょう。<br/><br/><br/><br/><br/><br/>以上3つが私が入社前にやっておけば良かったことになります。<br/>最後にもう1度<br/>
<ol>
<li>
<h3>タイピング強化</h3>
</li>
<li>
<h3>会社で使用する言語の参考書を1冊読んでおく</h3>
</li>
<li>
<h3>ドキュメントで調べる癖をつける</h3>
</li>
</ol>
<br/><br/>重要順は上から1.2.3となります。<br/>ぜひエンジニアに、これからなる人なりたい人は参考にしてみてください。<br/><br/><br/><br/><br/>新卒エンジニアが最初の半年に任された業務2021-09-18T12:28:58+00:002024-03-28T02:49:00+00:00清瀬遼平https://tech.torico-corp.com/blog/author/r.kiyose/https://tech.torico-corp.com/blog/%E6%96%B0%E5%8D%92%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%8C%E6%9C%80%E5%88%9D%E3%81%AE%E5%8D%8A%E5%B9%B4%E3%81%A7%E8%A1%8C%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8/<p>今年の春に入社しました、情報システム部の清瀬です。</p>
<p>私がTORICOに入社からそろそろ半年が経過しようとしています。このタイミングで、この半年どういった業務を担当してきたのかを記事にしようと思います。TORICOに興味をもっている方にTORICOのエンジニア業務がどんなものなのか、知ってもらえればと思います。</p>
<h3>社内アプリへの権限付与の自動化</h3>
<p></p>
<p>TOIRCOには <a href="https://id.torico-corp.com/">TORICO-ID</a> というログインを共通化できるアプリがあります。マンガ全巻ドットコム や マンガ展 と連携させることでログインやアカウント作成の簡略化ができます。このアプリは10近くある社内アプリにも使われています。社員全員がボタン一つで簡単にログインできるのですが、問題点が一つありました。それは、Admin サイトへの権限の問題です。</p>
<p>TORICO の社員と一般のユーザーを分けるフラグはあります。そのため、社員にのみ権限を与えることは今までもできていました。しかし、全社員に全ての社内アプリのAdmin権限を付与するわけにはいきません。そのため今までは情報システム部が手動で権限の付与をしていました。</p>
<p>そこで部署ごとに権限を付与するサイトを事前に決めておき、権限の付与の自動化を行えるようにしました。作成したモデルは具体的には</p>
<ul>
<li>Website
<ul>
<li>権限を与えるアプリのモデル</li>
<li>部署とM2Mの関係</li>
<li>django-allauth の Application と 1:N の関係</li>
</ul>
</li>
<li>ClientGroup
<ul>
<li>ログイン成功時にユーザーに与える、クライアント側の Django の Group のモデ</li>
<li>django-allauth の Application と 1:N の関係</li>
</ul>
</li>
<li>Department
<ul>
<li>部署・部門のモデル</li>
<li>Website と M2M の関係</li>
<li>ClientGroup と M2M の関係</li>
<li>User と M2M の関係</li>
</ul>
</li>
</ul>
<p>の3つです。TORICO では Mezzanin という Django の CMS を使用しているため、Mezzanine の権限も付与できるように Website を Application と 1:N の関係にしています。</p>
<p>あとは Department に Application と ClientGroup を結びつけておくことで、どの部署にどの権限を付与するのかを自動で判別できるようになりました。TORICO は現在拡大期であり毎月のように新しく入社される方がいるため、この機能は非常に役立っていると個人的には思います。また、エンジニアとしても予想外のメリットがありました。それは新しいプロジェクトのローカル環境を作成した際に、いちいち SQLクライアントツールで自分のアカウントにスタッフ権限を付与しなくて済むことです。特に情報システム部は担当するアプリが多いため、その一手間を省けるのは非常に便利です。</p>
<h3>古いシステムの Django 化</h3>
<p>長年TORICOを支えてきてくれたシステムですが、その中の幾つかを Django に移行するお手伝いもしました。元のコードを読みながら、その機能を Django で再現するという業務でした。元のコードは PHP で書かれているため、PHP と Python を比較するような作業で、個人的にはとても楽しかったです。なにより、古いシステムを新しいシステムに置き換えることで、一つ一つの処理が非常に効率が良くなり、格段にレスポンスを早くすることができました。あらためてフレームワークの凄さを目の当たりできてよかったです。TORICO にはまだ古いシステムが残っているので、来期も移行の業務を積極的にやっていきたいと思っています。</p>
<h3>MySQL 5.6 を 5.7 にバージョンアップする</h3>
<p>RDS の MySQL5.6 のサポート期限が8月末(延長されて22/3/1まで)までであるということで、MySQL のパージョンアップもさせていただきました。手順としては、</p>
<ol>
<li>dev 環境のバージョンアップを行い、全ての機能が使えるかテストする</li>
<li>1 で問題あった機能を修正する</li>
<li>スナップショットでバックアップをとる</li>
<li>修正したコードをデプロイして、本番のバージョンアップを行う</li>
</ol>
<p>今回のバージョンアップでは1の部分で問題がありました。対象のアプリでは、ログイン時に MySQL の OLD_PASSWORD というハッシュ関数を使っていたのですが、それがサポートされなくなりました。そのため、代替となる関数をアプリ側で作成しなければなりませんでした。幸い、既に再現をしてくれているコードがあったため、それを拝借するだけで問題はありませんでした。ただ、ログインというアプリの基幹機能の修正だったため、本番へのデプロイは非常に緊張しました。</p>
<p>また、本番のバージョンアップ時にオプションの設定を忘れるというミスをしてしまいました。古いものが引き継がれると勘違いしていたことが原因です。その結果タイムゾーンの設定が狂ってしまい、表示されるべきコンテンツが表示されないという状況になってしまいました。普段からお世話になっている先輩に手助けしていただいたため無事に解決できましたが、もう少しで大惨事となるところでした。今後はもっと慎重に、情報集めからテストまでを行わなければいけないなと反省しています。</p>
<h3>最後に</h3>
<p>今回は、私がこの半年で任せていただいた主要な業務をいくつか紹介させていただきました。以上に取り上げたもの以外にも、</p>
<ul>
<li>新機能の追加
<ul>
<li>5件</li>
</ul>
</li>
<li>Django 化
<ul>
<li>2件</li>
</ul>
</li>
</ul>
<ul>
<li>DB・ネットワーク
<ul>
<li>2件</li>
</ul>
</li>
<li>セキュリティ(別ブログに記載)
<ul>
<li>10件</li>
</ul>
</li>
<li>コードの軽微な修正
<ul>
<li>16件</li>
</ul>
</li>
</ul>
<p>と、様々な業務を体験できました。今回はシステムに関係する業務だけを取り上げましたが、他にも壁にドリルで穴を開けたり社内サーバーを移動させたりと、普段はできない業務も経験させていただきました。TORICO ではフルスタックであることが求められるため、業務も多岐にわたります。そこに魅力を感じた方は、ぜひ TORICO に応募してみてください。</p>実務経験0 入社一年目のエンジニアが任されたセキュリティの話2021-09-16T16:21:44+00:002024-03-28T02:50:04+00:00清瀬遼平https://tech.torico-corp.com/blog/author/r.kiyose/https://tech.torico-corp.com/blog/%E5%AE%9F%E5%8B%99%E7%B5%8C%E9%A8%930-%E5%85%A5%E7%A4%BE%E4%B8%80%E5%B9%B4%E7%9B%AE%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%8C%E4%BB%BB%E3%81%95%E3%82%8C%E3%81%9F%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%81%AE%E8%A9%B1/<p>2021年春に入社しました、情報システム部の清瀬です。</p>
<p>TORICOの情報システム部では、セキュリティの向上を上半期の目標に掲げておりました。上半期も終わりに近づいてきたということで、新卒で入社した私がどんなセキュリティ向上のために携わってきた業務を記事にしようとおもいます。多くのシステムでは Django を使用しているためコード部分は Django 前提の話になります。</p>
<h2>1. インフラレベルでの対応</h2>
<h3>AWS WAF の導入</h3>
<ul>
<li>SQLインジェクション対策</li>
<li>クロスサイトスクリプティング</li>
<li>その他、包括的な対策</li>
</ul>
<p>TORICOでは一部の社内用アプリを除き、全てのサービスをAWSでデプロイしています。それら全てのサーバを悪意のあるリクエストから守るために、AWS WAF を導入しました。</p>
<p>AWS WAF は2019年に大幅アップデートされ、今までのものは WAF Classic と名称を変更しました。新しいWAFの最大のメリットは、るAWSによって作られたルールを簡単に導入できる点です。例えば <a href="https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/aws-managed-rule-groups-list.html" target="_blank" title="AWSManagedRlesCommonRuleSet">AWSManagedRlesCommonRuleSet</a> では、OWASPに記載された主要な脆弱性・リスク10個をカバーしてくれています。クロスサイトスクリプティングや悪意のあるボットを排除してくれるルールをボタン一つで導入できるため、包括的なセキュリティ対策をすることができます。もちろん、今までのようにオリジナルのルールを作ることができます。そのため、社内からのアクセスには一部のルールを緩和させたりなどもできるようになっています。</p>
<p>余談ですが、WAFの導入の検証を担当したのは、まだ私がインターン1週間くらいの時でした。検証のために簡易的な Django アプリを Ec2 にデプロイしてWAFの導入でクロスサイトスクリプティングやSQLインジェクションがきちんとブロックされるのかを検証しました。インターンなのにこんな重要そうな仕事をさせていただけるのかと驚いたのを今でも覚えています。</p>
<h4>過去に使用していた IP アドレスのルーティングを解除</h4>
<p>これはセキュリティとは少し違う話かもしれませんが、ドメインの整理などもしました。一部の未使用のドメインが、既に手放してある IP アドレスと結びついたまま放置されていました。もし IP アドレスが悪意のあるサイトに使われていた場合、最悪ユーザーを悪意のあるサイトに誘導してしまうということも起こり得ます。そうならないためにも、不要なドメインを整理しました。</p>
<h3>2. アプリレベルでの対応</h3>
<h3><strong></strong>サービスのAdmin ・社内アプリ を社内からのみ許可する</h3>
<ul>
<li>不正なアクセス対策</li>
<li>個人情報の保護</li>
</ul>
<p>こちらは Django の Middleware を使って対応しました。サブネットマスクの表現方法などが複数あるため IP を取り扱うのは面倒くさそうだなと思っていましたが、python には ipaddress という便利なモジュールが標準で搭載されています。このモジュールを使うことで簡単に実装することができました。このタスクは上記の WAF でも実装できますが、1つのALBで複数の社内アプリを制御していたためアプリレベルで対応することにしました。アプリレベルのため、各アプリに適した制限を導入することも簡単にできます。</p>
<h3>ログインセッションキーを適切な設定にする</h3>
<ul>
<li>セッションハイジャック対策</li>
<li>CSRF対策</li>
</ul>
<p>大事なセッションを管理してくれるクッキーにはセキュリティ対策は欠かせません。TORICOの全てのサイトで、その大切なCookieの設定が適切になるように対策をしました。具体的には HttpOnly Secure SameSite=Lax の3つの設定です。これらの設定を簡単に説明すると、</p>
<p>HttpOnly: JavaScript からセッションにアクセスできないようにする。設定されていなかった場合、JS 経由で セッション情報が漏洩する危険がある。</p>
<p>Secure: https 通信でのみ セッションを送るようにする。設定されていなかった場合、暗号化されない Http 通信でセッションが送られてしまう危険がある。</p>
<p>SameSite: Strict, Lax, None の3つの設定がある。Lax の場合、別オリジンからの POST の時はセッションを送らない。None の場合、例えばECサイトの届け先を変更するフォームのある偽サイトからユーザーのセッションを使ってデータが送信されてしまう。</p>
<p>Django では 2系以上であれば settings で設定することができます。1系の場合は SameSite を設定することができません。クッキーセッションの注意点として、あまりにも厳しい設定にした場合、サービスがうまく動かなくなる可能性があります。特に SameSite は決済部分でエラーがでることがあるそうです。</p>
<h3>ファイル出力時に追跡IDとログを残す</h3>
<ul>
<li>情報漏洩元の究明</li>
</ul>
<p>TORICOではできるだけ業務を自動化するために、いろいろな情報をCSVなどに出力します。万が一そういったデータが社外に流出した時、どのデータが、どこから流出したのかを特定しやすいように追跡IDをそれぞれのファイルに追記するようにしました。ログにはリクエストの情報を全てを保存しているため、いつ・誰が・どの検索ワードで CSV を出力したのかを追うことができます。 Django には DB へのログ出力機能はないため、DBに出力する場合には自作する必要があります。</p>
<h2><span>3. データベースの対応</span></h2>
<h3>重要なフィールドの暗号化</h3>
<ul>
<li>個人情報の保護</li>
<li>データ漏洩時の影響を最小限にする</li>
</ul>
<p>重要なフィールドを可逆暗号化することで、万が一情報漏洩した時でもその影響を最小限に留められるようにしました。可逆暗号は Django のオリジナルのフィールドを作ることで簡単に対応することができました。具体的な実装方法については<a href="https://qiita.com/rkiyose/items/1d9783889a97885b0e92" target="_blank">こちら</a>の記事で書きました。また、一部の古い社内アプリではアカウントのパスワードがハッシュ化されていなかったため、 bcrypt という方法で不可逆暗号化もしました。</p>
<p>ソルトとペッパーの役割の違いなどが曖昧だったため、とても勉強になったタスクだったなと思っています。</p>
<h2>最後に</h2>
<p>上半期に行った主要なセキュリティ対策を紹介していきました。セキュリティは深刻な障害の原因になるため、普段のコーディングから気をつけていきたいと思います。</p>Amazon Pay Checkout v2 API の署名 (RSA-SHA256 (RS256) + RSA PSS Padding) を Python で行う2021-09-09T09:30:07+00:002024-03-28T11:06:35+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/amazon-pay-rsa-sha256-pss-padding-python/<p></p>
<p>Amazon Pay の API クライアントを書く際、Amazon のAPIサーバに送信するリクエストに、RSA-SHA256, RSA PSS パディングを使って署名を作り、リクエストに含めて送信する必要があります。</p>
<p>Java や Node はクライアントライブラリがあったので、それを使って簡単に署名できたのですが、弊社 TORICO ではサーバサイドは主に Python を使っており、既存のクライアントライブラリは無かったため、Node のライブラリを参考に署名コードを書きました。</p>
<h2>Amazon Pay のAPI</h2>
<p>今回は、Amazon Pay の Checkout v2 API を使う必要がありました。</p>
<p><a href="https://developer.amazon.com/ja/docs/amazon-pay/intro.html" target="_blank">https://developer.amazon.com/ja/docs/amazon-pay/intro.html</a></p>
<p>リクエストヘッダに含める署名については、<br/>この <a href="https://developer.amazon.com/ja/docs/amazon-pay-api-v2/signing-requests.html" target="_blank">CV2 (Checkout V2) の 署名リクエスト</a>のページに解説があります。</p>
<p>このページを見ていくと、「署名を計算します」の箇所に</p>
<p style="padding-left: 30px;">署名を計算するには、ステップ2で作成した署名する文字列に秘密鍵を使用して署名します。 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズムを使用します。結果をBase64エンコードして、この手順を完了します。 RSASSA-PSSを使用して計算されたすべての署名は、入力が同じであっても一意であることに注意してください。</p>
<p>とありますので、この「 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズム」を Pythonでコーディングする必要があります。</p>
<p>ところで、この最後の「 入力が同じであっても一意であることに注意してください」は、「〜一意でない」の間違いじゃないですかね。英語版ページは調べてませんが。</p>
<h2>Amazon Pay Scratchpad</h2>
<p><a href="https://pay-api.amazon.jp/tools/scratchpad/index.html" target="_blank">https://pay-api.amazon.jp/tools/scratchpad/index.html</a></p>
<p>検証リクエストを発行できるサイトが用意されていますので、署名ロジックの確認に使うと良さそうです。<br/>私は、今回の開発中は存在を知らなかったので、使っていません。</p>
<h2>Node.js の場合</h2>
<p>node.js の場合は、クライアントライブラリは<br/><a href="https://www.npmjs.com/package/@amazonpay/amazon-pay-api-sdk-nodejs%E2%80%A8" target="_blank">@amazonpay/amazon-pay-api-sdk-nodejs</a> があり、これを使うと署名を含めたAPIリクエストが一発で行えます。</p>
<p>ヘッダの署名を行っているコードは</p>
<p><a href="https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L142" target="_blank">https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L142</a> このあたりで、<br/>「 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズム」のコードは<br/><a href="https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L84" target="_blank">https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L84</a> ここです。</p>
<h2>Python の場合</h2>
<p>上記 node.js 相当のコードを書くわけですが、hmac ライブラリには相当のコードはありません。</p>
<p>RSA や パディングの基礎的なロジックは <a href="https://cryptography.io/en/latest/" target="_blank">cryptography</a> に入っており、実際の使い方は PyJWT がいい感じになっているので、PyJWT を参考にします。</p>
<p><a href="https://github.com/jpadilla/pyjwt/blob/master/jwt/algorithms.py#L232" target="_blank">https://github.com/jpadilla/pyjwt/blob/master/jwt/algorithms.py#L232</a></p>
<p>PSS パディングはここです。</p>
<p><a href="https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/asymmetric/padding.py#L19" target="_blank">https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/asymmetric/padding.py#L19</a></p>
<p>この PSS の第一引数の _mgf ってなんだ、と思いましたが、同モジュール中にある<br/><code>MGF1(RSAAlgorithm.SHA256())</code><br/>を入れたら動きました。</p>
<h3>署名部分のコード</h3>
<p>署名部分のコードはこのようになります。</p>
<pre>import base64<br/>from cryptography.hazmat.primitives import hashes<br/>from cryptography.hazmat.primitives.asymmetric import padding<br/>from cryptography.hazmat.primitives.serialization import load_pem_private_key<br/><br/>private_key = b'''-----BEGIN PRIVATE KEY-----<br/>MIIEvQIB....<br/>....N/Qn4=<br/>-----END PRIVATE KEY-----'''<br/><br/>string_to_sign = 'AMZN-PAY-RSASSA-PSS\nxxxxxxxxxxxxxxxxxxxxxxxx'<br/><br/>key = load_pem_private_key(private_key, password=None)<br/><br/>signature = key.sign(<br/> string_to_sign.encode('utf-8'),<br/> padding.PSS(padding.MGF1(hashes.SHA256()), 20),<br/> hashes.SHA256())<br/><br/>signature = base64.b64encode(signature).decode('utf-8')</pre>
<p><span>後になって気づきましたが、PyJWT に<span> </span></span><a href="https://github.com/jpadilla/pyjwt/blob/master/jwt/algorithms.py#L507" target="_blank">RSAPSSAlgorithm</a><span> という、今回の用途にぴったりなクラスがあったので、これを使うともうちょっとシンプルなコードになるかもしれません。</span></p>
<h3>署名元の文字列の生成も含めたコード</h3>
<p>checkoutSessions を行うコードはこのような感じです。</p>
<p>node.js のコードを参考にした箇所がいくつかあり、それらは実際には使われない、不要なコードになってます。</p>
<pre>import base64<br/>import datetime<br/>from hashlib import sha256<br/><br/>import requests<br/>from cryptography.hazmat.primitives import hashes<br/>from cryptography.hazmat.primitives.asymmetric import padding<br/>from cryptography.hazmat.primitives.serialization import load_pem_private_key<br/><br/>private_key = b'''-----BEGIN PRIVATE KEY-----<br/>MIIEv....<br/>....N/Qn4=<br/>-----END PRIVATE KEY-----'''<br/><br/>config = {<br/> 'publicKeyId': 'SANDBOX-AEXXXXXXXXXXXX',<br/> 'privateKey': private_key,<br/> 'region': 'jp',<br/> 'sandbox': True,<br/>}<br/><br/>constants = {<br/> 'SDK_VERSION': '2.1.4', 'API_VERSION': 'v2', 'RETRIES': 3,<br/> 'API_ENDPOINTS': {'na': 'pay-api.amazon.com', 'eu': 'pay-api.amazon.eu', 'jp': 'pay-api.amazon.jp'},<br/> 'REGION_MAP': {'na': 'na', 'us': 'na', 'de': 'eu', 'uk': 'eu', 'eu': 'eu', 'jp': 'jp'},<br/> 'AMAZON_SIGNATURE_ALGORITHM': 'AMZN-PAY-RSASSA-PSS',<br/>}<br/><br/>checkoutSessionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"<br/><br/>options = {<br/> 'method': "GET",<br/> 'urlFragment': f"/v2/checkoutSessions/{checkoutSessionId}",<br/> 'headers': {},<br/> 'payload': ''<br/>}<br/><br/>pay_date = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')<br/><br/>headers = {<br/> 'x-amz-pay-region': config['region'],<br/> 'x-amz-pay-host': 'pay-api.amazon.jp',<br/> 'x-amz-pay-date': pay_date,<br/> 'content-type': 'application/json',<br/> 'accept': 'application/json',<br/> 'user-agent': 'amazon-pay-api-sdk-nodejs/2.1.4 (JS/14.15.1; darwin)',<br/>}<br/><br/>lowercase_sorted_header_keys = <span>list</span>(<span>sorted</span>(headers.keys(), <span>key</span>=<span>lambda </span>x: x.lower()))<br/>signed_headers = ';'.join(lowercase_sorted_header_keys)<br/><br/>canonical_request = [<br/> options['method'],<br/> options['urlFragment'],<br/> '', # GETパラメータだが一旦無し<br/>] + [<br/> f'{h}:{headers[h]}' for h in lowercase_sorted_header_keys<br/>] + [<br/> '', # 空行入れる<br/> signed_headers,<br/> sha256(options['payload'].encode('utf-8')).hexdigest()<br/>]<br/><br/>canonical_request_bytes = ('\n'.join(canonical_request)).encode('utf-8')<br/><br/>string_to_sign = constants['AMAZON_SIGNATURE_ALGORITHM'] + '\n' + sha256(canonical_request_bytes).hexdigest()<br/><br/>key = load_pem_private_key(config['privateKey'], password=None)<br/><br/>signature = key.sign(<br/> string_to_sign.encode('utf-8'),<br/> padding.PSS(padding.MGF1(hashes.SHA256()), 20),<br/> hashes.SHA256())<br/><br/>signature = base64.b64encode(signature).decode('utf-8')<br/><br/>headers['authorization'] = \<br/> f"{constants['AMAZON_SIGNATURE_ALGORITHM']} " \<br/> f"PublicKeyId={config['publicKeyId']}, " \<br/> f"SignedHeaders={signed_headers}, " \<br/> f"Signature={signature}"<br/><br/>response = requests.get(<br/> f"https://pay-api.amazon.jp{options['urlFragment']}",<br/> headers=headers<br/>)<br/>print(response)<br/>print(response.json())</pre>Google Hangouts Chat にプログラムからメッセージを送信する2018-04-28T03:55:53+00:002024-03-28T13:33:38+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/google-hangouts-chat-send-message/<p></p>
<p>Google ハングアウトの後継のチャット(インスタントメッセージング)サービス、<a href="https://chat.google.com/" target="_blank">Chat</a> では、Webhook エンドポイントを使うことでとても簡単にチャットルームへのメッセージの送信ができます。</p>
<p>メッセージの送信に、チャット用の大規模なアプリ開発は不要です。Python でも curl でも JS でも、3行ぐらいでメッセージの送信ができます。</p>
<h2>Webhook エンドポイントの作成</h2>
<h3>1. Chat を開く</h3>
<p><a href="https://chat.google.com/">https://chat.google.com/</a></p>
<h3>2. チャットルームの作成</h3>
<p><img alt="" height="316" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/hangouts-chat-bot/chat-bot-01.png" width="366"/></p>
<p>左上のメニューから、「チャットルームを作成」を選び、</p>
<p><img alt="" height="240" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/hangouts-chat-bot/chat-bot-02.png" width="401"/></p>
<p>適当に名前をつける。</p>
<h3>3. Webhook エンドポイントの作成</h3>
<p><img alt="" height="419" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/hangouts-chat-bot/chat-bot-03.png" width="413"/></p>
<p>チャットルーム名をクリックするとメニューが開くので、「Webhookを設定」をクリック</p>
<p><img alt="" height="264" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/hangouts-chat-bot/chat-bot-04.png" width="645"/></p>
<p>+ WEBHOOKを追加 をクリック</p>
<p><img alt="" height="421" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/hangouts-chat-bot/chat-bot-05.png" width="639"/></p>
<p>適当に名前をつけて、「保存」</p>
<p><img alt="" height="259" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/hangouts-chat-bot/chat-bot-06.png" width="639"/></p>
<p>Webhook の URL ができる。このURLを記録しておく。</p>
<h2>メッセージを送信する</h2>
<h3>Python</h3>
<pre>import requests<br/><br/>webhook_url = 'https://chat.googleapis.com/v1/spaces/...%3D'<br/><br/>response = requests.post(<br/> webhook_url,<br/> json={"text": "こんにちは、世界!"}<br/>)</pre>
<p>requests を使えば、これだけでメッセージを送信できます。簡単ですね!</p>
<h3>curl</h3>
<pre>curl -X POST "https://chat.googleapis.com/v1/spaces/...%3D" \<br/>--header "Content-Type: application/json; charset=UTF-8" \<br/>--data '{"text": "こんにちは!"}'</pre>
<p>これで送信できます。</p>
<h3>その他</h3>
<p>その他のツールでメッセージを送信するには、<a href="https://developers.google.com/hangouts/chat/quickstart/incoming-bot-python" target="_blank">Incoming webhook with Python</a> を参考に</p>
<p>HTTPリクエストヘッダ: <code>Content-Type: application/json; charset=UTF-8</code></p>
<p>リクエストボディはJson: <code>{"text": "Hello from Python script!" }</code></p>
<p>で送信できます。</p>
<h3>400エラーが出たら</h3>
<p>テストコードを書いていたら、HTTPステータス400</p>
<pre>{<br/> "error": {<br/> "code": 400,<br/> "message": "Request contains an invalid argument.",<br/> "status": "INVALID_ARGUMENT"<br/> }<br/>}</pre>
<p>このようなエラーレスポンスが返ってきて困ってたのですが、この原因は単純なURLのコピペミスでした。URLの最後まで正しくコピペできているか、確認してください。</p>DB(MySQL)をネットワーク越しに簡単にコピーする。mysqldump + パイプで。python subprocess の例も2018-03-14T03:44:25+00:002024-03-28T02:50:08+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/mysql-dump-over-ssh-python-subprocess/<p></p>
<p>本番環境のデータベース(MySQL)をネットワーク越しに開発環境にコピーしたい時のプラクティスです。</p>
<p></p>
<h2>シェル + パイプ</h2>
<p>よくやるのが、bash等 でパイプを使って流し込む方法です。</p>
<pre>$ ssh <a href="mailto:user@example.com">user@production.example.com</a> mysqldump \<br/> --skip-lock-tables \<br/> --host=xxxx.rds.amazonaws.com \<br/> --user=xxxx \<br/> --password=xxxx \<br/> database_name table_name | \<br/> ssh <a href="mailto:user@dev.example.com">user@dev.example.com</a> mysql \<br/> --host=127.0.0.1 \<br/> --user=xxxx \<br/> --password=xxxx \<br/> --database=xxxx</pre>
<p>ssh で本番サーバ <a href="mailto:user@production.example.com">user@production.example.com</a> に接続し、mysqldump を実行。その標準出力を SSH 接続を通して手元まで持ってきます。</p>
<p>ssh でもう一つ、開発環境サーバ <a href="mailto:user@dev.example.com">user@dev.example.com</a> に SSH接続し、mysql を起動。先ほどの本番環境の mysqldump 結果をパイプでそのまま流し込みます。</p>
<p>速度が充分に早く、通信経路も ssh で暗号化されるため安全に、効率良くコピーできます。mysql に余計な穴を空ける必要もありません。</p>
<p>mysqldump のオプションで <code>--where</code> を付けて読み込むデータを絞り込んだりもできます。</p>
<p></p>
<h2>Python subprocess を使う</h2>
<h3>subprocess shell=True で実行</h3>
<p>(あまり面白くない。読みにくい。)</p>
<pre><span>import </span>subprocess<br/>subprocess.check_call(<span>"上記のコマンド"</span>, <span>shell</span>=<span>True</span>)</pre>
<p>subprocess で、Popen や check_call などの引数に <code>shell=True</code> を与えることで、シェルコマンドをそのまま実行できます。</p>
<p></p>
<h3>subprocess のパイプを使う (おすすめ)</h3>
<p>subprocess でシェルのパイプと同様の処理が行えます。</p>
<pre>dump_command = [<br/> <span>"ssh"</span>,<br/> <span>"user@production.example.com"</span>,<br/> <span>"mysqldump"</span>,<br/> <span>"--skip-lock-tables"</span>,<br/> <span>"--host=xxxx.rds.amazonaws.com"</span>,<br/> <span>"--user=xxxx"</span>,<br/> <span>"--password=xxxx"</span>,<br/> <span>"database_name"</span>,<br/> <span>"table_name"</span>,<br/>]<br/>dump_process = subprocess.Popen(<br/> dump_command, <span>stdout</span>=subprocess.PIPE)<br/><br/>import_command = [<br/> <span>"ssh"</span>,<br/> <span>"user@dev.example.com"</span>,<br/> <span>"mysql"</span>,<br/> <span>"--host=127.0.0.1"</span>,<br/> <span>"--user=xxxx"</span>,<br/> <span>"--password=xxxx"</span>,<br/> <span>"--database=xxxx"</span>,<br/>]<br/>import_process = subprocess.Popen(<br/> import_command, <span>stdout</span>=subprocess.PIPE, <span>stdin</span>=dump_process.stdout)<br/><br/>stdout, stderr = import_process.communicate()</pre>
<p>ダンププロセスの <code>stdout</code> を インポートプロセスの <code>stdin</code> に接続して 2 つのプロセスを実行します。</p>
<p>Python のコードにしておけば、再利用性・メンテナンス性を高くでき、使い回しに優れます。</p>
<p></p>
<h2>Django のデータベースコネクションを使う場合</h2>
<pre><span>from </span>django.db.transaction <span>import </span>get_connection<br/><br/>dump_command = [<br/> <span>"ssh"</span>,<br/> <span>"user@production.example.com"</span>,<br/> <span>"mysqldump"</span>,<br/> <span>"--skip-lock-tables"</span>,<br/> <span>"--host=xxxx.rds.amazonaws.com"</span>,<br/> <span>"--user=xxxx"</span>,<br/> <span>"--password=xxxx"</span>,<br/> <span>"database_name"</span>,<br/> <span>"table_name"</span>,<br/>]<br/>dump_process = subprocess.Popen(<br/> dump_command, <span>stdout</span>=subprocess.PIPE)<br/><br/>connection = get_connection(<span>using</span>=<span>'db_alias_name'</span>)<br/>cursor = connection.cursor()<br/><br/>cursor.execute(dump_process.stdout.read())</pre>
<p>ダンプコマンドの結果を read() して、そのまま Django データベース接続の cursor で流し込むこともできます。</p>
<p>※ダンプ結果を一旦メモリにためるため、データ量が多い場合ちゃんと動くかは不安です。そして他の例より効率は悪そうです。</p>
<p></p>Amazon マーケットプレイスWebサービス (MWS) APIから注文情報を取得する方法2017-05-11T00:29:59+00:002024-03-28T06:33:07+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/amazon-marketplace-mws-api/<p></p>
<p>当社では、Amazon のマーケットプレイスに出店していたり、FBA (フルフィルメントByアマゾン: Amazon社の倉庫に商品を納品し、販売を代行してもらう販売方法) を行っています。</p>
<p>マーケットプレイスにはAPIが用意されており、リクエストすることで受注情報など多くの情報を取得できるのですが、署名の計算に少し躓いたので書いておきます。</p>
<p></p>
<p><a href="http://docs.developer.amazonservices.com/ja_JP/dev_guide/">Amazon MWS 公式ガイド</a></p>
<h1>アクセスに必要な情報を集める</h1>
<h2>認証情報 (クレデンシャル)</h2>
<p>アクセスに必要な認証情報は、</p>
<ol>
<li><strong>出品者ID</strong> (マーチャントID, セラーIDと呼ばれることもある)</li>
<li><strong>AWSアクセスキーID</strong></li>
<li><strong>秘密キー(シークレットキー)</strong></li>
</ol>
<p>の3つです。もし、アクセスするアカウントがセラーセントラルのアカウントではなく、派生して作られた子アカウントである場合、別途「<strong>MWS認証トークン</strong>」が必要になります。</p>
<p>これらの情報は、すべて<a href="https://sellercentral.amazon.co.jp/gp/homepage.html" target="_blank">セラーセントラル</a>の「<code>設定</code>」→「<a href="https://sellercentral.amazon.co.jp/hz/sc/account-information/ref=ag_acctinfo_tnav_userperms_" target="_blank">ユーザー権限</a>」のページで取得できます。AWSアクセスキーID,秘密キー は、ページ下部の「Amazon MWS 開発者権限」の、「認証情報を表示」をクリックすると表示されます。</p>
<p></p>
<h2>マーケットプレイスID</h2>
<p>別途、マーケットプレイスID という文字列が必要になる場合があります。</p>
<p>マーケットプレイスIDの一覧はページから確認できます。</p>
<p><a href="https://docs.developer.amazonservices.com/ja_JP/dev_guide/DG_Endpoints.html" target="_blank">Amazon マーケットプレイス Web サービスエンドポイント</a></p>
<p>例えば、日本なら <code>A1VC38T7YXB528</code> です。</p>
<p></p>
<h1>APIアクセスのテストをする</h1>
<p>Amazon社がAPIのテストツール「Scratchpad」を公開しているので、それを使います。後述する HMAC の計算が正しいか確認する意味でも、このツールは必ず使ってみたほうが良いです。</p>
<ol>
<li><a href="https://mws.amazonservices.jp/scratchpad/index.html" target="_blank">Amazon MWS Scratchpad </a>を開く</li>
<li>左上「API Selection」を適当に選択。今回は、API セクション: <code>注文</code>、Operation: <code>ListOrders</code></li>
<li>Authentication 欄に認証情報を入力、SellerId: には 出品者ID, MWSAuthToken は元アカウントなら空、払い出された子アカウントならトークン文字列を入れる。AWSAccessKeyId、Secret Kye は先ほど取得した文字列を取得。</li>
<li>API必須パラメータの「MarketplaceId.Id.1」には、<code>A1VC38T7YXB528</code> を入力</li>
<li>API任意パラメータの「LastUpdatedAfter」のみ、<code>2017-05-05</code> のように入力</li>
<li>「送信」をクリックすると、結果が表示されます。</li>
</ol>
<p><img alt="" height="662" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/amazon-mws/ss-1.png" width="398"/></p>
<p></p>
<h3>HMAC の値を確認しておく</h3>
<p>結果が正常に表示された場合、「リクエスト」タブをクリックして開いてみると「署名対象の文字列」というセクションと、その下に計算した HMAC署名 が表示されています。APIを開発する時は、この情報を元に開発するとやりやすいです。「証明対象の文字列」に対して SHA 256 HMAC で計算を行い、その下に書かれている文字列が結果で得られるよう開発をしていきます。</p>
<p><img alt="" height="545" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/amazon-mws/ss-2.png" width="806"/></p>
<h1>APIライブラリを開発する</h1>
<p>Pythonで作ります。</p>
<p>署名方法は、Amazonで「署名バージョン2」と言われる方法です。</p>
<p><a href="https://docs.developer.amazonservices.com/ja_JP/dev_guide/DG_ClientLibraries.html" target="_blank">公式ドキュメントの署名のロジック説明 (Java のサンプルコードあり)</a></p>
<p></p>
<h3>HMACの計算ロジックの作成</h3>
<p>Python には hmac ライブラリがあるので、それを使えばすぐにできます。</p>
<p>Python3での例</p>
<pre>import hmac<br/>import hashlib<br/>import base64<br/><br/>secret_key = b"取得した秘密キー"<br/><br/>canonical = b"""<br/>POST<br/>… 「証明対象の文字列」をここにコピペ …<br/>"""<br/><br/>h = hmac.new(secret_key, canonical.strip(), hashlib.sha256)<br/><br/>print(h.hexdigest())<br/>print(base64.b64encode(h.digest()))</pre>
<p>これを実行すると、Scratchpad に表示されている「SHA 256 HMAC」「Base64 HMAC」と同じ値が取得できるはずです。</p>
<p></p>
<h3>署名対象の文字列の作成</h3>
<p>署名対象の文字列は、<strong>HTTPメソッド</strong>(POST)、<strong>ドメイン名</strong>(mws.amazonservices.jp)、<strong>パス</strong>(/Orders/2013-09-01)、それと<strong>クエリ文字列</strong>を、改行(<code>\n</code>)で連結して作ります。</p>
<p></p>
<h4>クエリ文字列の作成</h4>
<p>クエリ文字列は、検索パラメータ名をソートさせ、<code>&</code> と <code>=</code> で連結して作ります。値は URL エンコードします。</p>
<p>ディクショナリで値を用意してたとすると、</p>
<pre>import datetime<br/>import urllib.parse<br/><br/>AMAZON_CREDENTIAL = {<br/> 'SELLER_ID': 'セラーID',<br/> 'ACCESS_KEY_ID': 'AWSアクセスキーID',<br/> 'ACCESS_SECRET': 'アクセスシークレット',<br/>}<br/><br/>data = {<br/> 'AWSAccessKeyId': AMAZON_CREDENTIAL['ACCESS_KEY_ID'],<br/> 'Action': 'ListOrders',<br/> 'MarketplaceId.Id.1': 'A1VC38T7YXB528',<br/> 'SellerId': AMAZON_CREDENTIAL['SELLER_ID'],<br/> 'SignatureMethod': 'HmacSHA256',<br/> 'SignatureVersion': '2',<br/> 'Timestamp': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),<br/> 'Version': '2013-09-01',<br/>}<br/><br/>query_string = '&'.join('{}={}'.format(<br/> n, urllib.parse.quote(v, <strong>safe=''</strong>)) for n, v in <strong>sorted</strong>(data.items()))<br/><br/>print(query_string)</pre>
<p>このようなロジックで作成できます。</p>
<p><code>sorted</code> メソッドでキーで並び替えを行い、値は <code>urllib.parse.quote</code> で URLエンコードします。<code>safe=''</code> を入れないと <code>/</code> がエンコードされないので、入れます。</p>
<p></p>
<p>後は、改行で連結すれば署名対象文字列になります。</p>
<pre>canonical = "{}\n{}\n{}\n{}".format(<br/> 'POST', 'mws.amazonservices.jp', '/Orders/2013-09-01', query_string<br/>)<br/><br/>print(canonical)</pre>
<p></p>
<h3>署名をつけてリクエストする方法</h3>
<p>リクエストメソッドは POST です。ですが、<strong>POSTのデータは空</strong>で、パラメータはURLのクエリストリングに入れます。</p>
<p>署名は、クエリストリングの末尾に <code>&Signature=署名</code> という形で付与します。</p>
<p></p>
<h3>requests でリクエストしてみる</h3>
<p>実際にリクエストするコードを書いてみます。</p>
<pre style="background-color: #ffffff; color: #000000;"><span style="color: #000080;">import </span>base64<br/><span style="color: #000080;">import </span>datetime<br/><span style="color: #000080;">import </span>hashlib<br/><span style="color: #000080;">import </span>hmac<br/><span style="color: #000080;">import </span>urllib.parse<br/><br/><span style="color: #000080;">import </span>requests<br/><span style="color: #000080;">import </span>six<br/><br/>AMAZON_CREDENTIAL = {<br/> 'SELLER_ID': 'セラーID',<br/> 'ACCESS_KEY_ID': 'AWSアクセスキーID',<br/> 'ACCESS_SECRET': 'アクセスシークレット',<br/>}<br/><br/>DOMAIN = <span style="color: #008080;">'mws.amazonservices.jp'<br/></span>ENDPOINT = <span style="color: #008080;">'/Orders/2013-09-01'<br/></span><span style="color: #008080;"><br/></span><span style="color: #008080;"><br/></span><span style="color: #000080;">def </span>datetime_encode(dt):<br/> <span style="color: #000080;">return </span>dt.strftime(<span style="color: #008080;">'%Y-%m-%dT%H:%M:%SZ'</span>)<br/><br/><br/>timestamp = datetime_encode(datetime.datetime.utcnow())<br/><br/>last_update_after = datetime_encode(<br/> datetime.datetime.utcnow() - datetime.timedelta(<span style="color: #660099;">days</span>=<span style="color: #0000ff;">1</span>))<br/><br/>data = {<br/> <span style="color: #008080;">'AWSAccessKeyId'</span>: AMAZON_CREDENTIAL[<span style="color: #008080;">'ACCESS_KEY_ID'</span>],<br/> <span style="color: #008080;">'Action'</span>: <span style="color: #008080;">'ListOrders'</span>,<br/> <span style="color: #008080;">'MarketplaceId.Id.1'</span>: <span style="color: #008080;">'A1VC38T7YXB528'</span>,<br/> <span style="color: #008080;">'SellerId'</span>: AMAZON_CREDENTIAL[<span style="color: #008080;">'SELLER_ID'</span>],<br/> <span style="color: #008080;">'SignatureMethod'</span>: <span style="color: #008080;">'HmacSHA256'</span>,<br/> <span style="color: #008080;">'SignatureVersion'</span>: <span style="color: #008080;">'2'</span>,<br/> <span style="color: #008080;">'Timestamp'</span>: timestamp,<br/> <span style="color: #008080;">'Version'</span>: <span style="color: #008080;">'2013-09-01'</span>,<br/> <span style="color: #008080;">'LastUpdatedAfter'</span>: last_update_after,<br/>}<br/><br/>query_string = <span style="color: #008080;">'&'</span>.join(<span style="color: #008080;">'{}={}'</span>.format(<br/> n, urllib.parse.quote(v, <span style="color: #660099;">safe</span>=<span style="color: #008080;">''</span>)) <span style="color: #000080;">for </span>n, v <span style="color: #000080;">in </span><span style="color: #000080;">sorted</span>(data.items()))<br/><br/>canonical = <span style="color: #008080;">"{}</span><span style="color: #000080;">\n</span><span style="color: #008080;">{}</span><span style="color: #000080;">\n</span><span style="color: #008080;">{}</span><span style="color: #000080;">\n</span><span style="color: #008080;">{}"</span>.format(<br/> <span style="color: #008080;">'POST'</span>, DOMAIN, ENDPOINT, query_string<br/>)<br/><br/>h = hmac.new(<br/> six.b(AMAZON_CREDENTIAL[<span style="color: #008080;">'ACCESS_SECRET'</span>]),<br/> six.b(canonical), hashlib.sha256)<br/><br/>signature = urllib.parse.quote(base64.b64encode(h.digest()), <span style="color: #660099;">safe</span>=<span style="color: #008080;">''</span>)<br/><br/>url = <span style="color: #008080;">'https://{}{}?{}&Signature={}'</span>.format(<br/> DOMAIN, ENDPOINT, query_string, signature)<br/><br/>response = requests.post(url)<br/><br/><span style="color: #000080;">print</span>(response.content.decode())</pre>
<p></p>
<p>下品にベターっと書いてますが、これで動きます。</p>
<p>実際にはこれをライブラリ化して肉付けしていくと良いでしょう。</p>
<p></p>EPUBファイルから画像を抽出する2016-02-26T10:17:47+00:002024-03-28T12:41:20+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/epub%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8B%E3%82%89%E7%94%BB%E5%83%8F%E3%82%92%E6%8A%BD%E5%87%BA%E3%81%99%E3%82%8B/<p></p>
<p></p>
<p>電子書籍フォーマットとして広く使われている EPUB ファイルから、連番で画像を抽出する方法です。</p>
<p>ツール作りました! pip でインストールできます。<a href="https://github.com/ytyng/epub-extract-jpeg">https://github.com/ytyng/epub-extract-jpeg</a></p>
<p></p>
<div class="section" id="epub">
<h2>EPUB ファイルの概要</h2>
<p>EPUB ファイルとは、平たく言えば ZIP圧縮された XHTML です。 コミックで一般的に使われる形式では、1ページが1つの XHTML ファイルになっており、その中に 1 つの img タグが あり、画像ファイルにリンクされています。</p>
<p>そのため、手順としては</p>
<ul class="simple">
<li>EPUB ファイルを解凍</li>
<li>構成情報の XML ファイルを解析し、ページ画像の URL (パス) を取得</li>
<li>ページ画像を連番で改名コピー(移動)</li>
</ul>
<p>となります。</p>
</div>
<div class="section" id="id1">
<h2>1. EPUBファイルを解凍</h2>
<p>unzip で一発です。</p>
<div class="highlight-shell">
<div class="highlight">
<pre><span class="nv">$ </span>mkdir /tmp/epub-extract
<span class="nv">$ </span>unzip sample.epub -d /tmp/epub-extract
</pre>
</div>
</div>
</div>
<div class="section" id="xml-url">
<h2>2. 構成情報の XML ファイルを解析し、ページ画像の URL (パス) を取得</h2>
<p>まず、展開後のディレクトリにある META-INF/container.xml を開きます。 ここに、rootfile というタグがあるので、その full-path 属性を見ます。 full-path の XML ファイルが、各ページの目次のようなものになります。</p>
<p>full-path が示す XML ファイルで、manifest タグの中に item タグが複数あります。 これらは、EPUB 中の XHTML から使われているファイルです。</p>
</div>
<div class="section" id="id2">
<h2>3. ページ画像を連番で改名コピー(移動)</h2>
<p>この、item タグの中はおそらくページ順になっているので、このファイルをスクリプトで連番で改名コピーしながら収集すれば、ページ画像を抽出できます。</p>
<p>本来であれば、直接画像のパスを読むのではなく、ページの XHTML ファイルを開き、そこからリンクされている画像を収集していくのが正しいのですが、ページの XHTML の順と画像の item タグの順が一致しないケースは稀だと思いますので(EPUB 作成者が意図的に XHTML ファイルと画像ファイルの順番を一致させなかった場合などは、ページ数が正しく取得できません)、item タグの順で処理して基本的には問題無いでしょう。</p>
<p>これで、EPUB ファイルから画像ファイルを抽出する方法は終わりです。 最後に、Python スクリプトにした例を掲載しておきます。</p>
<div class="highlight-python">
<div class="highlight">
<pre><span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">print_function</span><span class="p">,</span> <span class="n">unicode_literals</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">shutil</span>
<span class="kn">from</span> <span class="nn">xml.etree</span> <span class="kn">import</span> <span class="n">ElementTree</span>
<span class="n">TEMP_DIR</span> <span class="o">=</span> <span class="s">'/tmp/epub-extract-{}'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()))</span>
<span class="k">def</span> <span class="nf">procedure</span><span class="p">(</span><span class="n">file_path</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">file_path</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"{} is not exist."</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">file_path</span><span class="p">),</span> <span class="nb">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
<span class="k">return</span>
<span class="n">output_dir</span><span class="p">,</span> <span class="n">ext</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">splitext</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
<span class="k">if</span> <span class="n">ext</span> <span class="o">!=</span> <span class="s">'.epub'</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"{} is not epub."</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">file_path</span><span class="p">),</span> <span class="nb">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
<span class="k">return</span>
<span class="k">if</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">output_dir</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"{} is already exists."</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">output_dir</span><span class="p">),</span> <span class="nb">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
<span class="k">return</span>
<span class="n">os</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">TEMP_DIR</span><span class="p">)</span>
<span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">(</span>
<span class="p">(</span><span class="s">'unzip'</span><span class="p">,</span> <span class="n">file_path</span><span class="p">,</span> <span class="s">"-d"</span><span class="p">,</span> <span class="n">TEMP_DIR</span><span class="p">),</span>
<span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">)</span><span class="o">.</span><span class="n">communicate</span><span class="p">()</span>
<span class="n">os</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">output_dir</span><span class="p">)</span>
<span class="n">container_xml_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">TEMP_DIR</span><span class="p">,</span> <span class="s">'META-INF'</span><span class="p">,</span> <span class="s">'container.xml'</span><span class="p">)</span>
<span class="n">etree</span> <span class="o">=</span> <span class="n">ElementTree</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">container_xml_path</span><span class="p">)</span>
<span class="n">rootfile_node</span> <span class="o">=</span> <span class="n">etree</span><span class="o">.</span><span class="n">find</span><span class="p">(</span>
<span class="s">".//{urn:oasis:names:tc:opendocument:xmlns:container}rootfile"</span><span class="p">)</span>
<span class="n">content_opf_path</span> <span class="o">=</span> <span class="n">rootfile_node</span><span class="o">.</span><span class="n">attrib</span><span class="p">[</span><span class="s">'full-path'</span><span class="p">]</span>
<span class="n">content_xml_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">TEMP_DIR</span><span class="p">,</span> <span class="n">content_opf_path</span><span class="p">)</span>
<span class="n">etree</span> <span class="o">=</span> <span class="n">ElementTree</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">content_xml_path</span><span class="p">)</span>
<span class="n">manifest</span> <span class="o">=</span> <span class="n">etree</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s">'.//{http://www.idpf.org/2007/opf}manifest'</span><span class="p">)</span>
<span class="n">items</span> <span class="o">=</span> <span class="n">manifest</span><span class="o">.</span><span class="n">findall</span><span class="p">(</span><span class="s">'.//{http://www.idpf.org/2007/opf}item'</span><span class="p">)</span>
<span class="n">image_paths</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">items</span><span class="p">:</span>
<span class="k">if</span> <span class="n">item</span><span class="o">.</span><span class="n">attrib</span><span class="p">[</span><span class="s">'media-type'</span><span class="p">]</span> <span class="o">==</span> <span class="s">'image/jpeg'</span><span class="p">:</span>
<span class="n">image_paths</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="n">attrib</span><span class="p">[</span><span class="s">'href'</span><span class="p">])</span>
<span class="n">root_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">content_xml_path</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">image_path</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">image_paths</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>
<span class="n">destination_image_name</span> <span class="o">=</span> <span class="s">'{:03d}.jpg'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="n">source_image_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">root_dir</span><span class="p">,</span> <span class="n">image_path</span><span class="p">)</span>
<span class="n">destination_image_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
<span class="n">output_dir</span><span class="p">,</span> <span class="n">destination_image_name</span><span class="p">)</span>
<span class="n">shutil</span><span class="o">.</span><span class="n">move</span><span class="p">(</span><span class="n">source_image_path</span><span class="p">,</span> <span class="n">destination_image_path</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'{} -> {}'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">image_path</span><span class="p">,</span> <span class="n">destination_image_name</span><span class="p">))</span>
<span class="n">shutil</span><span class="o">.</span><span class="n">rmtree</span><span class="p">(</span><span class="n">TEMP_DIR</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="k">for</span> <span class="n">arg</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]:</span>
<span class="n">procedure</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</pre>
追記: Github に上げて、pip でインストールできるようにしました。</div>
<div class="highlight"><a href="https://github.com/ytyng/epub-extract-jpeg">https://github.com/ytyng/epub-extract-jpeg</a></div>
<div class="highlight"></div>
</div>
</div>