JAG Contest 2016 Domestic E - 選挙活動

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

オーソドックスな幾何. 列挙して意味のある候補点を列挙する.この候補点に意味のない点が含まれていてもいいので雑に. 候補点を列挙するために,有権者から各多角形の頂点への直線を伸ばして,それを列挙してみる(見える範囲が変化する角度に対する線分を考えるとそれを列挙するのが合理的そうなことが分かる). で,得られた直線全てについて,2つの直線の交点を求めて,それを候補点とする.

候補点が求まれば,各点について,それを見ることのできる有権者の数を数えれば良い. 条件の「多角形の頂点または有権者のいる場所の座標のうち3点が同一直線状に存在することはない.」がありがたい制約で,これのおかげで幾何パートがかなり楽になっていると言っても良い. 多分実装の軽い方針で多角形と線分の交差判定をする必要があるんですが,その制約のおかげで以下のような図のケースを考えなくて良いため,楽になる. f:id:kyuridenamida:20160424212830p:plain

計算量はよく見積もってないけど多項式時間で終わるしそんなに大きくない.

ソース

#include <bits/stdc++.h>
using namespace std;
 
const double EPS = 1e-8;
const double INF = 1e12;
typedef complex<double> P;
typedef vector<P> G;
namespace std {
  bool operator < (const P& a, const P& b) {
    return real(a) != real(b) ? real(a) < real(b) : imag(a) < imag(b);
  }
}
 
double cross(const P& a, const P& b) {
  return imag(conj(a)*b);
}
 
double dot(const P& a, const P& b) {
  return real(conj(a)*b);
}
struct L : public vector<P> {
  L(const P &a, const P &b) {
    push_back(a); push_back(b);
  }
};
 
int ccw(P a, P b, P c) {
  b -= a; c -= a;
  if (cross(b, c) > 0)   return +1;     
  if (cross(b, c) < 0)   return -1;      
  if (dot(b, c) < 0)     return +2;       
  if (norm(b) < norm(c)) return -2;       
  return 0;
}
 
P crosspoint(const L &l, const L &m) {
  double A = cross(l[1] - l[0], m[1] - m[0]);
  double B = cross(l[1] - l[0], l[1] - m[0]);
  if (abs(A) < EPS && abs(B) < EPS) return m[0]; 
  if (abs(A) < EPS) assert(false); 
  return m[0] + B / A * (m[1] - m[0]);
}
 
// 以下の条件式で,0未満としているのは(ccwの返り値は-EPSなのは全く意味ない),線分が端点で接しているときは当たっていないことにしたいため.
bool intersectSS(const L &s, const L &t) {
  return ccw(s[0],s[1],t[0])*ccw(s[0],s[1],t[1]) < -EPS &&
         ccw(t[0],t[1],s[0])*ccw(t[0],t[1],s[1]) < -EPS;
}
bool intersectLL(const L &l, const L &m) {
  return abs(cross(l[1]-l[0], m[1]-m[0])) > EPS || 
         abs(cross(l[1]-l[0], m[0]-l[0])) < EPS;   
}
 
 
P in(){
    double x,y;
    cin >> x >> y;
    return P(x,y);
}
int main(){
    int N,M;
    cin >> N >> M;
    vector<L> ls;
    vector<G> gs; 
    vector<P> check;
    vector<P> look;
    for(int i = 0 ; i < N ; i++){
        int L;
        cin >> L;
        G g;
        for(int j = 0 ; j < L ; j++){
            g.push_back(in());
            check.push_back(g.back());
            look.push_back(g.back());
        }
        gs.push_back(g);
    }
    vector<P> people;
    for(int i = 0 ; i < M ; i++){
        people.push_back(in());
    }
    for(int i = 0 ; i < M ; i++){
        for(int j = 0 ; j < check.size() ; j++){
            ls.push_back(L(check[j],people[i]));
        }
    }
    
    for(int i = 0 ; i < ls.size() ; i++){
        for(int j = i + 1 ; j < ls.size() ; j++){
            if( intersectLL(ls[i],ls[j]) ){
                look.push_back(crosspoint(ls[i],ls[j]));
            }
        }
    }
    int ans = 0;
    for( auto lp : look ){
        int sub = 0;
        for(int i = 0 ; i < M ; i++){
            int f = 1;
            for(int j = 0 ; j < gs.size() ; j++){
                for(int k = 0 ; k < gs[j].size() ; k++){
                    L l(gs[j][k],gs[j][(k+1)%gs[j].size()]);
                    if( intersectSS(l,L(people[i],lp)) ) f = 0;
                }       
            }
            if( f ) sub++;
        }
        ans = max(ans,sub);
    }
    cout << ans << endl;
}

JAG Contest 2016 Domestic D - インビジブル

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

よくあるゲーム木のmin-max探索,後手は差を最小化,先手は差を最大化するように実装する. 状態数がどれくらいになるのかを見積もるのが本質. 実際山札の残り枚数と,どの区間(※)がスタックに残ってるかみたいなのを考えると,状態として O(NM*(N+M)2)程度しか無い.実際はパスの回数とかどっちのターンかみたいな状態も持つんだけど,それはとても小さい定数倍だからまあ問題ない. 実装を複雑化させないために構造体とmapを用いて実装したので重いけど通る.

(※) 手札の引き方はパスを無視すれば一意で,a_1b_1a_2b_2 ,...というふうな列になるので,この列の区間を考える.

「スタックが空になってから」パスを2回が終了条件な注意しないとWAする.

ソース

#include <bits/stdc++.h>
using namespace std;
 
 
 
struct State{
    vector<int> p1,p2;
    vector< pair<int,int> > stack;
    int turn;
    int passed;
};
bool operator < (const State &a,const State &b){
    if( a.passed != b.passed ) return a.passed < b.passed;
    if( a.turn != b.turn ) return a.turn < b.turn;
    
    if( a.p1 != b.p1 ) return a.p1 < b.p1;
    if( a.p2 != b.p2 ) return a.p2 < b.p2;
    return a.stack < b.stack;
}
 
map<State,int> memo;
int dfs(const State s){
    if( s.passed == 2 ) return 0;
    if( memo.count(s) ) return memo[s];
    int ans;
    if( s.turn == 0 ){
        ans = -1e9;
        if( s.p1.size() ){
            State cpy = s;
            cpy.stack.push_back({cpy.p1.back(),s.turn});
            cpy.p1.pop_back();
            cpy.passed = 0;
            cpy.turn ^= 1;
            ans = max(dfs(cpy),ans);
        }
        {
            State cpy = s;
            int profit = 0;
            int coef1 = 1;
            int coef2 = 1;
            for(int i = cpy.stack.size() - 1 ; i >= 0 ; i--){
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first == -1 ) coef1 = 0;
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first == -1 ) coef2 = 0;
                
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first != -1 )
                    profit +=  coef1 * cpy.stack[i].first;
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first != -1 )
                    profit -=  coef2 * cpy.stack[i].first;
            }
            if( cpy.stack.size() == 0 ) cpy.passed++;
            cpy.stack.clear();
            cpy.turn ^= 1;
            ans = max(dfs(cpy)+profit,ans);
        }
    }else{
        ans = +1e9;
        if( s.p2.size() ){
            State cpy = s;
            cpy.stack.push_back({cpy.p2.back(),s.turn});
            cpy.p2.pop_back();
            cpy.passed = 0;
            cpy.turn ^= 1;
            ans = min(dfs(cpy),ans);
        }
        {
            State cpy = s;
            int profit = 0;
            int coef1 = 1;
            int coef2 = 1;
            for(int i = cpy.stack.size() - 1 ; i >= 0 ; i--){
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first == -1 ) coef1 = 0;
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first == -1 ) coef2 = 0;
                
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first != -1 )
                    profit +=  coef1 * cpy.stack[i].first;
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first != -1 )
                    profit -=  coef2 * cpy.stack[i].first;
            }
            if( cpy.stack.size() == 0 ) cpy.passed++;
            cpy.stack.clear();
            cpy.turn ^= 1;
            ans = min(dfs(cpy)-profit,ans);
            
        }
    }
    return memo[s] = ans;
    
}
 
int main(){
    int n,m;
    cin >> n >> m;
    vector<int> a(n), b(m);
    for(int i = 0 ; i < n ; i++) cin >> a[i];
    for(int i = 0 ; i < m ; i++) cin >> b[i];
    reverse(a.begin(),a.end());
    reverse(b.begin(),b.end());
    
    State s = {a,b,{},0,0};
    cout << dfs(s) << endl;
    
}

JAG Contest 2016 Domestic B - 豪邸と宅配便

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

時刻 a に宅配便が届くと,[a-M,a+M)の範囲に書斎には居られない. なので,[0,T)の範囲で区間に含まれていない時刻を列挙すれば良い. 境界に気をつけつつ,愚直にシミュレーションすると時間計算量 O(NM+T) で解ける.いもす法を使うと O(N+T) になるけど制約的にどうでもいい.

ソース

#include <bits/stdc++.h>
using namespace std;
 
 
int imos[20100];
int main(){
    int N,M,T;
    cin >> N >> M >> T;
    for(int i = 0 ; i < N ; i++){
        int a;
        cin >> a;
        for(int j = 0 ; j <= M ; j++)
            if( a - j >= 0 ) imos[a-j] = true;
        for(int j = 0 ; j < M ; j++)
            if( a + j < T ) imos[a+j] = true;
    }
    
    int ok = 0;
    for(int i = 0 ; i < T ; i++){
        // cout << imos[i] << " " << i << endl;;
        if( !imos[i] ){
            ok++;
        }
    }
    cout << ok << endl;
}

JAG Contest 2016 Domestic A - 阿吽の呼吸

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

本質的には括弧列が与えられるのでvalidな括弧列ですかという問題に言い換えれば見通し良く解ける. カウンタを定義する.「A」が来たらカウンタをインクリメント,「Un」が来たらデクリメント,途中でカウンタが負の数になったらNO,最後まで処理してカウント!=0ならNO.

ソース

#include <bits/stdc++.h>
using namespace std;
 
int main(){
    int a = 0;
    int n;
    cin >> n;
    for(int i = 0 ; i < n ; i++){
        string t;
        cin >> t;
        if( t == "A" ){
            a++;
        }else{
            a--;
            if( a < 0 ){
                cout << "NO" << endl;
                return 0;
            }
        }
    }
    if( a == 0 ) cout << "YES" << endl;
    else cout << "NO" << endl;
}

Python3.5 Windowsにsklearn(+scipy,numpy)を入れる

なんか面倒くさかったので,困っている人のためにメモ

そもそもWindowsはscipyのインストールが面倒くさい.↓を参考にしてお手軽にインストールした.

Windows で VirtualEnv の Python2.7 に pip と wheel を使って コンパイルエラーが発生するパッケージ(例 scipy)をWindows用バイナリ提供サイトから入手してインストールする - グロブ

それでもまだ, ImportError: DLL load failed: The specified module could not be found. が出る.困った.どうやら,numpyをインストールするときも普通のnumpyではだめらしく,numpy-MKLをインストールしないといけないらしい(この辺は詳しくない). 上の記事と同様 numpy-mklをwheel形式でインストール (Python Extension Packages for Windows - Christoph Gohlke)

stackoverflow.com

そしたら動いた.

Python3+bs4で AtCoderのスクレイピング入門(初級者向け)

正直後から思ったんですが、urllib使うよりrequestsのほうが使いやすいと思うので、requestsを使うことをおすすめします (2016/07/17)

※ python2のurllibの情報が欲しい人はurllibの仕様が2系と違うのでこの記事を参考にしても意味ないです

Python3とbs4(beautifulsoup4)でAtCoderの便利スクリプトを作る.

対象読者はWebスクレイピングしたいけど,ブラウザ以外からのログインさえできる気がしないって人です.Pythonよくしらない人向けに書いたつもりでしたが,僕もPython初心者で教え方が全くわからないのでちょっと厳しいかも.

かなり手を抜いているのでコメントか直接何かしらの手段で質問してくれたら答えたいと思います.すみません.

bs4 をインストールしよう

bs4は何をしてくれるやつかっていうと,htmlとかxmlのタグをパースしてくれるというか,必要な情報抜き出してくれるやつです. 正規表現とかガリガリ駆使しながらタグ抜き出す作業するのは楽しいけどめんどいいので,そういった面倒臭さから僕達を遠ざけてくれます. 嬉しい!

pip install beautifulsoup4

でOK.pipがない人はインストールしといてください.

AtCoderPythonからログインしよう

AtCoderPythonからログインします.

ログインするためにはどういう手順を踏めばいい?

自分のアカウントでログインしたクッキーを維持しながらサイト内をWebスクレイピング(ウェブサイトから情報を抽出すること)しないといけません.

これはPython3に標準搭載されているurllibというインターネット上のリソースを取得するためのパッケージを使うと簡単に実現できます.

ログインはどのように行われるかというと,AtCoder(を含むいろいろなサイト)はPOSTリクエストという形式でログインに必要な入力データを持ってあるURLにリクエストすると,ログイン処理を行ってくれるように出来ています.

AtCoderのログインフォーム(https://arc001.contest.atcoder.jp/login) を見ると,以下のようになっています.

  • ユーザー名の部分
<input type="text" id="name" class="input-xlarge" name="name" value="" autocomplete="off">
  • パスワードの部分
<input type="password" id="name" class="input-xlarge" name="password" value="">

name部分が重要に注目してください."name"と"password"になっていますね. これに関するデータをPOST形式で渡すときの要素名になります.

実際にログインしてみる

さっそくAtCoderにログインしてみましょう.

ディクショナリをPOSTデータの実質の形式(password=パスワード&name=ユーザー名)にしてくれる関数urllib.parse.urlencode()があるので,それを使います.

username,password = 'ユーザー名','パスワード'
postdata = {
    'name': username,
    'password': password
}
encoded_postdata = urllib.parse.urlencode(postdata).encode('utf-8')
req = opener.open("https://arc001.contest.atcoder.jp/login",encoded_postdata)

print(req.read().decode('utf-8')) # req.read()だけだとバイナリで表示されてよくわからないのでutf8の文字列に変換

ログインに成功するとトップページに飛ぶので, 表示されたソースに「パスワードを忘れた方はこちら」とかが無ければログインに成功しています. おめでとうございます.

参考にさせて頂いたサイト様: http://stackoverflow.com/questions/5010915/how-do-i-clear-the-cookies-in-urllib-request-python3

AtCoderから情報を取得してこよう

問題番号とそれに対応するリストを取得してみる

ひとたび/loginにアクセスしてログインに成功したら,openerにクッキーの情報が紐付いてくれているので,あとは好き放題リクエストしまくりです.

では,問題番号とそれに対応するURLのリストを取得してみましょう. 課題ページ(http://arc001.contest.atcoder.jp/assignments )のソースを見て,問題へのリンクがあるところを抜き出すと,以下のようになっています.

<td class="center"><a class="linkwrapper" href="/tasks/arc001_1">A</a></td>
<td><a class="linkwrapper" href="/tasks/arc001_1">センター採点</a></td>

どうやら運の良いことに問題ページのURLにリンクされている⇔classがlinkwrapperであるタグで囲まれているということが分かりますね.

soupのオブジェクトを作って,select関数で.linkwrapperクラスのタグを全列挙してみましょう.

以下のコードにドットがついているのはcssのclassだからだと思います.

req = opener.open("http://arc001.contest.atcoder.jp/assignments")
soup = BeautifulSoup(req,"html.parser")
for tag in soup.select('.linkwrapper'):
  print(tag)

実行結果は以下のようになると思います. 実際にはtagは文字列型ではなくいろいろ操作ができるオブジェクトですが,あくまでprintした時の挙動はタグそのものを出力します.

<a class="linkwrapper" href="/tasks/arc001_1">A</a>
<a class="linkwrapper" href="/tasks/arc001_1">センター採点</a>
<a class="linkwrapper" href="/tasks/arc001_2">B</a>
<a class="linkwrapper" href="/tasks/arc001_2">リモコン</a>
<a class="linkwrapper" href="/tasks/arc001_3">C</a>
<a class="linkwrapper" href="/tasks/arc001_3">パズルのお手伝い</a>
<a class="linkwrapper" href="/tasks/arc001_4">D</a>
<a class="linkwrapper" href="/tasks/arc001_4">レースゲーム</a>

1つの問題につき2つリンクがあるので,これはお好みですが0,2,4,...番目だけ取り出せばいい感じになると思います.どうせこのへん賢くやっても仕様変更されたらおしまいなので,その時点で動いてればいいんじゃないでしょうか.

せっかくなので問題番号とそれに紐付いたURLというタプルのリストを作ってみましょう. PythonはすごいのでリストXに対してX[0::2]とすると2個飛ばしでリストを抜き出してくれます. こんなかんじになります.

X = []
for tag in soup.select('.linkwrapper')[0::2]:
    problemid = tag.text
    url = "http://arc001.contest.atcoder.jp"+tag.get("href")
    X.append((problemid,url))
print(X)

ここで,tagのメンバとかメソッドでtextとかget("href")とかありますが,これはそれぞれタグの囲まれている中身やタグの属性を取得してくれる便利なやつです.

出力結果は以下の通りです.

[('A', 'http://arc001.contest.atcoder.jp/tasks/arc001_1'), ('B', 'http://arc001.contest.atcoder.jp/tasks/arc001_2'), ('C', 'http://arc001.contest.atcoder.jp/tasks/arc001_3'), ('D', 'http://arc001.contest.atcoder.jp/tasks/arc001_4')]

ところで,リスト内包表記という記法を使ってタプルのリストを作るソースが以下のようになります. こういうワンライナーな使い方をすると可読性が低い気もしますが書きやすいです.

print([(tag.text,"http://arc001.contest.atcoder.jp"+tag.get("href")) for tag in soup.select('.linkwrapper')[0::2]])

問題文からサンプルを抜き出そう

あとはやることは同じです. ソースがどういう構造になってるか眺めながら,特徴になってきそうな部分を取り出してくるだけです. どうやら入出力の枠はpreタグで表現されているみたいです.これを全て取得すれば常勝では!?

しかし,気をつけることがあります.AtCoderの問題文は作る人によってhtmlのフォーマットが微妙に違うっぽいです. 具体的な例としては,preタグを入力形式・サンプル入出力以外にも使っている作問者がいるということが挙げられます.

ちなみにpreタグについている属性も人によってまちまちなので,あまりそこを情報として使うとよくないかも.

ただまあ,大体はお行儀の良い感じになっているはずなので,取得してみてバグったら修正を繰り返してればなんとかなるはずです.

ARC001で試してみる

ARC001のA問題(http://arc001.contest.atcoder.jp/tasks/arc001_1 )に対して,preタグでフィルタをかけてみましょう.

req = opener.open("http://arc001.contest.atcoder.jp/tasks/arc001_1")
soup = BeautifulSoup(req,"html.parser")
for tag in soup.select('pre'):
  print(tag)
<pre>
<var>N</var>
<var>c_1c_2c_3…c_N</var>
</pre>
<pre class="prettyprint linenums">
9
131142143
</pre>
<pre class="prettyprint linenums">
4 1
</pre>
<pre class="prettyprint linenums">
20
12341234123412341234
</pre>
<pre class="prettyprint linenums">
5 5
</pre>
<pre class="prettyprint linenums">
4
1111
</pre>
<pre class="prettyprint linenums">
4 0
</pre>

かなりいい感じに取れましたが,入力の項が混ざっています.これを取り除くために,一例として以下の方針が考えられます.

  1. prettyprintもしくはlinenumsクラスをselectに指定する

  2. 1つ目の取得結果を問答無用で取り除く

ARC001〜ARC004の範囲ならどちらの方針でも正しく動きそうです. 必要・十分条件とどれくらいのコンテストに対して成り立ちそうかみたいなのを強く意識すると正しい選択ができるかもしれませんが,これといった正解はないです.アドホックです.

まあとりあえずそのへんどう処理するかはおいおい考えるとして, とりあえず入力と出力を切り分けてみます.2.の方針のほうが多分多くのケースを網羅できるんですが,今回は1.の方針でやってみましょう.

opener = opener.open("http://arc001.contest.atcoder.jp/tasks/arc001_1")
soup = BeautifulSoup(req,"html.parser")
tags = soup.select('.prettyprint')
input_tags = tags[0::2]
output_tags = tags[1::2]
# len(input_tags) == len(output_tags) は勝手に仮定
for i in range(len(input_tags)):
  print("---- sample %d ----" % i) # こういう記法です
  print(input_tags[i].text.strip()) # strip()は前後の改行空白消してくれるやつです
  print("--")
  print(output_tags[i].text.strip())
  print()

出力結果は以下の通りになります.

---- sample 0 ----
9
131142143
--
4 1

---- sample 1 ----
20
12341234123412341234
--
5 5

---- sample 2 ----
4
1111
--
4 0

概ねこんな感じになりました. いい感じですね.あとはこのテキストをファイルに保存したりしたら自動ダウンローダーは完成です.おめでとうございます.

ところで,前後の改行空白を消すためにstrip()を使っていますが,不具合の元になる可能性はあります.が,そんな入力作るwriterいないと思いますし,サンプルに関してはミスって無駄に改行がある回とかがある可能性もあるのでstrip()する方針は間違ってなさそうです.

おわりに

いかがでしたでしょうか. 実はダウンローダーツールを作っていて,そのついでに並行でこの記事を書いてみました. 自分が扱ってるコードとここに書いたコードはいろいろ違うので,もしなんかこの部分動かないよ!ってのがあったらコメントください.

bs4についてもっと知りたい人は以下の記事が参考になると思います.僕もこれを見ました.ありがとうございます. http://qiita.com/itkr/items/513318a9b5b92bd56185

Python2 ニコニコ動画のコメント取得

コメント取得には,ニコニコ動画のIDではなく,固有のスレッドIDが必要らしい.

  1. http://flapi.nicovideo.jp/api/getflv/sm** でスレッドIDとニコニココメント取得APIをのアドレス取得(動画毎にapiの番号が違うから気をつけて).この操作にはニコニコ動画のログイン情報のクッキーが必要.
  2. 取得したAPIにgetリクエストすればコメントのxmlを得られる.リクエストの形式は「http://msg.nicovideo.jp/**/api/thread?version=20090904&thread=***&res_from=-***」という具合.res_from=-***を指定すると,直近***件のコメントを取得できる.
  3. コメントのパースを適当にする.

ソースコード

# -*- coding: utf-8 -*-
import urllib2, cookielib
from urlparse import parse_qs
import re

class NiconicoCommentGetter:
	def __init__(self,username,password):
		cj = cookielib.CookieJar()              # Cookieを格納するオブジェクト
		cjhdr = urllib2.HTTPCookieProcessor(cj) # Cookie管理を行うオブジェクト
		self.opener = urllib2.build_opener(cjhdr)    # OpenDirectorオブジェクトを返す
		self.opener.open("https://secure.nicovideo.jp/secure/login","mail=%s&password=%s" % (username,password) )
	
	def get(self,smID):
		# smIDからthreadIDの取得
		r = self.opener.open('http://flapi.nicovideo.jp/api/getflv/' + smID)
		html = r.read()
		res = parse_qs(urllib2.unquote(html))		
		
		# threadIDからコメントxmlの取得
		r = self.opener.open("%sthread?version=20090904&thread=%s&res_from=-1000" % (res['ms'][0],res['thread_id'][0]) )
		xml = r.read()
		
		# コメントのパース(無理やり,仕様変わってダメになるかも)
		comments = []
		rm = re.compile(">(.*)</chat")
		for line in xml.split('><') :
			g = rm.search(line)
			if( g != None ) : comments.append(g.group(1))
		return comments 

commentGetter = NiconicoCommentGetter("email", "pw")
for i in commentGetter.get("sm7"):
	print(i)

出力例

a
懐かしいな
最も古い動画?
こんなのあるんだ
sm7がなんで今更出現したの?
はえ^~
7コメ、記念カキコ
とうこうびなぞすぎ
6と8無いよ
なんでsm7が今更wwwwww
...(略)

参考資料

ニコニコ動画のコメント解析 – nanoway

urllib2でCookieを使う - ひきメモ