[python]python-oauth2使用時にカンマ区切りのクエリでTwitter APIに失敗する件

Twitter APIのお話。
sitestream api, user look apiはユーザIDのリストをカンマ区切りで渡すが
oauthモジュールだと%2C変換かかって失敗する。
%2Cを,にdecode backしたら上手くいったね!

#encoding=utf-8

'''
inet.py
'''

import sys
import urllib
from urllib import quote_plus, _is_unicode

import urlparse
from urlparse import parse_qs

import oauth2 as oauth


def urlencode(query, doseq=0):
    """Encode a sequence of two-element tuples or dictionary into a URL query string.

    If any values in the query arg are sequences and doseq is true, each
    sequence element is converted to a separate parameter.

    If the query arg is a sequence of two-element tuples, the order of the
    parameters in the output will match the order of parameters in the
    input.
    """

    if hasattr(query,"items"):
        # mapping objects
        query = query.items()
    else:
        # it's a bother at times that strings and string-like objects are
        # sequences...
        try:
            # non-sequence items should not work with len()
            # non-empty strings will fail this
            if len(query) and not isinstance(query[0], tuple):
                raise TypeError
            # zero-length sequences of all types will get here and succeed,
            # but that's a minor nit - since the original implementation
            # allowed empty dicts that type of behavior probably should be
            # preserved for consistency
        except TypeError:
            ty,va,tb = sys.exc_info()
            raise TypeError, "not a valid non-string sequence or mapping object", tb

    l = []
    if not doseq:
        # preserve old behavior
        for k, v in query:
            k = quote_plus(str(k), safe=',')
            v = quote_plus(str(v), safe=',')
            l.append(k + '=' + v)
    else:
        for k, v in query:
            k = quote_plus(str(k), safe=',')
            if isinstance(v, str):
                v = quote_plus(v, safe=',')
                l.append(k + '=' + v)
            elif _is_unicode(v):
                # is there a reasonable way to convert to ASCII?
                # encode generates a string, but "replace" or "ignore"
                # lose information and "strict" can raise UnicodeError
                v = quote_plus(v.encode("ASCII","replace"), safe=',')
                l.append(k + '=' + v)
            else:
                try:
                    # is this a sufficient test for sequence-ness?
                    len(v)
                except TypeError:
                    # not a sequence
                    v = quote_plus(str(v), safe=',')
                    l.append(k + '=' + v)
                else:
                    # loop over the sequence
                    for elt in v:
                        l.append(k + '=' + quote_plus(str(elt), safe=','))
    return '&'.join(l)



class EscapedRequest(oauth.Request):
	def __init__(self, *args, **kwargs):
		oauth.Request.__init__(self, *args, **kwargs)

	@classmethod
	def from_consumer_and_token(cls, consumer, token=None,
			http_method=oauth.HTTP_METHOD, http_url=None, parameters=None,
			body='', is_form_encoded=False):
		if not parameters:
			parameters = {}

		defaults = {
		    'oauth_consumer_key': consumer.key,
		    'oauth_timestamp': cls.make_timestamp(),
		    'oauth_nonce': cls.make_nonce(),
		    'oauth_version': cls.version,
		}

		defaults.update(parameters)
		parameters = defaults

		if token:
			parameters['oauth_token'] = token.key
			if token.verifier:
				parameters['oauth_verifier'] = token.verifier

		return EscapedRequest(http_method, http_url, parameters, body=body,
		               is_form_encoded=is_form_encoded)


	def to_url(self):
		#Serialize as a URL for a GET request
		base_url = urlparse.urlparse(self.url)
		query = base_url.query
		query = parse_qs(query)
		for k, v in self.items():
			# Decode comma back to ascii
			if '%2C' in v:
				v = v.replace('%2C', ',')
			query.setdefault(k, []).append(v)

		scheme = base_url.scheme
		netloc = base_url.netloc
		path = base_url.path
		params = base_url.params
		fragment = base_url.fragment

		url = (scheme, netloc, path, params,
		       urlencode(query, True), fragment)
		return urlparse.urlunparse(url)

[python]python-oauth2使用時にカンマ区切りのクエリでTwitter APIに失敗する件

Twitter APIのお話。
sitestream api, user look apiはユーザIDのリストをカンマ区切りで渡すが
urllib.urlencodeちゃんがカンマを%2Cに変換してくれちゃってBad request
が返ってくる。
urlencodeのカンマ変換なし版を作った。差分はquote_plus()にsafe=','を追
加しただけ。
oauthプロトコルのハンドル用に使ってるoauth2モジュールの
Request.to_url()も作ったurlencode()を使うようにちびっと変更。

#encoding=utf-8

'''
inet.py
'''

import sys
import urllib
from urllib import quote_plus, _is_unicode

import urlparse
from urlparse import parse_qs

import oauth2 as oauth


def urlencode(query, doseq=0):
    """Encode a sequence of two-element tuples or dictionary into a URL query string.

    If any values in the query arg are sequences and doseq is true, each
    sequence element is converted to a separate parameter.

    If the query arg is a sequence of two-element tuples, the order of the
    parameters in the output will match the order of parameters in the
    input.
    """

    if hasattr(query,"items"):
        # mapping objects
        query = query.items()
    else:
        # it's a bother at times that strings and string-like objects are
        # sequences...
        try:
            # non-sequence items should not work with len()
            # non-empty strings will fail this
            if len(query) and not isinstance(query[0], tuple):
                raise TypeError
            # zero-length sequences of all types will get here and succeed,
            # but that's a minor nit - since the original implementation
            # allowed empty dicts that type of behavior probably should be
            # preserved for consistency
        except TypeError:
            ty,va,tb = sys.exc_info()
            raise TypeError, "not a valid non-string sequence or mapping object", tb

    l = []
    if not doseq:
        # preserve old behavior
        for k, v in query:
            k = quote_plus(str(k), safe=',')
            v = quote_plus(str(v), safe=',')
            l.append(k + '=' + v)
    else:
        for k, v in query:
            k = quote_plus(str(k), safe=',')
            if isinstance(v, str):
                v = quote_plus(v, safe=',')
                l.append(k + '=' + v)
            elif _is_unicode(v):
                # is there a reasonable way to convert to ASCII?
                # encode generates a string, but "replace" or "ignore"
                # lose information and "strict" can raise UnicodeError
                v = quote_plus(v.encode("ASCII","replace"), safe=',')
                l.append(k + '=' + v)
            else:
                try:
                    # is this a sufficient test for sequence-ness?
                    len(v)
                except TypeError:
                    # not a sequence
                    v = quote_plus(str(v), safe=',')
                    l.append(k + '=' + v)
                else:
                    # loop over the sequence
                    for elt in v:
                        l.append(k + '=' + quote_plus(str(elt), safe=','))
    return '&'.join(l)



class EscapedRequest(oauth.Request):
	def __init__(self, *args, **kwargs):
		oauth.Request.__init__(self, *args, **kwargs)

	@classmethod
	def from_consumer_and_token(cls, consumer, token=None,
			http_method=oauth.HTTP_METHOD, http_url=None, parameters=None,
			body='', is_form_encoded=False):
		if not parameters:
			parameters = {}

		defaults = {
		    'oauth_consumer_key': consumer.key,
		    'oauth_timestamp': cls.make_timestamp(),
		    'oauth_nonce': cls.make_nonce(),
		    'oauth_version': cls.version,
		}

		defaults.update(parameters)
		parameters = defaults

		if token:
			parameters['oauth_token'] = token.key
			if token.verifier:
				parameters['oauth_verifier'] = token.verifier

		return EscapedRequest(http_method, http_url, parameters, body=body,
		               is_form_encoded=is_form_encoded)


	def to_url(self):
		#Serialize as a URL for a GET request
		base_url = urlparse.urlparse(self.url)
		query = base_url.query
		query = parse_qs(query)
		for k, v in self.items():
			# Decode comma back to ascii
			if '%2C' in v:
				v = v.replace('%2C', ',')
			query.setdefault(k, []).append(v)

		scheme = base_url.scheme
		netloc = base_url.netloc
		path = base_url.path
		params = base_url.params
		fragment = base_url.fragment

		url = (scheme, netloc, path, params,
		       urlencode(query, True), fragment)
		return urlparse.urlunparse(url)

c拡張でPyUnicodeObject, PyStringObjectの相互変換を楽にする II

前回マクロ化した文字列変換処理にバグがあった。
Py関数でオブジェクトを生成すると、内部的に参照カウントがインクリメントされる。
使わなくなったオブジェクトへの参照カウントは明示的にデクリメントしなくてはならない。
Pythonオブジェクトは参照カウントが0になったときに解放されるので、忘れるとメモリリークします。

malloc()したのにfree()忘れたようなイメージ。

前回作成したマクロは参照カウントを無視した内容になっていた。

/* 
 * PyUnicode_AsEncodedString はPyUnicodeObjectへの参照を返す。
 * 同時に、返したPyUnicodeObjectの参照カウントが +1 される。
 * このマクロでは +1 した参照カウントを -1 してないのでメモリ上に残ったままになる。
 */
#define UNI_STR(item)	PyString_AsString(PyUnicode_AsEncodedString(item, ENCODING_CHARSET, ENCODING_ONERROR))

以下、参照カウントを明示的に-1するよう修正したコード。

#include <Python.h>
#include "common.h"

/*
 * FUNCTION:
 * 		uni_str
 *
 * ARGS:
 * 		unicode	pythonユニコード文字列
 *
 * RETURN:
 *		ON SUCCESS:	PyStrinbObject *
 *		ON_ERROR:	NULL
 *
 * DESCRIPTION:
 * 		PyUnicodeObjectをPyStringObjectに変換する。
 * 		Encode先のcharsetはcommon.h, ENCODING_CHARSETにて切り替える。
 *
 */
char *uni_str(PyObject *unicode) {
	PyObject *encoded_str;
	char *ret;

	encoded_str = PyUnicode_AsEncodedString(unicode,
				ENCODING_CHARSET, ENCODING_ONERROR);
	if (encoded_str == NULL)
		return NULL;

	ret = PyString_AsString(encoded_str);
	Py_DECREF(encoded_str);

	if (ret == NULL)
		return NULL;

	return ret;
}

/*
 * FUNCTION:
 * 		uni_cat
 *
 * ARGS:
 * 		unicode	pythonユニコード文字列にconst char *をappend
 *
 * RETURN:
 *		ON SUCCESS:	PyUnicodeObject*
 *		ON_ERROR:	NULL
 *
 * DESCRIPTION:
 * 		PyUnicodeObjectに文字列連結。
 *
 */
PyObject *uni_cat(PyObject *unicode, const char *str) {
	PyObject *ret;
	PyObject *decoded_uni;

	decoded_uni = PyUnicode_Decode(str, strlen(str),
					ENCODING_CHARSET, ENCODING_ONERROR);
	if (decoded_uni == NULL)
		return NULL;

	ret = PyUnicode_Concat(unicode, decoded_uni);
	if (ret == NULL)
		return NULL;

	Py_DECREF(decoded_uni);

	return ret;
}

/*
 * FUNCTION:
 * 		str_uni
 *
 * ARGS:
 * 		str	c言語の文字列表現
 *
 * RETURN:
 *		ON SUCCESS:	PyUnicodeObject*
 *		ON_ERROR:	NULL
 *
 * DESCRIPTION:
 * 		c言語の文字列表現をPyUnicodeObjectにデコードする。
 *
 */
PyObject *str_uni(const char *str) {
	PyObject *ret;

	ret = PyUnicode_Decode(str, strlen(str),
				ENCODING_CHARSET, ENCODING_ONERROR);
	if (ret == NULL)
		return NULL;

	return ret;
}

c拡張でPyUnicodeObject, PyStringObjectの相互変換を楽にする

pythonでいうunicode⇔string変換。

'ほげほげ'.decode('utf-8', 'ignore')
u'ほげほげ'.encode('utf-8', 'ignore')

内部的にはunicodeはPyUnicodeObject, stringはPyStringObjectという構造体で管理してらっしゃる。この2つをCで楽に相互変換したい。ちなみに、デコードはこんな感じになる。

char *str = "ほげほげですよ";
PyUnicode_Decode(str, strlen(str), "utf-8", "ignore");

長いw マクロ化しちゃいます。

#ifndef __COMMON_H__
#define __COMMON_H_

#define DEBUG
#ifdef DEBUG
  #define LOG printf
#else
  #define LOG
#endif

#define ENCODING_CHARSET	"cp932"
#define ENCODING_ONERROR	"ignore"

/*
 * PyUnicodeObject → (char *)
 */
#define UNI_STR(item)	PyString_AsString(PyUnicode_AsEncodedString(item, ENCODING_CHARSET, ENCODING_ONERROR))

/*
 * (char *) → PyUnicodeObject
 */
#define STR_UNI(str)	PyUnicode_Decode(str, strlen(str), ENCODING_CHARSET, ENCODING_ONERROR)

/*
 * PyUnicodeObject + (char *)
 */
#define UNI_CAT(item, str)	PyUnicode_Concat(item, \
				STR_UNI(str))


#endif

c拡張でのunicode, strの扱い

ユニコードを受け取ってutf-8文字列に変換する。これだけでも難しかった…

#include <Python.h>

/* モジュールの関数 */
static PyObject *hello(PyObject* self, PyObject *args, PyObject *kwds)
{
	PyObject *first = NULL;
	PyObject *encoded = NULL;

	// PyObjectとして引数を取得する。
	if (!PyArg_ParseTuple(args, "O", &first)) {
		return NULL;
	}
	if (PyString_Check(first)) {
		printf("first=%s\n", PyString_AsString(first));
	} else if(PyUnicode_Check(first)) {
		encoded = PyUnicode_AsUTF8String(first);
		printf("first=%s, slength=%d, ulength=%d\n",
						PyString_AsString(encoded),
						PyString_Size(encoded),
						PyUnicode_GetSize(first));
	} else {
		return NULL;
	}
	Py_RETURN_NONE;
}

/* モジュールのメソッドテーブル */
static PyMethodDef methods[] = {
	{"hello", (PyCFunction)hello, METH_VARARGS | METH_KEYWORDS, "print hello world.\n"},
	{NULL, NULL, 0, NULL}
};

/* モジュールの初期化関数 */
PyMODINIT_FUNC initpypysample(void)
{
	(void)Py_InitModule("pypysample", methods);
}

Python C APiで拡張 on windows

cソースはmingw使ってビルド。

#include <Python.h>

/* モジュールの関数 */
static PyObject *hello(PyObject* self, PyObject *args)
{
	printf("Hello World!!\n");
	Py_RETURN_NONE;
}

/* モジュールのメソッドテーブル */
static PyMethodDef methods[] = {
	{"hello", (PyCFunction)hello, METH_VARARGS, "print hello world.\n"},
	{NULL, NULL, 0, NULL}
};

/* モジュールの初期化関数 */
PyMODINIT_FUNC initsample(void)
{
	(void)Py_InitModule("sample", methods);
}

ビルドコマンド

gcc -o ext_sample.o -c ext_sample.c -I"C:\python27\include"
gcc -shared ext_sample.o -o ext_sample.pyd -L"C:\python27\libs" -lpython27

注意点

  • initsample()だた、命名規則として「init<モジュール名>」とする必要がある。

matplotlibで日本語

日本語を扱う場合はfontファイルのパスを直値で設定するしかないのか?今のところそれ以外の解決方法が見つけられていないがメモ。

import matplotlib.pyplot as plt
import matplotlib.font_manager as mng

# MS ゴシック
fProp = mng.FontProperties(fname=u'C:\\Windows\\Fonts\\msgothic.ttc')

axis = plt.subplot(111)
axis.bar(x, seqList)

# グラフタイトルに日本語
plt.title(u'ぴったり10GB 11月', fontproperties=fProp)