エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

AppEngine

AppEngineからCloudStorageを使う

AppEngineのPython StandardEnvironmentからCloudStorageを使用します。DataStoreからCloudStorageに移行することで、保存データのバイト単価を1/10にすることができます。

CloudStorageを使用するには、appengine-gcs-clientをダウンロードして、アプリケーションのlibフォルダにcloudstorageフォルダをコピーします。

CloudStorageはバケットというフォルダのようなものに書き込みを行います。書き込み先のバケットをget_default_gcs_bucket_nameで取得することで、開発サーバでもCloudStorageは使用可能です。本番環境ではCloudStorageにあるアプリ名と同じ名称のデフォルトバケットに書き込まれます。

from google.appengine.api import app_identity

if 'lib' not in sys.path:
		sys.path[0:0] = ['lib']

import cloudstorage as gcs 

class ChunkManager(db.Model):
	def gcs_get_bucket_name(self):
		bucket_name = os.environ.get('BUCKET_NAME',	
					app_identity.get_default_gcs_bucket_name())
		return "/"+bucket_name+"/"

	def gcs_upload(self, filename, data):
		write_retry_params = gcs.RetryParams(backoff_factor=1.1)
		gcs_file = gcs.open(self.gcs_get_bucket_name()+filename,
						'w',
						content_type='application/octet-stream',
						retry_params=write_retry_params)
		gcs_file.write(data)
		gcs_file.close()

	def gcs_contents(self, filename):
		gcs_file = gcs.open(self.gcs_get_bucket_name()+filename)
		contents = gcs_file.read()
		gcs_file.close()
		return contents

	def gcs_download(self, filename, out):
		contents = self.gcs_contents(filename)
		out.write(contents)

	def gcs_delete(self, filename):
		try:
			gcs.delete(self.gcs_get_bucket_name()+filename)
		except gcs.NotFoundError:
			pass

参考:Google App EngineからGoogle Cloud Storageを利用する

AppEngineのDataStoreに新しいプロパティを追加する

既存のDataStoreのエンティティに新しいプロパティを追加した場合、保存済みのデータにはNoneが代入されます。Noneが代入されたプロパティはIndexに格納されていないため、Queryで参照することができません。

デザインパターンとしては、プロパティの追加に備えて、スキーマにversionというIntegerPropertyを追加しておきます。新しいプロパティを追加する際、versionが一定以下の場合をクエリして、新しいプロパティを初期化してputします。

しかし、そもそもversionを書き込んでいないスキーマの場合、まずはversionをスキーマに追加する必要があります。そのためには、全てのDataStoreのエンティティをループで回してversionを書き込んでputする必要があります。AppEngineのQueryは1000件以上取得できないため、このような場合はDataStoreのCursorを使用します。また、リクエストの60秒制約を解決するためにdeferred queueを使います。

以下、Python2.7 + AppEngine standard environment + dbの例です。

app.yamlにdeferredを追加します。

- url: /_ah/queue/deferred
  script: google.appengine.ext.deferred.deferred.application
  login: admin

builtins:
- deferred: on

cursorで回しながらversionを追加して上書きします。

from google.appengine.ext import deferred

def update_schema_loop(xbrl,num_fetched,num_updated,batch_size,tag):
	i=0

	to_put = []
	for one in xbrl:
		one=db.get(one.key())	#強い整合性を保証
		if not one.version:	#Noneチェック
			one.version=1	#新しいプロパティ
			to_put.append(one)
		i=i+1
		if i>=batch_size:
			break

	num_fetched += i

	if to_put:
		db.put(to_put)
		num_updated += len(to_put)

	return xbrl,num_fetched,num_updated,i

def update_schema_pdf(cursor=None, num_fetched=0, num_updated=0, batch_size=100):
	data_list = PdfDocument.all()
	data_list.with_cursor(start_cursor=cursor)
	data_list,num_fetched,num_updated,detected_cnt=update_schema_loop(data_list,num_fetched,num_updated,batch_size,"pdf")
	next_cursor = data_list.cursor()
	more=(detected_cnt!=0)
	if more:
		deferred.defer(
			update_schema_pdf, cursor=next_cursor, num_fetched=num_fetched, num_updated=num_updated)
	else:
		count = PdfDocument.all().count(limit=1000000)
		logging.info(
			'Complete update schema pdf with {} fetchs {} updates of {}!'.format(
				num_fetched,num_updated,count))

class UpdateSchemaHandler(webapp2.RequestHandler):
	"""Queues a task to start updating the model schema."""
	def get(self):
		deferred.defer(update_schema_pdf)
		self.response.write("""
		Schema update started. Check the console for task progress.
		""")

参考:Updating Your Model's Schema

PythonでGoogleCloudStorageにファイルをアップロードする

PythonでGoogleCloudStorageにファイルをアップロードします。

まず、IAMでサービスアカウントを作成し、サービスアカウントの鍵情報をjsonでダウンロードします。その後、CloudStorageのBucketの権限にサービスアカウントを追加します。

必要なライブラリをインストールします。

pip3 install google-cloud-storage
python3 upload.py src.zip dest.zip

ダウンロードしたJSONを使用してアップロードします。

import os
import sys
from google.cloud import storage

os.environ["GOOGLE_APPLICATION_CREDENTIALS"]='xxx.json'
client = storage.Client()
bucket = client.get_bucket('backup')

print("from : "+sys.argv[1])
print("to   : "+sys.argv[2])
blob = bucket.blob(sys.argv[2])
blob.upload_from_filename(filename=sys.argv[1])

GoogleAppEngineのhttps対応

GoogleAppEngineでhttps対応を行うには、カスタムドメインの設定からマネージドセキュリティを有効にします。

ssl

アプリ側は、Pythonからhttpsかhttpかを判定し、適切なURLにリダイレクトします。

def get_host(self):
	if self.request.scheme.lower() != 'https':
		return "http://"+self.request.host
	return "https://"+self.request.host

GoogleCloudStorageの公開アクセス設定

GoogleCloudStorageにアップロードしたファイルに誰でもURLでアクセスできるようにするためには、GoogleCloudStorageのBucketを公開アクセスに設定する必要があります。

公開アクセスの設定をするには、Bucketの権限設定でallUsersに読み取り権限を設定します。

公開アクセス


公開アクセスの設定を有効にすると、以下のようなURLでBucket内のファイルにアクセスすることができます。

https://storage.googleapis.com/[Bucket]/[File]

AppEngineでは32MB以上のファイルをアップロードできない

AppEngineではhttpリクエストのgetとpostが32MBまでに制限されます。32MBよりも大きなファイルをアップロードする場合は、Blobstoreを使用する必要があります。

Blobstore Python API Overview

DataStore版のBlobstoreは提供が終了しましたが、GoogleCloudStorage版のBlobstore APIはdeprecatedになっておらず、使用可能です。

Feature Deprecations

AppEngineで32MB以上のファイルをアップロードする方法がBlobstore以外存在しないため、今後も使えるのではないかと考えています。

GoogleAppEngine/GoでCloudStorageのファイルを配信する方法とハマりどころ

Blobstore APIを使用するとCloudStorageのバケットが自動で作成されるため、特にCloudStorageを意識せずに使用可能です。

AppEngineのdevserverでSearchIndexを保持する

AppEngineのdevserverでは、サーバを再起動するとFullTextSearchApiのSearchIndexが初期化されます。これを初期化されないようにするには、devserverのExtraFlagsにsearch_indexes_pathを与えます。

--search_indexes_path=/Users/abars/gae_datastore_tmp/tdnet.search


また、データストアの保存先フォルダを変えるのも、同様に、--datastore_pathを使用することもできます。

--datastore_path=/Users/abars/gae_datastore_tmp/tdnet.datastore

BigQueryのStreamingAPIで日付分割する

AppEngineからBigQueryにinsertする際、templateSuffixに日付を指定すると、TABLE_IDのSchemaを使用して、自動的にテーブルを日付分割することができます。

    SUFFIX_ID = "_"+datetime.datetime.now().strftime("%Y%m%d")
    body = { 'rows':[{'json':data_hash}] ,'ignoreUnknownValues':True ,'templateSuffix':SUFFIX_ID}
...
    response = bigquery.tabledata().insertAll(projectId=PROJECT_ID,
                                              datasetId=DATASET_ID,
                                              tableId=TABLE_ID,
                                              body=body).execute()

AppEngineのデータストアの自動バックアップ

AppEngineのデータストアのバックアップをcronで自動化する方法が公開されていました。 Scheduled Backupsでは、データストアをCloudStorageにバックアップします。

まず、バックアップ先のCloudStorageのバケットを作成します。

cloud_storage

次に、データストアの管理を有効にします。

datastore

AppEngineプロジェクトのcron.yamlにバックアップジョブを設定します。kindにはバックアップ対象のエンティティ名を、gs_bucket_nameに先ほど作成したバケットを登録します。

- description: backup
  url: /_ah/datastore_admin/backup.create?name=BackupToCloud&kind=Bookmark&filesystem=gs&gs_bucket_name=backup-tdnet-search
  schedule: every 12 hours
  target: ah-builtin-python-bundle

cronを実行すると、データストアの管理画面に表示されます。反映までに少し時間がかかります。

datastore_admin

Cloud Storage側にも反映されます。

bucket

今まで、データストアのバックアップは手動でぽちぽちするしかなかったのですが、自動化できるようになって便利です。

少人数でソーシャルゲームを作るツールチェイン

フェイスブックが買収したWhatsAppが社員数50人で4億5000万人に対してサービスを提供できたように、開発環境の進化とクラウド化によって、少人数でも大規模なアプリケーションが開発できるようになってきました。

今回は、エンジニア視点で、少人数で大規模なソーシャルゲームの開発を行うために最適なツールチェインを考えてみます。

Unity5


昔はゲームを作るときはゲームエンジンから作っていたものですが、Unityの登場によって、ゲームのコアだけを記述すればよくなりました。iOSとAndroidのマルチプラットフォーム化が必須な今、開発環境としてのWindowsとMacの対応も考えると、Unityを使わないという選択肢はない気がします。nGUIとIAPの対応で、外部プラグイン不要で完結するようになってきましたし、どうしても不足する機能は自分でプラグインを書けば良いという安心感もあります。また、最新のMAYAのfbxへの対応など、何もしなくてもゲームエンジンがメンテナンスされていくというのは、自社エンジンにはない魅力です。

Maya LT でローポリキャラクタモデリングに挑戦して Unity で動かしてみた

Unity Cloud Build


Unity Cloud Buildを使うと、リポジトリにpushするだけで、自動的にiOSとAndroidのアプリをビルドすることができます。これにより、物理的に離れた場所にいたとしても、チームメンバー全員がいつでも最新版で動作確認することができます。開発はPCだけで行えばよく、実機を有線で接続する必要もないので、単純な開発効率も上がります。iOSアプリをWindowsだけで開発できるというのは革命的です。

Unity Cloud Buildの使い方

Bitbucket


GitのPrivateリポジトリを無料で作ることができます。Unity Cloud Buildとの連携を考えると、リポジトリはクラウドに持った方が便利です。

Unity向け .gitignoreの設定について

Slack


メールだと定型句が必要ですが、チャットだと重要な点だけを書けるのでコミュニケーションの効率が上がります。コミュニケーションの手段としてのメールは今後、衰退していく気がします。ChatWork、HipChatとも比較しましたが、Slackが一番、アプリの出来がよくできていました。

Google Docs


仕様書やドキュメントなどは、複数人同時に編集できるGoogle Docsで管理すると便利です。日付付きのWordファイルやExcelファイルをメールで送る必要はありません。ゲームのリソースもGoogle Driveでやり取りするとスムーズです。

Google AppEngine


少人数で運営することを考えると、サーバ運営をしているリソースはありません。Google AppEngineはDataStoreなど、プログラム側にかなり強い制約がかかりますが、その制約によって、原理的にAppEngineで動けば必ずスケールすることが保証されます。また、マネージドサービスなため、脆弱性の発覚による依存ライブラリのバージョンアップなども不要です。すなわち、サーバの保守が不要になります。

唯一、国内での実績が乏しいのが採用のネックだったのですが、メルカリ アッテがAppEngineを採用したことで、その障壁もなくなりました。さらに、2016年9月には待望の東京リュージョンが開設され、遅延が減ります。証明書なしでSSLが使えるのも魅力です。ゲームサーバなら独自ドメインがいらないので手軽です。

ゲームサーバへのサーバレスアーキテクチャの適用は、今後のトレンドになるのではないでしょうか。

SSL の設定
サーバーレスアーキテクチャという技術分野についての簡単な調査

AppEngineでPDFのバイナリからテキストを抽出する

Google AppEngineでPDFからテキストを抽出するには、pdfminerを使うと便利です。pdfminerは、ピュアPythonで書かれた、PDF解析ライブラリです。

ライブラリのインポートは、GitHubのpdfminerのリポジトリからpdfminerフォルダをAppEngineのプロジェクトフォルダにコピーするだけです。以降、以下のようなコードでテキスト抽出を行うことができます。get_pdf_textの引数のcontentはPDFのBlobです。

from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfpage import PDFPage

from io import BytesIO
from StringIO import StringIO

def process_pdf(rsrcmgr, device, fp, pagenos=None, maxpages=0, password='', caching=True, check_extractable=True):
	interpreter = PDFPageInterpreter(rsrcmgr, device)
	for page in PDFPage.get_pages(fp, pagenos, maxpages=maxpages, password=password, caching=caching, check_extractable=check_extractable):
		interpreter.process_page(page)

def get_pdf_text(content):
	rsrcmgr = PDFResourceManager()
	retstr = StringIO()
	codec = 'utf-8'
	laparams = LAParams()
	device = TextConverter(rsrcmgr, retstr, codec=codec)

	input_io = BytesIO(content)
	try:
		process_pdf(rsrcmgr, device, input_io, None , 8)	#max 8 page
	except:
		return ""
	device.close()

	str = retstr.getvalue()
	retstr.close()

	self.text=db.Text(str, encoding="utf-8")

	return str

ただし、認識できなかった文字コードは(cid:xxx)のようなテキストが返ってくるので、見栄えが気になる場合は適当にgrepしてやる必要があります。また、process_pdfに大きなPDFを与えると、AppEngineのスモールインスタンスではメモリが不足するので、ページ数は適当に制約をかける必要があるかと思います。

他、ピュアPythonのPDF解析ライブラリとして、pypdf2もありますが、日本語のPDFがうまく解析できなかったので、pdfminerの方がオススメです。

AppEngineのSearchAPIで検索できない単語がある場合の対策

AppEngineのSearch APIでは、一般的な単語が含まれる場合は適切に検索できません。例えば、「三井倉庫」で検索しようとした場合、「倉庫」を含むだけで検索結果に出現してしまいます。

この問題の対策としては、三井倉庫ではなく、”三井倉庫”で検索する必要があります。しかし、一般のユーザに、意識的に””を入力してもらうのは難しいのが現状です。

そこで、AppEngineのSearch APIに投げる前に、クエリを加工するのがよいと考えています。

		query=query.replace(" "," ")
		query_list=query.split(" ")
		query_actual=""
		for one_query in query_list:
			if(one_query!="OR" and one_query!="AND" and one_query!="NOT"):
				if(not re.match(".*[ :><=\"].*",one_query)):
					one_query="\""+one_query+"\""
			if(query_actual!=""):
				query_actual+=" "
			query_actual+=one_query

		try:
			query=search.Query(
				query_string=query_actual,
				options=options,)
		except:
			return "query error"

上記コードでは、ORやANDやNOT、title:などの制御系クエリでない場合に、自動的に””を付加します。また、全角スペースを半角スペース(OR)に置換します。これで、普通のGoogle検索のように使うことができます。

iOS9とAppEngineでjsの読み込みに30秒かかる

iOS9に上げると、AppEngineのjsのGETリクエストのレイテンシが30秒になってしまいました。

ワークアラウンドとしては、以下のように、jsのドメインを別のドメインにする必要があります。独自ドメインを使用している場合は、規定で割り当てられる、appspot.comを流用できるかと思います。

変更前
script src="/javascript/categoryScript.js">

変更後
script src="yourid.appspot.com/javascript/categoryScript.js">


モバイルSafariの通信を、Safariでプロファイルすると、以下のようにレイテンシが30secになっています。

masonry_latency

同じ通信をAppEngineサイドから。最初のGETリクエストはAppEngineのログには残っておらず、30sec後の最後のリクエストだけが残っています。成功したリクエストはHTTP1.1ですね。

masonry2

最初は、iOS9から対応したHTTP2で、AppEngineとプロトコルの齟齬が発生しているんじゃないかなと推測していたのですが、AppEngineはhttpsでないとHTTP2で通信しないようです。HTTP2 and SPDY Indicatorでも、HTTP1.1となっていたので、HTTP2が問題ではなさそうです。

AppEngineでListPropertyを追加する場合の問題

SDK1.9.17のAppEngineのEntityにListPropertyを追加した場合、既存のEntityのfetchでBadValueErrorが発生します。Loading db.ListProperty() with AppEngine bulkloaderによると、これはAppEngineのバグとして報告されているようですが、まだ修正されていないようです。

この問題は、default_property=[]にしても解決しませんし、required=Falseに設定することもできません。この場合、問題を解決するために、プロパティを削除するか、既存のEntityを全て更新しなければなりません。

StringListPropertyも同様です。修正されるまで、ListPropertyは追加しないほうがよさそうです。

AppEngineでREMOTE_HOSTを取得する

Google App EngineではREMOTE_HOSTが取得できません。そのため、ユーザのIPアドレスは分かりますが、ホスト名は分かりません。また、Socket APIのgethostbyipは実装されていません。

そこで、別のサーバで取得したREMOTE_HOSTをJavaScript経由で受け渡すことを考えます。REMOTE_HOSTを取得するサーバは、App Engineとは別のドメインになるため、クロスドメインポリシーを気にする必要があります。

クロスドメインを超える簡単な方法は、JSONPを使用することです。JSONPは、サーバが返り値を含むJavaScriptを返すことで、ドメインを超えます。

サーバ側のスクリプトは以下のようになります。

<?php
header('content-type: application/json; charset=utf-8');
$hostname = gethostbyaddr($_SERVER['REMOTE_ADDR']);
print $_GET['callback'] . '("' . $hostname . '");';
?>

呼び出し側のスクリプトは以下のようになります。

<script type="application/javascript">
function get_host(host_name){
    alert(host_name); // alerts the host name
}
</script>
<script type="application/javascript" src="http://hoge/remote_host.php?callback=get_host"></script>

AppEngineからGoogle Analyticsにアクセスする

Core Reporting API


Google AnalyticsにはCore Reportion APIがあり、API経由でアクセス情報を取得することができます。これを使って、自分のサイトのアクセスランキングなどを作って、リアルタイムにサイトに表示することを考えます。


Api Clientによるアクセス


Core Reporting APIは、Googleの提供するapi clientを使用してアクセスすることができます。api clientは、oauth2による認証が必要です。実装例が、Google APIs Client Library for Python : Using Google App Engineに記載されているので、参考にします。

読んで頂ければ、用途によっていろいろな認証が定義されていることが分かります。最初に解説されているOAuth2DecoratorFromClientSecretsは、アクセスしているユーザで認証するため、今回のようにアクセスランキングを作るような用途には向きません。やりたいことは、server-to-serverのアクセスであり、AppAssertionCredentialsになります。

AppAssertionCredentialsは、Googleのサービスアカウントでログインする形になります。Analyticsのアカウント設定で、このサービスアカウントに表示権限を与えれば、目的は達成できます。

しかし、AppAssertionCredentialsは、開発用サーバでは動きません。開発用サーバでも動くようにするには、SignedJwtAssertionCredentialsを使う必要があります。


サービスアカウントの作成とAnalyticsへの追加


SignedJwtAssertionCredentialsは、Google Api Consoleで作成したサービスアカウントでAnalyticsにアクセスする形になります。認証は秘密鍵を使用します。

まず、Google Api ConsoleのAPIと認証のタブで、サービス アカウントを作成します。サービスアカウントを作成すると、秘密鍵を.p12形式でダウンロードすることができます。


service


AppEngineで使えるようにするために、.p12形式のファイルを、pem形式に変換します。

openssl pkcs12 -passin pass:notasecret -in privatekey.p12 -nocerts -passout pass:notasecret -out key.pem
openssl pkcs8 -nocrypt -in key.pem -passin pass:notasecret -topk8 -out privatekey.pem
rm key.pem


次に、Analyticsの設定で、サービスアカウントのメールアドレス(hoge@developer.gserviceaccount.com)に表示権限を与えます。


analytics


Pycryptoのインストール


SignedJwtAssertionCredentialsを使うにはpycryptoが必要ですので、app.yamlに定義します。

libraries:
- name: pycrypto
version: latest


本番環境はこれでOKですが、開発サーバは自前でpycryptoをinstallする必要があります。

sudo port install py27-crypto


コード例


SignedJwtAssertionCredentialsが使えるようになりました。

import httplib2

from google.appengine.api import memcache

from apiclient.errors import HttpError
from apiclient import discovery
from apiclient.discovery import build

from oauth2client.client import AccessTokenRefreshError
from oauth2client import appengine
from oauth2client import client
from oauth2client.client import SignedJwtAssertionCredentials

class AnalyticsGet():
def get(self,bbs_name):
KEY = "privatekey.pem"
SCOPES = [
'https://www.googleapis.com/auth/analytics',
'https://www.googleapis.com/auth/analytics.edit',
'https://www.googleapis.com/auth/analytics.manage.users',
'https://www.googleapis.com/auth/analytics.readonly',
]
SERVICE_ACCOUNT = "hoge@developer.gserviceaccount.com"
key = open(KEY).read()
credentials = SignedJwtAssertionCredentials(SERVICE_ACCOUNT,
key,
scope=SCOPES)
http = httplib2.Http()
httplib2.debuglevel = True
http = credentials.authorize(http)
service = build('analytics', 'v3', http=httplib2.Http(memcache))


後は、Hello Analyticsのサンプルプログラムの、get_first_profile_id以降を呼ぶような感じで、Analyticsのデータを取得することができます。

API制約

Google Analytics APIは、1日あたり、Analyticsのアカウント単位で50000アクセス、Profile IDにつき、10000アクセスまでに制約されます。Quota limit for google analytics APIによると、アカウント単位の制約は問い合わせで増加させることができるようですが、Profile IDの制約を増加させることはできないので、注意が必要です。ページランキングなどの場合は、動的に生成せず、一定間隔でcronなどでランキングを作成、表示するなどして、APIのアクセス数を減らす必要があります。

App Engineの検索APIで日付によるスコアリングを行う

Google App Engineの検索APIでは、SortExpressionを定義することができます。一番簡単な方法が、日付によるソートです。以下のコードで、新しい順に検索結果を取得することができます。

search.SortExpression(expression='date' direction=search.SortExpression.DESCENDING, default_value=0)


しかし、これだけだと、日付だけでソートされてしまうため、あまりユーザビリティが高くありません。ユーザからの評価の高いものや、ブックマーク数が多いものなどで重み付けをしたくなります。

expressionには、式が記述できるため、以下のようにすることで、任意のスコアで検索結果をソートすることができます。

search.SortExpression(expression='bookmark_cnt*5+like_cnt' ,
direction=search.SortExpression.DESCENDING, default_value=0)


しかし、古い投稿ほどブックマーク数は多くなりがちなので、まだ不十分です。新しい投稿ほど、先に表示しやすくしたくなります。しかし、dateプロパティはそのままだと式に使用できません。

そこで、日付をNumberFieldに変換して保存しておきます。

search.NumberField(name='sec', value=_get_sec(thread.create_date))


その上で、現在の日付で重み付けを行います。

now_sec=_get_sec(datetime.datetime.now())
reduct='(1+('+str(now_sec)+'-sec)/(3600*24*30))'; #一ヶ月で半分のスコアにする
search.SortExpression(expression='(bookmark_cnt*5+like_cnt)/'+reduct,
direction=search.SortExpression.DESCENDING, default_value=0)


これで、それなりにそれっぽい検索結果になります。

尚、デフォルトだと、検索条件を満たす1000件の範囲でのみソートが行われます。これだと、データ数が多くなると正常にソートできないため、SortOptionsのlimitプロパティを大きく設定することをオススメします。limit値は、最大で10000まで設定することができます。

Google App Engineが2014年4月1日から約30%の値下げ

Google App Engineの料金が2014年4月1日から値下げされることが発表されました。(新しい料金体系

App Engine pricing is drastically simplified. We've lowered pricing for instance-hours by 37.5%, dedicated memcache by 50% and Datastore writes by 33%. In addition, many services, including SNI SSL and PageSpeed are now offered to all applications at no extra cost.

従来の料金と比較してみました。

新料金旧料金値下げ幅
Instances $0.05 / instance / hour$0.08/Hour37.5%
Cloud Datastore
(NoSQL Database)
$0.06 / 100k read or write ops
Small operations free*
$0.18 / GB / month
write $0.90/Million Ops
read $0.60/Million Ops
small $0.10/Million Ops
storage $0.006/GByte-day
write 33.3%
read 0%
small 100%
storage 0%
Outgoing Network Traffic$0.12 / GB $0.12/GByte0%

ほとんどのアプリでは、インスタンスとデータストアが支配的なので、30%程度の値下げが期待できるかと思います。とても嬉しいです。また、small datastore operationがfreeになったので、Keys Onlyなオペレーションが捗ります。

気になるのはBackendsのFree Quotaの記載が無くなっていること。最近は、Modulesを推奨していることもあって、Backendsは非推奨の流れかもしれません。

AppEngineで画像を縮小すると右端に灰色の縦線が出る問題の回避法

AppEngineの開発環境では問題ないのですが、本番環境で画像を縮小した場合に、縮小率によっては、画像の右端と下端に灰色の縦線が出ることがあります。

問題の出るコードは以下です。

img = images.Image(image)
img.resize(width=w,height=h)
img.execute_transforms()

推測するに、バイリニアで縮小をかける際に、+1で画面外の画素を参照してしまっており、画面外の画素が黒となり、白との合成で灰色の線が出るのだと思います。

この問題を回避するには、+1のサイズの画像に縮小した後、cropで右端と下端を使用しない形に切り取ります。これによって、縦線が出ても、それを除去することができます。

img = images.Image(image)
margin=1
img.resize(width=w+margin,height=h+margin)
img.crop(0.0,0.0,1.0*w/(w+margin),1.0*h/(h+margin))
img.execute_transforms()

AppEngineでアルファ付きPNGをJPEG変換した場合に背景が黒くなる問題の対処法

AppEngineでサムネイルを作る場合に、元データにアルファが含まれている画像をJPEG変換した場合、背景を黒と仮定して合成されるため、真っ黒な画像が生成されます。

具体的に、次のようなコードの場合、背景は黒く塗りつぶされてしまいます。

jpeg=img.execute_transforms(output_encoding=images.JPEG)


背景を白と仮定して合成するには、次のように、compositeメソッドでJPEG変換を行います。compositeメソッドでは、第四引数に、背景色を指定することができます。

jpeg=images.composite([(img, 0, 0, 1.0, images.TOP_LEFT)], img.width, img.height, 0xffffffff, images.JPEG, 90)
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING