POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Vik Paruchuri

本記事は、原著者の許諾のもとに翻訳・掲載しております。

私は、BostonからBay Areaに数カ月前に引っ越してきました。Priya(彼女です)と私は、賃貸市場についてありとあらゆる怖い話を聞いていました。「San Franciscoでアパートを探す方法」をGoogleで検索すると、 たくさん のアドバイスがヒットすることからも、アパート探しの大変さがよく分かります。


Bostonは寒いです。でも、San Franciscoのアパート探しは怖いです。

大家さんが見学会を開くと、そこに必要な書類を全て持って行き、検討するだけでも自分から進んですぐに保証金を支払わなければならないと書いてありました。私たちが賃貸契約に関するプロセスを徹底的に調べてみると、アパート探しの多くはタイミングがカギだということが分かりました。何が何でも見学会を開きたいと考えている大家さんもいますが、そうでない人の場合、最初にアパートを見に来た人に貸す場合が多いです。成功のカギは、物件を見つけ、それが自分の条件に合うかを素早く判断し、大家さんに連絡してその部屋を見せてもらう約束を取り付けることです。

インターネットの口コミで評価の高かった PadmapperLiveLovely といったいくつかの賃貸アパートサイトを見てみましたが、どのサイトも掲載している情報がリアルタイムではなく、実際に見学して検討できるような物件は1つもありませんでした。また、地域を限定したり、駅に近いなどの追加条件を指定したりできるサイトもありませんでした。Bay Areaのアパートの物件情報のほとんどは、最初に Craigslist に掲載され、その後他のサイトがその情報を収集していくので、恐らく全ての物件が転載されているわけではないでしょうし、リアルタイムの情報が確認できるほどすぐに転載されるとは思えません。

私たちは、以下のような方法があれば便利なのにと思いました。

  • Craigslistに投稿があればリアルタイムに近いタイミングで通知する。
  • 希望する地域以外の物件を除外する。
  • 公共輸送に近いなどの追加条件に合わない物件を除外する。
  • 物件について、協力して評価する。
  • 気に入った物件について、簡単に家主と連絡が取れる。

これらについて考え、4つのステップで実現できると気付きました。

  • Craigslistから物件情報を取得する。
  • 自分たちの条件に合わない物件を除外する。
  • 条件に該当した物件情報をチームで使うチャットツールの Slack に投稿し、そこで投稿した物件について話し合って評価する。
  • このプロセス全体を持続的なループにラップし、(継続的に実行するように)サーバにデプロイする。

ここからは、各ステップをどのように構築したか、また、最終的なSlackのbotをアパート探しにどのように使ったかについて見ていきます。このbotを使って、Priyaと私は、(San Franciscoにしては)お手頃価格の気に入ったワンベッドルームの部屋をおよそ1週間後に見つけることができました。思っていたよりもずっと早かったです。

もしこの記事を読み進めていく中でこのコードに興味を持たれたら、完成したプロジェクトを こちら でご覧いただけます。また、README.mdへのリンクは こちら です。

ステップ1 – Craigslistから物件情報を取得する

bot構築の最初のステップは、Craigslistから物件情報を取得することです。Craigslistには、残念ながらAPIがありませんが、 python-Craigslist パッケージを使って投稿を取得することができます。 python-craigslist がWebページのコンテンツを取得してくれるので、 BeautifulSoup を使用してWebページから関連する項目を抽出して、構造化データに変換します。このパッケージのコードは比較的短く、目を通す価値があります。

CraigslistのSan Franciscoのアパート物件は、 https://sfbay.craigslist.org/search/sfc/apa に掲載されています。次のことを実行するコードをその下に記載しておきました。

  • python-craigslist にあるクラス、 CraigslistHousing をインポートする。
  • 以下の引数でこのクラスを初期化する。

    • site – 取得したいCraigslistのサイト。この site は、URLの最初の部分です。例: https://sfbay.craigslist.org
    • area – 取得したいサイトにあるサブエリア。この area は、URLの最後の部分です。例: https://sfbay.craigslist.org/sfc/ これでエリアがSan Franciscoに限定されます。
    • category – 探したい物件の種類。この category は、検索画面のURLの最後の部分です。例: https://sfbay.craigslist.org/search/sfc/apa これでアパート物件全てを表示します。
      * filters – 結果に適用したい条件。
    • max_price – 支払っても良い最高価格。
    • min_price – 探したい物件の最低価格。
  • ジェネレータ である get_results メソッドを使って、Craigslistから結果を取得する。

    • 引数 geotagged を渡して、各結果に座標を追加する。
    • 引数 limit を渡して、 20 個の結果だけを得る。
    • 引数 newest を渡して、最新の物件情報だけを得る。
  • resultresults ジェネレータから得て表示する。
from craigslist import CraigslistHousing

cl = CraigslistHousing(site='sfbay', area='sfc', category='apa',
                         filters={'max_price': 2000, 'min_price': 1000})

results = cl.get_results(sort_by='newest', geotagged=True, limit=20)
for result in results:
    print result

bot構築のステップ1があっという間に終わりました。これでCraigslistからの物件情報を取得できます。各 result は、複数のフィールドを含むディクショナリです。

{'datetime': '2016-07-20 16:39',
 'geotag': (37.783166, -122.418671),
 'has_image': True,
 'has_map': True,
 'id': '5692904929',
 'name': 'Be the first in line at Brendas restaurant!SQuiet studio available',
 'price': '$1995',
 'url': 'http://sfbay.craigslist.org/sfc/apa/5692904929.html',
 'where': 'tenderloin'}

以下が、フィールドの説明です。

  • datetime – その物件が投稿された日時。
  • geotag – その物件の座標位置。
  • has_image – Craigslistの投稿に画像があるか。
  • has_map – その物件に付随する地図があるか。
  • id – Craigslistで使用しているその物件のユニークなID。
  • name – Craigslistで表示されている物件名。
  • price – 1カ月の家賃。
  • url – その物件の全ての情報を見るためのURL。
  • where – その物件情報を掲載した人が提供している場所情報。

ステップ2 – 結果に条件を追加する

Craigslistから物件情報を取得することができたら、あとは結果に条件を追加して、見たいものだけを表示させるようにする必要があります。

結果をフィルタリング

Priyaと私は、以下のエリアで物件を探したいと思っていました。

地域を条件に追加するためには、まず、境界ボックス、つまりそのエリア周辺のボックスを定義する必要があります。


Lower Pacific Heights周辺にボックスを描いています。

上の境界ボックスは、 BoundingBox を使って作成したものです。必ず左下の csv を選んで、ボックスの座標を取得してください。

Google Mapsのようなツールを使って左下と右上の座標が分かれば、自分で境界ボックスを定義することも可能です。ボックスが決まれば、地域と座標のディクショナリを作成します。

BOXES = {
    "adams_point": [
        [37.80789, -122.25000],
        [37.81589,  -122.26081],
    ],
    "piedmont": [
        [37.82240, -122.24768],
        [37.83237, -122.25386],
    ],
    ...
}

各ディクショナリのキーは地域名で、各キーはリストのリストを含んでいます。最初の内部リストは境界ボックスの左下の座標で、2つ目は右上の座標です。そして、ある物件の座標がこのボックスの中に位置するかどうかによって、条件に該当するかどうかを確認することができます。

次のことを実行するコードをその下に記載しておきました。

  • BOXES の中のキーの1つ1つについてループする
  • 結果がボックスの中かどうかを確認する
  • ボックスの中であれば、適切な変数を設定する
def in_box(coords, box):
    if box[0][0] < coords[0] < box[1][0] and box[1][1] < coords[1] < box[0][1]:
        return True
    return False

geotag = result["geotag"]
area_found = False
area = ""
for a, coords in BOXES.items():
    if in_box(geotag, coords):
        area = a
        area_found = True

残念ながら、Craigslistから取得した結果全てに、付随する座標があるとは限りません。座標を計算することができる位置情報を指定しているかどうかは、物件を投稿する人によります。物件を投稿する人がCraigslistに慣れているほど、位置情報を含んでいる可能性が高くなります。

通常、高い家賃を設定しがちである業者が投稿した物件には、位置情報が掲載されています。物件の所有者の投稿には座標情報がない場合が多いですが、大抵契約条件が良いです。そのため、希望する地域にある座標情報のない物件が分かれば確実と言えます。地域のリストを作って文字列を照合し、希望する地域にある座標情報のない物件かどうかを確認します。多くの物件情報が間違った地域を示しているので、座標を利用するよりも正確性は劣りますが、ないよりはマシです。

NEIGHBORHOODS = ["berkeley north", "berkeley", "rockridge", "adams point", ... ]

NEIGHBORHOODS の1つ1つにループすることによって、名前で照合することができます。

location = result["where"]
for hood in NEIGHBORHOODS:
    if hood in location.lower():
        area = hood

これまでに書いた2つのコードで結果が処理されれば、住みたい地域に入っていない物件情報が除外されています。誤判定のものがいくつかあったり、地域や位置が指定されていない物件を見落としたりするかもしれませんが、このシステムは、物件情報の大部分を取得してくれます。

駅の近さを条件に加える

Priyaも私も頻繁にSan Franciscoを訪れることは分かっていたので、San Franciscoに住まないのであれば、交通の便が良いところに住みたいと思っていました。Bay Areaの主な公共輸送は BART です。Oakland、Berkeley、San Francisco、近郊エリアを結ぶ高速鉄道で、一部の区間では地下を走っています。

botにこの機能を構築するためには、まず駅のリストを定義する必要があります。駅の座標は Google Map から入手し、それらのディクショナリを作成します。

TRANSIT_STATIONS = {
    "oakland_19th_bart": [37.8118051,-122.2720873],
    "macarthur_bart": [37.8265657,-122.2686705],
    "rockridge_bart": [37.841286,-122.2566329],
    ...
}

全てのキーは駅の名前で、駅がある場所の緯度と経度が示された関連したリストを持っています。ディクショナリを作成したら、それぞれの結果に対して最寄駅を探します。

次のことを実行するコードをその下に記載しておきました。

  • TRANSIT_STATIONS に各キーとアイテムをループする。
  • coord_distance 関数を使って、2組の座標の距離をキロメーターで測定する。この関数に関する説明は こちら をご確認ください。
  • その物件に最も近い駅かどうかを確認する。

    • 駅が遠すぎる( 2 キロ以上もしくは、 1.2 マイル以上離れている)場合は無視する。
    • 前回選別された最寄り駅よりも近い駅であれば、採用する。
min_dist = None
near_bart = False
bart_dist = "N/A"
bart = ""
MAX_TRANSIT_DIST = 2 # kilometers

for station, coords in TRANSIT_STATIONS.items():
    dist = coord_distance(coords[0], coords[1], geotag[0], geotag[1])
    if (min_dist is None or dist < min_dist) and dist < MAX_TRANSIT_DIST:
        bart = station
        near_bart = True

    if (min_dist is None or dist < min_dist):
        bart_dist = dist

これで、各物件情報の最寄り駅を知ることができました。

ステップ3 – Slackのbotを作成する

設定

結果に条件を追加し終わったら、これまで準備してきたものをSlackに投稿します。Slackとは、チーム内でチャットができるアプリケーションのことです。Slack内にチームを作成すると、他のメンバーをそのチームに招待することができます。Slackの各チームには、メンバー同士がメッセージをやり取りできる複数のチャンネルが用意されています。チャンネル内にいる他のメンバーは、それぞれのメッセージに「いいね」や絵文字などの注釈を付けることができます。Slackに関する詳細は こちら をご覧ください。もしSlackを体験したいようであれば、私たちが運営する データサイエンスSlackコミュニティ に参加してみてください。

結果をSlackに投稿することによって、他の人たちと協力し合うことができ、どの物件が最適かを見極めることができます。結果を投稿するには、以下のことが必要です。

  • Slackにチームを作成する。作成方法は こちら をご覧ください。
  • 物件を投稿するチャンネルを作成する。役立つ情報が こちら に掲載されています。チャンネル名には、 #housing の使用をお勧めします。
  • SlackのAPIトークンを ここ から入手する。プロセスについては、 こちら をご確認ください。

これらのステップが完了したら、物件情報をSlackに投稿するためのコード作成に入ります。

コードの作成

適切なチャンネル名とトークンが入手できたら、結果をSlackに投稿することができます。投稿には、 python-slackclient を使用します。これはPythonのパッケージで、 Slack API の使用が一段と楽になります。 python-slackclient はSlackのトークンを使用することで初期化され、チームやメッセージを管理する多くのAPIエンドポイントにアクセスできるようになります。

次のことを実行するコードをその下に記載しておきました。

  • SLACK_TOKEN を使って SlackClient を初期化する。
  • 価格や物件のある地域、URLなどといった表示される必要のある全ての情報が含まれた result からメッセージの文字列を作成する。
  • ユーザ名 pybot とロボットのアバターを使って、Slackにメッセージを投稿する。
from slackclient import SlackClient

SLACK_TOKEN = "ENTER_TOKEN_HERE"
SLACK_CHANNEL = "#housing"

sc = SlackClient(SLACK_TOKEN)
desc = "{0} | {1} | {2} | {3} | <{4}>".format(result["area"], result["price"], result["bart_dist"], result["name"], result["url"])
sc.api_call(
    "chat.postMessage", channel=SLACK_CHANNEL, text=desc,
    username='pybot', icon_emoji=':robot_face:'

全てが接続されたら、botがSlackに物件情報を投稿するようになり、以下のように表示されます。

botが実行されると物件情報がこのように表示されます。物件情報に絵文字や「いいね」が付いているのがお分かりいただけると思います。

ステップ4 – 全てを運用可能にする

基本的な要素が全て揃ったので、コードを継続的に実行させる必要があります。最終的には、結果をリアルタイムでSlackに投稿できるようにするか、それに近い状態にしたいと思っています。全てを運用可能にするためには、幾つかのステップを行う必要があります。

  • データベースに物件情報を保存し、重複したものはSlackに投稿しない。
  • SLACK_TOKEN といった設定を他のコードから独立させ、調整しやすくする。
  • 継続的に実行するループを作成し、いつでも結果を取得できるようにする。

物件情報の保存

まずは、 SQLAlchemy というPythonパッケージを利用し、物件情報を保存します。SQLAlchemyは ORマッパー (ORM)で、Pythonでのデータベースの作業を簡単にしてくれます。SQLAlchemyを使うことで物件情報を保存するデータベースのテーブルやテーブルにデータを追加しやすくするデータベースコネクションを作成することができます。

SQLAlchemyを SQLite のデータベースエンジンと併せて使用し、 listings.db という1つのファイルに全てのデータを保存します。

次のことを実行するコードをその下に記載しておきました。

  • SQLAlchemyをインポートする。
  • 現在のディクショナリで作成される、SQLiteのデータベース listings.db と連結させる。
  • Craigslistの物件情報から、関連する全てのフィールドを含んだ Listing というテーブルを定義する。

    • 重複した物件情報がSlackに投稿されることを、 unique フィールドである cl_idlink が防ぐ。
  • 連結からデータベースのセッションを作成し、物件情報を保存できるようにする。
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///listings.db', echo=False)

Base = declarative_base()

class Listing(Base):
    """
    A table to store data on craigslist listings.
    """

    __tablename__ = 'listings'

    id = Column(Integer, primary_key=True)
    link = Column(String, unique=True)
    created = Column(DateTime)
    geotag = Column(String)
    lat = Column(Float)
    lon = Column(Float)
    name = Column(String)
    price = Column(Float)
    location = Column(String)
    cl_id = Column(Integer, unique=True)
    area = Column(String)
    bart_stop = Column(String)

Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()

データベースのモデルが出来上がったので、後は各物件情報をデータベースに保存する必要があります。重複したものは省いてくれます。

コードからコンフィギュレーションを独立させる

次に、コードからコンフィギュレーションを独立させます。まず、コンフィギュレーションを保存することができる settings.py というファイルを作成します。コンフィギュレーションには隠れた SLACK_TOKEN が含まれていますが、うっかりこれをgitにコミットしてしまったり、Githubにプッシュしたりしたくはありません。また、隠されていない BOXES といった他の設定も同様です。しかし、簡単に編集できるようにはしたいと思っています。

そこで以下の設定を settings.py に移動させます。

  • MIN_PRICE – 検索したい最低価格のリスト。
  • MAX_PRICE – 検索したい最高価格のリスト。
  • CRAIGSLIST_SITE – 検索したい地元のCraigslistサイト。
  • AREAS – 検索したい地元のCraigslistサイトのエリアリスト。
  • BOXES – 探したい地域の座標ボックス。
  • NEIGHBORHOODS – 物件情報に座標がない場合、地域にマッチする物件。
  • MAX_TRANSIT_DIST – 駅から離れてもよい一番遠い場所。
  • TRANSIT_STATIONS – 駅の座標。
  • CRAIGSLIST_HOUSING_SECTION – 探したいCraigslistハウジングの小区分。
  • SLACK_CHANNEL – botに投稿してほしいSlackのチャンネル。

また、gitによって無視される private.py というファイルを作成します。これには、以下のキーが含まれています。

  • SLACK_TOKEN – Slackのチームに投稿されるトークン。

完成した settings.py ファイルは、 こちら でご覧いただけます。

ループの作成

最後に、取得したコードが継続的に実行されるループを作成する必要があります。次のことを実行するコードをその下に記載しておきました。

  • コマンドラインから呼び出された時に・・・

    • 現在の時刻を含んだステータスメッセージを表示する。
    • do_scrape 関数を呼び出すことで、Craigslistから取得したコードを実行する。
    • ユーザが Ctrl + C とタイプしたら実行を終了する。
    • トレースバックと継続を表示することによって、他の例外に対処する。
    • 例外がなければ、成功メッセージを表示する(以下のコードでは else の節がそれに該当します)。
    • 定義済みのインターバルの間、再びスクレイピングを行うまでスリープさせる(初期設定では 20 分になっています)。
from scraper import do_scrape
import settings
import time
import sys
import traceback

if __name__ == "__main__":
    while True:
        print("{}: Starting scrape cycle".format(time.ctime()))
        try:
            do_scrape()
        except KeyboardInterrupt:
            print("Exiting....")
            sys.exit(1)
        except Exception as exc:
            print("Error with the scraping:", sys.exc_info()[0])
            traceback.print_exc()
        else:
            print("{}: Successfully finished scraping".format(time.ctime()))
        time.sleep(settings.SLEEP_INTERVAL)

また、コードを取得する頻度をコントロールするために、 SLEEP_INTERVALsettings.py に追加する必要があります。初期設定は 20 分になっています。

実行してみる

これでコードは完成しました。Slack botがどのように実行されるかご自身で確認してみてください。

ローカルコンピュータで実行する

Githubにある こちら から、今回のプロジェクトを確認できます。 README.md には、インストール方法が細かく記載されています。ただし、プログラムのインストールを経験したことがない方やLinuxを使っていない方は、 Docker の説明を参照することをお勧めします。Dockerとは、アプリケーションを簡単に作成したりデプロイしたりできるツールで、Slackのbotをローカルコンピュータで素早く実行させることができます。

DockerでSlackのbotをインストール並びに実行させる基本的な説明を以下に明記しておきます。

  • config というフォルダを作成し、その中に private.py というファイルを入れる。

    • private.py に記述してある設定は、 settings.py にある初期設定をオーバーライドする。
    • private.py に設定を追加することで、botの振る舞いをカスタマイズできる。
  • private.py 内にある上記の設定に新しく値を記述する。

    • 例えば、San Franciscoのエリアだけを探すには、 private.pyAREAS = ['sfc'] と記述する。
    • housing という名前ではないSlackチャンネルを投稿したい場合は、 SLACK_CHANNEL をエントリに追加する。
    • Bay Areaを探したくない場合は、最低でも以下の設定を更新する必要がある。
    • CRAIGSLIST_SITE
    • AREAS
    • BOXES
    • NEIGHBORHOODS
    • TRANSIT_STATIONS
    • CRAIGSLIST_HOUSING_SECTION
    • MIN_PRICE
    • MAX_PRICE
  • ここに記載されている説明 に基づいてDockerをインストールする。
  • 以下のコードは、初期設定のコンフィギュレーションでbotを実行する。

    • docker run -d -e SLACK_TOKEN={YOUR_SLACK_TOKEN} dataquestio/apartment-finder
  • 独自のコンフィギュレーションでbotを実行する。

    • docker run -d -e SLACK_TOKEN={YOUR_SLACK_TOKEN} -v {ABSOLUTE_PATH_TO_YOUR_CONFIG_FOLDER}:/opt/wwc/apartment-finder/config dataquestio/apartment-finder

botをデプロイする

常にコンピュータを起動させておきたくないのであれば、botをサーバにデプロイするのもよいです。そうすれば、継続的に実行しておくことができます。 DigitalOcean というホスティングプロバイダでサーバを作成することができます。DigitalOceanは インストールされたDocker を使って、自動的にサーバを作成してくれます。

DigitalOceanでDockerを使う方法は、 こちら に説明が記載されています。著者が言うところの”shell”を知らないのであれば、DigitalOceanのドロップレットにSSH接続する方法が こちら に記述されています。手引書に沿いたくない場合は、 こちら から始めてみてもいいでしょう。

DigitalOceanでサーバが作成できたら、SSH接続し、Dockerのインストール方法と上記にある使用説明書に沿って作業してみてください。

次のステップ

上記のステップが完了すれば、Slackのbotが自動的にアパートを探し出してくれるはずです。このbotを利用して、Priyaと私は素敵なアパートを見つけることができました。希望価格よりは高くなりましたが、私たちが思っていたSan Franciscoのワンベッドルームの価格よりも、最終的には安く借りられました。また、想像以上に大幅な時間削減にもなりました。私たちの場合はうまく行きましたが、botの改善点は幾つかあります。

  • Slackから賛成や反対の情報を得て、機械学習のモデルを訓練させる。
  • APIから自動的に駅の情報を取得する。
  • 公園やその他の施設など特定の場所を追加する。
  • ウォークスコアや地域の暮らしやすさのスコア、例えば犯罪など、を追加する。
  • 家主の電話番号とメールアドレスを自動的に抽出する。
  • 家主への電話や見学の日程調整を自動的に行う(もし、すでに実施されている方がいるのであれば、素晴らしいです)。

Github にあるプロジェクトに対して自由にプルリクエストを申請してください。また、もしこのツールが役立つと感じたようであれば、是非コメントを投稿してください。皆さんがどのようにこのツールを活用しているのかお伺いしたいです。