実際、すべてがうまくいかない...
私の友人がこの記事を書くように頼みました。 この記事では、特定のインターネットリソースの管理によって誤解された可能性のある彼の冒険について説明します。 そして、それらは、彼がどこに行くべきかについて、私の友人に不平を言うことができました。 したがって、私は彼の言葉で記事を書いています。 そして彼は去った。 ホンジュラスへ。 善のために。
問題
数年前(ついに!)私の人生でアパートを買う
不動産を検索するための中央ローカルサイト(大半の代理店と所有者がここにアパートを置いています)は、「少し不快になった」と言うのがやわらかかったためです。 その上のアパートの検索には、そのようなサービスの標準設定が含まれていました:建設年、階数、価格ではなく(!)最終/一階など そして、彼は、検索、私は彼に私に別のバスルーム付きのアパートを提供するように頼んだとき、時々彼はコンバイン付きのアパートを配った。 バルコニーについても同様の話がありました。 そして、彼は(検索)私のリクエストに対応していないアパートを時々出すので、おそらく彼は対応するアパートを見せないでしょう。 そして、私のサンプル(最上階のアパート、独立したユニット、5階以上、地下鉄からそれほど遠くない場所、そして何とか何とか)では、多くのアパートが入ることができませんでした...
コワルスキー、オプション!
サイトからすべてのアパートメントをローカルに自分でアンロードするために残っていることが1つだけありました。それらをデータベースに保存し、SQLを手に入れて(まあ、もっと便利なのはスレッド)、「運転」しました。
言うのは簡単ですが、するのは難しいです。 最初のアイデアは、サイトエンジンを調べて、その中の穴を探し、アパートに関するすべての情報が保存されているサーバーに到達し、そこからコピーすることでした。 しかし、
ターゲットサイトに不動産会社のセクションがありました。 コラボレーション、すべてのもの。 そこで、あなたが代理店であれば、特殊なソフトウェアへのアクセスを取得(購入?)できます。これにより、その手順とスクリーンショットから判断すると、代理店に代わって広告を自動的に送信できます(スパマーからですか?)。 理論的には、このソフトウェアでは、サーバー側に関する情報を検索し、そこからアパートに関する情報を引き出すこともできました。 ここで、私の資格は十分だと思います。 しかし、私はソフトウェアにアクセスできず、代理店になりたくありませんでした。
したがって、それを書く以外に何も残っていません...
パーサー
プログラムでサイトにアクセスし、すべてのアパートメントを「検索」して結果を解析し、ローカルデータベースに保存します。 パーサーをPythonで書くことにしました-当時は比較的新しい言語であり、そのレベルを上げると便利でした(したがって、コードは適切です)。
ページをダウンロードするために、標準のurllibが使用されました:
from urllib import FancyURLopener, quote_plus ... flatsPageContent = urlOpener.open(flatsPageURL).read()
(グーグルをアクティブにした後)lxmlライブラリーを使用することを決定したHTMLの構文解析:
from lxml.html import parse ... flatsPageDocument = parse(flatsPageFilePath).getroot() if flatsPageDocument is not None: flatsTables = flatsPageDocument.xpath('//*[@id="list"]')
これはすべて平凡で面白くない。 しかし、他の何かが面白かったです。
地下鉄は遠いですか?
馬のいない男であり、公共交通機関で厳密に移動するため、地下鉄が私の将来のアパートの近くにあることが重要でした。 そのような、メーターは2000以下です。 したがって、アパートに最も近い地下鉄駅とその距離を決定するというアイデアが生まれました。 そして、実装:
いくつかのコード
def getFlatLocation(flatPageName, flatAddress, mode, geoDBCursor): logging.info('Retrieving geo code info for flat \'%s\' (mode \'%s\')...' % (flatPageName, mode)) flatFullAddress = (flatBaseAddress + flatAddress).encode('utf8') geoCodeResult = '' isGeoCodeResultCached = 1 geoDBCursor.execute("SELECT geoCode FROM %s WHERE address = ?" % ("GeoG" if mode == 'G' else "GeoY"), (flatFullAddress,)) geoCodeResultRow = geoDBCursor.fetchone() if geoCodeResultRow is not None: geoCodeResult = geoCodeResultRow[0] if geoCodeResult is None or len(geoCodeResult) == 0: isGeoCodeResultCached = 0 geoCodeURL = ('http://maps.google.com/maps/api/geocode/json?sensor=false&address=' if mode == "G" else 'http://geocode-maps.yandex.ru/1.x/?format=json&geocode=') + quote_plus(flatFullAddress) urlOpener = UrlOpener() geoCodeResult = urlOpener.open(geoCodeURL).read() if geoCodeResult is None: geoCodeResult = '' logging.info('Geo code result for flat \'%s\' was fetched (mode \'%s\', from cache - %d)' % (flatPageName, mode, isGeoCodeResultCached)) flatLocation = 0 geoCodeJson = json.loads(geoCodeResult) if geoCodeJson is not None and (len(geoCodeJson['results']) if mode == 'G' else len(geoCodeJson['response'])): if isGeoCodeResultCached == 0: geoDBCursor.execute("INSERT INTO %s VALUES (?, ?)" % ("GeoG" if mode == 'G' else "GeoY"), (flatFullAddress, geoCodeResult)) if mode == "G": geoCodeLocation = geoCodeJson['results'][0]['geometry']['location'] flatLocation = {'lat': float(geoCodeLocation['lat']), 'lng': float(geoCodeLocation['lng'])} else: geoCodeLocation = geoCodeJson['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']['Point']['pos'] (flatLocationLng, flatLocationLat) = re.search('(.*) (.*)', geoCodeLocation).group(1, 2) flatLocation = {'lat': float(flatLocationLat), 'lng': float(flatLocationLng)} logging.info('Geo code info for flat \'%s\' was retrieved (mode \'%s\')' % (flatPageName, mode)) else: logging.warning('Geo code info for flat \'%s\' was NOT retrieved (mode \'%s\')' % (flatPageName, mode)) return (flatLocation, isGeoCodeResultCached)
コードからわかるように、GoogleとYandexはジオコーディングデータのソースとして使用されます。 なぜ1つだけではないのですか? 新しい通り(および古い、または誤って入力された通り)についてのみ、ソースの誰かが誤ったデータまたは平均化されたデータ(たとえば、市の中心の座標)を与える可能性があります。 したがって、2つのエンジンが同時に使用されるため、明らかに誤った結果をふるいにかけることができます。 GoogleとYandexの両方が、IPを使用した1日あたりのリクエスト数の割り当てを持っていることは明らかです。 したがって、「パンチング」アドレスの結果は、後続のパーサーの起動で使用するためにデータベースに慎重に保存されました。
Googleマップの助けを借りて、テーブルには、現在建設中の地下鉄駅を含む地下鉄駅の座標が入力されました。 そして、距離は単に
def calculateDistance(location1, location2): # haversine formula, see http://www.movable-type.co.uk/scripts/latlong.html for details R = 6371 * 1000 # Radius of the Earth in m dLat = (location2['lat'] - location1['lat']) * (math.pi / 180) dLng = (location2['lng'] - location1['lng']) * (math.pi / 180) a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(location1['lat'] * (math.pi / 180)) * math.cos(location2['lat'] * (math.pi / 180)) * math.sin(dLng / 2) * math.sin(dLng / 2) c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) d = R * c return d
そして、ここが最寄りの地下鉄駅です。
def getFlatDistanceInfo(flatLocation): flatSubwayStationDistances = map(lambda subwayStationInfo: calculateDistance(flatLocation, subwayStationInfo['location']), subwayStationInfos) flatNearestSubwayStationDistance = min(flatSubwayStationDistances) flatNearestSubwayStationName = subwayStationInfos[flatSubwayStationDistances.index(flatNearestSubwayStationDistance)]['name'] flatTownCenterDistance = flatSubwayStationDistances[0] return (flatNearestSubwayStationName, flatNearestSubwayStationDistance, flatTownCenterDistance)
アパートの価格の追跡
おそらく、「N市のアパートの価格は1か月あたりX%低下し始めました」などの記事を繰り返し読んだでしょう。 だから私はこの主題について私の意見がありました。
抽出されたアパートメントはすべてデータベースにローカルに保存されているため、価格の変化を追跡することができました。 古いデータベースを調べて、そこで回復可能なアパートに関する情報を見つけると、価格のデルタを計算することができました。
isFlatInfoUpdated = 0 flatPriceDelta = 0 if len(oldFlatsDBFilePath): oldFlatsDBCursor.execute('''SELECT flatPriceInfo FROM Flats WHERE flatPageURL = ? AND flatAddress = ? AND flatWholeSquare = ? AND flatLivingSquare = ? AND flatKitchenSquare = ?''', (flatPageURL, flatAddress, flatWholeSquare, flatLivingSquare, flatKitchenSquare,)) oldFlatInfoRow = oldFlatsDBCursor.fetchone() if oldFlatInfoRow is not None and oldFlatInfoRow[0] is not None: isFlatInfoUpdated = 1 oldFlatPriceInfo = oldFlatInfoRow[0] try: flatPriceDelta = float(flatPriceInfo) - float(oldFlatPriceInfo) except ValueError: pass
そのため、不動産市場の分析に関する記事を読むたびに、「自分の」アパートの価格がまったく上昇しなかったことを知って、私は微笑んだ。 たぶん私以外の人には必要なかったのでしょうか?
個別に必要ですか、それとも結合されますか?
私はプログラマーであり、プログラマーは多くのことを考えています。 共同バスルームでこれは可能ですか?
問題は、物件検索サイトがこの情報をアパートの説明ページ内に隠し、検索結果のリストに表示しないことでした。 そのため、「flatsDeepParseMode」という特別なパーサー操作モードが追加されました。 ことわざにあるように、「もっと深くする必要がある」(c)。 彼は、パーサーがアパートの検索結果のページだけでなく、アパートの説明ページも直接ダウンロードできるようにしました。 そして、すでにそれらから追加情報がバスルームなどに抽出されました。
耐障害性
ディープパースモードでは、スクリプトはサーバーに大きな負荷をかけ、数千ページのリターン要求でサーバーを攻撃する可能性がありました。 これは、サーバーの思慮深さにつながり、時にはリクエストを拒否することにもつながりました。 そのため、このような場合のスクリプトは、試行間のタイムアウトを徐々に増やして「再要求」のメカニズムをサポートし始めました。
変装
スクリプトが機能しなくなったら。 サーバーがそこに何かに答えられないというメッセージがあふれ、タイムアウトと何とか何とか。 不動産Webサイトの所有者は、クライアントサーバーに接続するユーザーエージェントをフィルター処理するために、特別に訓練された人々のグループを雇ったことが判明しました(突然ですか?)。 そして、私のスクリプトはディストリビューションの下にありました。 そして、それは単純に決定されました-スクリプトはブラウザのふりをしました:
class UrlOpener(FancyURLopener, object): version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11' pass
しかし、ひどいことが起こったら...
あなたは禁止されています!
はい、禁止されました。 そして、私だけでなく、判明しました。 午前中に仕事に着いた(どういうわけかお金を稼がなければならなかった)ので、サーバーがそこにあるというおなじみのエラーメッセージ、タイムアウト、何とか何とかを見ました。 ユーザーエージェントを別のブラウザーに置き換えることは役に立ちませんでした。 結局のところ、ブラウザでさえ不動産サイトを開くことができませんでした...はい、すべての静的IPはサーバー側で禁止されました。 なぜこれが起こったのか分かりません。 おそらく、「ある種のウイルスプログラムが多くのリクエストをサーバーに送信した」か、数十人の会社の従業員が住む場所を探すことにしたのでしょう。 しかし、そうであっても、私たちは禁止されました。
ちょうどそのようになりましたが、当社の弁護士はそのサイト(おそらく海外の同僚のアパート)で何かを探す必要がありました。 しかし、彼らは慢なリソースの管理を降ろさせませんでした。 そのようなことはしませんでした。 彼らは誰も殺しませんでした。 本当です 一般的に、私たちは禁止されました。 正直に。
機能、機能、機能...
パーサーはまだ多くのことを行うことができます:アパートを特定の価格に解析する、リモートアパートと新しく追加されたアパートをマークする、アパートの写真の数を数えるなど。
...
取って包む
私はまだ完璧なアパートを見つけました。 最上階、地下鉄の隣、すべてのもの。 パーサーを作成せずに見つけることができますか? わからない、多分。 しかし、それはスポーツマンらしくなく、どういうわけかプログラマティックではありません...
PS
そして、はい、私はハブの古い記事を思い出します。私と同じ
ちなみに、私のパーサーは動作しなくなるか、正しく動作しない場合があります(サイトでの変更の可能性があるため) 長い間使用されています。 そして、彼に注意してください、そうでなければ、彼らは禁止されることがあります(例がありました)。
コンポートコード?!
友人のリクエストに応じて、パーサのソースコードをbitbucket.orgに投稿します。 カブでは、抽出されたすべてのデータを視覚化する、かなり大きな、苦しんでいるSQLクエリを持つファイルを見つけることができます。 もちろん、コードは参照用です。
ご清聴ありがとうございました。