HackerNews翻訳してみた

HackerNewsを中心に、人気の英語記事を翻訳してお届けします。記事は元記事の著者に許可をとった上で翻訳・掲載をしています。

Yコンビネーター(2013年サマーバッチ)で学んだ9つのこと

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: Going Through Y Combinator (S13): Nine Lessons Learned by Zachary Townsend

本エントリは、Zac Townsendさんが Y Combinator への参加で得た学びについてです。Zacさんは、銀行向けAPIを提供する Standard Treasury の共同創業者です。エントリの最後にZacさんと彼の会社についての紹介を載せています。



昨年、私はYコンビネーター(YC)へ申し込むに至った経緯申し込み時に書いたことYCに申し込む際のアドバイスを、このブログで記事にしました。今はちょうどウィンターバッチ(冬期)の真っ最中で、すでに2014年サマーバッチ(夏期)の申し込みも始まっています。そこで今回は、YCで学んだことを9つ紹介します。

YCでの経験を書くのはかなり苦労しました。私の人生で最も生産的な時期であった一方、最も困難でストレスがたまった時期でもあります。この記事を書くにあたっては、一般に知られていることは安易に繰り返さないよう、また自分たちの経験を大げさに表現しないよう気をつけました。

YCで過ごした3カ月間、かつてない大きな不安に襲われ、楽観的思考や興奮も経験しました。厳しい環境でストレスにさらされ、寝不足の日々が続きます。自分のチームが(参加していた他のチームも)崩壊するのではないかと思ったほどです。YCはアドバイスをする時に、優しくオブラートに包むような言い方はしません。ストレートで残酷なほど的を射ています。情け容赦ない厳しい物言いにストレスを感じますが、それでもスリリングで忘れがたく、価値のある経験になりました。

YCを何かに例えるなら、時間をかけて調理するスロークッカー(煮込み鍋)がぴったりでしょう。彼らは毎週のように50の企業(現在は70)を相手に、たくさんのスロークッカーで食事を作っているようなものです。大事なのは時間、調理法、夕食の時間までに仕上げること。ある時は材料をすべて放り込んで、すばらしい夕食(企業)ができることもありますが、時にはゴミしか残らないこともあります。どう転ぶかは誰にも分からないのです。

それでも私が別の企業を立ち上げるなら、またYCの力を借りたいと思うでしょう。彼らが受け入れてくれるかは分かりませんが。

レッスン1: 可能であればYCに挑戦を。簡単ではないが、それだけの価値はある(後のレッスンにはもっと驚くこと間違いなし!)。

まずはスタートラインから。

YCへの参加が認められると、面接日の夜に電話がかかってきます。内容はシンプルで、YCに参加する意思があるか、マウンテンビュー市で開くオリエンテーションに参加可能か確認されます。もちろん「イエス」と答えました。

オリエンテーション

オリエンテーションは2部構成で、それぞれポール・グレアム(PG)とカースティ・ナソーが話します。今後はYC次期社長のサム・アルトマン(Sama)が担当するかもしれませんが、いずれにせよ骨子は変わらないでしょう。

まずYCで最も重要なことを、PGが2時間程度に凝縮して話します。彼の著書やエッセイに書かれていることばかりですが、それでも大きな刺激を受けました。彼が最も強調したのが、「今すぐ始めろ」ということ。YCに参加した日から「オフィスアワー」が始まり、プレゼンテーションを行うデモデーの日程も発表されます。夕食会までは数週間ありますが、この時点からYCでの活動が始まるのです。デモデーまでの日数は限られているので、すぐ動き出さないといけません。投資家に好印象を残せるかどうかは、デモデーまでの成果で決まります。もちろんその後の活動も重要ですが、時間が経ってしまっては意味がありません。

PGに続いてカースティ・ナソーが、企業設立の基本事項について説明します。関連書類、YCからの投資、YCVC(YCベンチャーキャピタルプログラム)の情報や出資金の使い方をアドバイスしてくれます。内容は企業法や企業財務、スタートアップの資金調達、福利厚生、給与など多岐にわたり、企業設立の手ほどきを受けました。

スタートアップ創業者は多くのことを学ばねばならず、当初はきついことも多いでしょう。でもそれが面白さでもあり挑戦に値するのです。この時点で得た教訓は、「あらゆる機会から学ぶ」ということ。創業者たるもの安全圏を飛び出さねばなりません。新しいことに挑戦し続ける覚悟が必要です。

レッスン2: 楽しみながら貪欲に吸収し、何でもやること。スタートアップ設立時は地味な作業がほとんど。弁護士とストックオプションの行使価格を相談したり、給与税を払っているか確認したり。でもそれができてこそ、すばらしい企業といえる。細部まで目を配りながらも迅速に決断を下さねばならない。

マウンテンビューへ

YCはバッチ期間中、参加者にYC近くに住むよう勧めています。YCのイベントにできるだけ参加することが、スタートアップ成功のカギと考えているからです。

マウンテンビューはサンフランシスコに比べて遊び場が少ないことも理由の1つです。若くて子どものいない参加者にとっては重要なポイントでしょう。仕事に打ち込む環境が必要です。PGはコーディングやユーザとの対話、睡眠、食事、エクササイズに集中するようアドバイスしています。マウンテンビューにいれば、このアドバイス従うのも容易になります。マウンテンビューなら、カストロ通りでさえ遊べる場所が限られていますからね。

レッスン3: 会社を興すなら気が散る要素は排除すること。企業構築への道を探り、他のことに時間を割かないように。それが難しいなら何もないマウンテンビューへ行こう。

True Link Financial社のカイとクレアと一緒に、ダンと私は小さな家に住みました。その家は数年間、YCの参加者から参加者へと渡ってきたものです。カイやクレアとは仕事の枠を超えて親友になりました。2人ともすばらしい人物で、大切な友人であり、いろいろなことに関心を持っています。私たちの仕事を批評し、サポートしてくれて、私たちが金融系ITサービスに携わることを他の友人より深く理解してくれました。

他のチームとの共同生活や交流には大きな意味がありました。彼らがいなければ私とダンは仲たがいしていたでしょう。どのチームにも共同生活が必要かどうかは分かりませんが、ニュージャージー出身の私たちの場合、カイたちがいなかったら金網デスマッチを始めていたかもしれません。

ダンと私は夏の間ずっと一緒に過ごしましたが、土曜日だけは離れて過ごすことにしました。別々の時間が必要だったのです。

レッスン4: 人との接触をすべて断つのではなく、必要なつながりは残そう。就寝中の共同創業者を殺さないために。

イデアを練り上げる

YCでの最初の数週間を費やし、何を構築するか具体化しようとしました。これがYCの言う"今すぐ始めろ"にあたるかどうかは、反論が出るかもしれませんね。

自分たちのビジネスモデルがどうなるかを頭の中で想像していったのです。この間ずっと複数の企業とミーティングを重ね、いかに銀行とのやり取りが厄介か話し合いました。

ダンと私はマウンテンビューで同じ区画を数百回も歩き回って、この問題を突き詰めていきました。

オリエンテーションからプロトタイプデーまでの約6週間で100社近くの顧客候補と会い、繰り返し話したのが次の6つのアイデアでした。商業銀行業務向けAPI(私たちがYCに申告していたテーマ)、企業向けWealthFront、ほぼリアルタイムで処理できる銀行間決済、財務調整支援、ゼロからの銀行設立、そして商業銀行業務向けAPIゲートウェイの販売です。同じチームのブレントは経験と情熱を持ちあわせており、プラットフォームのアイデアに大きな関心を寄せていました。

最後の項目を除いて、どのビジネスアイデアも記事の重要なポイントではありません。でも顧客との話し合いで大きなヒントを得ました。私たちは(お薦めできませんが)まだ何も構築していませんでした。でも顧客候補の意見から、彼らが何を求め、企業と銀行間のインタフェースで何がネックになっているのかを理解したのです。

話し合った企業の多くが、次のようなことを言っていました。(1)取引先の銀行に不満がある(2)技術に強い銀行を利用したい(3)支払処理を簡単に実現してくれるStripeやWePay、Balanced Paymentsのような企業と付き合う感覚で、銀行との取引も済ませたい。

私たちに運が向いてきました。

私たちのプロダクト

2012年10月、法人化の約6カ月前、YCに参加する8カ月前の頃です。ダンと私は自動決済機関(ACH)向けAPIについて銀行と話し合っていました。多くの銀行を結び、銀行間で即日決済が可能なシステムを構築するためです。J.P.モルガンやキャピタル・ワン、ウェルズ・ファーゴシリコンバレーバンク、シティナショナルバンク、シティなどと話し合いを進めていました。

その後、私たちがマウンテンビューに移って試行錯誤を続けていると、こうした銀行の一部がAPIゲートウェイが欲しいと言ってきたのです。彼らに説明したエクスペリエンスを、ホワイトレーベル型で売ろうか、それとも相互ブランドの形で売ろうかと悩みました。

私たちは銀行用ソフトウエアビジネスの世界にいました。商業銀行業務向けPaaSのAPIが、最初のプロダクトになりました。

レッスン5: 何度も話し合いを重ね、プロダクトにお金を払ってくれる人の話はいつでも聞くこと。そうした顧客は貴重であり、たいてい彼らの意見は正しい。

プロトタイプデー

プロトタイプデーはYCのハイライトともいえ、ミニ・デモデーのような雰囲気です。各チームの発表順はランダムに決められ、全チームが数分の持ち時間でプレゼンをします。バッチの初期段階なので、まだバッチメイトたちをほとんど知りません。プロトタイプデーは、各チームの取り組みやバッチメイトたちを知るいい機会です。

YCのパートナーたちから、事前準備は不要だと言われました。でも準備しておけばより集中できると考え、プレゼンにまとめることにしました。いかに自分たちのアイデアや会社を大勢に売り込むか、その方法を学ぶための機会にしようと思ったのです。

各チームのプレゼンが終わるごとに、PGが不備を突いてきます。発表順の1番や2番に選ばれないのがベストですが、PGの指摘が役に立つのは間違いありません。自分たちの売り込みに投資家がどう反応するか、それを知る最初のチャンスです。

プロトタイプデーは投票で締めくくります。各参加者が2つのチームに投票し、開票後に上位10チームが発表されます。過去のプロトタイプデーの結果は公表されていないようなので詳細は記しませんが、私たちのチームは健闘しました。

夕食会

毎週火曜日の夜には、YCが著名なスタートアップ創業者(もしくはロン・コンウェイ)を招いて夕食会を開きます。彼らの講演はとても興味深く、知識を得るというより刺激に満ちています。ベンチャーキャピタリストにだまされないためにはどうしたらいいか、そんなテーマがよく話題に上りました。夕食会での話はオフレコなので内容には触れませんが、楽しいイベントなのは確かです。

火曜日の夕食会は週に1度全員が集まるという意味でとても重要です。参加チームは夕食会の何時間も前に会場に来ていました。その週のオフィスアワーがまだまだある段階ですが、カースティやレヴィ(YCの2人の弁護士)にあれこれ質問したりバッチメイトと情報交換し合ったりして、夕食会前の数時間で好きなだけユーザのフィードバックを得るのです。

レッスン6:自分でデッドラインを決めよう。目標が具体的でなくとも、常に何かに向かって奮闘すること。人為的な尺度でも構わないので、進捗の度合いを確かめること。

夕食会は1週間ごとのマイルストーンでもあります。どのチームも火曜日のハイライトに向けてがむしゃらに働き、レポート、フィードバック、洗い直しを繰り返します。

バッチ

YCの大きな特徴は、バッチの参加企業に出資する点です。そして優秀なスピーカーが講演をする夕食会もあります。バッチメイト同士はすぐに友達になり、YCを卒業してからもずっと交流は続きます。ダンもブレントも私も、同じバッチの企業何社かに投資しました。

バッチメイトは一緒に冒険をする仲間です。プロダクト開発の力になってくれるし、落ち込んだ時は支えになってくれます(落ち込むことは何度もあります)。火曜日の夕食会の後は仲間からやる気(と競争心)をもらい、エネルギーがみなぎります。

オフィスアワー

オフィスアワーは現在7タイプあります。頻繁に行われるのは通常のオフィスアワーとグループ・オフィスアワーなので、この2つを説明しましょう。

通常のオフィスアワーでは10分から20分程度、YCのパートナーと面談します。過去1週間の進捗状況を報告し、直面している問題を相談して何でも疑問をぶつけます。私が参加した当時は各チームが主要パートナーを1人か2人選びましたが、現在はYCが各チームの専任パートナーを1人割り当てているようです。私たちはジェフ・ラルストンを選びました。私たちの性格に合っており、率直で、彼の意見はポイントを突いていました。

グループ・オフィスアワーは2週間ごとに行われます。私たちのグループ・パートナーはポール・ブックハイト、ケビン・ヘイル、そしてカースティでした。グループ・オフィスアワーは通常のオフィスアワーと似ていますが、他のチームにも公開されているので見学が可能です。各チームが順番に状況を報告し、パートナーと話し合います。他のチームが抱えている問題を(その解決法も)シェアできる点が有意義です。

銀行にソフトウエアを売る:1週間ではなかなか進まない

巨大で複雑なソフトウエアを、巨大で保守的な企業に売るのは大変です。なかなか話が進みません。YCのバッチに参加していた頃はいつだって、契約まで2週間しかないと思っていました。「来週はX銀行と契約だ」と毎週言っていたのです。投資家にもYCのパートナーにも友人にもそう言い、自分たちも信じていました。でもそれは間違いでした。

概念実証契約を結び、初めて正式契約に向けて交渉している今だからこそ自信を持って言えますが、私たちは世間知らずでした。企業へのセールスをどう進めるのか知らなかったのです。厳しい経験でしたが、この経験で学習しました。

とはいえYCには悪い印象を与えたでしょう。今から思えば私たちはオオカミ少年のようでした。PGは「成功するチームを見極める一番の方法は、週ごとの進捗を見ることだ」と言っていました。私たちは毎週、銀行とのミーティングを重ね、プロダクトとセキュリティを見直して膨大な仕事量をこなしていましたが、前進しているように見えたかは分かりません。

レッスン7:収拾不能な状況にあるのは自分のチームだけではない。どのチームも(すべてのチームが!)多かれ少なかれ混乱に陥っている。「それでいい」と気づくことがパワーになる。多くのバッチメイトは気づくのに時間がかかりすぎている。

12月にPGから進捗状況を尋ねられた時、自分たちがオオカミ少年になっていないか不安だとメールで伝えました(ポールは全チームにメールしたと思います)。するとPGから返事がきました。

あまり心配するな。この手の契約は、いつも時間がかかる。

私が君の立場ならモノにこだわらず、得られる売り上げに集中するだろう。収支が合うようになれば、すぐにも事態は好転する。収益さえ上がれば、時間のかかる顧客に命を取られることはない。成長率の足を引っ張ることはあっても致命傷にはならない。市場が大きければ、得られるものも大きいからね。

実際、この手の取引は時間がかかります。いつだって企業向けセールスは遅々として進まないものです。YC自体はすばらしいので私は友人全員に応募を勧めていますが、その一方で私たちのように限られた数の大きな取引をする企業にとっては難しい面もあります。たとえ成功への道のりを確実に歩んでいても、コンシューマー向け企業のように1週間ごとの確実な成長をアピールすることはできないからです。

期待と現実がマッチしていないのです。他の企業には可能でも、私たちには無理がありました。それでもYCは、どの企業も同じ型にはめようとします。YCの規模が大きくなるにつれ、法人向けサービスを目指す企業も加わるようになりました。今後YCがどう現状と向き合うのかが気になります。

デモデー前の時期尚早なファンドレイジング

最初の2カ月はファンドレイジングを避けるようYCはアドバイスしています。私たちが早々に始めたのは間違いでした。事実、ファンドレイジングのプロセスを壊してしまいました。ここまで状況を悪化させたらやり直せないのが普通だと、友人3人に言われたくらいです。それでもかなりの資金を集め、シードラウンドを乗り切りました。確かに混乱を招いたのですが、それでも数百万ドルを調達したのです。いずれ時間を見つけて私の犯したミスを投稿しようと思っています。とにかくYCのアドバイスは正解でした。準備が整うまでは資金調達に走るべきではありません。

私の意見では、デモデーの2週間ほど前がファンドレイジングを始める理想的なタイミングです。その時点で準備が整っているか確信がなくても、準備完了とみなして行動を起こさねばなりません。デモデーでは調達済みの金額を聞かれます、ゼロなんて回答はいけせん。かといって、もうラウンドを完了した(もしくは早々にあちこちから投資を断られた)と答えるのも良くないでしょう。

レッスン8: アドバイスを受け入れよう。自分が正しいと思っていても、友人やアドバイザーの方が正しいこともある。周りと歩調を合わせる必要はないが、自分の会社にとってやるべき時にやるべきことをしよう。準備が整うまで大手ベンチャーキャピタルとの顔合わせやミーティングを断る、その勇気が私たちにはなかったのだ。

リハーサルデー

デモデーのおよそ1週間前にはリハーサルデーが開催され、全員が初めてのピッチに挑戦します。全チームの発表後には投票が行われ、その時点で必要なアドバイスが得られます。もしかしたらPGが助言をくれるかもしれません。デモデーのクライマックスに向け、全力疾走の1週間が始まります。

プレゼンの内容は一語一句まで原稿に起こすものの、自然に聞こえるよう入念に練習しなくてはなりません。プレゼンが得意でないことはハッキリしましたが、見事とはいかないまでも、ぶっつけ本番でそこそこの発表はできます。実際、他のチームのプレゼンを(時にはふざけて)やったり、バッチメイトのために別の言いまわしやアイデア、構成などを考えたりするのがストレス解消になりました。でも自分たちがPGの助言に従った正確な表現に落とし込めたかどうかは分かりません。これに丸1週間を費やしました。

デモデー

デモデー前夜には同じ会場でアラムナイ・デモデーが開かれたのですが、あまりうまく発表できませんでした。この日からデモデー当日のプレゼンの時間まで、目が覚めている間は一瞬たりとも無駄にせず、練習に費やしたように感じます。マウンテンビューの家からコンピュータ歴史博物館の間を行ったり来たりして、1人で何度も繰り返しプレゼンしました。

ステージ上で初めて完璧なプレゼンができたのは、まさにデモデー当日。最高の気分を味わった瞬間でした。

プレゼン後は投資家に取り囲まれます。丸十時間も費やして質問に答え、ビジネスの市場価値を議論し、いくつか取引をまとめることになります。

とにかく疲れましたが爽快な気分でした。これこそがYCなのです。

レッスン9:とにかく楽しもう。会社を興すのは大変だが、ワクワク感も味わえる。根性と忍耐で何かを作り上げるのだから、どうせなら楽しむのが一番。

謝辞

この記事の編集を手伝い、アドバイスをくれた皆さんに感謝します。共同創業者のブレント・ゴールドマン、ダン・キマーリング。バッチメイトのアロン・フォイヤー、ジョン・ゲッドマーク、グレン・モリアーティ(教授)、クレア・マクドネル、ジェイク・ヘラー、ネイサン・ウェンゼル、パトリック・アウタリキー、マラン・ネルソン。そしてうるさいほど厳しく校正してくれた友人、ケイト・ブロックウェル。数えきれないほどの価値ある提案に心から感謝します。この記事にセミコロンが存在しているのも、ケイトのおかげです。

--

著者紹介

Zac Townsend

ZacはStandard Treasuryの共同創業者です。Standard Treasuryは開発者のために商業銀行業をシンプルにすることに特化したベンチャー起業です。 Zacは前職でStripeにて働いていました。Stripeは、ソフトウェア開発者がWeb上でのクレジット決済を簡単に扱うためのAPIを提供する企業です。

Stripe以前は、彼はニュージャージー州ニューワーク市の市長であるCory Bookerのシニアテクノロジーボリシーアドバイザーであり、またBennett Midland LLCにおいてアソシエイト、シニアアソシエイトとして市民や社会のマネジメントコンサルティングを指揮していました。彼は著書をもつ統計学者であり、公益のために働くデータサイエンティスト同士をマッチングするための非営利組織であるDataKindの共同創業者です。Zacはハーバード大学ニューヨーク大学PRIISM統計センターの会員でした。彼はブラウン大学で数学、経済学、政策の学士を取得し、ニューヨーク大学では財政学と統計手法に関する修士を取得しています。

Standard Treasury

Standard Treasuryはワールドワイドな開発者同士のプラットフォームを構築し、ホストし、メンテナンスし、またサポートすることによって、銀行と開発者そして開発者のエコシステムを結びつけ、銀行を手助けします。Standard Treasuryのパートナー銀行は、資金マネジメントと会計プロセスの徹底的な自動化によって、素晴らしい価値をカスタマーに提供すると同時に、カスタマーに対するサービスコストやサービスの解約率を減らし取引ボリュームを増やすことで、売上増加が見込めます。

カートゥーン作家のための新たなビジネスモデルを創造する

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: CREATING A NEW BUSINESS MODEL FOR CARTOONISTS by Grigoriy Kogan


長文ご容赦。ここ5年間、私は自分が描いたカートゥーン(一コマ漫画)を雑誌社に売って副収入を得てきました。いろいろな動物がビジネススーツを着ているカートゥーンです。しかし最近、このカートゥーン業界があまりにも時代遅れで、効率が悪いということを強く感じるようになってきました。そこで、この業界の古い秩序をひっくり返す「ある仕組み」を作り、カートゥーン作家が自身の作品からもっと収入を得られるようにしようと思い立ったのです。


この5年の間に、正社員として3つの職種を経験しました。造船技師からWebプロジェクトマネージャに転身し、今はマーケティングコンサルタントをしています。そして、その間ずっとカートゥーンを描いては雑誌に投稿し、掲載してもらっていました。


ザ・ニューヨーカー』や『ハーバード・ビジネスレビュー』などの雑誌で、下のようなカートゥーンを見かけたことがありませんか? そう、こんな感じの作品を描くのが私の仕事です。ギャラは、掲載された作品1枚につき1万5,000円から7万円程度。主に このような スーツを 着た 動物が バカなことを 言っている カートゥーンを描いて、それなりの小遣いを稼いできました。


"Sample Cartoon of Animals in Business Suits"

この作品は6万5,000円になった。"アンテロープのうねった角を描きたかった"というのがこれを描いた一番の動機だったと思う。


収入(年収にして20万~50万円ほど)に加えて、カートゥーンを描く喜びそのものも、この仕事のやりがいです。しかし時が経つにつれて、こうした「ギャグ・カートゥーン」を取り巻く現実に不満を持つようになってきました。まとめると、下記の3つになります。


  1. "歩留まり"が低すぎる。名を知られたカートゥーン作家ですら、平均で20枚描いて買い取ってもらえるのは1枚だけ。
  2. カートゥーンに対して、まともな支払いをしてくれる雑誌社が非常に少ない。もし1社も採用してくれなかったら、その作品の"潜在的収益力"はほぼゼロになってしまう。
  3. 雑誌社は未発表のカートゥーンしか購入してくれない。つまり1枚の作品が1度しか利益を生まないということ。


こういった事情から、カートゥーン作品の潜在的収益力は、最初はある程度高いにもかかわらず、時間の経過とともに急速に低下することになります。


"Cartoon Earning Potential Over Time"

一度こうなってしまうと、他の雑誌社に当たっても、その作品が将来利益を上げる可能性は極めて低い。


作家の多くは、引き出しの中で埃をかぶった、またはパソコンのハードディスクの中に埋もれた、何千枚ものカートゥーンを保管しているはずです。でもそれはさっき挙げた事情のせいであって、決してそれらの作品に価値がないわけではありません。


"Cartoon Published in 2009"

このカートゥーンは掲載時の2009年に1万5,000円で売れたが、そこから先、利益を生むことはなかった。


私は、そうした"未発表の作品の価値"をどうにかしてお金に変えられないかと考えるようになりました。もしそんな方法があれば、他の作家が未発表で眠らせている何千、何万ものカートゥーンを活用し、価値を生み出すことができるかもしれません。


そうしてたどり着いたのは、カートゥーンが雑誌などの印刷物だけではなく、ブログや会社のニュースレター、プレゼン資料などにも効果的に使えるという事実です。世の中には雑誌の編集者より、毎日プレゼン資料を作っているビジネスマンのほうが断然多いですからね。そこに(大きくはないけれど、ゼロではない)需要があるのではないかと考えました。ブロガーやビジネスマン、教師といった人々が、面白いカートゥーンを必要としているのです。実際、ジェイソン・コーエン42Floorsなどの有名なブロガーが、自分のブログにカートゥーンを載せています。


"Gag Cartoon Being Used on a Blog"

42Floorsのブログには、いつも内容に合ったギャグ・カートゥーンがうまく使われている。


もっとも私以外にも、すでにそのことに気づいている人たちはいたようです。


  1. ザ・ニューヨーカー』は雑誌に掲載されたカートゥーンをオンライン販売している。
  2. あるイギリスの会社は、さまざまな作家による大量のカートゥーンをオンライン販売している。ShutterStock(シャッターストック)は写真を販売するサイトだが、そのカートゥーン版。
  3. カートゥーン作家の中にはお金をかけて自分自身のWebサイトを立ち上げている人たちもいる。販売されているのは、その作家の作品のみ。


しかし、私はこの程度では満足できませんでした。これでは、大多数のカートゥーン作家が抱えている問題の解決にはなりません。以下のような理由からです。


  • そもそも『ザ・ニューヨーカー』にカートゥーンを掲載してもらうのはとても大変なので、サイトに作品を載せるまでが大変。運良く1枚すべり込ませたとしても、それ以外の作品はすべて手元に残る。
  • イギリスの会社が作ったサイトに掲載してもらうのは、もう少しハードルが低いかもしれないが、サイト自体がかなり時代遅れで非効率。気に入る作品を探して購入するまでの手順が煩雑すぎる。作品の収益を最大化するための企業努力がなされていない。
  • 他の企業や個人のサイトに勝てるだけの多機能なECサイトを開発するスキル/リソースを、ほとんどのカートゥーン作家は持っていない。


これらを考慮に入れた結果、私は路線変更することにしました。カートゥーンを描いて掲載してもらうという今までの努力をやめて、自分で「ある仕組み」を作ることにしたのです。これが完成すれば、カートゥーン作家は未発表の作品から収入を得られます。また高品質のカートゥーンを、リーズナブルな価格で手軽に探したいという買い手のニーズに応えることもできます。こうして GagCartoons.comは誕生しました。


GagCartoons.comは、さまざまな作家のカートゥーンを集めたサイトです。作品はトピックやキーワードで簡単に検索でき、ライセンス購入とダウンロードの手順も非常にシンプルです(たったの2クリックで済みます)。ライセンスの販売回数には制限を設けていないので、掲載をやめない限り、作品には将来利益を生み出す可能性が存在するということです。


"Earning Money from Licensing Cartoons"

カートゥーン作家は何もしなくても、それまで埃をかぶっていた作品から利益を得ることができる。


この仕組みがあれば、たとえカートゥーン作品が雑誌に投稿され出版された後でも、その作品価値はゼロになりません。また出版物掲載のための手続きとは違い、作家の作業は画像ファイルをアップロードするだけです。


さらに先ほど紹介した2つのサイトと違い、GagCartoons.comにアップすれば一時的に作品の潜在的収益力が上がるだけでなく、その後も上り続けていきます。なぜかというと、私が自分のマーケティングと最適化の知識を駆使して、これからどんどん販売を促進していくつもりだからです。


面倒な雑誌への投稿は一切やめにして、オンラインのライセンス販売だけで収入を得る作家も出てくるかもしれませんね。


"Earning Revenue from Cartoons Online"

今までよりずっと楽に作品から利益を上げる方法。


今はまだ"概念実証"(または"市場実験")の段階ですが、私は現在、このサイトを採算の取れるビジネスに育てるべく自分の時間の20%を投入しています。自分のためだけでなく、必ず他のカートゥーン作家たちのためになるはずだと信じて。


私自身はプロジェクトやビジネスの舞台裏についての読みものが好きなので、このプロジェクトについても、そうした裏側を書いていきたいと思います。「面白くて、ためになる」と思ってもらえるといいのですが。次回の記事では、プロジェクトの進捗情報や具体的な数字、当面の課題と対策などをシェアするつもりです。どうぞお楽しみに!

Webフレームワークとは何か

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: What is a Web Framework? by Jeff Knupp




Webアプリケーションフレームワーク、略して「Webフレームワーク」がWeb対応のアプリケーション構築に広く使われているのは、皆さんご存じですよね。単純なブログからAjax機能を搭載した複雑なアプリケーションまで、Web上のすべてのページはコードで記述されています。最近気になるのは、FlaskやDjangoのようなWebフレームワークに興味を持ってはいるけれど、実際にはWebフレームワークの目的や機能をちゃんと理解していない開発者が意外に多いということです。そこでこの記事では、ややもすれば見落とされがちなトピックであるWebフレームワークの基礎を取り上げることにしました。皆さんがこの記事を読み終える頃には、Webフレームワークとは何か、そしてそもそもなぜWebフレームワークが存在するのかという根本のところを、きちんと理解できるようになっているはずです。Webフレームワークの基礎知識があれば、新しいWebフレームワークの習得や、どのフレームワークを選ぶべきかの判断がずっと楽になるでしょう。

Webのしくみ

フレームワークの話に入る前に、まずWebの「しくみ」を理解しましょう。最初に、ブラウザにURLを打ち込んでEnterキーを押したら何が起こるのかを検証したいと思います。ブラウザの新しいタブを開いてhttp://www.jeffknupp.comに飛んでください。それでは、ページを表示するのにあなたのブラウザが取った手順を見ていきましょう(DNS検索の処理は除きます)。

"Webサーバ"と"Webサーバ"?

すべてのWebページはHTMLを用いてあなたのブラウザに送信されます。HTMLとはWebページの内容や構造を記述するために使われる、ブラウザ用の言語です。そしてそのHTMLをブラウザへ送るアプリケーションがWebサーバなのです。ただ紛らわしいことに、このアプリケーションを内臓するコンピュータも通常Webサーバと呼ばれています。 とにかく重要なのは「すべてのWebアプリケーションはブラウザにHTMLを送る」ということです。アプリケーションのロジックがどんなに複雑でも、Webアプリケーションの最終目的はブラウザにHTMLを送ることなのです(JSONCSSファイルのような異なるタイプのデータも送れますが、ここでは割愛します。コンセプトは同じです)。 では、Webアプリケーションはブラウザに何を送るかをどうやって判断しているのでしょうか。実はWebアプリケーションは、ブラウザに要求されたものすべてを送信するのです。

HTTP

ブラウザは、HTTPプロトコル(プログラミングの世界でのプロトコルとは既知の2者間通信を可能にするデータフォーマットとシーケンスのこと)を使って、Webサーバ(または「アプリケーションサーバ」) からWebサイトをダウンロードします。HTTPプロトコルrequest-response型をベースにしています。クライアント(あなたのブラウザ)が、物理的なコンピュータ上で稼動しているWebアプリケーションにデータを要求すると、Webアプリケーションはその要求に応答してブラウザが要求したデータを返します。

ここで重要なのは、この通信はクライアント(あなたのブラウザ)からしか始められないという点です。サーバ(Webサーバです)側から接続を開始したり、あなたのブラウザに不要なデータを送りつけたりすることは不可能です。もしWebサーバからデータを受信したとすれば、それはあなたのブラウザがそのデータを要求したからに他ならないのです。

HTTPメソッド

HTTPプロトコルのメッセージはすべて、それに相当するメソッド(動詞の形をとる)を持っています。各HTTPメソッドが、クライアントから送られてくるロジックの異なるリクエストに対応していて、クライアントサイドのさまざまな要求に応えてくれるのです。例えば、WebページのHTMLを要求することと、フォームを送信することは論理上異なるアクションととらえられます。従ってこの2つのアクションは、それぞれ異なるメソッドが使われるのです。

HTTP GET

GETメソッドとは、その言葉の示すとおり、Webサーバからデータを「得る(要求する)」メソッドです。GETリクエストは、HTTPリクエストの中でも間違いなく最もよく使われるリクエストでしょう。GETリクエストを受信したWebアプリケーションは、要求されたページのHTMLを返す以外のことはしません。つまりWebアプリケーションは、GETリクエストに応答しますが、アプリケーションの状態は変更しないのです(例えば、GETリクエストをもとに新規ユーザアカウントを作成するようなことはしません)。Webサイトの提供元であるアプリケーションを変更しないという点から、GETリクエストは通常「安全」なメソッドだと考えられています。

HTTP POST

当然ながらWebサイトは、単にページを眺めるだけのものではありません。入力フォームを用いてアプリケーションにデータを送信することもできます。そのためには、POSTと呼ばれるタイプのリクエストが必要です。POSTリクエストによってユーザが入力したデータがサーバに転送され、その結果Webアプリケーション側で何らかの処理が行われます。入力フォームに情報を入力してWebサイトにユーザ登録するというのは、フォームに含まれるデータがWebアプリケーションへPOSTされることで実現するのです。

GETリクエストと異なり、POSTリクエストは通常アプリケーション状態の変更を伴います。先ほどの例でいうと、フォームがPOSTされると新規ユーザアカウントが作成されます。またGETリクエストと違い、POSTリクエストでは必ずしも新しいHTMLページがクライアントに送信されるわけではありません。その代わりに、クライアントはレスポンスのステータスコードを使って、アプリケーションへの処理が成功したかどうかを判断します。

HTTPステータスコード

通常、Webサーバはステータスコード200を返します。これは「あなたの要求を実行しました。結果は成功です」という意味です。ステータスコードは常に3桁の数字で、Webアプリケーションは、要求に対して結果がどうなったかを示すステータスコードをレスポンスごとに返さなくてはなりません。「OK」を意味するステータスコード200は、GETリクエストに対して最も多く返されるコードです。一方、POSTリクエストに対してはステータスコード204(コンテンツなし)が返される場合があります。このコードは「要求は実行しましたが、提示するものが何もありません」という意味です。

ここで理解してほしいのは、POSTリクエストは、データを送信したページとは異なるURLへ送られるケースもあるということです。先ほどのユーザ登録の例で説明すると、入力フォームのあるwww.foo.com/signupでsubmit処理を行ったとしても、入力フォームの内容を保持したPOSTリクエストはwww.foo.com/process_signupにPOSTされる可能性があるということです。POSTリクエストの送り先は、入力フォームのHTMLに記述されています。

Webアプリケーション

HTTPのGETとPOSTが使えれば、ほとんどの処理が行えるようになります。なにしろ最もよく使われるHTTPメソッドの2トップですからね。では次にWebアプリケーションについてですが、これはHTTPリクエストを受け、HTTPレスポンスを返す役割を担っています。通常返す内容には、要求されたページのHTMLが含まれています。POSTリクエストはWebアプリケーションに何らかのアクションを起こさせます。例えば、データベースへの新規レコードの追加などです。他にも多くのHTTPメソッドがありますが、ここではGETとPOSTに焦点を絞りましょう。 最も単純なWebアプリケーションとはどんなものでしょうか。ここで、ポート80(よく使われるHTTPポート番号で、ほぼすべてのHTTPトラフィックがこのポートに送信されます)をリッスンするだけのアプリケーションを実装してみましょう。接続が確立したら、クライアントからのリクエストを待ち、リクエストを受け取ったらごく単純なHTMLを返します。 以下のようになります。

import socket

HOST = ''
PORT = 80
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
connection, address = listen_socket.accept()
request = connection.recv(1024)
connection.sendall("""HTTP/1.1 200 OK
Content-type: text/html


<html>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html>""")
connection.close()

(うまくいかない場合は、PORTを8080などに変えてみてください) このプログラムでは、接続もリクエストも1つしか扱いません。どんなURLがリクエストされても、HTTP 200ステータスコードを返します(ですので実際はWebサーバとは言えません)。Content-type: text/htmlの行は、ヘッダフィールドを表しています。ヘッダには、リクエストやレスポンスに関するメタ情報が含まれています。上記の場合、送信するデータはHTMLである(実際にはJSONなのですが)という情報をクライアントに伝えています。

リクエストの構造

上記のコードで、テスト用のHTTPリクエストを送っている箇所を見てください。レスポンスとよく似ていますよね。リクエストの1行目には<HTTP Method> <URL> <HTTP version>が指定されます。今回だとGET / HTTP/1.1がそれに当たります。2行目以降には、Accept: */*(どんなタイプのコンテンツでも受け取りますという意味)のようなヘッダが記述されます。ざっくりですが、リクエストはそういう構造です。 一方のWebサーバからのレスポンスを見ると、リクエストの1行目とよく似ています。フォーマットは<HTTP version> <HTTP Status-Code> <Status-Code Reason-Phrase>で、今回は HTTP/1.1 200 OK となっています。それに続くヘッダのフォーマットはリクエストと同じです。最後に、レスポンスの具体的なコンテンツが含まれます。コンテンツは文字列やバイナリオブジェクトに変換されることを覚えておいてください(ファイル指定の場合)。レスポンスの変換方法は、ヘッダ内のContent-typeでクライアントに伝えられます。

Webサーバの負荷

上記のコーディングをベースにしてWebアプリケーションを構築しようとすれば、多くの問題に直面するでしょう。

  1. リクエストされたURLの内容をチェックして、適切なページをクライアントに送るにはどうしたらよいか
  2. 単純なGETリクエストはよいとして、POSTリクエストをどのように扱うのか
  3. セッションやCookieなどの高度な概念をどのように扱うのか
  4. 大量の同時アクセスを処理するために、アプリケーションの拡張性をどの程度見積もるか

Webアプリケーションを構築する度に、こうした問題に煩わされるのは避けたいですよね。だからこそパッケージが存在するのです。パッケージはHTTPプロトコルの核となる詳細部分に対処し、上記の問題をすっきり解決してくれます。とはいえ、その基本機能は、私たちが先ほど作ったサンプルとほとんど変わりません。つまり、リクエストを受け取りHTMLを含んだHTTPレスポンスを返すのが、パッケージの主な仕事です。 ただしこの説明は、"クライアントサイド"のWebフレームワークについては全くあてはまりませんので注意してください。

課題の2トップ: ルーティングとテンプレート

Webアプリケーションを構築するとき、特に避けて通れないのが次の2つです。

  1. どうやってクライアントがリクエストしたURLを適切なアプリケーションにマップするか
  2. どうやって計算結果やデータベースから取得した情報を使い、リクエストされたHTMLを生成するか

どのWebフレームワークも何らかの手法でこれらの問題に対処していますが、そのアプローチはさまざまです。実例を挙げると分かりやすいので、これからDjangoとFlaskがとっている解決策を紹介しようと思います。ただその前に、MVCモデルについて簡単な説明が必要かもしれませんね。

DjangoでのMVC

DjangoMVCパターンを利用しているため、このフレームワークを採用する場合はコードもそれに対応する必要があります。MVCとは「モデル・ビュー・コントローラ」の頭文字をとったもので、アプリケーションそれぞれの持つ機能範囲を論理的に分離する手法です。データベーステーブルなどのリソースはモデルが受け持ちます(Pythonでのclassがリアルワールドオブジェクトを扱うのに似ています)。コントローラはアプリケーションのビジネスロジックを担当し、モデルの上層で機能します。ビューはすべての必要な情報にもとづいて、ページのHTMLを動的に生成します。

少しややこしいのですが、Djangoでは、コントローラがビューと呼ばれ、ビューがテンプレートと呼ばれています。しかし名前のつけ方を除けば、DjangoMVCアーキテクチャをそのまま採用していると言っていいでしょう。

Djangoでのルーティング

ルーティングとは、リクエストされたURLを、それに紐づいたHTMLを生成するコードに割り当てるプロセスのことです。特にシンプルなケースでは、同一コードですべてのリクエストを処理します(先ほどの例もそうです)。もう少し複雑なケースでは、すべてのURLを1対1でview functionに割り当てることもあります。例えば、www.foo.com/barというURLがリクエストされたら、handle_bar()という関数にレスポンスを生成する役割を持たせる、ということをどこかに記述しておくのです。このように、アプリケーションがサポートするすべてのURLを、それと紐づく関数とともに列挙して、マッピングテーブルを組み立てていきます。

ただしURLがリソースのIDなど有用なデータを含んでいる場合(例えばwww.foo.com/users/3/のような場合)、このアプローチは通用しません。こうしたURLをビュー関数に割り当てて、IDが3であるユーザを表示したい場合はどうすればいいのでしょうか。

Djangoでは、正規表現を含むURLを引数を持ったビュー関数に割り当てて、この問題を解決しています。例えば^/users/(?P<id>\d+)/$にマッチするURLは、正規表現のグループidでキャプチャした値を引数idに代入し、display_user(id) を呼ぶのです。こうすれば、/users/<some number>/というURLがどんなIDを含んでいても、必ずdisplay_user関数に割り当てられます。これらの正規表現は任意の複素数を扱うことができ、キーワードと位置パラメータの両方を持つことができます。

Flaskでのルーティング

Flaskのアプローチは少し違います。一般的に、リクエストされたURLと関数を結びつけるにはroute()デコレータを用います。次のFlaskのコードは、前述の正規表現や関数と同様の機能を果たします。

@app.route('/users/<id:int>/')
def display_user(id):
    # ...

ご覧の通り、デコレータは非常に単純化された正規表現の形式を使って、URLと引数をマップしています(セパレータには暗黙的に/を使用)。route()関数に引き渡すURLに<name:type>の形式で記述して、引数をキャプチャします。ここまでくれば、/info/about_us.htmlのような静的URLへのルーティングがどうなるか分かりますよね。ご想像のとおり、@app.route('/info/about_us.html')のように処理されます。

テンプレートを使ったHTML生成

引き続き、先に挙げた例題をもとに考えていきます。適切なコードを正しいURLに割り当てて動的にHTMLを生成すると同時に、Webデザイナーが画面を手直しできる余地を残すには、一体どうすればいいのでしょう。DjangoとFlaskはどちらもHTMLテンプレートを用いています。

HTMLテンプレートはstr.format()関数を使う方法と似ています。つまり、動的な値に対応するプレースホルダーを使って、期待されたアウトプットを出力するのです。プレースホルダーは、後からstr.format()へ渡される引数によって書き換えられます。Webページ全体を1つの長い文字列として記述し、ブレースを使って動的データをマークしておいて、最後にstr.format()を呼ぶ、という流れです。DjangoテンプレートとFlaskのテンプレートエンジンjinja2は、両方ともこうした使い方を想定して設計されています。

もちろん両者には違いもあります。Djangoにはテンプレートを使って実装するための基本的なサポート機能がついていますが、Jinja2では基本的に自分で任意のコードを実行する必要があります(まあ、すべてではありませんが)。また、Jinja2はレンダリングテンプレートの結果を積極的にキャッシュするため、同じ引数を持つ後続のリクエストは、再度レンダリングされるのではなく、キャッシュから返されます。

データベースインタラクション

基本理念に「至れり尽くせり」を掲げるDjangoには、ORM(O/Rマッパー)も用意されています。ORMの目的は2つ。1つはPythonのクラスをデータベーステーブルに割り当ること、もう1つはさまざまなデータベースエンジンの差異を取り除くことです(前者が本来の役割ですが)。ORMを好きだという人は珍しいですが(ドメイン同士のマッピングが完ぺきではないからでしょう)、Djangoはとにかく「フル装備」が売りなので大目に見られています。一方のFlaskには「マイクロフレームワーク」を名乗るだけあって、ORMは用意されていません(それでもFlaskはSQLAlchemyとの互換性が高いので、DjangoのORMに対抗できる唯一で最大のライバルです)。

ORMが搭載されているおかげで、Djangoを使えばフル機能のCRUDアプリケーションを構築することができます。CRUD(Create・Read・Update・Delete)アプリケーションはWebフレームワークのキモと言えます(ただしサーバサイドに限る)。Django(と、FlaskとSQLAlchemyのコンビ)は、いろいろなCRUD処理を各々のモデルに沿った形で作成します。

Webフレームワークのまとめ

さて、これでWebフレームワークの目的を分かってもらえたと思います。HTTPリクエストやレスポンスを処理するひな型やインフラのコードを開発者の目から隠すことです。どの程度隠すかはフレームワークによりますが、DjangoとFlaskは両極端な例だと言えるでしょう。Djangoはうっとうしいと思えるほどに、すべての状況を想定して作られています。Flaskは自分で「マイクロフレームワーク」だと宣言しているとおり、Webアプリケーションとして最小限の機能を提供し、汎用度の低いWebフレームワークのタスクについてはサードパーティーのパッケージに任せる方式をとっています。

とはいえ繰り返しになりますが、PythonのWebフレームワークがすることは最終的にはみんな同じです。HTTPリクエストを受け取り、HTMLを生成するコードを特定し、その内容をもとにHTTPレスポンスを生成します。実際、メジャーなサーバサイドのフレームワークはすべて、このような動きをしています(JavaScriptフレームワークは除く)。これでWebフレームワークの役割が分かったと思います。どうか自分の目的にあったフレームワークを選んでくださいね。

asyncioを用いたpythonの高速なスクレイピング

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: Fast scraping in python with asyncio by Georges Dubus


ウェブスクレイピングについては、pythonのディスカッションボードなどでもよく話題になっていますよね。いろいろなやり方があるのですが、これが最善という方法がないように思います。本格的なscrapyのようなフレームワークもあるし、mechanizeのように軽いライブラリもあります。自作もポピュラーですね。requestsbeautifulsoup、またpyqueryなどを使えばうまくできるでしょう。

どうしてこんなに様々な方法があるかというと、そもそも「スクレイピング」が複数の問題解決をカバーしている総合技術だからなのです。数百ものページからデータを抽出するという行為と、ウェブのワークフローの自動化(フォームに入力してデータを引き出すといったもの)に、同じツールを使う必要はないわけですから。私は自作派で、それは融通が利くからですが、大量のデータを抽出する時に自作はふさわしくありません。requestsは同期でリクエストを行うので、大量のリクエストが行われると待ち時間が長くなってしまうからです。

このブログ記事ではrequestsの代わりに、最新のasyncioライブラリをベースにした案を紹介しましょう。aiohttpです。これで小さなスクレイパーを書いてみましたが、とても高速なものができました。どうやったかお見せしましょう。


asyncioの基本

asyncioは、python3.4で導入された非同期I/Oライブラリです。Python3.3のpypiからも入手できます。なかなか複雑なので詳細までは触れませんが、このライブラリを使って非同期コードを書くために必要な部分についてのみ説明します。もっと詳しく知りたい人は、ドキュメントを読んでくださいね。

簡単に言えば、知っておくべきことは2つ。コルーチンとイベントループです。コルーチンは関数に似ていますが、任意の箇所で一旦処理を中断したあと、処理を再開することができます。例えば、I/Oを待っている間(HTTPリクエストなど)コルーチンは一旦中断し、他の作業を実行させます。コルーチンを再開させるには、戻り値が必要だと宣言するキーワードyield fromを用います。イベントループはコルーチンの実行を制御するために用います。

Asyncioについて学ぶことはたくさんありますが、当面はこれで十分でしょう。読んだだけでは分かりづらいかもしれませんから、実際にコードを見てみましょう。


aiohttp

aiohttpは、asyncioと連携するために設計されたライブラリです。requestsのAPIに似ています。現状では、あまりいいドキュメントがないのですが、役に立つ事例がいくつかあります。まずは基本的な使い方について説明しましょう。

最初にコルーチンを定義してページを取得し、出力します。asyncio.coroutineを用いて、関数をデコレートしてコルーチンとします。aiohttp.requestはコルーチンの一種で、readメソッドでもあります。ですから、これらを呼ぶときはyield fromを使う必要がありますが、そういう注意点を除けば、コードはとても分かりやすいものです。

@asyncio.coroutine
def print_page(url):
    response = yield from aiohttp.request('GET', url)
    body = yield from response.read_and_close(decode=True)
    print(body)

ご覧の通り、yield fromを用いれば、1つのコルーチンから新たな別のコルーチンを発生させることもできます。同期コードからコルーチンを発生させるには、イベントループが必要です。asyncio.get_event_loop()から基準となるコルーチンを取得して、run_until_complete()メソッドを用いてそのコルーチンを実行させればよいのです。元のコルーチンを実行させるには、ただ次のように記述します。

loop = asyncio.get_event_loop()
loop.run_until_complete(print_page('http://example.com'))

asyncio.waitという便利な関数があります。いくつかのコルーチンをリストとして取り出し、リスト内すべてのコルーチンを含有するひとつのコルーチンとして返してくれます。このようになります。

loop.run_until_complete(asyncio.wait([print_page('http://example.com/foo'),
                                      print_page('http://example.com/bar')]))

もうひとつ別の便利な関数としてはasyncio.as_completedがあります。こちらはコルーチンのリストを取り出し、処理が完了した順にコルーチンを再開するイテレータを返します。つまりこのイテレータを実行すると、それぞれの結果が出次第すぐに順次入手できるということです。


スクレ―ピング

さて、非同期HTTPリクエストのやり方が分かったところで、スクレイパーを書いてみましょうか。残っているのは、htmlを読み込む部分です。今回はbeautifulsoupを使ってみました。他の選択肢としてはpyquerylxmlなどがあります。

例題として、パイレート·ベイで配布されているlinuxのソフトウエア群の中からトレントリンクを取得する小さいスクレイパーを書いてみましょう。

まず、get requestsを処理するヘルパーコルーチンです。

@asyncio.coroutine
def get(*args, **kwargs):
    response = yield from aiohttp.request('GET', *args, **kwargs)
    return (yield from response.read_and_close(decode=True))

解析部分。この記事の目的はbeautifulsoup について掘り下げることではありませんから、シンプルにダンプ出力に留めておきます。ページの最初のmagnetリストを取得します。

def first_magnet(page):
    soup = bs4.BeautifulSoup(page)
    a = soup.find('a', title='Download this torrent using magnet')
    return a['href']

そしてコルーチンです。下記のurlについて、結果はシーダーの数でソートされます。つまり、リストの一番目が最もシードされているということになります。

@asyncio.coroutine
def print_magnet(query):
    url = 'http://thepiratebay.se/search/{}/0/7/0'.format(query)
    page = yield from get(url, compress=True)
    magnet = first_magnet(page)
    print('{}: {}'.format(query, magnet))

最後に、これらすべてをコールするコードはこのようになります。

distros = ['archlinux', 'ubuntu', 'debian']
loop = asyncio.get_event_loop()
f = asyncio.wait([print_magnet(d) for d in distros])
loop.run_until_complete(f)


まとめ

これで非同期の小規模スクレイパーができあがりました。様々なページが同時にダウンロードできます。requestsを使った同じコードよりも3倍も速く処理することができました。これで読者のみなさんも、自分独自のスクレイパーを書くことができますね。

このgistに、「おまけ」の分も含めた最終的なコードが掲載されています。

慣れてきたら、asyncioについてのドキュメントや、aiohttpのexamplesなども見てみるといいですよ。asyncioでどんなことができるか、いろいろな例が記載されています。

このアプローチの制約は(実際のところ、自作の場合すべてに当てはまるのですが)フォームを処理するためのスタンドアロンライブラリが見当たらない、という点です。Mechanize とscrapy にはいいヘルパー関数があって、簡単にフォームを送信できますが、その2つを使わない場合は自分で何とかしなくてはなりません。これは結構面倒なので、いつか自作でライブラリを書いてしまうかもしれません…(期待はしないでくださいね)。


おまけ:サーバをいじめないで

リクエストを一度に3つこなせるのはクールですが、5000となると話は別です。一度にあまりにも多いリクエストをしようとすると、やがて接続が切れてしまったり、そのウェブサイトにアクセスできなくなってしまったりするかもしれないからです。

このような事態を避けるためにsemaphoreを使います。これは同期ツールで、ある時点で使われるコルーチンの数を制限するのに使います。ループの前にsemaphoreをクリエイトして、同時に最大いくつまでリクエストを処理するかを引数で渡してやればいいのです。

sem = asyncio.Semaphore(5)

ここを入れ替えます。

page = yield from get(url, compress=True)

機能は同じですが、semaphoreによって保護されています。

with (yield from sem):
    page = yield from get(url, compress=True)

これで、最大でも同時に5つまでのリクエストしか処理されなくなりました。


おまけ:プログレスバー

もうひとつおまけです。tqdmプログレスバーを生成してくれるステキなライブラリです。このコルーチンはasyncio.waitと同様の動きをしますが、コルーチンの処理完了を示すプログレスバーを表示してくれます。

@asyncio.coroutine
def wait_with_progress(coros):
    for f in tqdm.tqdm(asyncio.as_completed(coros), total=len(coros)):
        yield from f

PythonでPostgresデータから決定木を構築する

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: Building a Decision Tree in Python from Postgres data by Gary Sieling


今回は、任意の人物の所得を人口統計データを使って予測する手法をご紹介します。使用するのは20年前の人口統計データです。

この例を用いて、関係データベースの情報から予測モデルを導き出す方法と、その途中で起こり得るトラブルについて触れたいと思います。

このデータの優れた点は、データの作成者が下記のようなアルゴリズムの精度をデータに添付している点です。こうした数値はスモークテストの結果評価に役立ちます。


       
           Algorithm               Error
        -- ----------------        -----
        1  C4.5                    15.54
        2  C4.5-auto               14.46
        3  C4.5 rules              14.94
        4  Voted ID3 (0.6)         15.64
        5  Voted ID3 (0.8)         16.47
        6  T2                      16.84
        7  1R                      19.54
        8  NBTree                  14.10
        9  CN2                     16.00
        10 HOODG                   14.82
        11 FSS Naive Bayes         14.05
        12 IDTM (Decision table)   14.46
        13 Naive-Bayes             16.12
        14 Nearest-neighbor (1)    21.42
        15 Nearest-neighbor (3)    20.35
        16 OC1                     15.04


このデータセットをPostgresのデータベースに読み込むには、まずデータ作成者のファイルの一番下にある空白行と「1×0 Cross Validator」と書かれた行を削除する必要があります。

次に、下記の手法でデータを読み込みます。テストデータも同じPostgresデータベースにロードしていますが、そこは気にしないでください。

ご覧の通り、PostgresではUNCパスを指定することができます。特筆するほどのことではないと思われるかもしれませんが、VisualStudioではUNCパスを読み込むことができないことを考えると、うれしい機能ではないでしょうか。


DROP TABLE income_trn;
 
CREATE TABLE income_trn 
    (age INTEGER, 
     workclass text, 
     fnlwgt INTEGER, 
     education text, 
     education_num INTEGER, 
     marital_status text, 
     occupation text, 
     relationship text, 
     race text, 
     sex text, 
     capital_gain INTEGER, 
     capital_loss INTEGER, 
     hours_per_week INTEGER, 
     native_country text,
     category text);
 
COPY income_trn 
FROM '\\\\nas\\Files\\Data\\income\\adult.data' DELIMITER ',' CSV;
 
DROP TABLE income_test;
CREATE TABLE income_test 
    (age INTEGER, 
     workclass text, 
     fnlwgt INTEGER, 
     education text, 
     education_num INTEGER, 
     marital_status text, 
     occupation text, 
     relationship text, 
     race text, 
     sex text, 
     capital_gain INTEGER, 
     capital_loss INTEGER, 
     hours_per_week INTEGER, 
     native_country text,
     category text);
 
COPY income_test 
FROM '\\\\nas\\Files\\Data\\income\\adult.test' DELIMITER ',' CSV;


Pythonの場合、こうしたデータならSQLAlchemyを使っても読み込むことができます。ただしPostgresドライバ("pg8000")は不安定なため、たまに次のようなエラーが起こる場合があります。


ProgrammingError: (ProgrammingError) 
('ERROR', '34000', 
'portal "pg8000_portal_12" does not exist') 
None None


エラーの原因はさまざまですが、そのひとつとしてPostgresの旧バージョンを使用しているケースが考えられます (著者は9.3を使用しました)。旧バージョンには閉じているカーソルのデータが読み込まれてしまうという問題もあるようです。


from sqlalchemy import *
engine = create_engine(
                "postgresql+pg8000://postgres:postgres@localhost/pacer",
                isolation_level="READ UNCOMMITTED"
            )
c = engine.connect()
 
meta = MetaData()
 
income_trn = Table('income_trn', meta, autoload=True, autoload_with=engine)
income_test = Table('income_test', meta, autoload=True, autoload_with=engine)


大量のデータを処理する場合、クエリの結果をモデルの中にストリーム処理する手法はとても有効です。ただし今回はデータサイズが小さいので、ストリーム処理は行いませんでした。また、ひとつの表に全データが入っている場合には、効率よくデータ処理ができるよう、任意にデータを半分に分割する方法を考えなければならないでしょう。


from sqlalchemy.sql import select
 
def get_data(table):
  s = select([table])
  result = c.execute(s)  
  return [row for row in result]
 
test_data = get_data(income_trn)
trn_data = get_data(income_test)


このデータには、もともとテキスト型の列と整数型の列が混ざって入っていました(職業と年齢など)。意外にもPython機械学習ライブラリは、このような混合データの認識が苦手なようです(少なくともディシジョンツリーは苦手です)。こうしたデータは一連のvalue値のみで構成されたデータとはまったく別物なので、特別な配慮が必要になります。

問題は、ライブラリがvalue値のリストを期待しているにも関わらず元データが数値型である、というケースです。この問題を解決するには、次のようなマッピングを行うグローバルな辞書の構築が必要になるでしょう。


maxVal = 0
vals = dict()
rev_vals = dict()
def f(x):
  global maxVal
  global vals
  if (not x in vals):
    maxVal = maxVal + 1
    vals[x] = maxVal
    rev_vals[maxVal] = x
  return vals[x]


ここで、属性を2つに分割しなければなりません。ひとつは出力に、もうひとつは出力を予測するための属性に分割します。


def get_selectors(data):
  return [ [f(x) for x in t[0:-1]] for t in data]
 
def get_predictors(data):
  return [0 if "<" in t[14] else 1 for t in data]
 
trn = get_selectors(trn_data)
trn_v = get_predictors(trn_data)


この事例で最も注目すべきは、なんといってもモデルの作成が驚くほど簡単なことです。例を見てみましょう。


from sklearn import tree
clf = tree.DecisionTreeRegressor()
clf = clf.fit(trn, trn_v)


結局、テストメソッドは自前で実装することになりました。混同行列は、クラスに定義されたデータの計算が得意ではないようですね。


test = get_selectors(test_data)
test_v = get_predictors(test_data)
 
testsRun = 0
testsPassed = 0
for t in test:
  if clf.predict(t) == test_v[testsRun]:
    testsPassed = testsPassed + 1
 
  testsRun = testsRun + 1
 
100 * testsPassed / testsRun
 
DecisionTreeClassifier: 78%
DecisionTreeRegressor: 79%


最後に、scikit-learnのドキュメントをチェックしてみてください。すべての事例にステキな図表がついていますね。ただ、ドキュメントを読めば分かりますが、ディシジョンツリーはかなり長くなる可能性があります。何千というルールが適用されることもあるので、よほどシンプルなケースでない限り図表化には向かないでしょう。


butterflyを使ってWebブラウザ上でターミナル入力を行う

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


Original article: butterfly your everyday terminal in your web browser by Florian Mounier


今回は、最近ハマっているものを紹介します。最初は遊びで作り始めたのですが、今では毎日使うようになってしまいました。それがこれです。

"butterfly screen cast"

このbutterflyを使えば、Webブラウザ上でターミナル入力を行うことができます。

試してみよう

まずは試してみてください。下記の通り、インストールは簡単。

$ sudo pip install butterfly # Install butterfly
$ butterfly.server.py        # Launch the web server

終わったら、ブラウザでlocalhost:57575にアクセスするだけです。

使ってみよう

サーバサイド

systemdでスタートアップ時にサーバを起動

butterflyはバックグラウンドで稼働するサーバに依存するため、systemdの設定が必要です。次のリンクからbutterfly.serviceファイルをダウンロードして、/etc/systemd/system/もしくは同等のディレクトリに保存してください。次に、下記のコマンドを実行します。

$ sudo systemctl enable butterfly
$ sudo systemctl start butterfly

これで、常にサーバが稼働した状態となります。

rootユーザおよび複数ユーザでの実行

butterflyはターミナルユーザもしくはrootユーザで実行できるように設計されています。rootで実行した場合、権限は接続を開始したユーザ、つまりWebページを開いたユーザに移ります。ターミナルユーザはlocalhost:57575/user/で変更可能です。

リモートマシンからのアクセス

butterfly.server.py --host="0.0.0.0"を実行しホスト名に"0.0.0.0"をバインドすれば、他のユーザにターミナルへの接続権限を与えることができます。ただしアクセス時にパスワードの入力が求められるものの、セキュリティ対策は万全ではないので注意が必要です。現時点ではあくまでもテスト仕様ということで、ローカルネットワーク内の運用にとどめておいてください。

任意のシェルを実行

シェルの実行はbutterfly.server.py --shell=/bin/fishのようにオプションを使って行います。

クライアントサイド

butterflyは他のターミナルと同じように動作しますが、いくつか特徴的な機能があります。

ブラウザショートカットの使用

ターミナルにフォーカスが当たっている時はショートカットキーが効かないため(ブラウザ指定のものは除く)、例えば CTRL + L を押してもアドレスバーに移動しません。それではどうするのかというと、最初に ALT + Z を押してやります。これでbutterflyに「次の組み合わせはショートカットキーですよ」ということを教えてやるのです。つまり、アドレスバーに移動するには、最初に ALT + Z 、そして CTRL + L を押せばいいわけですね(ブラウザがCSS3フィルタをサポートしていれば、画面がセピア色に変わるはずです)。

テキストのクイック選択

私がターミナルを使っていて常々欲しいなと思っていた機能、それがこの「テキストのクイック選択」です。どういうものか、説明するより実際にやってみましょう。まず CTRL + SHIFT + up を押して選択機能をONにします。次に CTRL + SHIFT + up/down でターミナル上のテキストを選択。最後にENTERを押すと、butterflyが選択されたテキストをプロンプトに反映してくれます。

"butterfly selection"

その他の機能

  • スタイルの指定にはCSSを使っているので、全面的に編集可能です。
  • JavaScriptでブラウザ上のターミナルの振る舞いを簡単に拡張できます(クイック選択もそうやって作りました)。
  • ブラウザをベースにした"モダン"なテキストエディタが流行っているらしいので、時代にも合っていますね。

"butterfly modern editors"

不足している機能

・メモリを食うため、スクロールバックは今のところ100,000行までです。将来的には、サーバがスクロールバックをディスクに書き込むよう修正するのもいいかもしれません。 ・ターミナル内へのhtmlの埋め込みは可能ですが、ESC]99;<html>BELLコマンドでの部分的なサポートになります。ちなみに、これを活用すれば面白い機能が実現できるはずです。 ・1670万色には未対応ですが、ESC[38;2;r;g;bmを使えばかなり近いところまで持っていけるのではと思います。 ・ターミナル上でCSS/jsファイルを編集するオプションには、テーマと拡張機能のパッケージリポジトリがありません。

最後に

リポジトリはgithubにアップされています。

個人的には、とにかく実装が楽しかったです。皆さんのコメントをお待ちしています。

Clojureで学ぶデータ構造:ハッシュテーブル

「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!この記事はここでも公開されています。


今回の記事は、Clojureでのハッシュテーブルの実装に関する記事です。長い記事で途中までの翻訳になりますがお楽しみください。



Original article: Data Structures in Clojure: Hash Tables by Max Countryman


前回のおさらい

前回の記事では連結リストについてお話ししました。具体的には、ミュータブルな片方向リストの実装方法を検証しましたね。片方向リストを選んだ理由についても、すでに説明済みです。ここで覚えておいてほしいのは、一般的にClojureではイミュータブルなデータ構造が用いられるということです。しかしミュータブルなデータ構造を利用した方が、アルゴリズムがよりシンプルで高速になるケースがあります。実際にClojureでプログラムを組む時はClojureのデータ構造を使いますが、このシリーズではClojureとデータ構造についての理解を深めるため、あえて違うアプローチをとります。他ではあまり深く掘り下げられることのないdefinterfacedeftypeを詳しく説明するためです。

今回は、前回作った連結リストをベースに、連想配列や辞書に代表される抽象データ型の機能を備えたデータ構造を構築します。具体的には、ハッシュテーブルを実装することになります。

ハッシュテーブルを実装する時は、ハッシュ衝突を回避しつつランタイムパフォーマンスを保証する必要があります。両者のバランスをとりながら実装を進めていきましょう。最終的に完成する構造体の実装はJavaClojureのクラスの実装にはかないませんが、コーディングを通してあらゆる言語のデータ構造に共通する普遍的な本質を見ることができるはずです。それはミュータブルであってもイミュータブルであっても変わることのない原理です。


ハッシュテーブル

抽象データ型である連想配列を実装したものには、ハッシュテーブルやハッシュマップがあります。連想配列の主な機能は、キーを使った値の挿入、検索、削除です。例えば、値"bar"を格納する時に、キー"foo"を指定することができます。こうしておけば、後からキー"foo"を使ってハッシュテーブルから値"bar"を取り出すことができます。それでは実際にキー"foo"と値"bar"を関連づけるデータ構造を構築していきましょう。

上で説明した連想マップのようなデータ構造は、多くの言語で使われています。例えばPythonプログラマは辞書を、Clojureプログラマはハッシュマップをよく使います。今から構築するハッシュテーブルはこれらのデータ構造とよく似た動きをしますが、重要な違いがあります。それは、私たちが実装する連想配列がミュータブル、つまり可変だということです。


パフォーマンスの特徴

高いパフォーマンスを保証できるというのが、ハッシュテーブルの持つ重要な特徴です。ハッシュテーブルにおける挿入、検索、削除の実行速度は平均するとΟ(1)になります。連結リストでも、リストの先頭にある要素にはΟ(1)の速さでアクセスすることができます。しかしリスト内の他の要素を検索する時は実行速度がΟ(n)になり、かなりの時間を費やすことになります。なぜなら検索時に、リスト内にある全要素を1つずつ参照する必要があるからです。

ハッシュテーブルにおいても、実質的なパフォーマンスは、キャッシュの局所性、インデックスの値を決めるハッシュ関数、連鎖法や開番地法などの衝突処理に左右されます。とはいえパフォーマンスに関する限り、ハッシュテーブルが連結リストに勝るのは間違いないでしょう。


実装の詳細

これから実装するハッシュテーブルには「バケット」という配列を利用します。バケットは、すべてのデータが格納されるデータ構造の基礎となる要素です。ハッシュテーブルを、このバケット配列をラップするインデクサーだと言い換えてもいいでしょう。実際は、インデックスはNodeに保持されます。前回構築した連結リストを思い出してみてください。ノードは次のノードへと連鎖していましたね。このような構造を連鎖法と呼びます。

仕組みを説明しましょう。簡単な配列を思い浮かべてください。今から実装するハッシュテーブルにキーと値を渡すと、そのキーと値はそれぞれ下位のバケット配列のあるインデックスに格納されます。詳細は後で説明しますので、今のところは、このデータ構造を使えばバケット配列のどの位置にキーと値のペアが格納されたのかが分かる、つまりインデックスがつけられていると理解しておいてください。

"Simple Array"

作業を簡略化するために前回実装したコードに手を加えて、3つのフィールドkeyvaluenextを持つ新しいNodeクラスを作ります。前回のクラスをそのまま使うこともできますが、修正を加えることでキーと値の設定と参照をよりシンプルにすることができます。


基本の実装

HashTableクラス

それでは前回実装した連結リストのコードをベースに、HashTableと呼ばれる新しいクラスを導入しましょう。Nodeクラスと同じように、使用する関数群をインターフェースを使って定義します。 コードは次のようになります。

(definterface INode
  (getKey [])
  (setKey [k])
  (getVal [])
  (setVal [v])
  (getNext [])
  (setNext [n]))

(deftype Node
  [^:volatile-mutable key
   ^:volatile-mutable val
   ^:volatile-mutable next]
  INode
  (getKey [this] key)
  (setKey [this k] (set! key k))
  (getVal [this] val)
  (setVal [this v] (set! val v))
  (getNext [this] next)
  (setNext [this n] (set! next n)))

(definterface IHashTable
  (insert [k v]))

(deftype HashTable
  [buckets size]
  IHashTable
  (insert [this k v]))

先ほど述べたとおり、前回の連結リストのコードに少し手を加えました。ハッシュテーブルの型とinsertメソッドはまだきちんと実装されていませんが、これについては後でカバーしますね。本格的な実装に入る前にまず、buckets配列を用いてキーと値のペアを正確に関連づける方法を詳しく見てみましょう。

キーと値のペアは、連結リストの一部としてそれぞれノードの中に格納されます。これは後で説明する衝突処理の項で重要になりますから、よく覚えておいてください。これらのノードはbuckets配列のインデックスにマップされます。配列の中から特定のノードに紐づいたインデックスを見つけるには、キーとなる文字列をハッシュします。"ハッシュテーブル"を"ハッシュ"するわけですね。ハッシュ値を有効なインデックスに確実に対応させるには、バケット配列の要素数ハッシュ値の数の両方が足りていなければなりません。そうすれば、常に有効なインデックスを用意することができます。

"Hash Mapped Key-Values"

ここまではいいですね。では、insertメソッドの実装に進みましょう。

(definterface IHashTable
  (bucketIdx [k])
  (setBucket [k v])
  (insert [k v]))

(deftype HashTable
  [buckets size]
  IHashTable
  (bucketIdx [_ k] (mod (hash k) size))
  (setBucket [this k v] (aset buckets (.bucketIdx this k) v))
  (insert [this k v]
    (.setBucket this k (Node. k v nil))))

asetは、配列のインデックスに値をセットするClojure特有の関数です。Javaの配列をClojureで使うことはめったにありませんが、Javaとの連携を効率化するためにasetagetalengthなどのメソッドは用意されています。今回はこれらのメソッドを使ってバケット配列を操作します。

これで、キーと値をバケット配列に挿入する準備ができました。このHashTableクラスは次のように使います。

=> (def hash-table
     (let [size 4]
       (HashTable. (make-array INode size) size)))
#'user/hash-table
=> (.insert hash-table "foo" "bar")
#<Node user.Node@2753d864>

これでハッシュテーブルが定義できました。sizeでサイズを指定したINodeを格納した配列が、引数として渡されていますね。ハッシュテーブルのサイズも、sizeの値で指定されています。おっと、ハッシュテーブルの中身を参照するメソッドを忘れていました。それでは、先に進む前にlookupメソッドを実装してみましょう。

(definterface IHashTable
  (bucketIdx [k])
  (getBucket [k])
  (setBucket [k v])
  (insert [k v])
  (lookup [k]))

(deftype HashTable
  [buckets size]
  IHashTable
  (bucketIdx [_ k] (mod (hash k) size))
  (getBucket [this k] (aget buckets (.bucketIdx this k)))
  (setBucket [this k v] (aset buckets (.bucketIdx this k) v))
  (insert [this k v]
    (.setBucket this k (Node. k v nil)))
  (lookup [this k]
    (when-let [node (.getBucket this k)]
      (.getVal node))))

できました。これでキーと値のペアを取り出すことができます。

=> (.lookup hash-table "foo")
"bar"

ハッシュテーブルを定義する時、sizeの値を使って4としました。なぜこの処理が重要なのでしょう。これは、バケット配列の要素数にも同じsizeの値を指定することで、ハッシュテーブルをバケット配列の長さで固定するためです。下部配列が一杯になるまでは、この設計で問題ありません。でも、その後はどうなるのでしょうか?


衝突

"Keys Hashing to the Same Index"

2つのキーが同じインデックスにハッシュされた状態を衝突と言います。ハッシュテーブルを扱う場合、衝突を回避する処理は避けて通れません。今回の実装では連鎖法と呼ばれる手法を用いますが、代わりに開番地法を採用することも可能です。これら2つをベースにした他の手法もありますが、この記事では扱いません。

現状では、2つのキーがハッシュテーブルで衝突を起こすと、ノードは上書きされて古いキーと値のペアは失われてしまいます。これは避けたい事態です。しかし実は、バケットに格納する値として連結リストを選んだのはこの問題への布石だったのです。

連結リストを使うと、なぜ衝突が回避できるのでしょうか。それは、衝突が起こると分かった時点で同じバケット上に新しいノードを追加できるからです。連結リストなら簡単にプリペンドできますからね。つまり、バケットにすでにノードが格納されている場合は、cons関数を用いてそのバケットに新しいノードを構成すればいいのです。この変更にともないlookupメソッドの処理も少し変え、指定されたバケット内のノードを1つずつ参照するように修正する必要があります。

では実際にコードを修正してみましょう。

(definterface IHashTable
  (findNode [k])
  (insert [k v])
  (lookup [k]))

(deftype HashTable
  [buckets size]
  IHashTable
  ...

  (findNode [this k]
    (when-let [bucket (.getBucket this k)]
      (loop [node bucket prev nil]
        (if (or (nil? node) (= (.getKey node) k))
          (vector prev node)
          (recur (.getNext node) node)))))

  (insert [this k v]
    (let [[prev node] (.findNode this k)]
      (cond
        ;; 1. bucket contains a linked list but not our key,
        ;;    set the next node
        (and prev (nil? node)) (.setNext prev (Node. k v nil))

        ;; 2. bucket contains a linked list and our key, reset 
        ;;    value
        node (.setVal node v)

        ;; 3. bucket is empty, create a new node and set the 
        ;;    bucket
        :else (.setBucket this k (Node. k v nil)))))

  (lookup [this k]
    (let [[_ node] (.findNode this k)]
      (when node
        (.getVal node)))))

これでハッシュの衝突を回避できました。cons関数を使ってバケットに新しいノードを構成するというアイデアがきちんと実装できています。また、findNodeというヘルパーメソッドも追加されました。このメソッドは、あるキーを指定するとバケット配列の中からそのキーを探しにいきます。もし該当があれば、そのキーを含んだノードと、存在すれば1つ前のノードを返します。こうして得た「1つ前のノード」と「キーを含むノード」の組み合わせから、insertメソッドのスコープ内で、以下のどのケースに該当するかを判別します。


  1. 前ノードがあってノードがない場合。バケットは占有されているが指定したキーは存在していないということが分かります。この場合cons関数を用いて、バケットに新しいノードを構成します。

  2. ノードがある場合。これは、バケットの連結リストの中にキーがすでに存在しているということなので、ノードの中の値を更新します。

  3. バケットが空で、キーが存在していない場合。キーと値を含んだ新しいノードをバケットに設定して、次ノードにポイントします。



ここまでの実装にはまだ問題があります。この実装の続きは原文からどうぞ。